diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..f9557a46 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,19 @@ +name: CI +on: + pull_request: +permissions: + contents: read +concurrency: + group: ci-${{ github.event.pull_request.number }} + cancel-in-progress: true +jobs: + lint: + uses: ./.github/workflows/lint.yml + tests: + strategy: + fail-fast: false + matrix: + runner: [ubuntu-24.04, windows-2022, macos-15] + uses: ./.github/workflows/tests.yml + with: + runner: ${{ matrix.runner }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 00000000..476d040a --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,35 @@ +name: Lint +on: + workflow_call: +permissions: + contents: read +env: + RUSTUP_TOOLCHAIN: '1.97.1' + CARGO_PROFILE_DEV_DEBUG: '0' + CARGO_INCREMENTAL: '0' + CARGO_BUILD_JOBS: '2' +jobs: + lint: + runs-on: ubuntu-24.04 + timeout-minutes: 40 + steps: + - uses: actions/checkout@v5 + with: + persist-credentials: false + - uses: actions/setup-python@v6 + with: + python-version: '3.10' + - name: Install Rust + run: rustup toolchain install "$RUSTUP_TOOLCHAIN" --profile minimal --component rustfmt --component clippy --no-self-update + - name: Rust formatting + run: cargo fmt --all -- --check + - name: Clippy + run: | + export PYO3_PYTHON="$(command -v python)" + cargo clippy --workspace --all-targets --locked -- -D warnings + - name: Python and C formatting + run: | + python -m pip install ruff==0.15.17 clang-format==22.1.5 + ruff check crates/binding-python tools/ci.py + ruff format --check crates/binding-python tools/ci.py + git ls-files -z 'crates/binding-c/*.c' 'crates/binding-c/*.h' | xargs -0 -r clang-format --dry-run --Werror diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000..83b986b5 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,63 @@ +name: Tests +on: + workflow_call: + inputs: + runner: + type: string + required: true +permissions: + contents: read +env: + RUSTUP_TOOLCHAIN: '1.97.1' + CARGO_PROFILE_DEV_DEBUG: '0' + CARGO_PROFILE_TEST_DEBUG: '0' + CARGO_INCREMENTAL: '0' + CARGO_BUILD_JOBS: '2' +jobs: + tests: + name: Rust, Python and C (${{ inputs.runner }}) + runs-on: ${{ inputs.runner }} + timeout-minutes: 90 + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@v5 + with: + persist-credentials: false + - uses: actions/setup-python@v6 + with: + python-version: '3.10' + - name: Install Rust + run: rustup toolchain install "$RUSTUP_TOOLCHAIN" --profile minimal --no-self-update + - uses: Swatinem/rust-cache@v2 + with: + key: tests-${{ runner.os }}-${{ runner.arch }} + - name: Run development tests (no distribution packages or uploads) + run: | + python -m venv .ci-venv + if [ "$RUNNER_OS" = Windows ]; then + source .ci-venv/Scripts/activate + else + source .ci-venv/bin/activate + fi + python -m pip install maturin==1.14.1 pytest==8.4.2 resvg-py==0.5.0 packaging==25.0 tomli==2.2.1 'numpy>=2.1.0' cmake==3.31.6 + export PYO3_PYTHON="$(python -c 'import sys; print(sys.executable)')" + python tools/ci.py versions + cargo test --workspace --locked + python tools/ci.py ctest + maturin develop --locked --manifest-path crates/binding-python/Cargo.toml + cd crates/binding-python + python -m pytest tests + - name: Upload visualization diagnostics on failure + if: failure() + uses: actions/upload-artifact@v7 + with: + name: visualization-diagnostics-${{ inputs.runner }} + path: | + crates/cqlib-core/src/visualization/**/figure/**/*.png + crates/cqlib-core/src/visualization/**/figure/*.svg + crates/binding-python/tests/visualization/figure/**/*.png + crates/binding-python/tests/visualization/figure/*.svg + if-no-files-found: ignore + retention-days: 7 diff --git a/.github/workflows/wheels-build.yml b/.github/workflows/wheels-build.yml new file mode 100644 index 00000000..0dc0d57f --- /dev/null +++ b/.github/workflows/wheels-build.yml @@ -0,0 +1,147 @@ +name: Build release artifacts +on: + workflow_call: +permissions: + contents: read +env: + RUSTUP_TOOLCHAIN: '1.97.1' + CARGO_PROFILE_DEV_DEBUG: '0' + CARGO_PROFILE_TEST_DEBUG: '0' + CARGO_INCREMENTAL: '0' + CARGO_BUILD_JOBS: '2' +jobs: + platforms: + name: Wheel and C SDK (${{ matrix.platform }}) + runs-on: ${{ matrix.runner }} + timeout-minutes: 180 + strategy: + fail-fast: false + matrix: + include: + - platform: windows-x86_64-msvc + runner: windows-2022 + - platform: linux-glibc2.28-x86_64 + runner: ubuntu-24.04 + image: quay.io/pypa/manylinux_2_28_x86_64:latest + - platform: linux-glibc2.28-aarch64 + runner: ubuntu-24.04-arm + image: quay.io/pypa/manylinux_2_28_aarch64:latest + - platform: macos11-x86_64 + runner: macos-15-intel + - platform: macos11-arm64 + runner: macos-15 + env: + MACOSX_DEPLOYMENT_TARGET: '11.0' + CMAKE_OSX_DEPLOYMENT_TARGET: '11.0' + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@v5 + with: + persist-credentials: false + - uses: actions/setup-python@v6 + if: runner.os != 'Linux' + with: + python-version: '3.10' + - name: Set up native build tools + if: runner.os != 'Linux' + run: | + rustup toolchain install "$RUSTUP_TOOLCHAIN" --profile minimal --no-self-update + python -m pip install maturin==1.14.1 packaging==25.0 tomli==2.2.1 cmake==3.31.6 + - name: Build C SDK and abi3 wheel, run C and installed-wheel tests + if: runner.os != 'Linux' + env: + PLATFORM: ${{ matrix.platform }} + run: python tools/ci.py release --platform "$PLATFORM" + - name: Build and test inside native manylinux 2.28 + if: runner.os == 'Linux' + env: + IMAGE: ${{ matrix.image }} + PLATFORM: ${{ matrix.platform }} + run: | + docker pull "$IMAGE" + docker image inspect "$IMAGE" --format '{{json .RepoDigests}}' + docker run --rm -v "$GITHUB_WORKSPACE:/io" -w /io \ + -e PLATFORM -e RUSTUP_TOOLCHAIN -e CARGO_PROFILE_DEV_DEBUG \ + -e CARGO_PROFILE_TEST_DEBUG -e CARGO_INCREMENTAL -e CARGO_BUILD_JOBS \ + "$IMAGE" bash -euo pipefail -c ' + export PATH="/opt/python/cp310-cp310/bin:$HOME/.cargo/bin:$PATH" + export CARGO_TARGET_DIR=/tmp/cqlib-manylinux-target + curl --proto "=https" --tlsv1.2 -sSf https://sh.rustup.rs -o /tmp/rustup.sh + sh /tmp/rustup.sh -y --profile minimal --default-toolchain "$RUSTUP_TOOLCHAIN" + python -m pip install maturin==1.14.1 packaging==25.0 tomli==2.2.1 cmake==3.31.6 + cargo test --locked -p cqlib-core -p binding-c -p cqlib + python tools/ci.py release --platform "$PLATFORM" + for version in 311 312 313 314; do + /opt/python/cp$version-cp$version/bin/python -m pip install packaging==25.0 + /opt/python/cp$version-cp$version/bin/python tools/ci.py wheel-test + done + ' + - uses: actions/setup-python@v6 + if: runner.os != 'Linux' + with: + python-version: '3.11' + - name: Test the same wheel on Python 3.11 + if: runner.os != 'Linux' + run: | + python -m pip install packaging==25.0 + python tools/ci.py wheel-test + - uses: actions/setup-python@v6 + if: runner.os != 'Linux' + with: + python-version: '3.12' + - name: Test the same wheel on Python 3.12 + if: runner.os != 'Linux' + run: | + python -m pip install packaging==25.0 + python tools/ci.py wheel-test + - uses: actions/setup-python@v6 + if: runner.os != 'Linux' + with: + python-version: '3.13' + - name: Test the same wheel on Python 3.13 + if: runner.os != 'Linux' + run: | + python -m pip install packaging==25.0 + python tools/ci.py wheel-test + - uses: actions/setup-python@v6 + if: runner.os != 'Linux' + with: + python-version: '3.14' + - name: Test the same wheel on Python 3.14 + if: runner.os != 'Linux' + run: | + python -m pip install packaging==25.0 + python tools/ci.py wheel-test + - uses: actions/upload-artifact@v7 + with: + name: release-${{ github.ref_name }}-${{ matrix.platform }} + path: target/ci-dist/* + if-no-files-found: error + retention-days: 30 + compression-level: 0 + sources: + name: Python sdist and Rust crate + runs-on: ubuntu-24.04 + timeout-minutes: 90 + steps: + - uses: actions/checkout@v5 + with: + persist-credentials: false + - uses: actions/setup-python@v6 + with: + python-version: '3.10' + - name: Install build tools + run: | + rustup toolchain install "$RUSTUP_TOOLCHAIN" --profile minimal --no-self-update + python -m pip install maturin==1.14.1 packaging==25.0 tomli==2.2.1 + - name: Package sdist and independently compile the Rust crate + run: python tools/ci.py sources + - uses: actions/upload-artifact@v7 + with: + name: release-${{ github.ref_name }}-sources + path: target/ci-dist/* + if-no-files-found: error + retention-days: 30 + compression-level: 0 diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml new file mode 100644 index 00000000..9287db4d --- /dev/null +++ b/.github/workflows/wheels.yml @@ -0,0 +1,40 @@ +name: Release artifacts (manual publishing only) +on: + push: + tags: ['v*'] +permissions: + contents: read +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false +jobs: + versions: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - uses: actions/checkout@v5 + with: + persist-credentials: false + - uses: actions/setup-python@v6 + with: + python-version: '3.10' + - run: python -m pip install packaging==25.0 tomli==2.2.1 + - name: Check tag and component versions + env: + RELEASE_TAG: ${{ github.ref_name }} + run: python tools/ci.py versions --tag "$RELEASE_TAG" + lint: + needs: versions + uses: ./.github/workflows/lint.yml + tests: + needs: versions + strategy: + fail-fast: false + matrix: + runner: [windows-2022, ubuntu-24.04, ubuntu-24.04-arm, macos-15-intel, macos-15] + uses: ./.github/workflows/tests.yml + with: + runner: ${{ matrix.runner }} + build: + needs: [lint, tests] + uses: ./.github/workflows/wheels-build.yml diff --git a/.typos.toml b/.typos.toml index ca2c9e93..fd32352e 100644 --- a/.typos.toml +++ b/.typos.toml @@ -14,6 +14,7 @@ MCH = "MCH" paeth = "paeth" pngs = "pngs" PNGs = "PNGs" +Strang = "Strang" [default.extend-identifiers] PNGs = "PNGs" diff --git a/Cargo.lock b/Cargo.lock index 3843aa30..ee324102 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -170,12 +170,14 @@ dependencies = [ "cbindgen", "cqlib-core", "libc", + "ndarray 0.17.2", "num-complex", + "rustworkx-core", ] [[package]] name = "binding-python" -version = "1.4.0-beta1" +version = "2.0.0-beta1" dependencies = [ "chrono", "cqlib-core", @@ -491,6 +493,7 @@ dependencies = [ "rustworkx-core", "serde", "serde_json", + "sha2", "smallvec", "symb_anafis", "thiserror 2.0.18", diff --git a/README.CN.md b/README.CN.md index 6f316bc1..3b5d4eec 100644 --- a/README.CN.md +++ b/README.CN.md @@ -115,19 +115,18 @@ let evaluated = qc.assign_parameters( & Some(bindings)).unwrap(); ### C ```c -#include +#include int main(void) { // 创建 2 比特 Bell 态线路 - cqlib_circuit_t *qc = cqlib_circuit_new(2); - cqlib_circuit_h(qc, 0); - cqlib_circuit_cx(qc, 0, 1); + CCircuit *qc = circuit_new(2); + circuit_h(qc, 0); + circuit_cx(qc, 0, 1); // 查看线路信息 - uint32_t n = cqlib_circuit_num_qubits(qc); // 2 - size_t ops = cqlib_circuit_operation_count(qc); // 2 + size_t ops = (size_t)circuit_num_operations(qc); // 2 - cqlib_circuit_free(qc); + circuit_free(qc); return 0; } ``` @@ -159,6 +158,8 @@ maturin develop --release -m crates/binding-python/Cargo.toml cargo build --release -p binding-c ``` +> 提示:MinGW gcc 链接 C 接口时,需改用 `cargo build --release -p binding-c --target x86_64-pc-windows-gnu` 构建,详见 [crates/binding-c/README.md](crates/binding-c/README.md)。 + 运行测试: ```bash @@ -172,6 +173,7 @@ pytest crates/binding-python/tests/ |-----------------|----------------------------------------------------------| | API 参考 (Rust) | [docs.rs/cqlib-core](https://docs.rs/cqlib-core) | | API 参考 (Python) | [qc.zdxlz.com/docs](https://qc.zdxlz.com/) | +| API 参考 (C) | [docs/api/cn/c/0_overview.md](docs/api/cn/c/0_overview.md) | | 源码仓库 (Gitee) | [gitee.com/cq-lib/cqlib](https://gitee.com/cq-lib/cqlib) | | 源码仓库 (GitHub) | [github.com/cq-lib/cqlib](https://github.com/cq-lib/cqlib) | diff --git a/README.md b/README.md index 4d9a8c6b..61968e94 100644 --- a/README.md +++ b/README.md @@ -119,19 +119,18 @@ let evaluated = qc.assign_parameters( & Some(bindings)).unwrap(); ### C ```c -#include +#include int main(void) { // Create a 2-qubit Bell state circuit - cqlib_circuit_t *qc = cqlib_circuit_new(2); - cqlib_circuit_h(qc, 0); - cqlib_circuit_cx(qc, 0, 1); + CCircuit *qc = circuit_new(2); + circuit_h(qc, 0); + circuit_cx(qc, 0, 1); // Inspect - uint32_t n = cqlib_circuit_num_qubits(qc); // 2 - size_t ops = cqlib_circuit_operation_count(qc); // 2 + size_t ops = (size_t)circuit_num_operations(qc); // 2 - cqlib_circuit_free(qc); + circuit_free(qc); return 0; } ``` @@ -163,6 +162,10 @@ maturin develop --release -m crates/binding-python/Cargo.toml cargo build --release -p binding-c ``` +> Tip: when linking with MinGW gcc, build with +> `cargo build --release -p binding-c --target x86_64-pc-windows-gnu` instead. +> See [crates/binding-c/README.md](crates/binding-c/README.md). + Run the test suite: ```bash @@ -176,6 +179,7 @@ pytest crates/binding-python/tests/ |------------------------|----------------------------------------------------------| | API Reference (Rust) | [docs.rs/cqlib-core](https://docs.rs/cqlib-core) | | API Reference (Python) | [qc.zdxlz.com/docs](https://qc.zdxlz.com/) | +| API Reference (C) | [docs/api/en/c/0_overview.md](docs/api/en/c/0_overview.md) | | Source Repository (Gitee) | [gitee.com/cq-lib/cqlib](https://gitee.com/cq-lib/cqlib) | | Source Repository (GitHub) | [github.com/cq-lib/cqlib](https://github.com/cq-lib/cqlib) | diff --git a/crates/binding-c/Cargo.toml b/crates/binding-c/Cargo.toml index 167b0715..7c61cde5 100644 --- a/crates/binding-c/Cargo.toml +++ b/crates/binding-c/Cargo.toml @@ -11,7 +11,9 @@ crate-type = ["rlib", "staticlib", "cdylib"] [dependencies] cqlib-core = { path = "../cqlib-core", version = "0.1.0-beta.1" } libc = "0.2" +ndarray.workspace = true num-complex.workspace = true +rustworkx-core.workspace = true [build-dependencies] cbindgen.workspace = true diff --git a/crates/binding-c/README.md b/crates/binding-c/README.md index aba6e031..d5904499 100644 --- a/crates/binding-c/README.md +++ b/crates/binding-c/README.md @@ -1,8 +1,10 @@ # C Binding for Cqlib -This crate exposes a small C ABI for building `cqlib-core` quantum circuits. -The current surface is intentionally limited to circuit construction and -symbolic gate parameters. +This crate exposes a C ABI for `cqlib-core`. The surface now covers circuit +construction, symbolic parameters, IR conversion (QCIS / QASM2 / QASM3), +device / topology / layout / noise models, quantum simulators (statevector, +density matrix, stabilizer), compilation, visualization, and error mitigation +(ZNE, virtual distillation) — roughly 190 free functions. ## Build @@ -12,65 +14,111 @@ cargo build -p binding-c --release The C header is generated at `crates/binding-c/include/cqlib_c.h`. -## Example +### Linking with MinGW gcc + +Build the gnu variant first, then compile. The source file must come +**before** the `-l` libraries — GNU ld resolves symbols left to right: ```bash -gcc crates/binding-c/examples/main.c \ - -I crates/binding-c/include \ - -L target/release \ - -lbinding_c \ - -lm \ - -o target/release/cqlib_c_example - -./target/release/cqlib_c_example +cargo build -p binding-c --release --target x86_64-pc-windows-gnu + +gcc -I crates/binding-c/include \ + crates/binding-c/examples/main.c \ + -L target/x86_64-pc-windows-gnu/release \ + -lbinding_c -lntdll -lws2_32 -lbcrypt -luserenv -ladvapi32 \ + -o demo ``` -## API +At runtime the executable must find `binding_c.dll`: either add +`target/x86_64-pc-windows-gnu/release` (as an **absolute** path) to `PATH` +or copy the DLL next to the executable. Relative `PATH` entries are +resolved against the process working directory and can silently fail. -### Circuit +### Linking on macOS / Linux -| Function | Description | -| --- | --- | -| `circuit_new(size_t num_qubits)` | Create a circuit. | -| `circuit_free(CircuitWrapper*)` | Free a circuit. | -| `circuit_num_qubits(const CircuitWrapper*)` | Return qubit count. | -| `circuit_num_operations(const CircuitWrapper*)` | Return operation count. | -| `circuit_num_parameters(const CircuitWrapper*)` | Return interned symbolic parameter count. | -| `circuit_validate(const CircuitWrapper*)` | Validate circuit consistency. | +No toolchain-matching is needed — the platform has a single ABI. Just +build with the default host target and link directly against the cdylib: + +```bash +cargo build -p binding-c --release -### Gates +gcc -I crates/binding-c/include \ + crates/binding-c/examples/main.c \ + -L target/release -lbinding_c \ + -o demo +``` -| Function | Description | -| --- | --- | -| `circuit_h/x/y/z(CircuitWrapper*, uint32_t qubit)` | Single-qubit fixed gates. | -| `circuit_rx/ry/rz(CircuitWrapper*, uint32_t qubit, double theta)` | Numeric rotations. | -| `circuit_cx/cz(CircuitWrapper*, uint32_t control, uint32_t target)` | Two-qubit gates. | -| `circuit_measure(CircuitWrapper*, uint32_t qubit)` | Measure a qubit. | -| `circuit_reset(CircuitWrapper*, uint32_t qubit)` | Reset a qubit. | +At runtime the dynamic linker must find `libbinding_c.dylib` (macOS) or +`libbinding_c.so` (Linux). Either set `DYLD_LIBRARY_PATH` / `LD_LIBRARY_PATH` +to `target/release`, or copy the shared library next to the executable. -### Parameters +## Usage -Bindings use `name:value,name2:value2` format. +```c +#include +#include -| Function | Description | -| --- | --- | -| `param_parse(const char*)` | Parse a symbolic parameter expression. | -| `param_free(ParameterWrapper*)` | Free a parameter. | -| `param_evaluate(const ParameterWrapper*, const char* bindings)` | Evaluate a parameter. | -| `circuit_rx_param/ry_param/rz_param(CircuitWrapper*, uint32_t qubit, const ParameterWrapper*)` | Symbolic rotations. | -| `circuit_assign_params(const CircuitWrapper*, const char* bindings)` | Return a new circuit with assigned parameters. | +int main(void) { + /* Bell state circuit */ + CCircuit *qc = circuit_new(2); + circuit_h(qc, 0); + circuit_cx(qc, 0, 1); + + /* Exact simulation */ + CStatevector *sv = statevector_from_circuit(qc); + double probs[4]; + statevector_probabilities(sv, probs, 4); /* 0.5, 0, 0, 0.5 */ + + statevector_free(sv); + circuit_free(qc); + return 0; +} +``` + +## Documentation + +The full API manual lives in [`docs/api/en/c/`](../../docs/api/en/c/0_overview.md) +and mirrors the Python manual's structure: per-function signatures, +parameters, return values, error codes, and examples for every module +(circuit, IR, device, QIS, compile, visualization, error mitigation). ## Return Values -- `int32_t`: `0` on success, negative on error. -- `-1`: null pointer or invalid C string. -- `-2`: qubit index out of bounds. -- `-3`: core circuit or parameter error. -- Pointer return values use `NULL` for errors. -- `param_evaluate` returns `0.0` on error. +Integer-returning functions use `0` for success and negative error codes on +failure: + +| Code | Meaning | +| --- | --- | +| `0` | Success | +| `-1` | Null pointer or invalid C string | +| `-2` | Qubit index out of bounds | +| `-3` | Circuit / structural error | +| `-4` | Parse error (unknown gate name, malformed IR, ...) | +| `-5` | IO error | +| `-6` | Compiler error | +| `-7` | Simulation error (non-Clifford gate, not run, ...) | +| `-8` | Invalid parameter (bad probability, buffer too small, ...) | + +Pointer-returning functions use `NULL` for errors. `param_evaluate` returns +`NaN` when the bindings string is invalid or a symbol is unbound. ## Tests ```bash cargo test -p binding-c ``` + +Rust FFI tests live in `crates/binding-c/tests/*.rs`. C-side smoke tests live +in `crates/binding-c/tests/test_*.c`, one per module (circuit, ir, device, +qis, compile, visualization, error_mitigation). Compile and run each +individually (build the gnu variant first, see Toolchain matching above): + +```bash +gcc -I crates/binding-c/include \ + crates/binding-c/tests/test_circuit.c \ + -L target/x86_64-pc-windows-gnu/release \ + -lbinding_c -lntdll -lws2_32 -lbcrypt -luserenv -ladvapi32 -lm \ + -o test_circuit + +./test_circuit # prints "binding-c circuit tests passed" +``` \ No newline at end of file diff --git a/crates/binding-c/build.rs b/crates/binding-c/build.rs index c16b6d32..187d71b0 100644 --- a/crates/binding-c/build.rs +++ b/crates/binding-c/build.rs @@ -18,6 +18,13 @@ fn main() { cbindgen::Builder::new() .with_crate(crate_dir) .with_language(cbindgen::Language::C) + // num_complex::Complex64 is referenced by QIS signatures but is not a + // cbindgen-known type; define it as a plain struct of two doubles. + .with_after_include( + "/* Complex64 mirrors num_complex::Complex as interleaved\n\ + * (real, imag) double pairs. */\n\ + typedef struct Complex64 {\n double re;\n double im;\n} Complex64;\n", + ) .generate() .expect("Unable to generate bindings") .write_to_file("include/cqlib_c.h"); diff --git a/crates/binding-c/examples/main.c b/crates/binding-c/examples/main.c index 3ff08a4f..cca26357 100644 --- a/crates/binding-c/examples/main.c +++ b/crates/binding-c/examples/main.c @@ -15,13 +15,13 @@ #include "cqlib_c.h" int main(void) { - CircuitWrapper* circuit = circuit_new(2); + CCircuit* circuit = circuit_new(2); if (circuit == NULL) { return 1; } - ParameterWrapper* theta = param_parse("theta"); - ParameterWrapper* phi = param_parse("phi"); + CParameter* theta = param_parse("theta"); + CParameter* phi = param_parse("phi"); if (theta == NULL || phi == NULL) { circuit_free(circuit); param_free(theta); @@ -41,7 +41,7 @@ int main(void) { printf("qubits=%zu operations=%zu parameters=%zu\n", circuit_num_qubits(circuit), circuit_num_operations(circuit), circuit_num_parameters(circuit)); - CircuitWrapper* assigned = circuit_assign_params(circuit, "theta:0.5,phi:1.25"); + CCircuit* assigned = circuit_assign_params(circuit, "theta:0.5,phi:1.25"); if (assigned == NULL) { circuit_free(circuit); param_free(theta); diff --git a/crates/binding-c/src/circuit.rs b/crates/binding-c/src/circuit.rs deleted file mode 100644 index d9d116c8..00000000 --- a/crates/binding-c/src/circuit.rs +++ /dev/null @@ -1,334 +0,0 @@ -// This code is part of Cqlib. -// -// (C) Copyright China Telecom Quantum Group 2026 -// -// This code is licensed under the Apache License, Version 2.0. You may -// obtain a copy of this license in the LICENSE.txt file in the root directory -// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -// -// Any modifications or derivative works of this code must retain this -// copyright notice, and modified files need to carry a notice indicating -// that they have been altered from the originals. - -//! Minimal C ABI for circuit construction and symbolic parameters. - -#![allow(clippy::not_unsafe_ptr_arg_deref)] - -use cqlib_core::circuit::{Circuit, Parameter, ParameterValue, Qubit}; -use std::collections::HashMap; -use std::ffi::CStr; -use std::os::raw::c_char; - -pub struct CircuitWrapper { - pub inner: Circuit, -} - -pub struct ParameterWrapper { - pub inner: Parameter, -} - -fn parse_bindings(bindings: *const c_char) -> Option> { - if bindings.is_null() { - return None; - } - - let c_str = unsafe { CStr::from_ptr(bindings) }; - let text = c_str.to_str().ok()?; - let mut map = HashMap::new(); - for pair in text.split(',') { - let pair = pair.trim(); - if pair.is_empty() { - continue; - } - let (name, value) = pair.split_once(':')?; - let value = value.trim().parse::().ok()?; - if !value.is_finite() { - return None; - } - map.insert(name.trim().to_string(), value); - } - Some(map) -} - -fn binding_refs(bindings: &HashMap) -> HashMap<&str, f64> { - bindings - .iter() - .map(|(name, value)| (name.as_str(), *value)) - .collect() -} - -fn check_qubit(circuit: &Circuit, qubit: u32) -> Result { - if qubit as usize >= circuit.num_qubits() { - return Err(-2); - } - Ok(Qubit::new(qubit)) -} - -fn apply_single( - ptr: *mut CircuitWrapper, - qubit: u32, - apply: impl FnOnce(&mut Circuit, Qubit) -> Result<(), cqlib_core::circuit::CircuitError>, -) -> i32 { - if ptr.is_null() { - return -1; - } - let wrapper = unsafe { &mut *ptr }; - let qubit = match check_qubit(&wrapper.inner, qubit) { - Ok(qubit) => qubit, - Err(code) => return code, - }; - apply(&mut wrapper.inner, qubit).map_or(-3, |_| 0) -} - -fn apply_two( - ptr: *mut CircuitWrapper, - first: u32, - second: u32, - apply: impl FnOnce(&mut Circuit, Qubit, Qubit) -> Result<(), cqlib_core::circuit::CircuitError>, -) -> i32 { - if ptr.is_null() { - return -1; - } - let wrapper = unsafe { &mut *ptr }; - let first = match check_qubit(&wrapper.inner, first) { - Ok(qubit) => qubit, - Err(code) => return code, - }; - let second = match check_qubit(&wrapper.inner, second) { - Ok(qubit) => qubit, - Err(code) => return code, - }; - apply(&mut wrapper.inner, first, second).map_or(-3, |_| 0) -} - -fn apply_param( - ptr: *mut CircuitWrapper, - qubit: u32, - param_ptr: *const ParameterWrapper, - apply: impl FnOnce( - &mut Circuit, - Qubit, - ParameterValue, - ) -> Result<(), cqlib_core::circuit::CircuitError>, -) -> i32 { - if ptr.is_null() || param_ptr.is_null() { - return -1; - } - let wrapper = unsafe { &mut *ptr }; - let qubit = match check_qubit(&wrapper.inner, qubit) { - Ok(qubit) => qubit, - Err(code) => return code, - }; - let param = unsafe { &(*param_ptr).inner }; - apply( - &mut wrapper.inner, - qubit, - ParameterValue::Param(param.clone()), - ) - .map_or(-3, |_| 0) -} - -/// Create a new quantum circuit with `num_qubits` logical qubits. -#[unsafe(no_mangle)] -pub extern "C" fn circuit_new(num_qubits: usize) -> *mut CircuitWrapper { - Box::into_raw(Box::new(CircuitWrapper { - inner: Circuit::new(num_qubits), - })) -} - -/// Free a quantum circuit. Passing NULL is allowed. -#[unsafe(no_mangle)] -pub extern "C" fn circuit_free(ptr: *mut CircuitWrapper) { - if !ptr.is_null() { - unsafe { - let _ = Box::from_raw(ptr); - } - } -} - -/// Return the number of qubits in the circuit, or 0 for NULL. -#[unsafe(no_mangle)] -pub extern "C" fn circuit_num_qubits(ptr: *const CircuitWrapper) -> usize { - if ptr.is_null() { - return 0; - } - unsafe { (*ptr).inner.num_qubits() } -} - -/// Return the number of operations in the circuit, or 0 for NULL. -#[unsafe(no_mangle)] -pub extern "C" fn circuit_num_operations(ptr: *const CircuitWrapper) -> usize { - if ptr.is_null() { - return 0; - } - unsafe { (*ptr).inner.operations().len() } -} - -/// Return the number of interned symbolic parameters in the circuit, or 0 for NULL. -#[unsafe(no_mangle)] -pub extern "C" fn circuit_num_parameters(ptr: *const CircuitWrapper) -> usize { - if ptr.is_null() { - return 0; - } - unsafe { (*ptr).inner.parameters().len() } -} - -/// Validate circuit consistency. -#[unsafe(no_mangle)] -pub extern "C" fn circuit_validate(ptr: *const CircuitWrapper) -> i32 { - if ptr.is_null() { - return -1; - } - unsafe { (*ptr).inner.validate().map_or(-3, |_| 0) } -} - -#[unsafe(no_mangle)] -pub extern "C" fn circuit_h(ptr: *mut CircuitWrapper, qubit: u32) -> i32 { - apply_single(ptr, qubit, Circuit::h) -} - -#[unsafe(no_mangle)] -pub extern "C" fn circuit_x(ptr: *mut CircuitWrapper, qubit: u32) -> i32 { - apply_single(ptr, qubit, Circuit::x) -} - -#[unsafe(no_mangle)] -pub extern "C" fn circuit_y(ptr: *mut CircuitWrapper, qubit: u32) -> i32 { - apply_single(ptr, qubit, Circuit::y) -} - -#[unsafe(no_mangle)] -pub extern "C" fn circuit_z(ptr: *mut CircuitWrapper, qubit: u32) -> i32 { - apply_single(ptr, qubit, Circuit::z) -} - -#[unsafe(no_mangle)] -pub extern "C" fn circuit_rx(ptr: *mut CircuitWrapper, qubit: u32, theta: f64) -> i32 { - if !theta.is_finite() { - return -3; - } - apply_single(ptr, qubit, |circuit, qubit| circuit.rx(qubit, theta)) -} - -#[unsafe(no_mangle)] -pub extern "C" fn circuit_ry(ptr: *mut CircuitWrapper, qubit: u32, theta: f64) -> i32 { - if !theta.is_finite() { - return -3; - } - apply_single(ptr, qubit, |circuit, qubit| circuit.ry(qubit, theta)) -} - -#[unsafe(no_mangle)] -pub extern "C" fn circuit_rz(ptr: *mut CircuitWrapper, qubit: u32, theta: f64) -> i32 { - if !theta.is_finite() { - return -3; - } - apply_single(ptr, qubit, |circuit, qubit| circuit.rz(qubit, theta)) -} - -#[unsafe(no_mangle)] -pub extern "C" fn circuit_cx(ptr: *mut CircuitWrapper, control: u32, target: u32) -> i32 { - apply_two(ptr, control, target, Circuit::cx) -} - -#[unsafe(no_mangle)] -pub extern "C" fn circuit_cz(ptr: *mut CircuitWrapper, control: u32, target: u32) -> i32 { - apply_two(ptr, control, target, Circuit::cz) -} - -#[unsafe(no_mangle)] -pub extern "C" fn circuit_measure(ptr: *mut CircuitWrapper, qubit: u32) -> i32 { - apply_single(ptr, qubit, |circuit, qubit| { - circuit.measure(qubit).map(|_| ()) - }) -} - -#[unsafe(no_mangle)] -pub extern "C" fn circuit_reset(ptr: *mut CircuitWrapper, qubit: u32) -> i32 { - apply_single(ptr, qubit, Circuit::reset) -} - -/// Parse a symbolic parameter expression. -#[unsafe(no_mangle)] -pub extern "C" fn param_parse(expr: *const c_char) -> *mut ParameterWrapper { - if expr.is_null() { - return std::ptr::null_mut(); - } - - let c_str = unsafe { CStr::from_ptr(expr) }; - let expr = match c_str.to_str() { - Ok(expr) => expr, - Err(_) => return std::ptr::null_mut(), - }; - - match Parameter::try_from(expr) { - Ok(param) => Box::into_raw(Box::new(ParameterWrapper { inner: param })), - Err(_) => std::ptr::null_mut(), - } -} - -/// Free a parameter. Passing NULL is allowed. -#[unsafe(no_mangle)] -pub extern "C" fn param_free(ptr: *mut ParameterWrapper) { - if !ptr.is_null() { - unsafe { - let _ = Box::from_raw(ptr); - } - } -} - -/// Evaluate a parameter expression with bindings formatted as "name:value,name2:value2". -#[unsafe(no_mangle)] -pub extern "C" fn param_evaluate(ptr: *const ParameterWrapper, bindings: *const c_char) -> f64 { - if ptr.is_null() { - return 0.0; - } - let bindings = parse_bindings(bindings); - let refs = bindings.as_ref().map(binding_refs); - unsafe { (*ptr).inner.evaluate(&refs).unwrap_or(0.0) } -} - -#[unsafe(no_mangle)] -pub extern "C" fn circuit_rx_param( - ptr: *mut CircuitWrapper, - qubit: u32, - param_ptr: *const ParameterWrapper, -) -> i32 { - apply_param(ptr, qubit, param_ptr, Circuit::rx) -} - -#[unsafe(no_mangle)] -pub extern "C" fn circuit_ry_param( - ptr: *mut CircuitWrapper, - qubit: u32, - param_ptr: *const ParameterWrapper, -) -> i32 { - apply_param(ptr, qubit, param_ptr, Circuit::ry) -} - -#[unsafe(no_mangle)] -pub extern "C" fn circuit_rz_param( - ptr: *mut CircuitWrapper, - qubit: u32, - param_ptr: *const ParameterWrapper, -) -> i32 { - apply_param(ptr, qubit, param_ptr, Circuit::rz) -} - -/// Return a new circuit with matching symbolic parameters assigned. -#[unsafe(no_mangle)] -pub extern "C" fn circuit_assign_params( - circuit: *const CircuitWrapper, - bindings: *const c_char, -) -> *mut CircuitWrapper { - if circuit.is_null() { - return std::ptr::null_mut(); - } - - let bindings = parse_bindings(bindings); - let refs = bindings.as_ref().map(binding_refs); - match unsafe { (*circuit).inner.assign_parameters(&refs) } { - Ok(inner) => Box::into_raw(Box::new(CircuitWrapper { inner })), - Err(_) => std::ptr::null_mut(), - } -} diff --git a/crates/binding-c/src/circuit/advanced.rs b/crates/binding-c/src/circuit/advanced.rs new file mode 100644 index 00000000..24dba0a3 --- /dev/null +++ b/crates/binding-c/src/circuit/advanced.rs @@ -0,0 +1,1067 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! C ABI for circuit parameter metadata and advanced circuit instructions +//! (binding gap checklist sections 1.4 and 1.5). +//! +//! Section 1.4 exposes the circuit's parameter registry: interning +//! ([`circuit_add_parameter`]), two-step list access, the storage form +//! ([`CCircuitParam`]) produced by [`circuit_map_param`] and consumed by +//! [`circuit_resolve_parameter`]/[`circuit_parameter_value`], the symbol +//! tables and the global phase ([`CParameterValue`]). +//! +//! Section 1.5 covers advanced construction: identity-gate alias +//! ([`circuit_id`]), explicit qubit sets ([`circuit_from_qubits`]), custom +//! unitary matrices ([`circuit_unitary`], [`circuit_unitary_with_params`]), +//! composite circuit-backed gates ([`CCircuitGate`] via [`circuit_to_gate`] +//! and [`circuit_circuit_gate`]), multi-controlled gates +//! ([`circuit_multi_control`]), delays ([`circuit_delay`]), resolved +//! operation snapshots ([`CValueOperation`] via [`circuit_index`]) and +//! batch removal ([`circuit_remove_operations`]). +//! +//! `circuit_set_global_phase_param` and `circuit_add_qubits` are already +//! exported from `properties.rs` and are therefore not duplicated here. + +use crate::circuit::symbolic_matrix::CSymbolicMatrix; +use crate::circuit::{CCircuit, CParameter, apply_single, check_qubit}; +use crate::device::standard_gate_from_name; +use cqlib_core::circuit::symbolic_matrix::SymbolicComplex; +use cqlib_core::circuit::{ + Circuit, CircuitGate, CircuitParam, Instruction, Parameter, ParameterValue, Qubit, UnitaryGate, + ValueOperation, +}; +use ndarray::Array2; +use num_complex::Complex64; +use std::ffi::{CStr, CString}; +use std::os::raw::c_char; + +// ===== Tags, C-side mirrors and handles ===== + +/// `CCircuitParam.tag` value: the parameter is a fixed number stored in `value`. +pub const CIRCUIT_PARAM_TAG_FIXED: u8 = 0; +/// `CCircuitParam.tag` value: the parameter is an interned expression stored +/// as `index` into the circuit's parameter table. +pub const CIRCUIT_PARAM_TAG_INDEX: u8 = 1; + +/// `CParameterValue.tag` value: the value is the fixed number in `value`. +pub const PARAMETER_VALUE_TAG_FIXED: u8 = 0; +/// `CParameterValue.tag` value: the value is symbolic; `param` holds a newly +/// allocated `CParameter*` that the caller frees with `param_free`. +pub const PARAMETER_VALUE_TAG_PARAM: u8 = 1; + +/// Upper bound on `num_qubits` accepted by the unitary helpers. Keeps the +/// flattened matrix dimension (`4^n` doubles) far away from `usize` overflow. +const MAX_UNITARY_QUBITS: usize = 20; + +/// C-side mirror of the storage-form `CircuitParam`. +/// +/// Instances are produced by `circuit_map_param` and consumed by +/// `circuit_resolve_parameter` / `circuit_parameter_value`. +#[repr(C)] +pub struct CCircuitParam { + /// One of `CIRCUIT_PARAM_TAG_*`. + pub tag: u8, + /// Parameter-table index, valid when `tag == CIRCUIT_PARAM_TAG_INDEX`. + pub index: u32, + /// Fixed value, valid when `tag == CIRCUIT_PARAM_TAG_FIXED`. + pub value: f64, +} + +/// C-side mirror of `ParameterValue` (a resolved parameter value). +/// +/// When `tag == PARAMETER_VALUE_TAG_PARAM`, `param` is a newly allocated +/// `CParameter*` that the caller owns and must free with `param_free`; +/// `param` is NULL for the fixed variant. +#[repr(C)] +pub struct CParameterValue { + /// One of `PARAMETER_VALUE_TAG_*`. + pub tag: u8, + /// Fixed value, valid when `tag == PARAMETER_VALUE_TAG_FIXED`. + pub value: f64, + /// Symbolic parameter handle, valid when `tag == PARAMETER_VALUE_TAG_PARAM`. + pub param: *mut CParameter, +} + +/// Opaque handle around a resolved value-level operation snapshot returned +/// by `circuit_index`. Read-only; free with `value_operation_free`. +pub struct CValueOperation { + pub inner: ValueOperation, +} + +/// Opaque handle around a composite circuit-backed gate (`CircuitGate`), +/// produced by `circuit_to_gate`. Free with `circuit_gate_free`. +pub struct CCircuitGate { + pub inner: CircuitGate, +} + +// ===== Internal helpers ===== + +/// Converts a `CCircuitParam` mirror into the core storage form. +fn circuit_param_from_c(raw: &CCircuitParam) -> Result { + match raw.tag { + CIRCUIT_PARAM_TAG_FIXED => Ok(CircuitParam::Fixed(raw.value)), + CIRCUIT_PARAM_TAG_INDEX => Ok(CircuitParam::Index(raw.index)), + _ => Err(-8), + } +} + +/// Converts a core storage form into the `CCircuitParam` mirror. +fn circuit_param_to_c(param: CircuitParam) -> CCircuitParam { + match param { + CircuitParam::Fixed(value) => CCircuitParam { + tag: CIRCUIT_PARAM_TAG_FIXED, + index: 0, + value, + }, + CircuitParam::Index(index) => CCircuitParam { + tag: CIRCUIT_PARAM_TAG_INDEX, + index, + value: 0.0, + }, + } +} + +/// Converts a `ParameterValue` into the `CParameterValue` mirror, allocating +/// a fresh `CParameter` handle for the symbolic variant. +fn parameter_value_to_c(value: ParameterValue) -> CParameterValue { + match value { + ParameterValue::Fixed(value) => CParameterValue { + tag: PARAMETER_VALUE_TAG_FIXED, + value, + param: std::ptr::null_mut(), + }, + ParameterValue::Param(param) => CParameterValue { + tag: PARAMETER_VALUE_TAG_PARAM, + value: 0.0, + param: Box::into_raw(Box::new(CParameter { inner: param })), + }, + } +} + +/// Parses a NUL-terminated parameter expression string. +fn parse_parameter_expr(expr: *const c_char) -> Result { + if expr.is_null() { + return Err(-1); + } + let text = unsafe { CStr::from_ptr(expr) }.to_str().map_err(|_| -4)?; + Parameter::try_from(text).map_err(|_| -4) +} + +/// Reads a qubit-id array, validating every id against the circuit width. +fn checked_qubits(circuit: &Circuit, qubits: *const u32, len: usize) -> Result, i32> { + if len == 0 { + return Ok(Vec::new()); + } + if qubits.is_null() { + return Err(-1); + } + let slice = unsafe { std::slice::from_raw_parts(qubits, len) }; + let mut out = Vec::with_capacity(len); + for &id in slice { + out.push(check_qubit(circuit, id)?); + } + Ok(out) +} + +/// Reads an array of parameter handles into owned parameter values. +fn param_ptrs_to_values( + params: *const *const CParameter, + len: usize, +) -> Result, i32> { + if len == 0 { + return Ok(Vec::new()); + } + if params.is_null() { + return Err(-1); + } + let slice = unsafe { std::slice::from_raw_parts(params, len) }; + let mut out = Vec::with_capacity(len); + for ¶m in slice { + if param.is_null() { + return Err(-1); + } + let param = unsafe { (*param).inner.clone() }; + out.push(ParameterValue::Param(param)); + } + Ok(out) +} + +/// Copies a symbol-name list into `out` (two-step pattern). Each written +/// entry is a freshly allocated C string the caller frees with +/// `cqlib_string_free`. Returns the total number of symbols. +fn write_symbol_list(symbols: &[String], out: *mut *mut c_char, len: usize) -> usize { + let total = symbols.len(); + if !out.is_null() { + for (i, symbol) in symbols.iter().take(total.min(len)).enumerate() { + let written = CString::new(symbol.as_str()) + .map(|s| s.into_raw()) + .unwrap_or(std::ptr::null_mut()); + unsafe { *out.add(i) = written }; + } + } + total +} + +// ===== Section 1.4: parameter metadata ===== + +/// Intern a symbolic parameter into the circuit's parameter registry. +/// +/// Interning alone does not mark the parameter as used; it only registers +/// the expression and its symbols. Returns 0 on success, -1 on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_add_parameter(ptr: *mut CCircuit, param: *const CParameter) -> i32 { + if ptr.is_null() || param.is_null() { + return -1; + } + let wrapper = unsafe { &mut *ptr }; + let param = unsafe { (*param).inner.clone() }; + wrapper.inner.add_parameter(param); + 0 +} + +/// Return the number of interned parameters, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_parameters_len(ptr: *const CCircuit) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.parameters().len() } +} + +/// Copy cloned parameter handles into `out` (two-step pattern; call +/// `circuit_parameters_len` first). Each written element is a newly +/// allocated `CParameter*` the caller frees with `param_free`. Returns the +/// total number of interned parameters. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_parameters( + ptr: *const CCircuit, + out: *mut *mut CParameter, + len: usize, +) -> usize { + if ptr.is_null() { + return 0; + } + let parameters = unsafe { (*ptr).inner.parameters() }; + let total = parameters.len(); + if !out.is_null() { + for (i, param) in parameters.iter().take(total.min(len)).enumerate() { + let handle = Box::into_raw(Box::new(CParameter { + inner: param.clone(), + })); + unsafe { *out.add(i) = handle }; + } + } + total +} + +/// Canonicalize a parameter expression and intern it, returning its +/// storage form (fixed value or parameter-table index). Returns 0 on +/// success, -1 on NULL input, -3 on evaluation failure. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_map_param( + ptr: *mut CCircuit, + param: *const CParameter, + out: *mut CCircuitParam, +) -> i32 { + if ptr.is_null() || param.is_null() || out.is_null() { + return -1; + } + let wrapper = unsafe { &mut *ptr }; + let param = unsafe { (*param).inner.clone() }; + match wrapper.inner.map_param(param) { + Ok(mapped) => { + unsafe { *out = circuit_param_to_c(mapped) }; + 0 + } + Err(_) => -3, + } +} + +/// Resolve a storage-form parameter back into its symbolic expression. +/// Returns a newly allocated `CParameter*` (free with `param_free`), or +/// NULL on NULL input, invalid tag or resolution failure. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_resolve_parameter( + ptr: *const CCircuit, + param: *const CCircuitParam, +) -> *mut CParameter { + if ptr.is_null() || param.is_null() { + return std::ptr::null_mut(); + } + let storage = match circuit_param_from_c(unsafe { &*param }) { + Ok(storage) => storage, + Err(_) => return std::ptr::null_mut(), + }; + match unsafe { (*ptr).inner.resolve_parameter(&storage) } { + Ok(parameter) => Box::into_raw(Box::new(CParameter { inner: parameter })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Inspect a storage-form parameter without interning anything new. +/// +/// Writes a `CParameterValue` to `*out`: a fixed number, or a newly +/// allocated `CParameter*` for the symbolic case (free with `param_free`). +/// Returns 0 on success, -1 on NULL input, -3 for an invalid index, -8 for +/// an invalid tag. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_parameter_value( + ptr: *const CCircuit, + param: *const CCircuitParam, + out: *mut CParameterValue, +) -> i32 { + if ptr.is_null() || param.is_null() || out.is_null() { + return -1; + } + let storage = match circuit_param_from_c(unsafe { &*param }) { + Ok(storage) => storage, + Err(code) => return code, + }; + match unsafe { (*ptr).inner.parameter_value(&storage) } { + Ok(value) => { + unsafe { *out = parameter_value_to_c(value) }; + 0 + } + Err(_) => -3, + } +} + +/// Return the number of registered symbol names, or 0 for NULL. +/// +/// This is the full registry and may include symbols that are interned but +/// no longer referenced by the circuit's operations. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_symbols_len(ptr: *const CCircuit) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.symbols().len() } +} + +/// Copy registered symbol names into `out` (two-step pattern; call +/// `circuit_symbols_len` first). Each written element is a freshly +/// allocated C string the caller frees with `cqlib_string_free`. Returns +/// the total number of registered symbols. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_symbols( + ptr: *const CCircuit, + out: *mut *mut c_char, + len: usize, +) -> usize { + if ptr.is_null() { + return 0; + } + let symbols: Vec = unsafe { (*ptr).inner.symbols().iter().cloned().collect() }; + write_symbol_list(&symbols, out, len) +} + +/// Return the number of symbols referenced by the circuit's executable IR, +/// or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_used_symbols_len(ptr: *const CCircuit) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.used_symbols().len() } +} + +/// Copy the IR-referenced symbol names into `out` (two-step pattern; call +/// `circuit_used_symbols_len` first). Each written element is a freshly +/// allocated C string the caller frees with `cqlib_string_free`. Returns +/// the total number of used symbols. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_used_symbols( + ptr: *const CCircuit, + out: *mut *mut c_char, + len: usize, +) -> usize { + if ptr.is_null() { + return 0; + } + let symbols: Vec = unsafe { (*ptr).inner.used_symbols().iter().cloned().collect() }; + write_symbol_list(&symbols, out, len) +} + +/// Check whether a symbol is referenced by the circuit's executable IR +/// (global phase and operation parameters, including nested bodies). +/// Returns 1 when used, 0 when not, -1 on NULL input, -4 on invalid UTF-8. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_uses_symbol(ptr: *const CCircuit, name: *const c_char) -> i32 { + if ptr.is_null() || name.is_null() { + return -1; + } + let name = match unsafe { CStr::from_ptr(name) }.to_str() { + Ok(name) => name, + Err(_) => return -4, + }; + unsafe { (*ptr).inner.uses_symbol(name) as i32 } +} + +/// Inspect the circuit's global phase. +/// +/// Writes a `CParameterValue` to `*out`: a fixed number, or a newly +/// allocated `CParameter*` for the symbolic case (free with `param_free`). +/// Returns 0 on success, -1 on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_global_phase(ptr: *const CCircuit, out: *mut CParameterValue) -> i32 { + if ptr.is_null() || out.is_null() { + return -1; + } + let parameter = unsafe { (*ptr).inner.global_phase() }; + unsafe { *out = parameter_value_to_c(ParameterValue::from(parameter)) }; + 0 +} + +// ===== Section 1.5: advanced instructions ===== + +/// Append an identity gate on `qubit`; alias of `circuit_i` kept for +/// frontend naming compatibility. Returns 0 on success, negative on error. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_id(ptr: *mut CCircuit, qubit: u32) -> i32 { + apply_single(ptr, qubit, Circuit::i) +} + +/// Create a new circuit from an explicit list of qubit ids. +/// +/// Returns a newly allocated `CCircuit*` (free with `circuit_free`), or +/// NULL on NULL input or duplicate qubits. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_from_qubits(qubits: *const u32, len: usize) -> *mut CCircuit { + if len > 0 && qubits.is_null() { + return std::ptr::null_mut(); + } + let ids = if len == 0 { + &[][..] + } else { + unsafe { std::slice::from_raw_parts(qubits, len) } + }; + let qubits: Vec = ids.iter().map(|&id| Qubit::new(id)).collect(); + match Circuit::from_qubits(qubits) { + Ok(inner) => Box::into_raw(Box::new(CCircuit { inner })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Append a custom unitary gate defined by a numeric matrix. +/// +/// `matrix` is a row-major flattened array of `2^num_qubits * 2^num_qubits` +/// `Complex64` values (interleaved re/im, same layout as `circuit_to_matrix` +/// output). Returns 0 on success, -1 on NULL input, -2 on out-of-bounds +/// qubits, -3 on shape or application failure, -4 on invalid UTF-8 label, +/// -8 when `num_qubits` exceeds 20. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_unitary( + ptr: *mut CCircuit, + label: *const c_char, + num_qubits: usize, + matrix: *const Complex64, + qubits: *const u32, + qubits_len: usize, +) -> i32 { + if ptr.is_null() || label.is_null() || matrix.is_null() { + return -1; + } + if num_qubits > MAX_UNITARY_QUBITS { + return -8; + } + let label = match unsafe { CStr::from_ptr(label) }.to_str() { + Ok(label) => label, + Err(_) => return -4, + }; + let dim = 1usize << num_qubits; + let expected = dim * dim; + let flat = unsafe { std::slice::from_raw_parts(matrix, expected) }.to_vec(); + let mat = match Array2::from_shape_vec((dim, dim), flat) { + Ok(mat) => mat, + Err(_) => return -8, + }; + let gate = match UnitaryGate::new(label, num_qubits as u16, 0).with_matrix(mat) { + Ok(gate) => gate, + Err(_) => return -3, + }; + let wrapper = unsafe { &mut *ptr }; + let qubits = match checked_qubits(&wrapper.inner, qubits, qubits_len) { + Ok(qubits) => qubits, + Err(code) => return code, + }; + wrapper.inner.unitary(gate, qubits).map_or(-3, |_| 0) +} + +/// Append a parameterized custom unitary gate defined by a symbolic matrix. +/// +/// `re_exprs` and `im_exprs` are row-major flattened arrays of +/// `2^num_qubits * 2^num_qubits` parameter-expression strings (e.g. +/// `"cos(theta)"`); `im_exprs` may be NULL, in which case all imaginary +/// parts are zero. `param_names` lists the positional parameter names that +/// application arguments are bound to, in order. Returns 0 on success, -1 +/// on NULL input, -2 on out-of-bounds qubits, -3 on shape or application +/// failure, -4 on invalid UTF-8 or expression syntax, -8 on invalid sizes. +#[allow(clippy::too_many_arguments)] +#[unsafe(no_mangle)] +pub extern "C" fn circuit_unitary_with_params( + ptr: *mut CCircuit, + label: *const c_char, + num_qubits: usize, + re_exprs: *const *const c_char, + im_exprs: *const *const c_char, + param_names: *const *const c_char, + param_names_len: usize, + qubits: *const u32, + qubits_len: usize, + params: *const *const CParameter, + params_len: usize, +) -> i32 { + if ptr.is_null() || label.is_null() || re_exprs.is_null() { + return -1; + } + if num_qubits > MAX_UNITARY_QUBITS || param_names_len > u16::MAX as usize { + return -8; + } + let label = match unsafe { CStr::from_ptr(label) }.to_str() { + Ok(label) => label, + Err(_) => return -4, + }; + let dim = 1usize << num_qubits; + let expected = dim * dim; + let re_slice = unsafe { std::slice::from_raw_parts(re_exprs, expected) }; + let im_slice = if im_exprs.is_null() { + None + } else { + Some(unsafe { std::slice::from_raw_parts(im_exprs, expected) }) + }; + let mut entries = Vec::with_capacity(expected); + for i in 0..expected { + let re = match parse_parameter_expr(re_slice[i]) { + Ok(re) => re, + Err(code) => return code, + }; + let im = match im_slice { + Some(slice) => match parse_parameter_expr(slice[i]) { + Ok(im) => im, + Err(code) => return code, + }, + None => Parameter::from(0.0), + }; + entries.push(SymbolicComplex::new(re, im)); + } + let matrix = match Array2::from_shape_vec((dim, dim), entries) { + Ok(matrix) => matrix, + Err(_) => return -8, + }; + let mut names = Vec::with_capacity(param_names_len); + if param_names_len > 0 { + if param_names.is_null() { + return -1; + } + let name_slice = unsafe { std::slice::from_raw_parts(param_names, param_names_len) }; + for &name in name_slice { + let name = match unsafe { CStr::from_ptr(name) }.to_str() { + Ok(name) => name, + Err(_) => return -4, + }; + names.push(name.to_string()); + } + } + let gate = match UnitaryGate::new(label, num_qubits as u16, names.len() as u16) + .with_symbolic_matrix(names, matrix) + { + Ok(gate) => gate, + Err(_) => return -3, + }; + let wrapper = unsafe { &mut *ptr }; + let qubits = match checked_qubits(&wrapper.inner, qubits, qubits_len) { + Ok(qubits) => qubits, + Err(code) => return code, + }; + let params = match param_ptrs_to_values(params, params_len) { + Ok(params) => params, + Err(code) => return code, + }; + wrapper + .inner + .unitary_with_params(gate, qubits, params) + .map_or(-3, |_| 0) +} + +/// Returns the number of `Complex64` elements (`rows * cols`) that +/// `unitary_gate_matrix_for_params` writes for `matrix`, or 0 for NULL or a +/// non-square symbolic matrix. +#[unsafe(no_mangle)] +pub extern "C" fn unitary_gate_matrix_for_params_len(matrix: *const CSymbolicMatrix) -> usize { + if matrix.is_null() { + return 0; + } + let inner = unsafe { &(*matrix).inner }; + if inner.nrows() != inner.ncols() { + return 0; + } + inner.nrows().saturating_mul(inner.ncols()) +} + +/// Evaluates a custom unitary gate's numeric matrix at positional parameter +/// values. +/// +/// The C ABI has no standalone unitary-gate handle, so the gate definition is +/// supplied the same way `circuit_unitary_with_params` builds one: a symbolic +/// `matrix` (`2^n x 2^n`) plus the ordered formal `param_names` that bind +/// application arguments positionally to the matrix symbols. This wraps +/// [`UnitaryGate::matrix_for_params`], including its shape and unitarity +/// checks. +/// +/// The result is written to `buffer` in row-major order as interleaved +/// (real, imag) `Complex64` values (two-step pattern; pair with +/// `unitary_gate_matrix_for_params_len`). Returns 0 on success, -1 on NULL +/// input, -3 on shape, parameter-count, non-finite-value or unitarity failure, +/// -4 on invalid UTF-8 parameter names, or -8 on invalid sizes or a too-small +/// buffer. +#[unsafe(no_mangle)] +pub extern "C" fn unitary_gate_matrix_for_params( + matrix: *const CSymbolicMatrix, + param_names: *const *const c_char, + param_names_len: usize, + params: *const f64, + params_len: usize, + buffer: *mut Complex64, + buffer_len: usize, +) -> i32 { + if matrix.is_null() || buffer.is_null() { + return -1; + } + if param_names_len > 0 && param_names.is_null() { + return -1; + } + if params_len > 0 && params.is_null() { + return -1; + } + let symbolic = unsafe { &(*matrix).inner }.clone(); + let dim = symbolic.nrows(); + if dim == 0 + || symbolic.ncols() != dim + || !dim.is_power_of_two() + || dim.trailing_zeros() as usize > MAX_UNITARY_QUBITS + || param_names_len > u16::MAX as usize + { + return -8; + } + let total = dim.saturating_mul(dim); + if buffer_len < total { + return -8; + } + let mut names = Vec::with_capacity(param_names_len); + if param_names_len > 0 { + let name_slice = unsafe { std::slice::from_raw_parts(param_names, param_names_len) }; + for &name in name_slice { + if name.is_null() { + return -1; + } + let name = match unsafe { CStr::from_ptr(name) }.to_str() { + Ok(name) => name, + Err(_) => return -4, + }; + names.push(name.to_string()); + } + } + let values = if params_len == 0 { + Vec::new() + } else { + unsafe { std::slice::from_raw_parts(params, params_len) }.to_vec() + }; + let num_qubits = dim.trailing_zeros() as u16; + let gate = match UnitaryGate::new("unitary", num_qubits, names.len() as u16) + .with_symbolic_matrix(names, symbolic) + { + Ok(gate) => gate, + Err(_) => return -3, + }; + let evaluated = match gate.matrix_for_params(&values) { + Ok(matrix) => matrix, + Err(_) => return -3, + }; + let out = unsafe { std::slice::from_raw_parts_mut(buffer, total) }; + for (slot, value) in out.iter_mut().zip(evaluated.iter()) { + *slot = *value; + } + 0 +} + +/// Freeze a copy of the circuit into a reusable composite gate. +/// +/// The input circuit stays valid (it is cloned). Returns a newly allocated +/// `CCircuitGate*`, or NULL on NULL input, invalid UTF-8 name or failure. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_to_gate(ptr: *const CCircuit, name: *const c_char) -> *mut CCircuitGate { + if ptr.is_null() || name.is_null() { + return std::ptr::null_mut(); + } + let name = match unsafe { CStr::from_ptr(name) }.to_str() { + Ok(name) => name, + Err(_) => return std::ptr::null_mut(), + }; + match unsafe { (*ptr).inner.clone() }.to_gate(name) { + Ok(Instruction::CircuitGate(gate)) => { + Box::into_raw(Box::new(CCircuitGate { inner: *gate })) + } + _ => std::ptr::null_mut(), + } +} + +/// Free a composite gate handle. Passing NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_gate_free(ptr: *mut CCircuitGate) { + if !ptr.is_null() { + unsafe { + let _ = Box::from_raw(ptr); + } + } +} + +/// Return the gate's name as a newly allocated C string (free with +/// `cqlib_string_free`), or NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_gate_name(ptr: *const CCircuitGate) -> *mut c_char { + if ptr.is_null() { + return std::ptr::null_mut(); + } + match CString::new(unsafe { (*ptr).inner.name() }) { + Ok(name) => name.into_raw(), + Err(_) => std::ptr::null_mut(), + } +} + +/// Return the number of qubits the gate acts on, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_gate_num_qubits(ptr: *const CCircuitGate) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.num_qubits() } +} + +/// Return the number of parameters the gate accepts, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_gate_num_params(ptr: *const CCircuitGate) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.num_params() } +} + +/// Return the number of positional parameter names in the gate's call +/// signature, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_gate_signature_params_len(ptr: *const CCircuitGate) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.signature_params().len() } +} + +/// Copy the gate's positional parameter-signature names into `out` +/// (two-step pattern; call `circuit_gate_signature_params_len` first). +/// Each written element is a freshly allocated C string the caller frees +/// with `cqlib_string_free`. Returns the total number of signature names. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_gate_signature_params( + ptr: *const CCircuitGate, + out: *mut *mut c_char, + len: usize, +) -> usize { + if ptr.is_null() { + return 0; + } + let names: Vec = unsafe { (*ptr).inner.signature_params().iter().cloned().collect() }; + write_symbol_list(&names, out, len) +} + +/// Append a composite gate to the circuit, binding `params` positionally +/// to the gate's parameter signature. Returns 0 on success, -1 on NULL +/// input, -2 on out-of-bounds qubits, -3 on arity or application failure. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_circuit_gate( + ptr: *mut CCircuit, + gate: *const CCircuitGate, + qubits: *const u32, + qubits_len: usize, + params: *const *const CParameter, + params_len: usize, +) -> i32 { + if ptr.is_null() || gate.is_null() { + return -1; + } + let wrapper = unsafe { &mut *ptr }; + let qubits = match checked_qubits(&wrapper.inner, qubits, qubits_len) { + Ok(qubits) => qubits, + Err(code) => return code, + }; + let params = match param_ptrs_to_values(params, params_len) { + Ok(params) => params, + Err(code) => return code, + }; + let gate = unsafe { (*gate).inner.clone() }; + wrapper + .inner + .circuit_gate(gate, qubits, params) + .map_or(-3, |_| 0) +} + +/// Append a multi-controlled standard gate. +/// +/// `gate_name` is a standard-gate name ("X", "RX", "SWAP", ...; the same +/// set accepted by the noise/device modules). `controls` and `targets` +/// together form the operation's qubit list; `params` are the gate's +/// parameters. Returns 0 on success, -1 on NULL input, -2 on +/// out-of-bounds qubits, -3 when the gate cannot be controlled or the +/// arity mismatches, -4 on invalid UTF-8, -8 on unknown gate name. +#[allow(clippy::too_many_arguments)] +#[unsafe(no_mangle)] +pub extern "C" fn circuit_multi_control( + ptr: *mut CCircuit, + gate_name: *const c_char, + controls: *const u32, + controls_len: usize, + targets: *const u32, + targets_len: usize, + params: *const *const CParameter, + params_len: usize, +) -> i32 { + if ptr.is_null() || gate_name.is_null() { + return -1; + } + let gate_name = match unsafe { CStr::from_ptr(gate_name) }.to_str() { + Ok(gate_name) => gate_name, + Err(_) => return -4, + }; + let gate = match standard_gate_from_name(gate_name) { + Some(gate) => gate, + None => return -8, + }; + let wrapper = unsafe { &mut *ptr }; + let controls = match checked_qubits(&wrapper.inner, controls, controls_len) { + Ok(controls) => controls, + Err(code) => return code, + }; + let targets = match checked_qubits(&wrapper.inner, targets, targets_len) { + Ok(targets) => targets, + Err(code) => return code, + }; + let params = match param_ptrs_to_values(params, params_len) { + Ok(params) => params, + Err(code) => return code, + }; + wrapper + .inner + .multi_control(gate, controls, targets, params) + .map_or(-3, |_| 0) +} + +/// Append a delay (idle period) on `qubit` with a numeric duration. +/// Returns 0 on success, -1 on NULL input, -2 on out-of-bounds qubit, -3 +/// on non-finite duration. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_delay(ptr: *mut CCircuit, qubit: u32, duration: f64) -> i32 { + if ptr.is_null() { + return -1; + } + let wrapper = unsafe { &mut *ptr }; + let qubit = match check_qubit(&wrapper.inner, qubit) { + Ok(qubit) => qubit, + Err(code) => return code, + }; + wrapper + .inner + .delay(qubit, ParameterValue::Fixed(duration)) + .map_or(-3, |_| 0) +} + +/// Append a delay (idle period) on `qubit` with a symbolic duration. +/// Returns 0 on success, -1 on NULL input, -2 on out-of-bounds qubit, -3 +/// on application failure. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_delay_param( + ptr: *mut CCircuit, + qubit: u32, + duration: *const CParameter, +) -> i32 { + if ptr.is_null() || duration.is_null() { + return -1; + } + let wrapper = unsafe { &mut *ptr }; + let qubit = match check_qubit(&wrapper.inner, qubit) { + Ok(qubit) => qubit, + Err(code) => return code, + }; + let duration = unsafe { (*duration).inner.clone() }; + wrapper + .inner + .delay(qubit, ParameterValue::Param(duration)) + .map_or(-3, |_| 0) +} + +/// Return a resolved snapshot of the top-level operation at `index`. +/// +/// The returned `CValueOperation*` is read-only and independent of later +/// circuit mutations; free it with `value_operation_free`. Returns NULL on +/// NULL input or out-of-bounds index. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_index(ptr: *const CCircuit, index: usize) -> *mut CValueOperation { + if ptr.is_null() { + return std::ptr::null_mut(); + } + match unsafe { (*ptr).inner.index(index) } { + Ok(inner) => Box::into_raw(Box::new(CValueOperation { inner })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Free an operation snapshot handle. Passing NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn value_operation_free(ptr: *mut CValueOperation) { + if !ptr.is_null() { + unsafe { + let _ = Box::from_raw(ptr); + } + } +} + +/// Return the operation's instruction name (e.g. "H", "delay") as a newly +/// allocated C string (free with `cqlib_string_free`), or NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn value_operation_name(ptr: *const CValueOperation) -> *mut c_char { + if ptr.is_null() { + return std::ptr::null_mut(); + } + match CString::new(unsafe { (*ptr).inner.name() }) { + Ok(name) => name.into_raw(), + Err(_) => std::ptr::null_mut(), + } +} + +/// Return the operation's instruction category (e.g. "standard", "delay") +/// as a newly allocated C string (free with `cqlib_string_free`), or NULL +/// on error. +#[unsafe(no_mangle)] +pub extern "C" fn value_operation_instruction_type(ptr: *const CValueOperation) -> *mut c_char { + if ptr.is_null() { + return std::ptr::null_mut(); + } + match CString::new(unsafe { (*ptr).inner.instruction_type() }) { + Ok(name) => name.into_raw(), + Err(_) => std::ptr::null_mut(), + } +} + +/// Return the operation's optional metadata label as a newly allocated C +/// string (free with `cqlib_string_free`). Returns NULL when the operation +/// has no label or on error. +#[unsafe(no_mangle)] +pub extern "C" fn value_operation_label(ptr: *const CValueOperation) -> *mut c_char { + if ptr.is_null() { + return std::ptr::null_mut(); + } + match unsafe { (*ptr).inner.label.as_deref() } { + Some(label) => match CString::new(label) { + Ok(label) => label.into_raw(), + Err(_) => std::ptr::null_mut(), + }, + None => std::ptr::null_mut(), + } +} + +/// Return the number of qubits the operation acts on, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn value_operation_num_qubits(ptr: *const CValueOperation) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.qubits.len() } +} + +/// Copy the operation's qubit ids into `out` (two-step pattern; call +/// `value_operation_num_qubits` first). Returns the total qubit count. +#[unsafe(no_mangle)] +pub extern "C" fn value_operation_qubits( + ptr: *const CValueOperation, + out: *mut u32, + len: usize, +) -> usize { + if ptr.is_null() { + return 0; + } + let qubits = unsafe { &(*ptr).inner.qubits }; + let total = qubits.len(); + if !out.is_null() { + for (i, qubit) in qubits.iter().take(total.min(len)).enumerate() { + unsafe { *out.add(i) = qubit.id() }; + } + } + total +} + +/// Return the number of parameters carried by the operation, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn value_operation_num_params(ptr: *const CValueOperation) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.params.len() } +} + +/// Inspect one of the operation's parameters. +/// +/// Writes a `CParameterValue` to `*out`: a fixed number, or a newly +/// allocated `CParameter*` for the symbolic case (free with `param_free`). +/// Returns 0 on success, -1 on NULL input, -3 on out-of-bounds index. +#[unsafe(no_mangle)] +pub extern "C" fn value_operation_param( + ptr: *const CValueOperation, + index: usize, + out: *mut CParameterValue, +) -> i32 { + if ptr.is_null() || out.is_null() { + return -1; + } + let params = unsafe { &(*ptr).inner.params }; + match params.get(index) { + Some(value) => { + unsafe { *out = parameter_value_to_c(value.clone()) }; + 0 + } + None => -3, + } +} + +/// Remove multiple top-level operations by index. +/// +/// `indices` are interpreted against the operation list before deletion; +/// duplicates are ignored and deletion is atomic on failure. Returns 0 on +/// success (including an empty index list), -1 on NULL input, -3 when any +/// index is out of bounds or a classical value is still in use. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_remove_operations( + ptr: *mut CCircuit, + indices: *const usize, + len: usize, +) -> i32 { + if ptr.is_null() { + return -1; + } + let indices: Vec = if len == 0 { + Vec::new() + } else { + if indices.is_null() { + return -1; + } + unsafe { std::slice::from_raw_parts(indices, len) }.to_vec() + }; + let wrapper = unsafe { &mut *ptr }; + wrapper.inner.remove_operations(indices).map_or(-3, |_| 0) +} diff --git a/crates/binding-c/src/circuit/ansatz.rs b/crates/binding-c/src/circuit/ansatz.rs new file mode 100644 index 00000000..ccd17463 --- /dev/null +++ b/crates/binding-c/src/circuit/ansatz.rs @@ -0,0 +1,1522 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! C ABI for variational ansatz templates (TwoLocal, BasicEntanglerLayers, +//! QAOAAnsatz, feature maps, StronglyEntanglingLayers and the Pauli evolution +//! ansatz), facade constructors and entanglement topology helpers. + +#![allow(clippy::not_unsafe_ptr_arg_deref)] + +use crate::circuit::CCircuit; +use crate::device::standard_gate_from_name; +use crate::qis::CHamiltonian; +use cqlib_core::circuit::ansatz::{ + Ansatz, BasicEntanglerLayers, EntanglementTopology, EvolutionStrategy, IQPFeatureMap, + PauliEvolutionAnsatz, PauliFeatureMap, QAOAAnsatz, StronglyEntanglingLayers, TwoLocal, + ZFeatureMap, ZZFeatureMap, +}; +use cqlib_core::qis::PauliString; +use cqlib_core::qis::evolution::TrotterMode; +use std::ffi::CStr; +use std::os::raw::c_char; + +// ===== Topology / strategy tags ===== + +/// `two_local_entanglement` topology tag: linear nearest-neighbor chain. +pub const ENTANGLEMENT_LINEAR: i32 = 0; +/// Topology tag: linear chain plus wrap-around edge. +pub const ENTANGLEMENT_CIRCULAR: i32 = 1; +/// Topology tag: all-to-all pairs. +pub const ENTANGLEMENT_FULL: i32 = 2; +/// Topology tag: custom explicit pairs (flat control/target id array). +pub const ENTANGLEMENT_CUSTOM: i32 = 3; + +/// `qaoa_ansatz_evolution_strategy` tag: exact term-wise evolution. +pub const EVOLUTION_STRATEGY_EXACT: i32 = 0; +/// Strategy tag: automatic exact/Trotter selection. +pub const EVOLUTION_STRATEGY_AUTO: i32 = 1; +/// Strategy tag: explicit Trotter product formula. +pub const EVOLUTION_STRATEGY_TROTTER: i32 = 2; + +/// Trotter mode tag: first-order product formula. +pub const TROTTER_FIRST_ORDER: i32 = 0; +/// Trotter mode tag: second-order product formula. +pub const TROTTER_SECOND_ORDER: i32 = 1; +/// Trotter mode tag: randomized first-order formula. +pub const TROTTER_RANDOMIZED: i32 = 2; + +/// `pauli_evolution_ansatz_evolution_info` `trotter_mode` tag: no Trotter +/// decomposition was emitted (single-pass exact path). +pub const TROTTER_MODE_NONE: i32 = -1; + +// ===== Internal helpers ===== + +fn parse_gate_name(name: *const c_char) -> Result { + if name.is_null() { + return Err(-1); + } + let name = unsafe { CStr::from_ptr(name) }.to_str().map_err(|_| -4)?; + standard_gate_from_name(name).ok_or(-8) +} + +fn parse_prefix(prefix: *const c_char) -> Result { + if prefix.is_null() { + return Err(-1); + } + unsafe { CStr::from_ptr(prefix) } + .to_str() + .map(str::to_string) + .map_err(|_| -4) +} + +/// Parses a topology tag plus an optional flat custom-pair array into an +/// `EntanglementTopology`. Returns -1 on NULL input, -8 on an unknown tag or +/// an odd custom pair count. +fn parse_topology( + topology: i32, + pairs: *const u32, + pairs_len: usize, +) -> Result { + match topology { + ENTANGLEMENT_LINEAR => Ok(EntanglementTopology::Linear), + ENTANGLEMENT_CIRCULAR => Ok(EntanglementTopology::Circular), + ENTANGLEMENT_FULL => Ok(EntanglementTopology::Full), + ENTANGLEMENT_CUSTOM => { + if !pairs_len.is_multiple_of(2) { + return Err(-8); + } + let mut list = Vec::with_capacity(pairs_len / 2); + if pairs_len > 0 { + if pairs.is_null() { + return Err(-1); + } + let slice = unsafe { std::slice::from_raw_parts(pairs, pairs_len) }; + for chunk in slice.chunks(2) { + list.push((chunk[0] as usize, chunk[1] as usize)); + } + } + Ok(EntanglementTopology::Custom(list)) + } + _ => Err(-8), + } +} + +// ===== TwoLocal ===== + +/// Opaque handle around a [`TwoLocal`] ansatz template. +pub struct CTwoLocal { + pub inner: TwoLocal, +} + +/// Creates a TwoLocal ansatz with defaults: `reps = 3`, rotation `[RY]`, +/// entangler `CX`, linear topology, final rotation layer kept. +#[unsafe(no_mangle)] +pub extern "C" fn two_local_new(num_qubits: usize) -> *mut CTwoLocal { + Box::into_raw(Box::new(CTwoLocal { + inner: TwoLocal::new(num_qubits), + })) +} + +/// Frees a TwoLocal handle. Passing NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn two_local_free(ptr: *mut CTwoLocal) { + if !ptr.is_null() { + unsafe { + drop(Box::from_raw(ptr)); + } + } +} + +/// Sets the number of repetition layers. Returns 0, or -1 on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn two_local_reps(ptr: *mut CTwoLocal, reps: usize) -> i32 { + if ptr.is_null() { + return -1; + } + let inner = unsafe { &mut (*ptr).inner }; + *inner = inner.clone().reps(reps); + 0 +} + +/// Sets the rotation gates from gate names (must be RX, RY or RZ; validated +/// on build). Returns 0, -1 on NULL input, -4 on invalid UTF-8, -8 on an +/// unknown gate name. +#[unsafe(no_mangle)] +pub extern "C" fn two_local_rotation_gates( + ptr: *mut CTwoLocal, + names: *const *const c_char, + len: usize, +) -> i32 { + if ptr.is_null() { + return -1; + } + if len > 0 && names.is_null() { + return -1; + } + let mut gates = Vec::with_capacity(len); + for i in 0..len { + let name = unsafe { *names.add(i) }; + match parse_gate_name(name) { + Ok(gate) => gates.push(gate), + Err(code) => return code, + } + } + let inner = unsafe { &mut (*ptr).inner }; + *inner = inner.clone().rotation_gates(gates); + 0 +} + +/// Sets the entanglement gate by name (CX, CY or CZ; validated on build). +/// Returns 0, -1 on NULL input, -4 on invalid UTF-8, -8 on an unknown name. +#[unsafe(no_mangle)] +pub extern "C" fn two_local_entanglement_gate(ptr: *mut CTwoLocal, name: *const c_char) -> i32 { + if ptr.is_null() { + return -1; + } + match parse_gate_name(name) { + Ok(gate) => { + let inner = unsafe { &mut (*ptr).inner }; + *inner = inner.clone().entanglement_gate(gate); + 0 + } + Err(code) => code, + } +} + +/// Sets the entanglement topology. +/// +/// For `ENTANGLEMENT_CUSTOM`, `pairs` is a flat array of +/// `pairs_len / 2` control/target qubit-id pairs. Returns 0, -1 on NULL +/// input, -8 on an unknown topology or an odd pair count. +#[unsafe(no_mangle)] +pub extern "C" fn two_local_entanglement( + ptr: *mut CTwoLocal, + topology: i32, + pairs: *const u32, + pairs_len: usize, +) -> i32 { + if ptr.is_null() { + return -1; + } + match parse_topology(topology, pairs, pairs_len) { + Ok(topo) => { + let inner = unsafe { &mut (*ptr).inner }; + *inner = inner.clone().entanglement(topo); + 0 + } + Err(code) => code, + } +} + +/// Sets whether to skip the final rotation layer. Returns 0, or -1 on NULL. +#[unsafe(no_mangle)] +pub extern "C" fn two_local_skip_final_rotation_layer(ptr: *mut CTwoLocal, skip: bool) -> i32 { + if ptr.is_null() { + return -1; + } + let inner = unsafe { &mut (*ptr).inner }; + *inner = inner.clone().skip_final_rotation_layer(skip); + 0 +} + +/// Validates the ansatz configuration. Returns 0 on success, -1 on NULL +/// input, -3 on invalid configuration. +#[unsafe(no_mangle)] +pub extern "C" fn two_local_validate(ptr: *const CTwoLocal) -> i32 { + if ptr.is_null() { + return -1; + } + unsafe { (*ptr).inner.validate().map_or(-3, |_| 0) } +} + +/// Returns the number of independent parameters, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn two_local_num_parameters(ptr: *const CTwoLocal) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.num_parameters() } +} + +/// Returns the number of qubits, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn two_local_num_qubits(ptr: *const CTwoLocal) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.num_qubits() } +} + +/// Builds the parameterized circuit. Parameters are named +/// `"{prefix}_{index}"`. Returns a newly allocated `CCircuit*` (free with +/// `circuit_free`), or NULL on NULL input, invalid UTF-8 or build failure. +#[unsafe(no_mangle)] +pub extern "C" fn two_local_build_circuit( + ptr: *const CTwoLocal, + prefix: *const c_char, +) -> *mut CCircuit { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let prefix = match parse_prefix(prefix) { + Ok(prefix) => prefix, + Err(_) => return std::ptr::null_mut(), + }; + match unsafe { (*ptr).inner.build_circuit(&prefix) } { + Ok(circuit) => Box::into_raw(Box::new(CCircuit { inner: circuit })), + Err(_) => std::ptr::null_mut(), + } +} + +// ===== ZFeatureMap ===== + +/// Opaque handle around a [`ZFeatureMap`] template. +pub struct CZFeatureMap { + pub inner: ZFeatureMap, +} + +/// Creates a ZFeatureMap with default `reps = 2`. +#[unsafe(no_mangle)] +pub extern "C" fn z_feature_map_new(num_qubits: usize) -> *mut CZFeatureMap { + Box::into_raw(Box::new(CZFeatureMap { + inner: ZFeatureMap::new(num_qubits), + })) +} + +/// Frees a ZFeatureMap handle. Passing NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn z_feature_map_free(ptr: *mut CZFeatureMap) { + if !ptr.is_null() { + unsafe { + drop(Box::from_raw(ptr)); + } + } +} + +/// Sets the number of repetition layers. Returns 0, or -1 on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn z_feature_map_reps(ptr: *mut CZFeatureMap, reps: usize) -> i32 { + if ptr.is_null() { + return -1; + } + let inner = unsafe { &mut (*ptr).inner }; + *inner = inner.clone().reps(reps); + 0 +} + +/// Validates the template. Returns 0 on success, -1 on NULL input, -3 on an +/// invalid configuration. +#[unsafe(no_mangle)] +pub extern "C" fn z_feature_map_validate(ptr: *const CZFeatureMap) -> i32 { + if ptr.is_null() { + return -1; + } + unsafe { (*ptr).inner.validate().map_or(-3, |_| 0) } +} + +/// Returns the number of independent parameters, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn z_feature_map_num_parameters(ptr: *const CZFeatureMap) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.num_parameters() } +} + +/// Returns the number of qubits, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn z_feature_map_num_qubits(ptr: *const CZFeatureMap) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.num_qubits() } +} + +/// Builds the parameterized circuit (parameters named `"{prefix}_{index}"`). +/// Returns a newly allocated `CCircuit*`, or NULL on NULL input, invalid +/// UTF-8 or build failure. +#[unsafe(no_mangle)] +pub extern "C" fn z_feature_map_build_circuit( + ptr: *const CZFeatureMap, + prefix: *const c_char, +) -> *mut CCircuit { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let prefix = match parse_prefix(prefix) { + Ok(prefix) => prefix, + Err(_) => return std::ptr::null_mut(), + }; + match unsafe { (*ptr).inner.build_circuit(&prefix) } { + Ok(circuit) => Box::into_raw(Box::new(CCircuit { inner: circuit })), + Err(_) => std::ptr::null_mut(), + } +} + +// ===== ZZFeatureMap ===== + +/// Opaque handle around a [`ZZFeatureMap`] template. +pub struct CZZFeatureMap { + pub inner: ZZFeatureMap, +} + +/// Creates a ZZFeatureMap with defaults: `reps = 2`, full entanglement. +#[unsafe(no_mangle)] +pub extern "C" fn zz_feature_map_new(num_qubits: usize) -> *mut CZZFeatureMap { + Box::into_raw(Box::new(CZZFeatureMap { + inner: ZZFeatureMap::new(num_qubits), + })) +} + +/// Frees a ZZFeatureMap handle. Passing NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn zz_feature_map_free(ptr: *mut CZZFeatureMap) { + if !ptr.is_null() { + unsafe { + drop(Box::from_raw(ptr)); + } + } +} + +/// Sets the number of repetition layers. Returns 0, or -1 on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn zz_feature_map_reps(ptr: *mut CZZFeatureMap, reps: usize) -> i32 { + if ptr.is_null() { + return -1; + } + let inner = unsafe { &mut (*ptr).inner }; + *inner = inner.clone().reps(reps); + 0 +} + +/// Sets the entanglement topology. +/// +/// For `ENTANGLEMENT_CUSTOM`, `pairs` is a flat array of +/// `pairs_len / 2` control/target qubit-id pairs. Returns 0, -1 on NULL +/// input, -8 on an unknown topology or an odd pair count. +#[unsafe(no_mangle)] +pub extern "C" fn zz_feature_map_entanglement( + ptr: *mut CZZFeatureMap, + topology: i32, + pairs: *const u32, + pairs_len: usize, +) -> i32 { + if ptr.is_null() { + return -1; + } + match parse_topology(topology, pairs, pairs_len) { + Ok(topo) => { + let inner = unsafe { &mut (*ptr).inner }; + *inner = inner.clone().entanglement(topo); + 0 + } + Err(code) => code, + } +} + +/// Validates the template. Returns 0 on success, -1 on NULL input, -3 on an +/// invalid configuration. +#[unsafe(no_mangle)] +pub extern "C" fn zz_feature_map_validate(ptr: *const CZZFeatureMap) -> i32 { + if ptr.is_null() { + return -1; + } + unsafe { (*ptr).inner.validate().map_or(-3, |_| 0) } +} + +/// Returns the number of independent parameters, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn zz_feature_map_num_parameters(ptr: *const CZZFeatureMap) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.num_parameters() } +} + +/// Returns the number of qubits, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn zz_feature_map_num_qubits(ptr: *const CZZFeatureMap) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.num_qubits() } +} + +/// Builds the parameterized circuit (parameters named `"{prefix}_{index}"`). +/// Returns a newly allocated `CCircuit*`, or NULL on NULL input, invalid +/// UTF-8 or build failure. +#[unsafe(no_mangle)] +pub extern "C" fn zz_feature_map_build_circuit( + ptr: *const CZZFeatureMap, + prefix: *const c_char, +) -> *mut CCircuit { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let prefix = match parse_prefix(prefix) { + Ok(prefix) => prefix, + Err(_) => return std::ptr::null_mut(), + }; + match unsafe { (*ptr).inner.build_circuit(&prefix) } { + Ok(circuit) => Box::into_raw(Box::new(CCircuit { inner: circuit })), + Err(_) => std::ptr::null_mut(), + } +} + +// ===== IQPFeatureMap ===== + +/// Opaque handle around an [`IQPFeatureMap`] template. +pub struct CIQPFeatureMap { + pub inner: IQPFeatureMap, +} + +/// Creates an IQPFeatureMap with defaults: `reps = 2`, full entanglement. +#[unsafe(no_mangle)] +pub extern "C" fn iqp_feature_map_new(num_qubits: usize) -> *mut CIQPFeatureMap { + Box::into_raw(Box::new(CIQPFeatureMap { + inner: IQPFeatureMap::new(num_qubits), + })) +} + +/// Frees an IQPFeatureMap handle. Passing NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn iqp_feature_map_free(ptr: *mut CIQPFeatureMap) { + if !ptr.is_null() { + unsafe { + drop(Box::from_raw(ptr)); + } + } +} + +/// Sets the number of repetition layers. Returns 0, or -1 on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn iqp_feature_map_reps(ptr: *mut CIQPFeatureMap, reps: usize) -> i32 { + if ptr.is_null() { + return -1; + } + let inner = unsafe { &mut (*ptr).inner }; + *inner = inner.clone().reps(reps); + 0 +} + +/// Sets the entanglement topology for the two-qubit diagonal interactions. +/// +/// For `ENTANGLEMENT_CUSTOM`, `pairs` is a flat array of +/// `pairs_len / 2` control/target qubit-id pairs. Returns 0, -1 on NULL +/// input, -8 on an unknown topology or an odd pair count. +#[unsafe(no_mangle)] +pub extern "C" fn iqp_feature_map_entanglement( + ptr: *mut CIQPFeatureMap, + topology: i32, + pairs: *const u32, + pairs_len: usize, +) -> i32 { + if ptr.is_null() { + return -1; + } + match parse_topology(topology, pairs, pairs_len) { + Ok(topo) => { + let inner = unsafe { &mut (*ptr).inner }; + *inner = inner.clone().entanglement(topo); + 0 + } + Err(code) => code, + } +} + +/// Validates the template. Returns 0 on success, -1 on NULL input, -3 on an +/// invalid configuration. +#[unsafe(no_mangle)] +pub extern "C" fn iqp_feature_map_validate(ptr: *const CIQPFeatureMap) -> i32 { + if ptr.is_null() { + return -1; + } + unsafe { (*ptr).inner.validate().map_or(-3, |_| 0) } +} + +/// Returns the number of independent parameters, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn iqp_feature_map_num_parameters(ptr: *const CIQPFeatureMap) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.num_parameters() } +} + +/// Returns the number of qubits, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn iqp_feature_map_num_qubits(ptr: *const CIQPFeatureMap) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.num_qubits() } +} + +/// Builds the parameterized circuit (parameters named `"{prefix}_{index}"`). +/// Returns a newly allocated `CCircuit*`, or NULL on NULL input, invalid +/// UTF-8 or build failure. +#[unsafe(no_mangle)] +pub extern "C" fn iqp_feature_map_build_circuit( + ptr: *const CIQPFeatureMap, + prefix: *const c_char, +) -> *mut CCircuit { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let prefix = match parse_prefix(prefix) { + Ok(prefix) => prefix, + Err(_) => return std::ptr::null_mut(), + }; + match unsafe { (*ptr).inner.build_circuit(&prefix) } { + Ok(circuit) => Box::into_raw(Box::new(CCircuit { inner: circuit })), + Err(_) => std::ptr::null_mut(), + } +} + +// ===== PauliFeatureMap ===== + +/// Opaque handle around a [`PauliFeatureMap`] template. +pub struct CPauliFeatureMap { + pub inner: PauliFeatureMap, +} + +/// Creates a PauliFeatureMap with defaults: `reps = 2`, paulis `["Z", "ZZ"]`, +/// full entanglement and parameter prefix `"x"`. +#[unsafe(no_mangle)] +pub extern "C" fn pauli_feature_map_new(num_qubits: usize) -> *mut CPauliFeatureMap { + Box::into_raw(Box::new(CPauliFeatureMap { + inner: PauliFeatureMap::new(num_qubits), + })) +} + +/// Frees a PauliFeatureMap handle. Passing NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn pauli_feature_map_free(ptr: *mut CPauliFeatureMap) { + if !ptr.is_null() { + unsafe { + drop(Box::from_raw(ptr)); + } + } +} + +/// Sets the number of repetition layers. Returns 0, or -1 on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn pauli_feature_map_reps(ptr: *mut CPauliFeatureMap, reps: usize) -> i32 { + if ptr.is_null() { + return -1; + } + let inner = unsafe { &mut (*ptr).inner }; + *inner = inner.clone().reps(reps); + 0 +} + +/// Sets the Pauli evolution templates. +/// +/// `texts` and `labels` are parallel arrays of `len` NUL-terminated C +/// strings; each text is parsed as a `PauliString`. Returns 0, -1 on NULL +/// input, -4 on invalid UTF-8, -8 on a Pauli string that fails to parse. +#[unsafe(no_mangle)] +pub extern "C" fn pauli_feature_map_paulis( + ptr: *mut CPauliFeatureMap, + texts: *const *const c_char, + labels: *const *const c_char, + len: usize, +) -> i32 { + if ptr.is_null() { + return -1; + } + if len > 0 && (texts.is_null() || labels.is_null()) { + return -1; + } + let mut paulis = Vec::with_capacity(len); + for i in 0..len { + let text = unsafe { *texts.add(i) }; + let label = unsafe { *labels.add(i) }; + if text.is_null() || label.is_null() { + return -1; + } + let text_str = match unsafe { CStr::from_ptr(text) }.to_str() { + Ok(s) => s, + Err(_) => return -4, + }; + let label_str = match unsafe { CStr::from_ptr(label) }.to_str() { + Ok(s) => s, + Err(_) => return -4, + }; + let pauli = match text_str.parse::() { + Ok(p) => p, + Err(_) => return -8, + }; + paulis.push((pauli, label_str.to_string())); + } + let inner = unsafe { &mut (*ptr).inner }; + *inner = inner.clone().paulis(paulis); + 0 +} + +/// Sets the entanglement topology for the multi-qubit interactions. +/// +/// For `ENTANGLEMENT_CUSTOM`, `pairs` is a flat array of +/// `pairs_len / 2` control/target qubit-id pairs. Returns 0, -1 on NULL +/// input, -8 on an unknown topology or an odd pair count. +#[unsafe(no_mangle)] +pub extern "C" fn pauli_feature_map_entanglement( + ptr: *mut CPauliFeatureMap, + topology: i32, + pairs: *const u32, + pairs_len: usize, +) -> i32 { + if ptr.is_null() { + return -1; + } + match parse_topology(topology, pairs, pairs_len) { + Ok(topo) => { + let inner = unsafe { &mut (*ptr).inner }; + *inner = inner.clone().entanglement(topo); + 0 + } + Err(code) => code, + } +} + +/// Sets the fallback parameter prefix used when `build_circuit` receives an +/// empty prefix. Returns 0, -1 on NULL input, -4 on invalid UTF-8. +#[unsafe(no_mangle)] +pub extern "C" fn pauli_feature_map_parameter_prefix( + ptr: *mut CPauliFeatureMap, + prefix: *const c_char, +) -> i32 { + if ptr.is_null() { + return -1; + } + match parse_prefix(prefix) { + Ok(prefix) => { + let inner = unsafe { &mut (*ptr).inner }; + *inner = inner.clone().parameter_prefix(prefix); + 0 + } + Err(code) => code, + } +} + +/// Validates the template. Returns 0 on success, -1 on NULL input, -3 on an +/// invalid configuration. +#[unsafe(no_mangle)] +pub extern "C" fn pauli_feature_map_validate(ptr: *const CPauliFeatureMap) -> i32 { + if ptr.is_null() { + return -1; + } + unsafe { (*ptr).inner.validate().map_or(-3, |_| 0) } +} + +/// Returns the number of independent parameters, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn pauli_feature_map_num_parameters(ptr: *const CPauliFeatureMap) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.num_parameters() } +} + +/// Returns the number of qubits, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn pauli_feature_map_num_qubits(ptr: *const CPauliFeatureMap) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.num_qubits() } +} + +/// Builds the parameterized circuit (parameters named `"{prefix}_{index}"`; +/// an empty prefix falls back to the configured parameter prefix). Returns a +/// newly allocated `CCircuit*`, or NULL on NULL input, invalid UTF-8 or +/// build failure. +#[unsafe(no_mangle)] +pub extern "C" fn pauli_feature_map_build_circuit( + ptr: *const CPauliFeatureMap, + prefix: *const c_char, +) -> *mut CCircuit { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let prefix = match parse_prefix(prefix) { + Ok(prefix) => prefix, + Err(_) => return std::ptr::null_mut(), + }; + match unsafe { (*ptr).inner.build_circuit(&prefix) } { + Ok(circuit) => Box::into_raw(Box::new(CCircuit { inner: circuit })), + Err(_) => std::ptr::null_mut(), + } +} + +// ===== Facade constructors ===== + +/// Creates an EfficientSU2 ansatz: `[RY, RZ]` rotation layers with `CX` +/// entanglement. Returns a newly allocated `CTwoLocal*`, or NULL on an +/// invalid topology. +#[unsafe(no_mangle)] +pub extern "C" fn efficient_su2( + num_qubits: usize, + reps: usize, + topology: i32, + pairs: *const u32, + pairs_len: usize, +) -> *mut CTwoLocal { + match parse_topology(topology, pairs, pairs_len) { + Ok(topo) => Box::into_raw(Box::new(CTwoLocal { + inner: cqlib_core::circuit::ansatz::efficient_su2(num_qubits, reps, topo), + })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Creates a RealAmplitudes ansatz: `[RY]` rotation layers with `CX` +/// entanglement. Returns a newly allocated `CTwoLocal*`, or NULL on an +/// invalid topology. +#[unsafe(no_mangle)] +pub extern "C" fn real_amplitudes( + num_qubits: usize, + reps: usize, + topology: i32, + pairs: *const u32, + pairs_len: usize, +) -> *mut CTwoLocal { + match parse_topology(topology, pairs, pairs_len) { + Ok(topo) => Box::into_raw(Box::new(CTwoLocal { + inner: cqlib_core::circuit::ansatz::real_amplitudes(num_qubits, reps, topo), + })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Creates a ZZFeatureMap with the given topology. Named +/// `zz_feature_map_build` to avoid clashing with the `zz_feature_map_new` +/// type constructor. Returns a newly allocated `CZZFeatureMap*`, or NULL on +/// an invalid topology. +#[unsafe(no_mangle)] +pub extern "C" fn zz_feature_map_build( + num_qubits: usize, + reps: usize, + topology: i32, + pairs: *const u32, + pairs_len: usize, +) -> *mut CZZFeatureMap { + match parse_topology(topology, pairs, pairs_len) { + Ok(topo) => Box::into_raw(Box::new(CZZFeatureMap { + inner: cqlib_core::circuit::ansatz::zz_feature_map(num_qubits, reps, topo), + })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Creates a PauliFeatureMap with explicit Pauli templates. `texts` and +/// `labels` are parallel arrays of `paulis_len` NUL-terminated C strings. +/// Named `pauli_feature_map_build` to avoid clashing with the +/// `pauli_feature_map_new` type constructor. Returns a newly allocated +/// `CPauliFeatureMap*`, or NULL on an invalid topology or Pauli string. +#[unsafe(no_mangle)] +pub extern "C" fn pauli_feature_map_build( + num_qubits: usize, + reps: usize, + texts: *const *const c_char, + labels: *const *const c_char, + paulis_len: usize, + topology: i32, + pairs: *const u32, + pairs_len: usize, +) -> *mut CPauliFeatureMap { + let paulis = parse_pauli_templates(texts, labels, paulis_len); + let paulis = match paulis { + Ok(paulis) => paulis, + Err(_) => return std::ptr::null_mut(), + }; + match parse_topology(topology, pairs, pairs_len) { + Ok(topo) => Box::into_raw(Box::new(CPauliFeatureMap { + inner: cqlib_core::circuit::ansatz::pauli_feature_map(num_qubits, reps, paulis, topo), + })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Parses parallel Pauli-text/label arrays into core templates. +fn parse_pauli_templates( + texts: *const *const c_char, + labels: *const *const c_char, + len: usize, +) -> Result, i32> { + if len > 0 && (texts.is_null() || labels.is_null()) { + return Err(-1); + } + let mut paulis = Vec::with_capacity(len); + for i in 0..len { + let text = unsafe { *texts.add(i) }; + let label = unsafe { *labels.add(i) }; + if text.is_null() || label.is_null() { + return Err(-1); + } + let text_str = unsafe { CStr::from_ptr(text) }.to_str().map_err(|_| -4)?; + let label_str = unsafe { CStr::from_ptr(label) }.to_str().map_err(|_| -4)?; + let pauli = text_str.parse::().map_err(|_| -8)?; + paulis.push((pauli, label_str.to_string())); + } + Ok(paulis) +} + +// ===== Entanglement topology helpers ===== + +/// Returns the flattened length (two entries per pair) generated by +/// `generate_pairs` for the topology, or 0 on an invalid topology. For +/// `ENTANGLEMENT_CUSTOM`, `pairs` is a flat array of `pairs_len / 2` +/// control/target qubit-id pairs echoed verbatim. +#[unsafe(no_mangle)] +pub extern "C" fn entanglement_generate_pairs_len( + topology: i32, + pairs: *const u32, + pairs_len: usize, + num_qubits: usize, +) -> usize { + match parse_topology(topology, pairs, pairs_len) { + Ok(topo) => topo.generate_pairs(num_qubits).len() * 2, + Err(_) => 0, + } +} + +/// Copies the flattened pairs `[c0, t0, c1, t1, ...]` into `out` (two-step +/// pattern; call `entanglement_generate_pairs_len` first). Returns 0 on +/// success, -1 on NULL input, -8 when `len` does not match the required +/// length or an argument is invalid. +#[unsafe(no_mangle)] +pub extern "C" fn entanglement_generate_pairs( + topology: i32, + pairs: *const u32, + pairs_len: usize, + num_qubits: usize, + out: *mut u32, + len: usize, +) -> i32 { + let topo = match parse_topology(topology, pairs, pairs_len) { + Ok(topo) => topo, + Err(code) => return code, + }; + let flat = topo.generate_pairs(num_qubits); + if flat.len() * 2 != len { + return -8; + } + if len > 0 && out.is_null() { + return -1; + } + for (i, (control, target)) in flat.iter().enumerate() { + unsafe { + *out.add(i * 2) = *control as u32; + *out.add(i * 2 + 1) = *target as u32; + } + } + 0 +} + +/// Returns the flattened length (`k` entries per tuple) generated by +/// `generate_k_tuples` for the topology, or 0 on an invalid argument +/// combination. +#[unsafe(no_mangle)] +pub extern "C" fn entanglement_generate_k_tuples_len( + topology: i32, + pairs: *const u32, + pairs_len: usize, + k: usize, + num_qubits: usize, +) -> usize { + match parse_topology(topology, pairs, pairs_len) { + Ok(topo) => topo.generate_k_tuples(k, num_qubits).len() * k, + Err(_) => 0, + } +} + +/// Copies the flattened k-tuples `[t0_0, t0_1, ..., t1_0, ...]` into `out` +/// (two-step pattern; call `entanglement_generate_k_tuples_len` first). +/// Returns 0 on success, -1 on NULL input, -8 when `len` does not match the +/// required length or an argument is invalid. +#[unsafe(no_mangle)] +pub extern "C" fn entanglement_generate_k_tuples( + topology: i32, + pairs: *const u32, + pairs_len: usize, + k: usize, + num_qubits: usize, + out: *mut u32, + len: usize, +) -> i32 { + let topo = match parse_topology(topology, pairs, pairs_len) { + Ok(topo) => topo, + Err(code) => return code, + }; + let tuples = topo.generate_k_tuples(k, num_qubits); + if tuples.len() * k != len { + return -8; + } + if len > 0 && out.is_null() { + return -1; + } + let mut idx = 0; + for tuple in &tuples { + for &qubit in tuple { + unsafe { + *out.add(idx) = qubit as u32; + } + idx += 1; + } + } + 0 +} + +// ===== StronglyEntanglingLayers ===== + +/// Opaque handle around a [`StronglyEntanglingLayers`] template. +pub struct CStronglyEntanglingLayers { + pub inner: StronglyEntanglingLayers, +} + +/// Creates a StronglyEntanglingLayers template with defaults: `reps = 1`, +/// entangler `CX`, ranges cycling through `1..num_qubits`. +#[unsafe(no_mangle)] +pub extern "C" fn strongly_entangling_layers_new( + num_qubits: usize, +) -> *mut CStronglyEntanglingLayers { + Box::into_raw(Box::new(CStronglyEntanglingLayers { + inner: StronglyEntanglingLayers::new(num_qubits), + })) +} + +/// Frees a StronglyEntanglingLayers handle. Passing NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn strongly_entangling_layers_free(ptr: *mut CStronglyEntanglingLayers) { + if !ptr.is_null() { + unsafe { + drop(Box::from_raw(ptr)); + } + } +} + +/// Sets the number of layers. Returns 0, or -1 on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn strongly_entangling_layers_reps( + ptr: *mut CStronglyEntanglingLayers, + reps: usize, +) -> i32 { + if ptr.is_null() { + return -1; + } + let inner = unsafe { &mut (*ptr).inner }; + *inner = inner.clone().reps(reps); + 0 +} + +/// Sets the two-qubit entangling gate by name (CX, CY or CZ). Returns 0, -1 +/// on NULL input, -4 on invalid UTF-8, -8 on an unknown gate name. +#[unsafe(no_mangle)] +pub extern "C" fn strongly_entangling_layers_entanglement_gate( + ptr: *mut CStronglyEntanglingLayers, + name: *const c_char, +) -> i32 { + if ptr.is_null() { + return -1; + } + match parse_gate_name(name) { + Ok(gate) => { + let inner = unsafe { &mut (*ptr).inner }; + *inner = inner.clone().entanglement_gate(gate); + 0 + } + Err(code) => code, + } +} + +/// Sets explicit entanglement ranges reused cyclically across layers. An +/// empty list (`len == 0`) stores the empty list, which fails validation +/// until replaced. Returns 0, or -1 on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn strongly_entangling_layers_ranges( + ptr: *mut CStronglyEntanglingLayers, + ranges: *const usize, + len: usize, +) -> i32 { + if ptr.is_null() { + return -1; + } + if len > 0 && ranges.is_null() { + return -1; + } + let list = if len == 0 { + Vec::new() + } else { + unsafe { std::slice::from_raw_parts(ranges, len) }.to_vec() + }; + let inner = unsafe { &mut (*ptr).inner }; + *inner = inner.clone().ranges(list); + 0 +} + +/// Validates the template. Returns 0 on success, -1 on NULL input, -3 on an +/// invalid configuration. +#[unsafe(no_mangle)] +pub extern "C" fn strongly_entangling_layers_validate( + ptr: *const CStronglyEntanglingLayers, +) -> i32 { + if ptr.is_null() { + return -1; + } + unsafe { (*ptr).inner.validate().map_or(-3, |_| 0) } +} + +/// Returns the number of independent parameters, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn strongly_entangling_layers_num_parameters( + ptr: *const CStronglyEntanglingLayers, +) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.num_parameters() } +} + +/// Returns the number of qubits, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn strongly_entangling_layers_num_qubits( + ptr: *const CStronglyEntanglingLayers, +) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.num_qubits() } +} + +/// Builds the parameterized circuit (parameters named `"{prefix}_{index}"`). +/// Returns a newly allocated `CCircuit*`, or NULL on NULL input, invalid +/// UTF-8 or build failure. +#[unsafe(no_mangle)] +pub extern "C" fn strongly_entangling_layers_build_circuit( + ptr: *const CStronglyEntanglingLayers, + prefix: *const c_char, +) -> *mut CCircuit { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let prefix = match parse_prefix(prefix) { + Ok(prefix) => prefix, + Err(_) => return std::ptr::null_mut(), + }; + match unsafe { (*ptr).inner.build_circuit(&prefix) } { + Ok(circuit) => Box::into_raw(Box::new(CCircuit { inner: circuit })), + Err(_) => std::ptr::null_mut(), + } +} + +// ===== PauliEvolutionAnsatz ===== + +/// Opaque handle around a [`PauliEvolutionAnsatz`] template. +pub struct CPauliEvolutionAnsatz { + pub inner: PauliEvolutionAnsatz, +} + +/// Creates a Pauli evolution ansatz from a Hamiltonian (cloned and +/// simplified in the process). Returns NULL on NULL input or an empty +/// operator. +#[unsafe(no_mangle)] +pub extern "C" fn pauli_evolution_ansatz_new( + hamiltonian: *const CHamiltonian, +) -> *mut CPauliEvolutionAnsatz { + if hamiltonian.is_null() { + return std::ptr::null_mut(); + } + match PauliEvolutionAnsatz::new(unsafe { (*hamiltonian).inner.clone() }) { + Ok(inner) => Box::into_raw(Box::new(CPauliEvolutionAnsatz { inner })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Frees a Pauli evolution ansatz handle. Passing NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn pauli_evolution_ansatz_free(ptr: *mut CPauliEvolutionAnsatz) { + if !ptr.is_null() { + unsafe { + drop(Box::from_raw(ptr)); + } + } +} + +/// Overrides the time-parameter symbol used by the evolution lowering; an +/// empty name resets it to the default. Returns 0, -1 on NULL input, -4 on +/// invalid UTF-8. +#[unsafe(no_mangle)] +pub extern "C" fn pauli_evolution_ansatz_with_time_param_name( + ptr: *mut CPauliEvolutionAnsatz, + name: *const c_char, +) -> i32 { + if ptr.is_null() { + return -1; + } + match parse_prefix(name) { + Ok(name) => { + let inner = unsafe { &mut (*ptr).inner }; + *inner = inner.clone().with_time_param_name(name); + 0 + } + Err(code) => code, + } +} + +/// C snapshot of the core `EvolutionInfo` returned by +/// [`PauliEvolutionAnsatz::evolution_info`]. +#[repr(C)] +pub struct CPauliEvolutionInfo { + /// 1 when the decomposition is mathematically exact, 0 otherwise. + pub is_exact: u8, + /// 1 when all Hamiltonian terms mutually commute, 0 otherwise. + pub all_terms_commute: u8, + /// Reserved padding to keep the struct layout stable. + pub _pad: [u8; 6], + /// Number of decomposition repetitions emitted into the circuit. + pub steps: usize, + /// Number of Pauli terms in the (simplified) Hamiltonian. + pub num_terms: usize, + /// One of `TROTTER_FIRST_ORDER`, `TROTTER_SECOND_ORDER` or + /// `TROTTER_RANDOMIZED`, or `TROTTER_MODE_NONE` when the single-pass + /// exact path was used. + pub trotter_mode: i32, + /// Seed for `TROTTER_RANDOMIZED`; 0 otherwise. + pub trotter_seed: u64, +} + +/// Reads the evolution strategy info (the C mirror of +/// [`PauliEvolutionAnsatz::evolution_info`]) into `out`. +/// +/// `trotter_mode` is `TROTTER_MODE_NONE` when the single-pass exact path was +/// used; otherwise it is one of the `TROTTER_*` tags, and `trotter_seed` is +/// only meaningful for `TROTTER_RANDOMIZED`. Returns 0 on success, or -1 on +/// NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn pauli_evolution_ansatz_evolution_info( + ptr: *const CPauliEvolutionAnsatz, + out: *mut CPauliEvolutionInfo, +) -> i32 { + if ptr.is_null() || out.is_null() { + return -1; + } + let info = unsafe { &(*ptr).inner }.evolution_info(); + let trotter_mode = match info.trotter_mode { + None => TROTTER_MODE_NONE, + Some(TrotterMode::FirstOrder) => TROTTER_FIRST_ORDER, + Some(TrotterMode::SecondOrder) => TROTTER_SECOND_ORDER, + Some(TrotterMode::Randomized(seed)) => { + unsafe { + (*out).trotter_seed = seed; + } + TROTTER_RANDOMIZED + } + }; + unsafe { + (*out).is_exact = info.is_exact as u8; + (*out).all_terms_commute = info.all_terms_commute as u8; + (*out).steps = info.steps; + (*out).num_terms = info.num_terms; + (*out).trotter_mode = trotter_mode; + } + 0 +} + +/// Opaque handle around a [`BasicEntanglerLayers`] template. +pub struct CBasicEntanglerLayers { + pub inner: BasicEntanglerLayers, +} + +/// Creates a BasicEntanglerLayers template with defaults: `reps = 1`, +/// rotation `RX`, entangler `CX`. +#[unsafe(no_mangle)] +pub extern "C" fn basic_entangler_layers_new(num_qubits: usize) -> *mut CBasicEntanglerLayers { + Box::into_raw(Box::new(CBasicEntanglerLayers { + inner: BasicEntanglerLayers::new(num_qubits), + })) +} + +/// Frees a BasicEntanglerLayers handle. Passing NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn basic_entangler_layers_free(ptr: *mut CBasicEntanglerLayers) { + if !ptr.is_null() { + unsafe { + drop(Box::from_raw(ptr)); + } + } +} + +/// Sets the number of layers. Returns 0, or -1 on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn basic_entangler_layers_reps(ptr: *mut CBasicEntanglerLayers, reps: usize) -> i32 { + if ptr.is_null() { + return -1; + } + let inner = unsafe { &mut (*ptr).inner }; + *inner = inner.clone().reps(reps); + 0 +} + +/// Sets the rotation gate by name (RX, RY, RZ or Phase). Returns 0, -1 on +/// NULL input, -4 on invalid UTF-8, -8 on an unknown gate name. +#[unsafe(no_mangle)] +pub extern "C" fn basic_entangler_layers_rotation_gate( + ptr: *mut CBasicEntanglerLayers, + name: *const c_char, +) -> i32 { + if ptr.is_null() { + return -1; + } + match parse_gate_name(name) { + Ok(gate) => { + let inner = unsafe { &mut (*ptr).inner }; + *inner = inner.clone().rotation_gate(gate); + 0 + } + Err(code) => code, + } +} + +/// Sets the entanglement gate by name (CX, CY or CZ). Returns 0, -1 on NULL +/// input, -4 on invalid UTF-8, -8 on an unknown gate name. +#[unsafe(no_mangle)] +pub extern "C" fn basic_entangler_layers_entanglement_gate( + ptr: *mut CBasicEntanglerLayers, + name: *const c_char, +) -> i32 { + if ptr.is_null() { + return -1; + } + match parse_gate_name(name) { + Ok(gate) => { + let inner = unsafe { &mut (*ptr).inner }; + *inner = inner.clone().entanglement_gate(gate); + 0 + } + Err(code) => code, + } +} + +/// Returns the number of independent parameters, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn basic_entangler_layers_num_parameters( + ptr: *const CBasicEntanglerLayers, +) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.num_parameters() } +} + +/// Returns the number of qubits, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn basic_entangler_layers_num_qubits(ptr: *const CBasicEntanglerLayers) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.num_qubits() } +} + +/// Builds the parameterized circuit (parameters named `"{prefix}_{index}"`). +/// Returns a newly allocated `CCircuit*`, or NULL on NULL input, invalid +/// UTF-8 or build failure. +#[unsafe(no_mangle)] +pub extern "C" fn basic_entangler_layers_build_circuit( + ptr: *const CBasicEntanglerLayers, + prefix: *const c_char, +) -> *mut CCircuit { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let prefix = match parse_prefix(prefix) { + Ok(prefix) => prefix, + Err(_) => return std::ptr::null_mut(), + }; + match unsafe { (*ptr).inner.build_circuit(&prefix) } { + Ok(circuit) => Box::into_raw(Box::new(CCircuit { inner: circuit })), + Err(_) => std::ptr::null_mut(), + } +} + +// ===== QAOAAnsatz ===== + +/// Opaque handle around a [`QAOAAnsatz`] template. +pub struct CQAOAAnsatz { + pub inner: QAOAAnsatz, +} + +/// Creates a QAOA ansatz from a cost Hamiltonian (cloned). The number of +/// qubits is inferred from the operator and the mixer defaults to the +/// standard X-mixer with `reps = 1`. Returns NULL on NULL input or an +/// invalid cost operator. +#[unsafe(no_mangle)] +pub extern "C" fn qaoa_ansatz_new(cost: *const CHamiltonian) -> *mut CQAOAAnsatz { + if cost.is_null() { + return std::ptr::null_mut(); + } + match QAOAAnsatz::new(unsafe { (*cost).inner.clone() }) { + Ok(inner) => Box::into_raw(Box::new(CQAOAAnsatz { inner })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Frees a QAOA ansatz handle. Passing NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn qaoa_ansatz_free(ptr: *mut CQAOAAnsatz) { + if !ptr.is_null() { + unsafe { + drop(Box::from_raw(ptr)); + } + } +} + +/// Sets the number of alternating layers (depth p). Returns 0, or -1 on +/// NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn qaoa_ansatz_reps(ptr: *mut CQAOAAnsatz, reps: usize) -> i32 { + if ptr.is_null() { + return -1; + } + let inner = unsafe { &mut (*ptr).inner }; + *inner = inner.clone().reps(reps); + 0 +} + +/// Overrides the mixer Hamiltonian (cloned; must match the cost operator's +/// qubit count). Returns 0, -1 on NULL input, -3 on a qubit-count mismatch +/// or invalid mixer. +#[unsafe(no_mangle)] +pub extern "C" fn qaoa_ansatz_mixer(ptr: *mut CQAOAAnsatz, mixer: *const CHamiltonian) -> i32 { + if ptr.is_null() { + return -1; + } + if mixer.is_null() { + return -1; + } + let mixer = unsafe { (*mixer).inner.clone() }; + let inner = unsafe { &mut (*ptr).inner }; + match inner.clone().mixer(mixer) { + Ok(ansatz) => { + *inner = ansatz; + 0 + } + Err(_) => -3, + } +} + +/// Prepends an initial-state circuit (cloned; must match the qubit count). +/// Returns 0, -1 on NULL input, -3 on a qubit-count mismatch. +#[unsafe(no_mangle)] +pub extern "C" fn qaoa_ansatz_initial_state( + ptr: *mut CQAOAAnsatz, + circuit: *const CCircuit, +) -> i32 { + if ptr.is_null() || circuit.is_null() { + return -1; + } + let circuit = unsafe { (*circuit).inner.clone() }; + let inner = unsafe { &mut (*ptr).inner }; + match inner.clone().initial_state(circuit) { + Ok(ansatz) => { + *inner = ansatz; + 0 + } + Err(_) => -3, + } +} + +/// Sets the Hamiltonian evolution strategy. +/// +/// `EVOLUTION_STRATEGY_EXACT` ignores the remaining arguments; +/// `EVOLUTION_STRATEGY_AUTO` uses `steps`; `EVOLUTION_STRATEGY_TROTTER` +/// uses `mode` (`TROTTER_FIRST_ORDER`, `TROTTER_SECOND_ORDER` or +/// `TROTTER_RANDOMIZED`), `steps` and `seed`. Returns 0, -1 on NULL input, +/// -8 on an unknown tag. +#[unsafe(no_mangle)] +pub extern "C" fn qaoa_ansatz_evolution_strategy( + ptr: *mut CQAOAAnsatz, + strategy: i32, + mode: i32, + steps: usize, + seed: u64, +) -> i32 { + if ptr.is_null() { + return -1; + } + let evolution = match strategy { + EVOLUTION_STRATEGY_EXACT => EvolutionStrategy::Exact, + EVOLUTION_STRATEGY_AUTO => EvolutionStrategy::Auto { steps }, + EVOLUTION_STRATEGY_TROTTER => { + let mode = match mode { + TROTTER_FIRST_ORDER => TrotterMode::FirstOrder, + TROTTER_SECOND_ORDER => TrotterMode::SecondOrder, + TROTTER_RANDOMIZED => TrotterMode::Randomized(seed), + _ => return -8, + }; + EvolutionStrategy::Trotter { mode, steps } + } + _ => return -8, + }; + let inner = unsafe { &mut (*ptr).inner }; + *inner = inner.clone().evolution_strategy(evolution); + 0 +} + +/// Validates the ansatz configuration. Returns 0 on success, -1 on NULL +/// input, -3 on invalid configuration. +#[unsafe(no_mangle)] +pub extern "C" fn qaoa_ansatz_validate(ptr: *const CQAOAAnsatz) -> i32 { + if ptr.is_null() { + return -1; + } + unsafe { (*ptr).inner.validate().map_or(-3, |_| 0) } +} + +/// Returns the number of parameters (2 per layer), or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn qaoa_ansatz_num_parameters(ptr: *const CQAOAAnsatz) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.num_parameters() } +} + +/// Returns the number of qubits, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn qaoa_ansatz_num_qubits(ptr: *const CQAOAAnsatz) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.num_qubits() } +} + +/// Builds the parameterized circuit (parameters named +/// `"{prefix}_gamma_{layer}"` / `"{prefix}_beta_{layer}"`). Returns a newly +/// allocated `CCircuit*`, or NULL on NULL input, invalid UTF-8 or failure. +#[unsafe(no_mangle)] +pub extern "C" fn qaoa_ansatz_build_circuit( + ptr: *const CQAOAAnsatz, + prefix: *const c_char, +) -> *mut CCircuit { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let prefix = match parse_prefix(prefix) { + Ok(prefix) => prefix, + Err(_) => return std::ptr::null_mut(), + }; + match unsafe { (*ptr).inner.build_circuit(&prefix) } { + Ok(circuit) => Box::into_raw(Box::new(CCircuit { inner: circuit })), + Err(_) => std::ptr::null_mut(), + } +} diff --git a/crates/binding-c/src/circuit/cfg.rs b/crates/binding-c/src/circuit/cfg.rs new file mode 100644 index 00000000..4f39a169 --- /dev/null +++ b/crates/binding-c/src/circuit/cfg.rs @@ -0,0 +1,1301 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! C ABI for structured control-flow graphs ([`CircuitCFG`]). +//! +//! The CFG is a graph view of a circuit: every conditional, loop, and switch +//! header owns a control-flow region recording body entries, merge blocks, +//! and outer operation metadata. Typical usage is +//! `circuit_cfg_from_circuit` -> inspect blocks/terminators/edges/regions -> +//! optionally rebuild -> `circuit_cfg_to_circuit`. +//! +//! Blocks are addressed by stable node indices (u32). `UINT32_MAX` is the +//! "missing block / missing entry" sentinel. Classical conditions are +//! exchanged through [`CClassicalExpr`] / [`CClassicalVar`] handles from the +//! `classical` module. + +#![allow(clippy::not_unsafe_ptr_arg_deref)] + +use crate::circuit::CCircuit; +use crate::circuit::classical::classical_type_parts; +use crate::circuit::classical::{CClassicalExpr, CClassicalType, CClassicalVar}; +use crate::circuit::operations::COperation; +use cqlib_core::circuit::Qubit; +use cqlib_core::circuit::cfg::{ + BasicBlock, CircuitCFG, ControlFlowRegion, FlowEdge, OperationMetadata, SwitchRegionCase, + Terminator, +}; +use rustworkx_core::petgraph::prelude::NodeIndex; +use std::ffi::{CStr, CString}; +use std::os::raw::c_char; + +/// Opaque handle around a [`CircuitCFG`]. +pub struct CCircuitCFG { + pub inner: CircuitCFG, +} + +// ===== Flow-edge tags ===== + +/// Outgoing-edge tag: true edge out of `if` / `while` / `for` headers. +pub const CFG_FLOW_TRUE_BRANCH: u32 = 1; +/// Outgoing-edge tag: false edge out of `if` / `while` / `for` headers. +pub const CFG_FLOW_FALSE_BRANCH: u32 = 2; +/// Outgoing-edge tag: fallthrough jump / structured merge. +pub const CFG_FLOW_UNCONDITIONAL: u32 = 3; +/// Outgoing-edge tag: exact-value switch case (payload in case value halves). +pub const CFG_FLOW_CASE: u32 = 4; +/// Outgoing-edge tag: switch default edge. +pub const CFG_FLOW_DEFAULT_CASE: u32 = 5; +/// Outgoing-edge tag: structured `break` edge. +pub const CFG_FLOW_BREAK: u32 = 6; +/// Outgoing-edge tag: structured `continue` edge. +pub const CFG_FLOW_CONTINUE: u32 = 7; + +// ===== Terminator tags ===== + +/// Terminator tag: boolean branch header (`Branch`). +pub const CFG_TERMINATOR_BRANCH: u32 = 1; +/// Terminator tag: unsigned range loop header (`ForLoop`). +pub const CFG_TERMINATOR_FOR_LOOP: u32 = 2; +/// Terminator tag: exact-value multi-way branch header (`Switch`). +pub const CFG_TERMINATOR_SWITCH: u32 = 3; +/// Terminator tag: unconditional jump. +pub const CFG_TERMINATOR_JUMP: u32 = 4; +/// Terminator tag: structured break. +pub const CFG_TERMINATOR_BREAK: u32 = 5; +/// Terminator tag: structured continue. +pub const CFG_TERMINATOR_CONTINUE: u32 = 6; +/// Terminator tag: end of execution. +pub const CFG_TERMINATOR_RETURN: u32 = 7; + +// ===== Region tags ===== + +/// Region tag: structured conditional region. +pub const CFG_REGION_IF: u32 = 1; +/// Region tag: structured while-loop region. +pub const CFG_REGION_WHILE: u32 = 2; +/// Region tag: structured range-loop region. +pub const CFG_REGION_FOR: u32 = 3; +/// Region tag: structured exact-value switch region. +pub const CFG_REGION_SWITCH: u32 = 4; + +// ===== Internal helpers ===== + +/// Validates a C block index and converts it to a stable graph index. +fn node_index(node: u32) -> Option { + if node == u32::MAX { + return None; + } + Some(NodeIndex::new(node as usize)) +} + +/// Looks up a block by C node index (read-only). +fn find_block(cfg: &CircuitCFG, node: u32) -> Option<&BasicBlock> { + let index = node_index(node)?; + cfg.blocks() + .find_map(|(i, block)| (i == index).then_some(block)) +} + +/// Converts a flow-edge tag plus packed case value into a core edge weight. +fn flow_from_tag(tag: u32, case_lo: u64, case_hi: u64) -> Option { + match tag { + CFG_FLOW_TRUE_BRANCH => Some(FlowEdge::TrueBranch), + CFG_FLOW_FALSE_BRANCH => Some(FlowEdge::FalseBranch), + CFG_FLOW_UNCONDITIONAL => Some(FlowEdge::Unconditional), + CFG_FLOW_CASE => Some(FlowEdge::Case( + u128::from(case_lo) | (u128::from(case_hi) << 64), + )), + CFG_FLOW_DEFAULT_CASE => Some(FlowEdge::DefaultCase), + CFG_FLOW_BREAK => Some(FlowEdge::Break), + CFG_FLOW_CONTINUE => Some(FlowEdge::Continue), + _ => None, + } +} + +/// Converts a core edge weight into a tag plus packed case value halves. +fn flow_parts(flow: &FlowEdge) -> (u32, u64, u64) { + match flow { + FlowEdge::TrueBranch => (CFG_FLOW_TRUE_BRANCH, 0, 0), + FlowEdge::FalseBranch => (CFG_FLOW_FALSE_BRANCH, 0, 0), + FlowEdge::Unconditional => (CFG_FLOW_UNCONDITIONAL, 0, 0), + FlowEdge::Case(value) => (CFG_FLOW_CASE, *value as u64, (*value >> 64) as u64), + FlowEdge::DefaultCase => (CFG_FLOW_DEFAULT_CASE, 0, 0), + FlowEdge::Break => (CFG_FLOW_BREAK, 0, 0), + FlowEdge::Continue => (CFG_FLOW_CONTINUE, 0, 0), + } +} + +/// Maps a terminator to its C tag. +fn terminator_tag(terminator: &Terminator) -> u32 { + match terminator { + Terminator::Branch(_) => CFG_TERMINATOR_BRANCH, + Terminator::ForLoop { .. } => CFG_TERMINATOR_FOR_LOOP, + Terminator::Switch(_) => CFG_TERMINATOR_SWITCH, + Terminator::Jump(_) => CFG_TERMINATOR_JUMP, + Terminator::Break(_) => CFG_TERMINATOR_BREAK, + Terminator::Continue(_) => CFG_TERMINATOR_CONTINUE, + Terminator::Return => CFG_TERMINATOR_RETURN, + } +} + +/// Maps a control-flow region to its C tag. +fn region_tag(region: &ControlFlowRegion) -> u32 { + match region { + ControlFlowRegion::If { .. } => CFG_REGION_IF, + ControlFlowRegion::While { .. } => CFG_REGION_WHILE, + ControlFlowRegion::For { .. } => CFG_REGION_FOR, + ControlFlowRegion::Switch { .. } => CFG_REGION_SWITCH, + } +} + +/// Extracts outer operation metadata, or empty metadata for NULL. +fn outer_metadata(outer: *const COperation) -> OperationMetadata { + if outer.is_null() { + return OperationMetadata { + qubits: Default::default(), + params: Default::default(), + label: None, + }; + } + let op = unsafe { &(*outer).inner }; + OperationMetadata { + qubits: op.qubits.clone(), + params: op.params.clone(), + label: op.label.clone(), + } +} + +// ===== Lifecycle and round trip ===== + +/// Creates an empty CFG with densely numbered qubits `0..num_qubits`. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_cfg_new(num_qubits: usize) -> *mut CCircuitCFG { + Box::into_raw(Box::new(CCircuitCFG { + inner: CircuitCFG::new(num_qubits), + })) +} + +/// Creates an empty CFG with the supplied logical qubit ids in insertion +/// order. Returns a newly allocated `CCircuitCFG*` (free with +/// `circuit_cfg_free`), or NULL on NULL `qubits` when `qubits_len > 0`. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_cfg_from_qubits( + qubits: *const u32, + qubits_len: usize, +) -> *mut CCircuitCFG { + if qubits_len > 0 && qubits.is_null() { + return std::ptr::null_mut(); + } + let list = if qubits_len == 0 { + Vec::new() + } else { + unsafe { std::slice::from_raw_parts(qubits, qubits_len) } + .iter() + .map(|&id| Qubit::new(id)) + .collect() + }; + Box::into_raw(Box::new(CCircuitCFG { + inner: CircuitCFG::from_qubits(list), + })) +} + +/// Expands a structured circuit into a validated CFG. Returns a newly +/// allocated `CCircuitCFG*` (free with `circuit_cfg_free`), or NULL on NULL +/// input or build failure. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_cfg_from_circuit(ptr: *const CCircuit) -> *mut CCircuitCFG { + if ptr.is_null() { + return std::ptr::null_mut(); + } + match CircuitCFG::from_circuit(unsafe { &(*ptr).inner }) { + Ok(inner) => Box::into_raw(Box::new(CCircuitCFG { inner })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Frees a CFG handle. Passing NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_cfg_free(ptr: *mut CCircuitCFG) { + if !ptr.is_null() { + unsafe { + drop(Box::from_raw(ptr)); + } + } +} + +/// Reconstructs a structured circuit from this CFG. Returns a newly +/// allocated `CCircuit*` (free with `circuit_free`), or NULL on NULL input +/// or lowering failure. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_cfg_to_circuit(ptr: *const CCircuitCFG) -> *mut CCircuit { + if ptr.is_null() { + return std::ptr::null_mut(); + } + match unsafe { (*ptr).inner.to_circuit() } { + Ok(inner) => Box::into_raw(Box::new(CCircuit { inner })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Checks graph, operation, parameter, edge, terminator, and region +/// invariants. Returns 0 on success, -1 on NULL input, -3 on validation +/// failure. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_cfg_validate(ptr: *const CCircuitCFG) -> i32 { + if ptr.is_null() { + return -1; + } + unsafe { (*ptr).inner.validate().map_or(-3, |_| 0) } +} + +// ===== Global properties ===== + +/// Returns the number of logical qubits, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_cfg_num_qubits(ptr: *const CCircuitCFG) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.num_qubits() } +} + +/// Returns the number of blocks currently stored in the graph, or 0 for +/// NULL. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_cfg_num_blocks(ptr: *const CCircuitCFG) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.num_blocks() } +} + +/// Copies the logical qubit ids in insertion order into `buffer`. Returns 0 +/// on success, -1 on NULL input, -8 when `len` is smaller than +/// `circuit_cfg_num_qubits`. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_cfg_qubits(ptr: *const CCircuitCFG, buffer: *mut u32, len: usize) -> i32 { + if ptr.is_null() { + return -1; + } + let qubits = unsafe { (*ptr).inner.qubits() }; + if len < qubits.len() { + return -8; + } + if qubits.is_empty() { + return 0; + } + if buffer.is_null() { + return -1; + } + let slice = unsafe { std::slice::from_raw_parts_mut(buffer, qubits.len()) }; + for (slot, qubit) in slice.iter_mut().zip(qubits.iter()) { + *slot = qubit.id(); + } + 0 +} + +/// Returns the number of mutable classical variables in the static type +/// table, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_cfg_classical_vars_len(ptr: *const CCircuitCFG) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.classical_vars().len() } +} + +/// Copies the mutable classical variable types as (tag, width) snapshots +/// into `buffer`. Returns 0 on success, -1 on NULL input, -8 when `len` is +/// too small. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_cfg_classical_vars( + ptr: *const CCircuitCFG, + buffer: *mut CClassicalType, + len: usize, +) -> i32 { + if ptr.is_null() { + return -1; + } + let types = unsafe { (*ptr).inner.classical_vars() }; + if len < types.len() { + return -8; + } + if types.is_empty() { + return 0; + } + if buffer.is_null() { + return -1; + } + for (i, ty) in types.iter().enumerate() { + let (tag, width) = classical_type_parts(*ty); + unsafe { + *buffer.add(i) = CClassicalType { tag, width }; + } + } + 0 +} + +/// Returns the number of immutable classical values in the static type +/// table, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_cfg_classical_values_len(ptr: *const CCircuitCFG) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.classical_values().len() } +} + +/// Copies the immutable classical value types as (tag, width) snapshots +/// into `buffer`. Returns 0 on success, -1 on NULL input, -8 when `len` is +/// too small. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_cfg_classical_values( + ptr: *const CCircuitCFG, + buffer: *mut CClassicalType, + len: usize, +) -> i32 { + if ptr.is_null() { + return -1; + } + let types = unsafe { (*ptr).inner.classical_values() }; + if len < types.len() { + return -8; + } + if types.is_empty() { + return 0; + } + if buffer.is_null() { + return -1; + } + for (i, ty) in types.iter().enumerate() { + let (tag, width) = classical_type_parts(*ty); + unsafe { + *buffer.add(i) = CClassicalType { tag, width }; + } + } + 0 +} + +/// Returns the designated entry block, or `UINT32_MAX` when none has been +/// set or the handle is NULL. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_cfg_entry_block(ptr: *const CCircuitCFG) -> u32 { + if ptr.is_null() { + return u32::MAX; + } + unsafe { + (*ptr) + .inner + .entry_block() + .map_or(u32::MAX, |i| i.index() as u32) + } +} + +// ===== Construction ===== + +/// Adds an empty block and returns its stable node index, or `UINT32_MAX` +/// on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_cfg_add_block(ptr: *mut CCircuitCFG) -> u32 { + if ptr.is_null() { + return u32::MAX; + } + let wrapper = unsafe { &mut *ptr }; + wrapper.inner.add_block(BasicBlock::new()).index() as u32 +} + +/// Adds a directed semantic edge between existing blocks. `flow` is one of +/// the `CFG_FLOW_*` tags; for `CFG_FLOW_CASE` the 128-bit case value is +/// passed as two 64-bit halves (lo least-significant). Returns 0 on +/// success, -1 on NULL input, -2 when either endpoint does not exist, -8 +/// on an unknown flow tag. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_cfg_add_edge( + ptr: *mut CCircuitCFG, + source: u32, + target: u32, + flow: u32, + case_lo: u64, + case_hi: u64, +) -> i32 { + if ptr.is_null() { + return -1; + } + let Some(flow) = flow_from_tag(flow, case_lo, case_hi) else { + return -8; + }; + let Some(source) = node_index(source) else { + return -2; + }; + let Some(target) = node_index(target) else { + return -2; + }; + let wrapper = unsafe { &mut *ptr }; + match wrapper.inner.add_edge(source, target, flow) { + Some(_) => 0, + None => -2, + } +} + +/// Sets the graph entry block. The index is only checked by +/// `circuit_cfg_validate`. Returns 0 on success, -1 on NULL input, -2 on +/// the sentinel index. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_cfg_set_entry_block(ptr: *mut CCircuitCFG, node: u32) -> i32 { + if ptr.is_null() { + return -1; + } + let Some(index) = node_index(node) else { + return -2; + }; + let wrapper = unsafe { &mut *ptr }; + wrapper.inner.set_entry_block(index); + 0 +} + +/// Appends one ordinary (non-control-flow) operation to a block. Returns 0 +/// on success, -1 on NULL input or NULL `op`, -2 when the block does not +/// exist. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_cfg_push_operation( + ptr: *mut CCircuitCFG, + node: u32, + op: *const COperation, +) -> i32 { + if ptr.is_null() || op.is_null() { + return -1; + } + let Some(index) = node_index(node) else { + return -2; + }; + let operation = unsafe { (*op).inner.clone() }; + let wrapper = unsafe { &mut *ptr }; + match wrapper.inner.block_mut(index) { + Some(block) => { + block.push_operation(operation); + 0 + } + None => -2, + } +} + +/// Appends a batch of cloned operations to the end of block `node`. +/// +/// Every handle in `ops` is validated and cloned before any mutation, so the +/// block is left untouched when any entry is NULL. Returns 0 on success, +/// -1 on NULL input, -2 when `node` does not address a block. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_cfg_extend_operations( + ptr: *mut CCircuitCFG, + node: u32, + ops: *const *const COperation, + len: usize, +) -> i32 { + if ptr.is_null() { + return -1; + } + if len > 0 && ops.is_null() { + return -1; + } + let slice = if len == 0 { + &[][..] + } else { + unsafe { std::slice::from_raw_parts(ops, len) } + }; + let mut batch = Vec::with_capacity(len); + for &op in slice { + if op.is_null() { + return -1; + } + batch.push(unsafe { (*op).inner.clone() }); + } + let Some(index) = node_index(node) else { + return -2; + }; + let wrapper = unsafe { &mut *ptr }; + match wrapper.inner.block_mut(index) { + Some(block) => { + block.extend_operations(batch); + 0 + } + None => -2, + } +} + +/// Sets or replaces the label of block `node`. +/// +/// Returns 0 on success, -1 on NULL input, -2 when `node` does not address +/// a block, -4 when `label` is not valid UTF-8. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_cfg_with_label( + ptr: *mut CCircuitCFG, + node: u32, + label: *const c_char, +) -> i32 { + if ptr.is_null() || label.is_null() { + return -1; + } + let text = match unsafe { CStr::from_ptr(label) }.to_str() { + Ok(text) => text, + Err(_) => return -4, + }; + let Some(index) = node_index(node) else { + return -2; + }; + let wrapper = unsafe { &mut *ptr }; + match wrapper.inner.block_mut(index) { + Some(block) => { + let labeled = block.clone().with_label(text); + *block = labeled; + 0 + } + None => -2, + } +} + +// ===== Block enumeration and inspection ===== + +/// Copies the stable node indices of all blocks in graph order into +/// `buffer`. Returns 0 on success, -1 on NULL input, -8 when `len` is +/// smaller than `circuit_cfg_num_blocks`. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_cfg_blocks(ptr: *const CCircuitCFG, buffer: *mut u32, len: usize) -> i32 { + if ptr.is_null() { + return -1; + } + let wrapper = unsafe { &*ptr }; + let count = wrapper.inner.num_blocks(); + if len < count { + return -8; + } + if count == 0 { + return 0; + } + if buffer.is_null() { + return -1; + } + for (i, (index, _)) in wrapper.inner.blocks().enumerate() { + unsafe { + *buffer.add(i) = index.index() as u32; + } + } + 0 +} + +/// Returns the block's diagnostic label as a newly allocated C string +/// (free with `cqlib_string_free`), NULL when the block is unlabeled, does +/// not exist, or the handle is NULL. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_cfg_block_label(ptr: *const CCircuitCFG, node: u32) -> *mut c_char { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let wrapper = unsafe { &*ptr }; + let Some(block) = find_block(&wrapper.inner, node) else { + return std::ptr::null_mut(); + }; + match block.label().and_then(|label| CString::new(label).ok()) { + Some(text) => text.into_raw(), + None => std::ptr::null_mut(), + } +} + +/// Returns whether the block has neither operations nor a terminator: 1 +/// when empty, 0 otherwise, -1 on NULL input, -2 when the block does not +/// exist. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_cfg_block_is_empty(ptr: *const CCircuitCFG, node: u32) -> i32 { + if ptr.is_null() { + return -1; + } + let wrapper = unsafe { &*ptr }; + match find_block(&wrapper.inner, node) { + Some(block) => block.is_empty() as i32, + None => -2, + } +} + +/// Returns the number of ordinary operations (terminator excluded) in the +/// block, or 0 on NULL input or when the block does not exist. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_cfg_block_operations_len(ptr: *const CCircuitCFG, node: u32) -> usize { + if ptr.is_null() { + return 0; + } + let wrapper = unsafe { &*ptr }; + find_block(&wrapper.inner, node).map_or(0, |block| block.len()) +} + +/// Clones the block's ordinary operations into `out`: each written element +/// is a freshly allocated `COperation*` freed separately with +/// `operation_free`. Returns 0 on success, -1 on NULL input, -2 when the +/// block does not exist, -8 when `len` is too small. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_cfg_block_operations( + ptr: *const CCircuitCFG, + node: u32, + out: *mut *mut COperation, + len: usize, +) -> i32 { + if ptr.is_null() { + return -1; + } + let wrapper = unsafe { &*ptr }; + let Some(block) = find_block(&wrapper.inner, node) else { + return -2; + }; + let operations = block.operations(); + if len < operations.len() { + return -8; + } + if operations.is_empty() { + return 0; + } + if out.is_null() { + return -1; + } + for (i, operation) in operations.iter().enumerate() { + let handle = Box::into_raw(Box::new(COperation { + inner: operation.clone(), + })); + unsafe { + *out.add(i) = handle; + } + } + 0 +} + +/// Returns whether a terminator has been assigned: 1 when assigned, 0 +/// when not, -1 on NULL input, -2 when the block does not exist. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_cfg_block_has_terminator(ptr: *const CCircuitCFG, node: u32) -> i32 { + if ptr.is_null() { + return -1; + } + let wrapper = unsafe { &*ptr }; + match find_block(&wrapper.inner, node) { + Some(block) => block.has_terminator() as i32, + None => -2, + } +} + +/// Returns the block terminator tag: one of the `CFG_TERMINATOR_*` +/// constants, 0 when the block has no terminator, -1 on NULL input, -2 +/// when the block does not exist. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_cfg_block_terminator_tag(ptr: *const CCircuitCFG, node: u32) -> i32 { + if ptr.is_null() { + return -1; + } + let wrapper = unsafe { &*ptr }; + match find_block(&wrapper.inner, node) { + Some(block) => block + .terminator() + .map_or(0, |terminator| terminator_tag(terminator) as i32), + None => -2, + } +} + +// ===== Terminator detail queries ===== + +/// Returns the jump target of a `Jump`, `Break`, or `Continue` terminator, +/// or `UINT32_MAX` for other terminators, missing terminators, NULL +/// input, or unknown blocks. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_cfg_terminator_target(ptr: *const CCircuitCFG, node: u32) -> u32 { + if ptr.is_null() { + return u32::MAX; + } + let wrapper = unsafe { &*ptr }; + let Some(block) = find_block(&wrapper.inner, node) else { + return u32::MAX; + }; + match block.terminator() { + Some(Terminator::Jump(target)) + | Some(Terminator::Break(target)) + | Some(Terminator::Continue(target)) => target.index() as u32, + _ => u32::MAX, + } +} + +/// Clones the condition of a `Branch` or `Switch` terminator into a newly +/// allocated `CClassicalExpr*` (free with `classical_expr_free`). Returns +/// NULL on NULL input, unknown block, missing terminator, or non-branching +/// terminators. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_cfg_terminator_condition( + ptr: *const CCircuitCFG, + node: u32, +) -> *mut CClassicalExpr { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let wrapper = unsafe { &*ptr }; + let Some(block) = find_block(&wrapper.inner, node) else { + return std::ptr::null_mut(); + }; + match block.terminator() { + Some(Terminator::Branch(condition)) | Some(Terminator::Switch(condition)) => { + Box::into_raw(Box::new(CClassicalExpr { + inner: condition.clone(), + })) + } + _ => std::ptr::null_mut(), + } +} + +/// Writes cloned handles describing a `ForLoop` terminator: the loop +/// variable plus the start, stop, and step expressions. `var` is released +/// with `classical_var_free` and the expressions with +/// `classical_expr_free`. Returns 0 on success, -1 on NULL input, -2 when +/// the block does not exist, -8 when the terminator is not a loop header. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_cfg_terminator_for_info( + ptr: *const CCircuitCFG, + node: u32, + var: *mut *mut CClassicalVar, + start: *mut *mut CClassicalExpr, + stop: *mut *mut CClassicalExpr, + step: *mut *mut CClassicalExpr, +) -> i32 { + if ptr.is_null() { + return -1; + } + let wrapper = unsafe { &*ptr }; + let Some(block) = find_block(&wrapper.inner, node) else { + return -2; + }; + let Some(Terminator::ForLoop { + var: loop_var, + start: start_expr, + stop: stop_expr, + step: step_expr, + }) = block.terminator() + else { + return -8; + }; + if var.is_null() || start.is_null() || stop.is_null() || step.is_null() { + return -1; + } + unsafe { + *var = Box::into_raw(Box::new(CClassicalVar { inner: *loop_var })); + *start = Box::into_raw(Box::new(CClassicalExpr { + inner: start_expr.clone(), + })); + *stop = Box::into_raw(Box::new(CClassicalExpr { + inner: stop_expr.clone(), + })); + *step = Box::into_raw(Box::new(CClassicalExpr { + inner: step_expr.clone(), + })); + } + 0 +} + +// ===== Edge queries ===== + +/// Returns the number of outgoing edges of a block, or 0 on NULL input or +/// when the block does not exist. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_cfg_outgoing_edges_len(ptr: *const CCircuitCFG, node: u32) -> usize { + if ptr.is_null() { + return 0; + } + let wrapper = unsafe { &*ptr }; + let Some(index) = node_index(node) else { + return 0; + }; + if find_block(&wrapper.inner, node).is_none() { + return 0; + } + wrapper.inner.outgoing_edges(index).count() +} + +/// Copies the outgoing edges of a block into parallel arrays: `targets` +/// receives node indices, `flow_tags` receives `CFG_FLOW_*` tags, and +/// `case_lo` / `case_hi` receive the 128-bit case value for +/// `CFG_FLOW_CASE` edges (zeros otherwise). Returns 0 on success, -1 on +/// NULL input, -2 when the block does not exist, -8 when `len` is too +/// small. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_cfg_outgoing_edges( + ptr: *const CCircuitCFG, + node: u32, + out_targets: *mut u32, + flow_tags: *mut u32, + case_lo: *mut u64, + case_hi: *mut u64, + len: usize, +) -> i32 { + if ptr.is_null() { + return -1; + } + let wrapper = unsafe { &*ptr }; + let Some(index) = node_index(node) else { + return -2; + }; + if find_block(&wrapper.inner, node).is_none() { + return -2; + } + let mut edges: Vec<(u32, u32, u64, u64)> = wrapper + .inner + .outgoing_edges(index) + .map(|(target, flow)| { + let (tag, lo, hi) = flow_parts(&flow); + (target.index() as u32, tag, lo, hi) + }) + .collect(); + // petgraph iterates a node's edges in reverse insertion order; reverse + // back so edges are written in insertion order (TrueBranch before + // FalseBranch for conditional headers). + edges.reverse(); + if len < edges.len() { + return -8; + } + if edges.is_empty() { + return 0; + } + if out_targets.is_null() || flow_tags.is_null() || case_lo.is_null() || case_hi.is_null() { + return -1; + } + for (i, (target, tag, lo, hi)) in edges.iter().enumerate() { + unsafe { + *out_targets.add(i) = *target; + *flow_tags.add(i) = *tag; + *case_lo.add(i) = *lo; + *case_hi.add(i) = *hi; + } + } + 0 +} + +// ===== Region queries ===== + +/// Returns the control-flow region tag owned by a block: one of the +/// `CFG_REGION_*` constants, 0 when the block owns no region, -1 on NULL +/// input, -2 when the block does not exist. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_cfg_region_tag(ptr: *const CCircuitCFG, node: u32) -> i32 { + if ptr.is_null() { + return -1; + } + let wrapper = unsafe { &*ptr }; + let Some(index) = node_index(node) else { + return -2; + }; + if find_block(&wrapper.inner, node).is_none() { + return -2; + } + wrapper + .inner + .control_flow_region(index) + .map_or(0, |region| region_tag(region) as i32) +} + +/// Returns whether the block owns a while or for region: 1 when it is a +/// loop header, 0 when not, -1 on NULL input, -2 when the block does not +/// exist. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_cfg_is_loop_header(ptr: *const CCircuitCFG, node: u32) -> i32 { + if ptr.is_null() { + return -1; + } + let wrapper = unsafe { &*ptr }; + let Some(index) = node_index(node) else { + return -2; + }; + if find_block(&wrapper.inner, node).is_none() { + return -2; + } + wrapper.inner.is_loop_header(index) as i32 +} + +/// Writes the `If` region layout: `then_entry`, `else_entry` (or the merge +/// block when no else body exists), `merge_block`, plus `has_else` (1/0). +/// Returns 0 on success, -1 on NULL input, -2 when the block does not +/// exist, -8 when the block owns no `If` region. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_cfg_region_if( + ptr: *const CCircuitCFG, + node: u32, + then_entry: *mut u32, + else_entry: *mut u32, + merge_block: *mut u32, + has_else: *mut i32, +) -> i32 { + if ptr.is_null() { + return -1; + } + let wrapper = unsafe { &*ptr }; + let Some(index) = node_index(node) else { + return -2; + }; + let Some(ControlFlowRegion::If { + then_entry: then_index, + else_entry: else_index, + merge_block: merge_index, + has_else: has_else_body, + .. + }) = wrapper.inner.control_flow_region(index) + else { + return match find_block(&wrapper.inner, node) { + Some(_) => -8, + None => -2, + }; + }; + if then_entry.is_null() || else_entry.is_null() || merge_block.is_null() || has_else.is_null() { + return -1; + } + unsafe { + *then_entry = then_index.index() as u32; + *else_entry = else_index.index() as u32; + *merge_block = merge_index.index() as u32; + *has_else = *has_else_body as i32; + } + 0 +} + +/// Writes the `While` / `For` region layout: `body_entry` and +/// `exit_block`. Returns 0 on success, -1 on NULL input, -2 when the block +/// does not exist, -8 when the block owns no loop region. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_cfg_region_loop( + ptr: *const CCircuitCFG, + node: u32, + body_entry: *mut u32, + exit_block: *mut u32, +) -> i32 { + if ptr.is_null() { + return -1; + } + let wrapper = unsafe { &*ptr }; + let Some(index) = node_index(node) else { + return -2; + }; + let Some(region) = wrapper.inner.control_flow_region(index) else { + return match find_block(&wrapper.inner, node) { + Some(_) => -8, + None => -2, + }; + }; + let (body, exit) = match region { + ControlFlowRegion::While { + body_entry, + exit_block, + .. + } + | ControlFlowRegion::For { + body_entry, + exit_block, + .. + } => (body_entry, exit_block), + ControlFlowRegion::If { .. } | ControlFlowRegion::Switch { .. } => { + return -8; + } + }; + if body_entry.is_null() || exit_block.is_null() { + return -1; + } + unsafe { + *body_entry = body.index() as u32; + *exit_block = exit.index() as u32; + } + 0 +} + +/// Returns the number of explicit cases of a `Switch` region, or 0 on NULL +/// input, unknown block, or non-switch region. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_cfg_region_switch_cases_len(ptr: *const CCircuitCFG, node: u32) -> usize { + if ptr.is_null() { + return 0; + } + let wrapper = unsafe { &*ptr }; + let Some(index) = node_index(node) else { + return 0; + }; + match wrapper.inner.control_flow_region(index) { + Some(ControlFlowRegion::Switch { cases, .. }) => cases.len(), + _ => 0, + } +} + +/// Writes the `Switch` region layout: `case_lo[i]` / `case_hi[i]` receive +/// the 128-bit match value of case `i`, `case_entries[i]` its body entry +/// block; `default_entry` (or the merge block when no default body +/// exists), `merge_block`, and `has_default` (1/0). Returns 0 on success, +/// -1 on NULL input, -2 when the block does not exist, -8 when the block +/// owns no `Switch` region or `len` is smaller than the case count. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_cfg_region_switch( + ptr: *const CCircuitCFG, + node: u32, + case_lo: *mut u64, + case_hi: *mut u64, + case_entries: *mut u32, + len: usize, + default_entry: *mut u32, + merge_block: *mut u32, + has_default: *mut i32, +) -> i32 { + if ptr.is_null() { + return -1; + } + let wrapper = unsafe { &*ptr }; + let Some(index) = node_index(node) else { + return -2; + }; + let Some(ControlFlowRegion::Switch { + cases, + default_entry: default_index, + merge_block: merge_index, + has_default: has_default_body, + .. + }) = wrapper.inner.control_flow_region(index) + else { + return match find_block(&wrapper.inner, node) { + Some(_) => -8, + None => -2, + }; + }; + if len < cases.len() { + return -8; + } + if default_entry.is_null() || merge_block.is_null() || has_default.is_null() { + return -1; + } + if !cases.is_empty() { + if case_lo.is_null() || case_hi.is_null() || case_entries.is_null() { + return -1; + } + for (i, case) in cases.iter().enumerate() { + unsafe { + *case_lo.add(i) = case.value as u64; + *case_hi.add(i) = (case.value >> 64) as u64; + *case_entries.add(i) = case.entry.index() as u32; + } + } + } + unsafe { + *default_entry = default_index.index() as u32; + *merge_block = merge_index.index() as u32; + *has_default = *has_default_body as i32; + } + 0 +} + +// ===== Region setters ===== + +/// Guards region attachment: resolves the header block and stores the +/// region produced by `build` (as [`CircuitCFG::set_control_flow_region`] +/// does, entry blocks are only cross-checked by `circuit_cfg_validate`). +/// Returns 0 on success, -1 on NULL input, -2 when the header block does +/// not exist. +fn set_control_flow_region( + ptr: *mut CCircuitCFG, + node: u32, + build: impl FnOnce() -> ControlFlowRegion, +) -> i32 { + if ptr.is_null() { + return -1; + } + let Some(index) = node_index(node) else { + return -2; + }; + let wrapper = unsafe { &mut *ptr }; + if wrapper.inner.block_mut(index).is_none() { + return -2; + } + wrapper.inner.set_control_flow_region(index, build()); + 0 +} + +/// Attaches an `If` region to a header block. `outer_op` supplies the +/// outer operation metadata and may be NULL (empty metadata). Entry blocks +/// are only checked by `circuit_cfg_validate`. Returns 0 on success, -1 on +/// NULL input, -2 when the header block does not exist. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_cfg_set_if_region( + ptr: *mut CCircuitCFG, + node: u32, + then_entry: u32, + else_entry: u32, + merge_block: u32, + has_else: i32, + outer_op: *const COperation, +) -> i32 { + set_control_flow_region(ptr, node, move || ControlFlowRegion::If { + then_entry: NodeIndex::new(then_entry as usize), + else_entry: NodeIndex::new(else_entry as usize), + merge_block: NodeIndex::new(merge_block as usize), + has_else: has_else != 0, + outer: outer_metadata(outer_op), + }) +} + +/// Attaches a `While` region to a header block. `outer_op` supplies the +/// outer operation metadata and may be NULL. Entry blocks are only checked +/// by `circuit_cfg_validate`. Returns 0 on success, -1 on NULL input, -2 +/// when the header block does not exist. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_cfg_set_while_region( + ptr: *mut CCircuitCFG, + node: u32, + body_entry: u32, + exit_block: u32, + outer_op: *const COperation, +) -> i32 { + set_control_flow_region(ptr, node, move || ControlFlowRegion::While { + body_entry: NodeIndex::new(body_entry as usize), + exit_block: NodeIndex::new(exit_block as usize), + outer: outer_metadata(outer_op), + }) +} + +/// Attaches a `For` region to a header block. `outer_op` supplies the +/// outer operation metadata and may be NULL. Entry blocks are only checked +/// by `circuit_cfg_validate`. Returns 0 on success, -1 on NULL input, -2 +/// when the header block does not exist. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_cfg_set_for_region( + ptr: *mut CCircuitCFG, + node: u32, + body_entry: u32, + exit_block: u32, + outer_op: *const COperation, +) -> i32 { + set_control_flow_region(ptr, node, move || ControlFlowRegion::For { + body_entry: NodeIndex::new(body_entry as usize), + exit_block: NodeIndex::new(exit_block as usize), + outer: outer_metadata(outer_op), + }) +} + +/// Attaches a `Switch` region to a header block. Case `i` pairs +/// `case_lo[i]` (least-significant) with `case_hi[i]` as its 128-bit match +/// value and `case_entries[i]` as its body entry block. `outer_op` supplies +/// the outer operation metadata and may be NULL. Entry blocks are only +/// checked by `circuit_cfg_validate`. Returns 0 on success, -1 on NULL +/// input or NULL case arrays when `len > 0`, -2 when the header block does +/// not exist. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_cfg_set_switch_region( + ptr: *mut CCircuitCFG, + node: u32, + case_lo: *const u64, + case_hi: *const u64, + case_entries: *const u32, + len: usize, + default_entry: u32, + merge_block: u32, + has_default: i32, + outer_op: *const COperation, +) -> i32 { + let cases = if len == 0 { + Vec::new() + } else { + if case_lo.is_null() || case_hi.is_null() || case_entries.is_null() { + return -1; + } + unsafe { + let lo = std::slice::from_raw_parts(case_lo, len); + let hi = std::slice::from_raw_parts(case_hi, len); + let entries = std::slice::from_raw_parts(case_entries, len); + lo.iter() + .zip(hi.iter()) + .zip(entries.iter()) + .map(|((&lo, &hi), &entry)| SwitchRegionCase { + value: u128::from(lo) | (u128::from(hi) << 64), + entry: NodeIndex::new(entry as usize), + }) + .collect() + } + }; + set_control_flow_region(ptr, node, move || ControlFlowRegion::Switch { + cases, + default_entry: NodeIndex::new(default_entry as usize), + merge_block: NodeIndex::new(merge_block as usize), + has_default: has_default != 0, + outer: outer_metadata(outer_op), + }) +} + +// ===== Terminator setters ===== + +/// Guards terminator assignment: resolves the block and applies the +/// terminator produced by `build`. Returns 0 on success, -1 on NULL input, +/// -2 when the block does not exist. +fn set_terminator(ptr: *mut CCircuitCFG, node: u32, build: impl FnOnce() -> Terminator) -> i32 { + if ptr.is_null() { + return -1; + } + let Some(index) = node_index(node) else { + return -2; + }; + let wrapper = unsafe { &mut *ptr }; + match wrapper.inner.block_mut(index) { + Some(block) => { + block.set_terminator(build()); + 0 + } + None => -2, + } +} + +/// Replaces the block terminator with an unconditional jump. Returns 0 on +/// success, -1 on NULL input, -2 when the block does not exist. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_cfg_set_terminator_jump( + ptr: *mut CCircuitCFG, + node: u32, + target: u32, +) -> i32 { + set_terminator(ptr, node, || { + Terminator::Jump(NodeIndex::new(target as usize)) + }) +} + +/// Replaces the block terminator with `Return` (end of execution). +/// Returns 0 on success, -1 on NULL input, -2 when the block does not +/// exist. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_cfg_set_terminator_return(ptr: *mut CCircuitCFG, node: u32) -> i32 { + set_terminator(ptr, node, || Terminator::Return) +} + +/// Replaces the block terminator with a structured `Break` referencing the +/// broken loop's header. Returns 0 on success, -1 on NULL input, -2 when +/// the block does not exist. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_cfg_set_terminator_break( + ptr: *mut CCircuitCFG, + node: u32, + target: u32, +) -> i32 { + set_terminator(ptr, node, || { + Terminator::Break(NodeIndex::new(target as usize)) + }) +} + +/// Replaces the block terminator with a structured `Continue` referencing +/// the loop header. Returns 0 on success, -1 on NULL input, -2 when the +/// block does not exist. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_cfg_set_terminator_continue( + ptr: *mut CCircuitCFG, + node: u32, + target: u32, +) -> i32 { + set_terminator(ptr, node, || { + Terminator::Continue(NodeIndex::new(target as usize)) + }) +} diff --git a/crates/binding-c/src/circuit/classical.rs b/crates/binding-c/src/circuit/classical.rs new file mode 100644 index 00000000..b7416e01 --- /dev/null +++ b/crates/binding-c/src/circuit/classical.rs @@ -0,0 +1,1025 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! C ABI for classical variables, expressions, and measurement targets. + +use crate::circuit::CCircuit; +use cqlib_core::circuit::{ + ClassicalExpr, ClassicalExprKind, ClassicalType, ClassicalValue, ClassicalVar, Qubit, +}; +use std::collections::HashMap; + +/// Opaque handle around [`ClassicalVar`] for C ABI. +pub struct CClassicalVar { + pub inner: ClassicalVar, +} + +/// Opaque handle around [`ClassicalExpr`] for C ABI. +pub struct CClassicalExpr { + pub inner: ClassicalExpr, +} + +// ===== Classical type tags ===== + +/// Classical type tag exposed to C. +/// | Value | Type | +/// |-------|--------| +/// | 0 | Bit | +/// | 1 | Bool | +/// | 2 | UInt | +/// | 3 | BitVec | +pub const CQLIB_CLASSICAL_TYPE_BIT: u32 = 0; + +pub const CQLIB_CLASSICAL_TYPE_BOOL: u32 = 1; + +pub const CQLIB_CLASSICAL_TYPE_UINT: u32 = 2; + +pub const CQLIB_CLASSICAL_TYPE_BIT_VEC: u32 = 3; + +// ===== Classical expression kind tags ===== + +/// Classical expression node kind tag exposed to C by `classical_expr_kind`. +/// | Value | Kind | +/// |-------|---------------| +/// | 0 | Var | +/// | 1 | Value | +/// | 2 | BoolLiteral | +/// | 3 | BitLiteral | +/// | 4 | UIntLiteral | +/// | 5 | BitVecLiteral | +/// | 6 | Unary | +/// | 7 | Binary | +/// | 8 | Compare | +/// | 9 | Cast | +/// | 10 | Select | +/// | 11 | ExtractBit | +/// | 12 | ExtractBits | +/// | 13 | Concat | +/// | 14 | PackBits | +pub const CQLIB_CLASSICAL_EXPR_VAR: u32 = 0; + +pub const CQLIB_CLASSICAL_EXPR_VALUE: u32 = 1; + +pub const CQLIB_CLASSICAL_EXPR_BOOL_LITERAL: u32 = 2; + +pub const CQLIB_CLASSICAL_EXPR_BIT_LITERAL: u32 = 3; + +pub const CQLIB_CLASSICAL_EXPR_UINT_LITERAL: u32 = 4; + +pub const CQLIB_CLASSICAL_EXPR_BIT_VEC_LITERAL: u32 = 5; + +pub const CQLIB_CLASSICAL_EXPR_UNARY: u32 = 6; + +pub const CQLIB_CLASSICAL_EXPR_BINARY: u32 = 7; + +pub const CQLIB_CLASSICAL_EXPR_COMPARE: u32 = 8; + +pub const CQLIB_CLASSICAL_EXPR_CAST: u32 = 9; + +pub const CQLIB_CLASSICAL_EXPR_SELECT: u32 = 10; + +pub const CQLIB_CLASSICAL_EXPR_EXTRACT_BIT: u32 = 11; + +pub const CQLIB_CLASSICAL_EXPR_EXTRACT_BITS: u32 = 12; + +pub const CQLIB_CLASSICAL_EXPR_CONCAT: u32 = 13; + +pub const CQLIB_CLASSICAL_EXPR_PACK_BITS: u32 = 14; + +/// C-side snapshot of a [`ClassicalType`]: tag + width in bits. +#[repr(C)] +pub struct CClassicalType { + /// One of `CQLIB_CLASSICAL_TYPE_*`. + pub tag: u32, + /// Type width in bits (1 for Bit/Bool). + pub width: u32, +} + +/// C-side snapshot of an immutable classical value referenced by an expression. +#[repr(C)] +pub struct CClassicalValueInfo { + /// Circuit-local value table index. + pub index: u32, + /// One of `CQLIB_CLASSICAL_TYPE_*`. + pub tag: u32, + /// Type width in bits (1 for Bit/Bool). + pub width: u32, +} + +/// Converts a C (tag, width) pair into a core `ClassicalType`. +fn classical_type_from_tag(tag: u32, width: u32) -> Option { + match tag { + CQLIB_CLASSICAL_TYPE_BIT => Some(ClassicalType::Bit), + CQLIB_CLASSICAL_TYPE_BOOL => Some(ClassicalType::Bool), + CQLIB_CLASSICAL_TYPE_UINT => ClassicalType::uint(width), + CQLIB_CLASSICAL_TYPE_BIT_VEC => ClassicalType::bit_vec(width), + _ => None, + } +} + +/// Converts a core `ClassicalType` into a C (tag, width) pair. +pub(crate) fn classical_type_parts(ty: ClassicalType) -> (u32, u32) { + match ty { + ClassicalType::Bit => (CQLIB_CLASSICAL_TYPE_BIT, 1), + ClassicalType::Bool => (CQLIB_CLASSICAL_TYPE_BOOL, 1), + ClassicalType::UInt(width) => (CQLIB_CLASSICAL_TYPE_UINT, width.get()), + ClassicalType::BitVec(width) => (CQLIB_CLASSICAL_TYPE_BIT_VEC, width.get()), + } +} + +/// Maps an expression node kind to its C tag. +fn expr_kind_tag(kind: &ClassicalExprKind) -> u32 { + match kind { + ClassicalExprKind::Var(_) => CQLIB_CLASSICAL_EXPR_VAR, + ClassicalExprKind::Value(_) => CQLIB_CLASSICAL_EXPR_VALUE, + ClassicalExprKind::BoolLiteral(_) => CQLIB_CLASSICAL_EXPR_BOOL_LITERAL, + ClassicalExprKind::BitLiteral(_) => CQLIB_CLASSICAL_EXPR_BIT_LITERAL, + ClassicalExprKind::UIntLiteral { .. } => CQLIB_CLASSICAL_EXPR_UINT_LITERAL, + ClassicalExprKind::BitVecLiteral { .. } => CQLIB_CLASSICAL_EXPR_BIT_VEC_LITERAL, + ClassicalExprKind::Unary { .. } => CQLIB_CLASSICAL_EXPR_UNARY, + ClassicalExprKind::Binary { .. } => CQLIB_CLASSICAL_EXPR_BINARY, + ClassicalExprKind::Compare { .. } => CQLIB_CLASSICAL_EXPR_COMPARE, + ClassicalExprKind::Cast { .. } => CQLIB_CLASSICAL_EXPR_CAST, + ClassicalExprKind::Select { .. } => CQLIB_CLASSICAL_EXPR_SELECT, + ClassicalExprKind::ExtractBit { .. } => CQLIB_CLASSICAL_EXPR_EXTRACT_BIT, + ClassicalExprKind::ExtractBits { .. } => CQLIB_CLASSICAL_EXPR_EXTRACT_BITS, + ClassicalExprKind::Concat { .. } => CQLIB_CLASSICAL_EXPR_CONCAT, + ClassicalExprKind::PackBits { .. } => CQLIB_CLASSICAL_EXPR_PACK_BITS, + } +} + +/// Clones the expression behind a handle, or `None` for NULL. +fn take_expr(ptr: *const CClassicalExpr) -> Option { + if ptr.is_null() { + return None; + } + Some(unsafe { (*ptr).inner.clone() }) +} + +/// Boxes an expression into a new owned C handle. +fn boxed_expr(expr: ClassicalExpr) -> *mut CClassicalExpr { + Box::into_raw(Box::new(CClassicalExpr { inner: expr })) +} + +// ===== Section 1.1: classical variables and measurement ===== + +/// Allocate a mutable classical variable owned by the circuit. +/// +/// `ty_tag` is one of `CQLIB_CLASSICAL_TYPE_*`; `width` is the bit width used +/// for `UInt`/`BitVec` (zero width is rejected). Returns NULL on NULL circuit +/// or invalid type parameters. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_var(ptr: *mut CCircuit, ty_tag: u32, width: u32) -> *mut CClassicalVar { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let Some(ty) = classical_type_from_tag(ty_tag, width) else { + return std::ptr::null_mut(); + }; + let wrapper = unsafe { &mut *ptr }; + let var = wrapper.inner.var(ty); + Box::into_raw(Box::new(CClassicalVar { inner: var })) +} + +/// Free a classical variable handle. Passing NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn classical_var_free(ptr: *mut CClassicalVar) { + if !ptr.is_null() { + unsafe { + let _ = Box::from_raw(ptr); + } + } +} + +/// Return the circuit-local variable id, or `UINT32_MAX` for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn classical_var_id(ptr: *const CClassicalVar) -> u32 { + if ptr.is_null() { + return u32::MAX; + } + unsafe { (*ptr).inner.id() } +} + +/// Return the circuit-local variable table index, or `UINT32_MAX` for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn classical_var_index(ptr: *const CClassicalVar) -> u32 { + if ptr.is_null() { + return u32::MAX; + } + unsafe { (*ptr).inner.index() } +} + +/// Write the variable type (tag + width) to the out-parameters. +/// Returns 0 on success, -1 on NULL pointers. +#[unsafe(no_mangle)] +pub extern "C" fn classical_var_ty( + ptr: *const CClassicalVar, + tag: *mut u32, + width: *mut u32, +) -> i32 { + if ptr.is_null() || tag.is_null() || width.is_null() { + return -1; + } + let (ty_tag, ty_width) = classical_type_parts(unsafe { (*ptr).inner.ty() }); + unsafe { + *tag = ty_tag; + *width = ty_width; + } + 0 +} + +/// Create a new expression that reads the current runtime value of the +/// variable. Returns NULL on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn classical_var_expr(ptr: *const CClassicalVar) -> *mut CClassicalExpr { + if ptr.is_null() { + return std::ptr::null_mut(); + } + boxed_expr(unsafe { (*ptr).inner.expr() }) +} + +/// Measure several qubits and return an expression reading the immutable +/// `BitVec` result. The first qubit maps to bit index 0 (least-significant). +/// Returns NULL on error (NULL pointers, empty list, qubit out of bounds). +#[unsafe(no_mangle)] +pub extern "C" fn circuit_measure_bits( + ptr: *mut CCircuit, + qubits: *const u32, + len: usize, +) -> *mut CClassicalExpr { + if ptr.is_null() || qubits.is_null() || len == 0 { + return std::ptr::null_mut(); + } + let wrapper = unsafe { &mut *ptr }; + let slice = unsafe { std::slice::from_raw_parts(qubits, len) }; + let qubits: Vec = slice.iter().map(|&id| Qubit::new(id)).collect(); + match wrapper.inner.measure_bits(qubits) { + Ok(measurement) => boxed_expr(measurement.expr()), + Err(_) => std::ptr::null_mut(), + } +} + +/// Measure one qubit and store the result into `target` (a `Bit` variable). +/// Returns an expression reading the immutable measurement value, or NULL on +/// error (NULL pointers, qubit out of bounds, wrong target type). +#[unsafe(no_mangle)] +pub extern "C" fn circuit_measure_into( + ptr: *mut CCircuit, + qubit: u32, + target: *const CClassicalVar, +) -> *mut CClassicalExpr { + if ptr.is_null() || target.is_null() { + return std::ptr::null_mut(); + } + let wrapper = unsafe { &mut *ptr }; + let target = unsafe { (*target).inner }; + match wrapper.inner.measure_into(Qubit::new(qubit), target) { + Ok(measurement) => boxed_expr(measurement.expr()), + Err(_) => std::ptr::null_mut(), + } +} + +/// Measure several qubits and store the `BitVec` result into `target`. +/// The target width must equal the number of qubits. Returns an expression +/// reading the immutable measurement value, or NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_measure_bits_into( + ptr: *mut CCircuit, + qubits: *const u32, + len: usize, + target: *const CClassicalVar, +) -> *mut CClassicalExpr { + if ptr.is_null() || qubits.is_null() || len == 0 || target.is_null() { + return std::ptr::null_mut(); + } + let wrapper = unsafe { &mut *ptr }; + let slice = unsafe { std::slice::from_raw_parts(qubits, len) }; + let qubits: Vec = slice.iter().map(|&id| Qubit::new(id)).collect(); + let target = unsafe { (*target).inner }; + match wrapper.inner.measure_bits_into(qubits, target) { + Ok(measurement) => boxed_expr(measurement.expr()), + Err(_) => std::ptr::null_mut(), + } +} + +/// Return the number of classical variables allocated by the circuit, or 0 +/// for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_classical_vars_len(ptr: *const CCircuit) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.classical_vars().len() } +} + +/// Copy the circuit's classical variable types into `buffer` as (tag, width) +/// snapshots. Returns 0 on success, -1 on NULL pointers, -8 when `len` does +/// not equal `circuit_classical_vars_len`. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_classical_vars( + ptr: *const CCircuit, + buffer: *mut CClassicalType, + len: usize, +) -> i32 { + if ptr.is_null() || buffer.is_null() { + return -1; + } + let types = unsafe { (*ptr).inner.classical_vars() }; + if types.len() != len { + return -8; + } + for (i, ty) in types.iter().enumerate() { + let (tag, width) = classical_type_parts(*ty); + unsafe { + *buffer.add(i) = CClassicalType { tag, width }; + } + } + 0 +} + +/// Check that a classical variable belongs to this circuit and keeps its +/// recorded type. Returns 0 on success, -1 on NULL pointers, -3 otherwise. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_validate_classical_var( + ptr: *const CCircuit, + var: *const CClassicalVar, +) -> i32 { + if ptr.is_null() || var.is_null() { + return -1; + } + let wrapper = unsafe { &*ptr }; + let var = unsafe { (*var).inner }; + wrapper.inner.validate_classical_var(var).map_or(-3, |_| 0) +} + +/// Check that every classical variable and value read by `expr` belongs to +/// this circuit. Returns 0 on success, -1 on NULL pointers, -3 otherwise. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_validate_classical_expr( + ptr: *const CCircuit, + expr: *const CClassicalExpr, +) -> i32 { + if ptr.is_null() || expr.is_null() { + return -1; + } + let wrapper = unsafe { &*ptr }; + let expr = unsafe { &(*expr).inner }; + wrapper + .inner + .validate_classical_expr(expr) + .map_or(-3, |_| 0) +} + +// ===== Section 1.2: classical expressions and store ===== + +/// Free a classical expression handle. Passing NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn classical_expr_free(ptr: *mut CClassicalExpr) { + if !ptr.is_null() { + unsafe { + let _ = Box::from_raw(ptr); + } + } +} + +/// Create a `Bit` literal expression (false = 0, true = 1). +#[unsafe(no_mangle)] +pub extern "C" fn classical_expr_bit_literal(value: bool) -> *mut CClassicalExpr { + boxed_expr(ClassicalExpr::bit_literal(value)) +} + +/// Create a `Bool` literal expression. +#[unsafe(no_mangle)] +pub extern "C" fn classical_expr_bool_literal(value: bool) -> *mut CClassicalExpr { + boxed_expr(ClassicalExpr::bool_literal(value)) +} + +/// Create a `UInt(width)` literal. The 128-bit value is passed as two 64-bit +/// halves (lo = least-significant). Returns NULL when width is zero, exceeds +/// 128, or the value does not fit in the width. +#[unsafe(no_mangle)] +pub extern "C" fn classical_expr_uint_literal(width: u32, lo: u64, hi: u64) -> *mut CClassicalExpr { + let value = u128::from(lo) | (u128::from(hi) << 64); + match ClassicalExpr::uint_literal(width, value) { + Ok(expr) => boxed_expr(expr), + Err(_) => std::ptr::null_mut(), + } +} + +/// Create a `BitVec(width)` literal. The 128-bit packed little-endian value is +/// passed as two 64-bit halves (lo = least-significant). Returns NULL when +/// width is zero, exceeds 128, or the value does not fit in the width. +#[unsafe(no_mangle)] +pub extern "C" fn classical_expr_bit_vec_literal( + width: u32, + lo: u64, + hi: u64, +) -> *mut CClassicalExpr { + let value = u128::from(lo) | (u128::from(hi) << 64); + match ClassicalExpr::bit_vec_literal(width, value) { + Ok(expr) => boxed_expr(expr), + Err(_) => std::ptr::null_mut(), + } +} + +/// Create an expression that reads the current runtime value of `var`. +/// Returns NULL on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn classical_expr_var(var: *const CClassicalVar) -> *mut CClassicalExpr { + if var.is_null() { + return std::ptr::null_mut(); + } + boxed_expr(unsafe { (*var).inner.expr() }) +} + +/// Create a `not` expression over a `Bool` or `Bit` operand. +/// Returns NULL on NULL input or invalid operand type. +#[unsafe(no_mangle)] +pub extern "C" fn classical_expr_not(expr: *const CClassicalExpr) -> *mut CClassicalExpr { + let Some(expr) = take_expr(expr) else { + return std::ptr::null_mut(); + }; + match ClassicalExpr::try_not(expr) { + Ok(expr) => boxed_expr(expr), + Err(_) => std::ptr::null_mut(), + } +} + +/// Create an `and` expression over matching `Bool` or matching `Bit` +/// operands. Returns NULL on NULL input or type mismatch. +#[unsafe(no_mangle)] +pub extern "C" fn classical_expr_and( + lhs: *const CClassicalExpr, + rhs: *const CClassicalExpr, +) -> *mut CClassicalExpr { + let (Some(lhs), Some(rhs)) = (take_expr(lhs), take_expr(rhs)) else { + return std::ptr::null_mut(); + }; + match ClassicalExpr::try_and(lhs, rhs) { + Ok(expr) => boxed_expr(expr), + Err(_) => std::ptr::null_mut(), + } +} + +/// Create an `or` expression over matching `Bool` or matching `Bit` operands. +/// Returns NULL on NULL input or type mismatch. +#[unsafe(no_mangle)] +pub extern "C" fn classical_expr_or( + lhs: *const CClassicalExpr, + rhs: *const CClassicalExpr, +) -> *mut CClassicalExpr { + let (Some(lhs), Some(rhs)) = (take_expr(lhs), take_expr(rhs)) else { + return std::ptr::null_mut(); + }; + match ClassicalExpr::try_or(lhs, rhs) { + Ok(expr) => boxed_expr(expr), + Err(_) => std::ptr::null_mut(), + } +} + +/// Create an `xor` expression over matching `Bool` or matching `Bit` +/// operands. Returns NULL on NULL input or type mismatch. +#[unsafe(no_mangle)] +pub extern "C" fn classical_expr_xor( + lhs: *const CClassicalExpr, + rhs: *const CClassicalExpr, +) -> *mut CClassicalExpr { + let (Some(lhs), Some(rhs)) = (take_expr(lhs), take_expr(rhs)) else { + return std::ptr::null_mut(); + }; + match ClassicalExpr::try_xor(lhs, rhs) { + Ok(expr) => boxed_expr(expr), + Err(_) => std::ptr::null_mut(), + } +} + +/// Create an equality comparison over same-typed operands, producing `Bool`. +/// Returns NULL on NULL input or type mismatch. +#[unsafe(no_mangle)] +pub extern "C" fn classical_expr_eq( + lhs: *const CClassicalExpr, + rhs: *const CClassicalExpr, +) -> *mut CClassicalExpr { + let (Some(lhs), Some(rhs)) = (take_expr(lhs), take_expr(rhs)) else { + return std::ptr::null_mut(); + }; + match ClassicalExpr::eq(lhs, rhs) { + Ok(expr) => boxed_expr(expr), + Err(_) => std::ptr::null_mut(), + } +} + +/// Create an inequality comparison over same-typed operands, producing `Bool`. +/// Returns NULL on NULL input or type mismatch. +#[unsafe(no_mangle)] +pub extern "C" fn classical_expr_ne( + lhs: *const CClassicalExpr, + rhs: *const CClassicalExpr, +) -> *mut CClassicalExpr { + let (Some(lhs), Some(rhs)) = (take_expr(lhs), take_expr(rhs)) else { + return std::ptr::null_mut(); + }; + match ClassicalExpr::ne(lhs, rhs) { + Ok(expr) => boxed_expr(expr), + Err(_) => std::ptr::null_mut(), + } +} + +/// Create an unsigned less-than comparison over matching `UInt` operands, +/// producing `Bool`. Returns NULL on NULL input or invalid operand types. +#[unsafe(no_mangle)] +pub extern "C" fn classical_expr_lt( + lhs: *const CClassicalExpr, + rhs: *const CClassicalExpr, +) -> *mut CClassicalExpr { + let (Some(lhs), Some(rhs)) = (take_expr(lhs), take_expr(rhs)) else { + return std::ptr::null_mut(); + }; + match ClassicalExpr::lt(lhs, rhs) { + Ok(expr) => boxed_expr(expr), + Err(_) => std::ptr::null_mut(), + } +} + +/// Create an unsigned less-than-or-equal comparison over matching `UInt` +/// operands, producing `Bool`. Returns NULL on NULL input or invalid types. +#[unsafe(no_mangle)] +pub extern "C" fn classical_expr_le( + lhs: *const CClassicalExpr, + rhs: *const CClassicalExpr, +) -> *mut CClassicalExpr { + let (Some(lhs), Some(rhs)) = (take_expr(lhs), take_expr(rhs)) else { + return std::ptr::null_mut(); + }; + match ClassicalExpr::le(lhs, rhs) { + Ok(expr) => boxed_expr(expr), + Err(_) => std::ptr::null_mut(), + } +} + +/// Create an unsigned greater-than comparison over matching `UInt` operands, +/// producing `Bool`. Returns NULL on NULL input or invalid operand types. +#[unsafe(no_mangle)] +pub extern "C" fn classical_expr_gt( + lhs: *const CClassicalExpr, + rhs: *const CClassicalExpr, +) -> *mut CClassicalExpr { + let (Some(lhs), Some(rhs)) = (take_expr(lhs), take_expr(rhs)) else { + return std::ptr::null_mut(); + }; + match ClassicalExpr::gt(lhs, rhs) { + Ok(expr) => boxed_expr(expr), + Err(_) => std::ptr::null_mut(), + } +} + +/// Create an unsigned greater-than-or-equal comparison over matching `UInt` +/// operands, producing `Bool`. Returns NULL on NULL input or invalid types. +#[unsafe(no_mangle)] +pub extern "C" fn classical_expr_ge( + lhs: *const CClassicalExpr, + rhs: *const CClassicalExpr, +) -> *mut CClassicalExpr { + let (Some(lhs), Some(rhs)) = (take_expr(lhs), take_expr(rhs)) else { + return std::ptr::null_mut(); + }; + match ClassicalExpr::ge(lhs, rhs) { + Ok(expr) => boxed_expr(expr), + Err(_) => std::ptr::null_mut(), + } +} + +/// Create a ternary select: `condition ? then_expr : else_expr`. The condition +/// must be `Bool` and both branches must have the same type. Returns NULL on +/// NULL input or invalid operand types. +#[unsafe(no_mangle)] +pub extern "C" fn classical_expr_select( + condition: *const CClassicalExpr, + then_expr: *const CClassicalExpr, + else_expr: *const CClassicalExpr, +) -> *mut CClassicalExpr { + let (Some(condition), Some(then_expr), Some(else_expr)) = ( + take_expr(condition), + take_expr(then_expr), + take_expr(else_expr), + ) else { + return std::ptr::null_mut(); + }; + match ClassicalExpr::select(condition, then_expr, else_expr) { + Ok(expr) => boxed_expr(expr), + Err(_) => std::ptr::null_mut(), + } +} + +/// Extract one bit (index 0 = least-significant) from a `UInt` or `BitVec` +/// expression, producing `Bit`. Returns NULL on NULL input, non-integer +/// operand, or out-of-bounds index. +#[unsafe(no_mangle)] +pub extern "C" fn classical_expr_extract_bit( + value: *const CClassicalExpr, + index: u32, +) -> *mut CClassicalExpr { + let Some(value) = take_expr(value) else { + return std::ptr::null_mut(); + }; + match ClassicalExpr::extract_bit(value, index) { + Ok(expr) => boxed_expr(expr), + Err(_) => std::ptr::null_mut(), + } +} + +/// Extract a contiguous bit range `[offset, offset + width)` from a `UInt` or +/// `BitVec` expression, producing `BitVec(width)`. Offset 0 starts at the +/// least-significant bit. Returns NULL on NULL input or invalid range. +#[unsafe(no_mangle)] +pub extern "C" fn classical_expr_extract_bits( + value: *const CClassicalExpr, + offset: u32, + width: u32, +) -> *mut CClassicalExpr { + let Some(value) = take_expr(value) else { + return std::ptr::null_mut(); + }; + match ClassicalExpr::extract_bits(value, offset, width) { + Ok(expr) => boxed_expr(expr), + Err(_) => std::ptr::null_mut(), + } +} + +/// Pack single-bit expressions into a `BitVec`; the first bit becomes output +/// bit index 0. `bits` is an array of expression handles. Returns NULL on NULL +/// entries, empty input, or non-`Bit` operands. +#[unsafe(no_mangle)] +pub extern "C" fn classical_expr_pack_bits( + bits: *const *const CClassicalExpr, + len: usize, +) -> *mut CClassicalExpr { + let mut exprs = Vec::with_capacity(len); + if !bits.is_null() { + for i in 0..len { + match take_expr(unsafe { *bits.add(i) }) { + Some(expr) => exprs.push(expr), + None => return std::ptr::null_mut(), + } + } + } + match ClassicalExpr::pack_bits(exprs) { + Ok(expr) => boxed_expr(expr), + Err(_) => std::ptr::null_mut(), + } +} + +/// Concatenate `Bit`/`BitVec` expressions into a larger `BitVec`; the first +/// part occupies the least-significant output bits. `parts` is an array of +/// expression handles. Returns NULL on NULL entries, empty input, or invalid +/// part types. +#[unsafe(no_mangle)] +pub extern "C" fn classical_expr_concat( + parts: *const *const CClassicalExpr, + len: usize, +) -> *mut CClassicalExpr { + let mut exprs = Vec::with_capacity(len); + if !parts.is_null() { + for i in 0..len { + match take_expr(unsafe { *parts.add(i) }) { + Some(expr) => exprs.push(expr), + None => return std::ptr::null_mut(), + } + } + } + match ClassicalExpr::concat(exprs) { + Ok(expr) => boxed_expr(expr), + Err(_) => std::ptr::null_mut(), + } +} + +/// Explicitly cast a `Bit` expression to `Bool`. Returns NULL on NULL input +/// or non-`Bit` operand. +#[unsafe(no_mangle)] +pub extern "C" fn classical_expr_bit_to_bool(expr: *const CClassicalExpr) -> *mut CClassicalExpr { + let Some(expr) = take_expr(expr) else { + return std::ptr::null_mut(); + }; + match ClassicalExpr::bit_to_bool(expr) { + Ok(expr) => boxed_expr(expr), + Err(_) => std::ptr::null_mut(), + } +} + +/// Explicitly cast a `BitVec` expression to a little-endian `UInt` of the same +/// width. Returns NULL on NULL input or non-`BitVec` operand. +#[unsafe(no_mangle)] +pub extern "C" fn classical_expr_bit_vec_to_uint( + expr: *const CClassicalExpr, +) -> *mut CClassicalExpr { + let Some(expr) = take_expr(expr) else { + return std::ptr::null_mut(); + }; + match ClassicalExpr::bit_vec_to_uint(expr) { + Ok(expr) => boxed_expr(expr), + Err(_) => std::ptr::null_mut(), + } +} + +/// Convert this `Bit` expression to `Bool` (alias of `bit_to_bool`). +/// Returns NULL on NULL input or non-`Bit` operand. +#[unsafe(no_mangle)] +pub extern "C" fn classical_expr_to_bool(expr: *const CClassicalExpr) -> *mut CClassicalExpr { + let Some(expr) = take_expr(expr) else { + return std::ptr::null_mut(); + }; + match ClassicalExpr::to_bool(expr) { + Ok(expr) => boxed_expr(expr), + Err(_) => std::ptr::null_mut(), + } +} + +/// Convert this `BitVec` expression to a little-endian `UInt` (alias of +/// `bit_vec_to_uint`). Returns NULL on NULL input or non-`BitVec` operand. +#[unsafe(no_mangle)] +pub extern "C" fn classical_expr_to_uint(expr: *const CClassicalExpr) -> *mut CClassicalExpr { + let Some(expr) = take_expr(expr) else { + return std::ptr::null_mut(); + }; + match ClassicalExpr::to_uint(expr) { + Ok(expr) => boxed_expr(expr), + Err(_) => std::ptr::null_mut(), + } +} + +/// Return a structurally simplified copy of the expression. Simplification +/// eliminates literal-driven redundancies without evaluating runtime reads. +/// Returns NULL on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn classical_expr_simplified(ptr: *const CClassicalExpr) -> *mut CClassicalExpr { + let Some(expr) = take_expr(ptr) else { + return std::ptr::null_mut(); + }; + boxed_expr(expr.simplified()) +} + +/// Write the expression node kind tag (one of `CQLIB_CLASSICAL_EXPR_*`) to +/// `kind`. Returns 0 on success, -1 on NULL pointers. +#[unsafe(no_mangle)] +pub extern "C" fn classical_expr_kind(ptr: *const CClassicalExpr, kind: *mut u32) -> i32 { + if ptr.is_null() || kind.is_null() { + return -1; + } + unsafe { + *kind = expr_kind_tag((*ptr).inner.kind()); + } + 0 +} + +/// Write the expression type (tag + width) to the out-parameters. +/// Returns 0 on success, -1 on NULL pointers. +#[unsafe(no_mangle)] +pub extern "C" fn classical_expr_ty( + ptr: *const CClassicalExpr, + tag: *mut u32, + width: *mut u32, +) -> i32 { + if ptr.is_null() || tag.is_null() || width.is_null() { + return -1; + } + let (ty_tag, ty_width) = classical_type_parts(unsafe { (*ptr).inner.ty() }); + unsafe { + *tag = ty_tag; + *width = ty_width; + } + 0 +} + +/// Return the number of distinct immutable classical values read by the +/// expression, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn classical_expr_values_len(ptr: *const CClassicalExpr) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.values().len() } +} + +/// Copy snapshots (index, tag, width) of every immutable classical value read +/// by the expression into `buffer`. Returns 0 on success, -1 on NULL pointers, +/// -8 when `len` does not equal `classical_expr_values_len`. +#[unsafe(no_mangle)] +pub extern "C" fn classical_expr_values( + ptr: *const CClassicalExpr, + buffer: *mut CClassicalValueInfo, + len: usize, +) -> i32 { + if ptr.is_null() || buffer.is_null() { + return -1; + } + let values = unsafe { (*ptr).inner.values() }; + if values.len() != len { + return -8; + } + for (i, value) in values.iter().enumerate() { + let (tag, width) = classical_type_parts(value.ty()); + let info = CClassicalValueInfo { + index: value.index(), + tag, + width, + }; + unsafe { + *buffer.add(i) = info; + } + } + 0 +} + +/// Return the number of distinct classical variables read by the expression, +/// or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn classical_expr_vars_len(ptr: *const CClassicalExpr) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.vars().len() } +} + +/// Fill `buffer` with owned handles (of type `CClassicalVar*`) for every +/// distinct classical variable read by the expression, in ascending id order. +/// Each returned handle must be released with `classical_var_free`. Returns 0 +/// on success, -1 on NULL pointers, -8 when `len` does not equal +/// `classical_expr_vars_len`. +#[unsafe(no_mangle)] +pub extern "C" fn classical_expr_vars( + ptr: *const CClassicalExpr, + buffer: *mut *mut CClassicalVar, + len: usize, +) -> i32 { + if ptr.is_null() || buffer.is_null() { + return -1; + } + let vars = unsafe { (*ptr).inner.vars() }; + if vars.len() != len { + return -8; + } + for (i, var) in vars.iter().enumerate() { + let handle = Box::into_raw(Box::new(CClassicalVar { inner: *var })); + unsafe { + *buffer.add(i) = handle; + } + } + 0 +} + +/// Store the runtime value of `value` into the mutable variable `target`. +/// The expression type must match the target type. Returns 0 on success, -1 +/// on NULL pointers, -3 on validation failure. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_store( + ptr: *mut CCircuit, + target: *const CClassicalVar, + value: *const CClassicalExpr, +) -> i32 { + if ptr.is_null() || target.is_null() || value.is_null() { + return -1; + } + let wrapper = unsafe { &mut *ptr }; + let target = unsafe { (*target).inner }; + let value = unsafe { (*value).inner.clone() }; + wrapper.inner.store(target, value).map_or(-3, |_| 0) +} + +// ===== Section 1.3: expression predicates, type literals, and id remapping ===== + +/// Returns 1 when the expression is the `Bit` literal `true` (1), 0 otherwise, +/// or -1 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn classical_expr_is_bit_true(ptr: *const CClassicalExpr) -> i32 { + if ptr.is_null() { + return -1; + } + i32::from(unsafe { (*ptr).inner.is_bit_true() }) +} + +/// Returns 1 when the expression is the `Bit` literal `false` (0), 0 otherwise, +/// or -1 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn classical_expr_is_bit_false(ptr: *const CClassicalExpr) -> i32 { + if ptr.is_null() { + return -1; + } + i32::from(unsafe { (*ptr).inner.is_bit_false() }) +} + +/// Returns 1 when the expression is the `Bool` literal `true`, 0 otherwise, +/// or -1 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn classical_expr_is_bool_true(ptr: *const CClassicalExpr) -> i32 { + if ptr.is_null() { + return -1; + } + i32::from(unsafe { (*ptr).inner.is_bool_true() }) +} + +/// Returns 1 when the expression is the `Bool` literal `false`, 0 otherwise, +/// or -1 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn classical_expr_is_bool_false(ptr: *const CClassicalExpr) -> i32 { + if ptr.is_null() { + return -1; + } + i32::from(unsafe { (*ptr).inner.is_bool_false() }) +} + +/// Create the zero literal of a classical type. `ty_tag` is one of +/// `CQLIB_CLASSICAL_TYPE_*`; `width` is the bit width used for `UInt`/`BitVec`. +/// Returns NULL on invalid type parameters or a width exceeding the 128-bit +/// literal representation. +#[unsafe(no_mangle)] +pub extern "C" fn classical_type_zero_literal(ty_tag: u32, width: u32) -> *mut CClassicalExpr { + let Some(ty) = classical_type_from_tag(ty_tag, width) else { + return std::ptr::null_mut(); + }; + match ty.zero_literal() { + Ok(expr) => boxed_expr(expr), + Err(_) => std::ptr::null_mut(), + } +} + +/// Create the one literal of a classical type. `ty_tag` is one of +/// `CQLIB_CLASSICAL_TYPE_*`; `width` is the bit width used for `UInt`/`BitVec`. +/// Returns NULL on invalid type parameters or a width exceeding the 128-bit +/// literal representation. +#[unsafe(no_mangle)] +pub extern "C" fn classical_type_one_literal(ty_tag: u32, width: u32) -> *mut CClassicalExpr { + let Some(ty) = classical_type_from_tag(ty_tag, width) else { + return std::ptr::null_mut(); + }; + match ty.one_literal() { + Ok(expr) => boxed_expr(expr), + Err(_) => std::ptr::null_mut(), + } +} + +/// Return a copy of the expression with circuit-local classical ids remapped. +/// +/// `old_var_ids[i]` maps the variable with that id to id `new_var_ids[i]`, and +/// `old_value_indices[i]` maps the immutable value with that index to index +/// `new_value_indices[i]`. Entries whose old id does not occur in the +/// expression are ignored, but every variable and value actually read by the +/// expression must be covered, otherwise the remap fails. Returns a new +/// expression handle, or NULL on NULL inputs (including NULL arrays for +/// non-zero lengths) or a missing mapping. +#[unsafe(no_mangle)] +pub extern "C" fn classical_expr_remap_classical_ids( + ptr: *const CClassicalExpr, + old_var_ids: *const u32, + new_var_ids: *const u32, + var_len: usize, + old_value_indices: *const u32, + new_value_indices: *const u32, + value_len: usize, +) -> *mut CClassicalExpr { + if ptr.is_null() { + return std::ptr::null_mut(); + } + if var_len > 0 && (old_var_ids.is_null() || new_var_ids.is_null()) { + return std::ptr::null_mut(); + } + if value_len > 0 && (old_value_indices.is_null() || new_value_indices.is_null()) { + return std::ptr::null_mut(); + } + let expr = unsafe { &(*ptr).inner }; + let vars = expr.vars(); + let mut var_map: HashMap = HashMap::with_capacity(var_len); + for i in 0..var_len { + let (old, new) = (unsafe { *old_var_ids.add(i) }, unsafe { + *new_var_ids.add(i) + }); + if let Some(var) = vars.iter().find(|var| var.id() == old) { + var_map.insert(*var, ClassicalVar::new(var.circuit_id(), new, var.ty())); + } + } + let values = expr.values(); + let mut value_map: HashMap = HashMap::with_capacity(value_len); + for i in 0..value_len { + let (old, new) = (unsafe { *old_value_indices.add(i) }, unsafe { + *new_value_indices.add(i) + }); + if let Some(value) = values.iter().find(|value| value.index() == old) { + value_map.insert( + *value, + ClassicalValue::new(value.circuit_id(), new, value.ty()), + ); + } + } + match expr.remap_classical_ids(&var_map, &value_map) { + Ok(remapped) => boxed_expr(remapped), + Err(_) => std::ptr::null_mut(), + } +} diff --git a/crates/binding-c/src/circuit/control_flow.rs b/crates/binding-c/src/circuit/control_flow.rs new file mode 100644 index 00000000..c7a5e05e --- /dev/null +++ b/crates/binding-c/src/circuit/control_flow.rs @@ -0,0 +1,473 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! C ABI for structured classical control flow: `if`, `if`/`else`, `while`, +//! unsigned `for`, exact-value `switch`, `break`, `continue`, and raw control +//! operation re-append. +//! +//! The Rust builders take closures that append operations into a temporary +//! body region of the circuit. C has no closures, so every builder in this +//! module drives the same core "external control body transaction" machinery +//! through a C callback ([`CqlibBodyFn`]): the callback receives the owning +//! circuit pointer plus an opaque `user_data` pointer and appends body +//! operations with the regular circuit functions (`circuit_h`, +//! `circuit_measure`, ...). When the callback returns, the appended +//! operations are captured into the control body. +//! +//! Callback contract: +//! +//! - The `CCircuit` pointer passed to the callback is the same pointer that +//! was passed to the control-flow function. It is only valid for the +//! duration of the callback; body operations must be appended through it. +//! - `user_data` is passed through untouched and may be NULL. +//! - Return `0` on success. Any non-zero return aborts the whole control +//! operation: everything the callback appended is rolled back and the +//! value is returned to the original caller. + +use crate::circuit::CCircuit; +use crate::circuit::classical::{CClassicalExpr, CClassicalVar}; +use cqlib_core::circuit::{ + ClassicalControlOp, ClassicalExpr, ControlBody, ControlBodyTransaction, ExternalControlScope, + ForOp, IfOp, Instruction, SwitchCase, SwitchOp, WhileOp, +}; +use std::os::raw::c_void; + +/// Callback that fills one structured control-flow body. +/// +/// Contract: +/// +/// - `circuit` is the same `CCircuit` pointer that was passed to the +/// control-flow function being built. It is valid only for the duration of +/// the callback. Body operations must be appended through it using the +/// regular circuit functions (`circuit_h`, `circuit_x`, `circuit_measure`, +/// nested control-flow builders, ...). +/// - `user_data` is the pointer that was passed to the control-flow function. +/// It is never dereferenced by the bindings and may be NULL. +/// - Return `0` on success. Any non-zero return aborts the whole control +/// operation: all operations appended by the callback are rolled back and +/// the returned value is propagated to the caller of the control-flow +/// function. Returning the error code of a failed gate call (for example +/// `return circuit_x(circuit, 0);`) is the idiomatic pattern. +pub type CqlibBodyFn = Option i32>; + +/// Runs one callback body region and captures the appended operations. +/// +/// `ptr` must be a valid circuit with `transaction` open. On a non-zero +/// callback status the half-built region is left for the caller to roll back +/// with `rollback_control_body_transaction`. +fn run_body_callback( + ptr: *mut CCircuit, + transaction: &ControlBodyTransaction, + scope: ExternalControlScope, + callback: extern "C" fn(*mut CCircuit, *mut c_void) -> i32, + user_data: *mut c_void, +) -> Result { + unsafe { + (*ptr).inner.enter_external_control_body(transaction, scope); + } + let status = callback(ptr, user_data); + if status != 0 { + return Err(status); + } + Ok(unsafe { (*ptr).inner.finish_external_control_body(transaction) }) +} + +/// Commits the transaction on success or rolls the circuit back on failure. +fn close_control_region( + ptr: *mut CCircuit, + transaction: ControlBodyTransaction, + result: Result, +) -> Result { + if result.is_ok() { + unsafe { + (*ptr).inner.commit_control_body_transaction(transaction); + } + } else { + unsafe { + (*ptr).inner.rollback_control_body_transaction(transaction); + } + } + result +} + +/// Builds an `if` operation from captured bodies and appends it. +fn append_if_op( + ptr: *mut CCircuit, + condition: ClassicalExpr, + then_body: ControlBody, + else_body: Option, +) -> Result<(), i32> { + let op = IfOp::new(condition, then_body, else_body).map_err(|_| -3)?; + append_control_op(ptr, ClassicalControlOp::If(op)) +} + +/// Appends a raw classical-control operation. +fn append_control_op(ptr: *mut CCircuit, op: ClassicalControlOp) -> Result<(), i32> { + unsafe { (*ptr).inner.append_control(op) }.map_err(|_| -3) +} + +/// Appends a structured `if` operation controlled by a boolean classical expression. +/// +/// `body_fn` is invoked once with `ptr` and `user_data`; every operation it +/// appends becomes the `then` body of the `if` operation. See [`CqlibBodyFn`] +/// for the callback contract. +/// +/// Returns `0` on success, `-1` for NULL arguments, `-3` when the condition +/// fails validation (for example a non-`Bool` type or handles from another +/// circuit), or the non-zero status returned by `body_fn` after rollback. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_if( + ptr: *mut CCircuit, + condition: *const CClassicalExpr, + body_fn: CqlibBodyFn, + user_data: *mut c_void, +) -> i32 { + let Some(body_fn) = body_fn else { + return -1; + }; + if ptr.is_null() || condition.is_null() { + return -1; + } + let condition = unsafe { (*condition).inner.clone() }; + if unsafe { (*ptr).inner.validate_classical_expr(&condition) }.is_err() { + return -3; + } + + let transaction = unsafe { (*ptr).inner.begin_control_body_transaction() }; + let result = run_body_callback( + ptr, + &transaction, + ExternalControlScope::Branch, + body_fn, + user_data, + ) + .and_then(|then_body| append_if_op(ptr, condition, then_body, None)); + match close_control_region(ptr, transaction, result) { + Ok(()) => 0, + Err(code) => code, + } +} + +/// Appends a structured `if`/`else` operation controlled by a boolean classical expression. +/// +/// `then_fn` runs first, then `else_fn`; each receives `ptr` and `user_data` +/// and its appended operations become the corresponding branch body. +/// +/// Returns `0` on success, `-1` for NULL arguments, `-3` on validation failure, +/// or the non-zero status returned by either callback after rollback. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_if_else( + ptr: *mut CCircuit, + condition: *const CClassicalExpr, + then_fn: CqlibBodyFn, + else_fn: CqlibBodyFn, + user_data: *mut c_void, +) -> i32 { + let Some(then_fn) = then_fn else { + return -1; + }; + let Some(else_fn) = else_fn else { + return -1; + }; + if ptr.is_null() || condition.is_null() { + return -1; + } + let condition = unsafe { (*condition).inner.clone() }; + if unsafe { (*ptr).inner.validate_classical_expr(&condition) }.is_err() { + return -3; + } + + let transaction = unsafe { (*ptr).inner.begin_control_body_transaction() }; + let result = run_body_callback( + ptr, + &transaction, + ExternalControlScope::Branch, + then_fn, + user_data, + ) + .and_then(|then_body| { + run_body_callback( + ptr, + &transaction, + ExternalControlScope::Branch, + else_fn, + user_data, + ) + .and_then(|else_body| append_if_op(ptr, condition, then_body, Some(else_body))) + }); + match close_control_region(ptr, transaction, result) { + Ok(()) => 0, + Err(code) => code, + } +} + +/// Appends a structured `while` loop controlled by a boolean classical expression. +/// +/// `body_fn` is invoked once with `ptr` and `user_data`; its appended +/// operations become the loop body. The condition is re-evaluated at runtime. +/// `circuit_break_loop`/`circuit_continue_loop` may be called from the +/// callback, but only as the final operation of the body (terminal control +/// transfer). +/// +/// Returns `0` on success, `-1` for NULL arguments, `-3` on validation failure, +/// or the non-zero status returned by `body_fn` after rollback. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_while( + ptr: *mut CCircuit, + condition: *const CClassicalExpr, + body_fn: CqlibBodyFn, + user_data: *mut c_void, +) -> i32 { + let Some(body_fn) = body_fn else { + return -1; + }; + if ptr.is_null() || condition.is_null() { + return -1; + } + let condition = unsafe { (*condition).inner.clone() }; + if unsafe { (*ptr).inner.validate_classical_expr(&condition) }.is_err() { + return -3; + } + + let transaction = unsafe { (*ptr).inner.begin_control_body_transaction() }; + let result = run_body_callback( + ptr, + &transaction, + ExternalControlScope::Loop, + body_fn, + user_data, + ) + .and_then(|body| { + let op = WhileOp::new(condition, body).map_err(|_| -3)?; + append_control_op(ptr, ClassicalControlOp::While(op)) + }); + match close_control_region(ptr, transaction, result) { + Ok(()) => 0, + Err(code) => code, + } +} + +/// Appends an unsigned runtime range loop with half-open `[start, stop)` semantics. +/// +/// Parameter order follows the core API: `var`, `start`, `stop`, `step`. +/// `var` must be a `UInt` variable of this circuit; `start`, `stop`, and +/// `step` must be `UInt` expressions of the same width. `body_fn` is invoked +/// once with `ptr` and `user_data`; the loop variable expression can be +/// rebuilt from `var` with `classical_expr_var` when needed inside the body. +/// `circuit_break_loop`/`circuit_continue_loop` are allowed in the callback +/// only as the final operation of the body. +/// +/// Returns `0` on success, `-1` for NULL arguments, `-3` on validation failure +/// (type mismatch or foreign handles), or the non-zero status returned by +/// `body_fn` after rollback. +#[unsafe(no_mangle)] +#[allow(clippy::too_many_arguments)] +pub extern "C" fn circuit_for_uint( + ptr: *mut CCircuit, + var: *const CClassicalVar, + start: *const CClassicalExpr, + stop: *const CClassicalExpr, + step: *const CClassicalExpr, + body_fn: CqlibBodyFn, + user_data: *mut c_void, +) -> i32 { + let Some(body_fn) = body_fn else { + return -1; + }; + if ptr.is_null() || var.is_null() || start.is_null() || stop.is_null() || step.is_null() { + return -1; + } + let var = unsafe { (*var).inner }; + let start = unsafe { (*start).inner.clone() }; + let stop = unsafe { (*stop).inner.clone() }; + let step = unsafe { (*step).inner.clone() }; + let valid = unsafe { + (*ptr) + .inner + .validate_classical_var(var) + .and_then(|()| (*ptr).inner.validate_classical_expr(&start)) + .and_then(|()| (*ptr).inner.validate_classical_expr(&stop)) + .and_then(|()| (*ptr).inner.validate_classical_expr(&step)) + }; + if valid.is_err() { + return -3; + } + + let transaction = unsafe { (*ptr).inner.begin_control_body_transaction() }; + let result = run_body_callback( + ptr, + &transaction, + ExternalControlScope::Loop, + body_fn, + user_data, + ) + .and_then(|body| { + let op = ForOp::new(var, start, stop, step, body).map_err(|_| -3)?; + append_control_op(ptr, ClassicalControlOp::For(op)) + }); + match close_control_region(ptr, transaction, result) { + Ok(()) => 0, + Err(code) => code, + } +} + +/// Appends a structured exact-value `switch` operation over a `UInt` expression. +/// +/// `target` must be a `UInt` expression. Case values are `u128` split into +/// two `u64` halves: `value = lo | (hi << 64)`. Every value must fit the +/// target width and appear at most once. `case_bodies[i]` builds the body of +/// `case_values[i]`; `default_body` may be NULL, in which case the switch has +/// no default branch. All callbacks receive the same `user_data`. +/// +/// Returns `0` on success, `-1` for NULL arguments (including a NULL case +/// body when `case_count > 0`), `-3` on validation failure (non-`UInt` +/// target, duplicate or out-of-range case values), or the first non-zero +/// status returned by a callback after rollback. +#[unsafe(no_mangle)] +#[allow(clippy::too_many_arguments)] +pub extern "C" fn circuit_switch( + ptr: *mut CCircuit, + target: *const CClassicalExpr, + case_values_lo: *const u64, + case_values_hi: *const u64, + case_bodies: *const CqlibBodyFn, + case_count: usize, + default_body: CqlibBodyFn, + user_data: *mut c_void, +) -> i32 { + if ptr.is_null() || target.is_null() { + return -1; + } + if case_count > 0 + && (case_values_lo.is_null() || case_values_hi.is_null() || case_bodies.is_null()) + { + return -1; + } + let target = unsafe { (*target).inner.clone() }; + if unsafe { (*ptr).inner.validate_classical_expr(&target) }.is_err() { + return -3; + } + + let transaction = unsafe { (*ptr).inner.begin_control_body_transaction() }; + let mut cases = Vec::with_capacity(case_count); + let mut captured = Ok(()); + for index in 0..case_count { + let case_fn = unsafe { *case_bodies.add(index) }; + let Some(case_fn) = case_fn else { + captured = Err(-1); + break; + }; + let value = unsafe { + (*case_values_lo.add(index) as u128) | ((*case_values_hi.add(index) as u128) << 64) + }; + match run_body_callback( + ptr, + &transaction, + ExternalControlScope::Switch, + case_fn, + user_data, + ) { + Ok(body) => cases.push(SwitchCase::new(value, body)), + Err(code) => { + captured = Err(code); + break; + } + } + } + + let result = captured.and_then(|()| { + let default_body = match default_body { + Some(default_fn) => Some(run_body_callback( + ptr, + &transaction, + ExternalControlScope::Switch, + default_fn, + user_data, + )?), + None => None, + }; + let op = SwitchOp::new(target, cases, default_body).map_err(|_| -3)?; + append_control_op(ptr, ClassicalControlOp::Switch(op)) + }); + match close_control_region(ptr, transaction, result) { + Ok(()) => 0, + Err(code) => code, + } +} + +/// Appends a `break` to the nearest enclosing loop or switch body. +/// +/// Legal only inside a `circuit_while`, `circuit_for_uint`, or +/// `circuit_switch` body callback (or a body nested inside one), and only as +/// the final operation of that body (terminal control transfer). +/// +/// Returns `0` on success, `-1` for a NULL circuit, `-3` when no enclosing +/// loop or switch body is active or the transfer is not terminal. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_break_loop(ptr: *mut CCircuit) -> i32 { + if ptr.is_null() { + return -1; + } + unsafe { (*ptr).inner.break_loop() }.map_or(-3, |()| 0) +} + +/// Appends a `continue` to the nearest enclosing loop body. +/// +/// Legal only inside a `circuit_while` or `circuit_for_uint` body callback +/// (or a body nested inside one), and only as the final operation of that +/// body (terminal control transfer). +/// +/// Returns `0` on success, `-1` for a NULL circuit, `-3` when no enclosing +/// loop body is active or the transfer is not terminal. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_continue_loop(ptr: *mut CCircuit) -> i32 { + if ptr.is_null() { + return -1; + } + unsafe { (*ptr).inner.continue_loop() }.map_or(-3, |()| 0) +} + +/// Appends a copy of an existing classical-control operation. +/// +/// `ClassicalControlOp` values cannot be constructed directly from C yet +/// (they require operation handles); this function takes the control +/// operation stored at `index` in `source`'s operation list and appends a +/// validated copy to `ptr`. The same circuit may be passed as both `ptr` and +/// `source`, which duplicates one of its control operations. Cross-circuit +/// copies require the operation to be self-contained (no classical handles +/// from `source`). +/// +/// Returns `0` on success, `-1` for NULL arguments, `-8` when `index` is out +/// of range or the operation at `index` is not a control-flow operation, +/// `-3` when the copy fails validation (foreign classical handles, illegal +/// `break`/`continue` scoping, or unknown qubits). +#[unsafe(no_mangle)] +pub extern "C" fn circuit_append_control( + ptr: *mut CCircuit, + source: *const CCircuit, + index: usize, +) -> i32 { + if ptr.is_null() || source.is_null() { + return -1; + } + let op = unsafe { + let operations = (*source).inner.operations(); + let Some(operation) = operations.get(index) else { + return -8; + }; + let Instruction::ClassicalControl(op) = &operation.instruction else { + return -8; + }; + op.clone() + }; + append_control_op(ptr, op).map_or(-3, |()| 0) +} diff --git a/crates/binding-c/src/circuit/dag.rs b/crates/binding-c/src/circuit/dag.rs new file mode 100644 index 00000000..7b5ea4d9 --- /dev/null +++ b/crates/binding-c/src/circuit/dag.rs @@ -0,0 +1,1792 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! C ABI for circuit dependency DAGs. + +#![allow(clippy::not_unsafe_ptr_arg_deref)] + +use crate::circuit::advanced::CValueOperation; +use crate::circuit::operations::COperation; +use crate::circuit::{CCircuit, CParameter}; +use cqlib_core::circuit::dag::{CircuitDag, DagControlFlow, DagWire}; +use cqlib_core::circuit::gate::instruction::Instruction; +use cqlib_core::circuit::{Operation, Qubit}; +use rustworkx_core::petgraph::prelude::NodeIndex; +use std::ffi::CString; +use std::os::raw::c_char; + +/// Opaque handle around a [`CircuitDag`]. +pub struct CCircuitDag { + pub inner: CircuitDag, +} + +/// Opaque snapshot of a [`DagControlFlow`] structure attached to a DAG +/// node. Produced by `circuit_dag_control_flow`; free with +/// `dag_control_flow_free`. +pub struct CDagControlFlow { + inner: DagControlFlow, +} + +/// Control-flow kind tag: `if/else`. +pub const DAG_CONTROL_FLOW_IF: u32 = 0; +/// Control-flow kind tag: `while`. +pub const DAG_CONTROL_FLOW_WHILE: u32 = 1; +/// Control-flow kind tag: `for`. +pub const DAG_CONTROL_FLOW_FOR: u32 = 2; +/// Control-flow kind tag: `switch`. +pub const DAG_CONTROL_FLOW_SWITCH: u32 = 3; +/// Control-flow kind tag: `break`. +pub const DAG_CONTROL_FLOW_BREAK: u32 = 4; +/// Control-flow kind tag: `continue`. +pub const DAG_CONTROL_FLOW_CONTINUE: u32 = 5; + +/// Run-collection criterion: contiguous one-qubit gates. +pub const DAG_RUN_CRITERION_1Q_GATE: u32 = 0; +/// Run-collection criterion: contiguous two-qubit gates. +pub const DAG_RUN_CRITERION_2Q_GATE: u32 = 1; + +/// DAG wire tag: quantum bit timeline carrying a qubit id. +pub const DAG_WIRE_QUBIT: u32 = 0; +/// DAG wire tag: mutable classical storage carrying a variable id. +pub const DAG_WIRE_CLASSICAL_VAR: u32 = 1; +/// DAG wire tag: immutable classical value carrying a value id. +pub const DAG_WIRE_CLASSICAL_VALUE: u32 = 2; +/// DAG wire tag: global ordering resource with no payload. +pub const DAG_WIRE_GLOBAL_ORDER: u32 = 3; + +fn wire_tag(wire: &DagWire) -> (u32, u32) { + match wire { + DagWire::Qubit(qubit) => (DAG_WIRE_QUBIT, qubit.id()), + DagWire::ClassicalVar(var) => (DAG_WIRE_CLASSICAL_VAR, var.id()), + DagWire::ClassicalValue(value) => (DAG_WIRE_CLASSICAL_VALUE, value.index()), + DagWire::GlobalOrder => (DAG_WIRE_GLOBAL_ORDER, 0), + } +} + +/// Rebuilds a wire from its C tag and payload id. Classical wires carry +/// circuit-scoped handles that C cannot reconstruct, so their tags are +/// rejected on input and reported as `-8`. +fn dag_wire_from_tag(tag: u32, id: u32) -> Option { + match tag { + DAG_WIRE_QUBIT => Some(DagWire::Qubit(Qubit::new(id))), + DAG_WIRE_GLOBAL_ORDER => Some(DagWire::GlobalOrder), + _ => None, + } +} + +/// Copies node indices into a C `u32` buffer. Returns 0 on success, -1 for +/// a NULL buffer (when there is data to write), -8 when `len` is too small. +fn copy_node_ids(nodes: &[NodeIndex], buffer: *mut u32, len: usize) -> i32 { + if len < nodes.len() { + return -8; + } + if nodes.is_empty() { + return 0; + } + if buffer.is_null() { + return -1; + } + let slice = unsafe { std::slice::from_raw_parts_mut(buffer, nodes.len()) }; + for (slot, node) in slice.iter_mut().zip(nodes.iter()) { + *slot = node.index() as u32; + } + 0 +} + +/// Copies a list of node lists (layers or runs) in CSR style: flattened +/// node ids plus boundary offsets with `offsets[0] == 0`. Returns 0 on +/// success, -1 for NULL buffers, -8 when capacities are too small. +fn copy_node_groups( + groups: &[Vec], + nodes: *mut u32, + nodes_cap: usize, + offsets: *mut usize, + offsets_cap: usize, +) -> i32 { + let total: usize = groups.iter().map(Vec::len).sum(); + if offsets_cap < groups.len() + 1 || nodes_cap < total { + return -8; + } + if offsets.is_null() { + return -1; + } + let offset_slice = unsafe { std::slice::from_raw_parts_mut(offsets, groups.len() + 1) }; + offset_slice[0] = 0; + let mut running = 0usize; + for (slot, group) in offset_slice.iter_mut().skip(1).zip(groups.iter()) { + running += group.len(); + *slot = running; + } + if total > 0 { + if nodes.is_null() { + return -1; + } + let slice = unsafe { std::slice::from_raw_parts_mut(nodes, total) }; + let mut filled = 0usize; + for group in groups { + for node in group { + slice[filled] = node.index() as u32; + filled += 1; + } + } + } + 0 +} + +fn is_one_qubit_gate_c(operation: &Operation) -> bool { + operation.qubits.len() == 1 + && matches!( + operation.instruction, + Instruction::Standard(_) + | Instruction::McGate(_) + | Instruction::UnitaryGate(_) + | Instruction::CircuitGate(_) + ) +} + +fn is_two_qubit_gate_c(operation: &Operation) -> bool { + operation.qubits.len() == 2 + && matches!( + operation.instruction, + Instruction::Standard(_) + | Instruction::McGate(_) + | Instruction::UnitaryGate(_) + | Instruction::CircuitGate(_) + ) +} + +fn control_flow_kind_tag(flow: &DagControlFlow) -> u32 { + match flow { + DagControlFlow::If { .. } => DAG_CONTROL_FLOW_IF, + DagControlFlow::While { .. } => DAG_CONTROL_FLOW_WHILE, + DagControlFlow::For { .. } => DAG_CONTROL_FLOW_FOR, + DagControlFlow::Switch { .. } => DAG_CONTROL_FLOW_SWITCH, + DagControlFlow::Break => DAG_CONTROL_FLOW_BREAK, + DagControlFlow::Continue => DAG_CONTROL_FLOW_CONTINUE, + } +} + +/// Lists body DAGs in a stable order: `if` is `[then, else?]`, `while` and +/// `for` carry a single body, `switch` lists case bodies in order, and +/// `break`/`continue` carry none. +fn control_flow_bodies(flow: &DagControlFlow) -> Vec<&CircuitDag> { + match flow { + DagControlFlow::If { + then_body, + else_body, + } => { + let mut bodies = vec![then_body.as_ref()]; + if let Some(else_body) = else_body { + bodies.push(else_body.as_ref()); + } + bodies + } + DagControlFlow::While { body } | DagControlFlow::For { body } => vec![body.as_ref()], + DagControlFlow::Switch { cases, .. } => { + cases.iter().map(|case| case.body.as_ref()).collect() + } + DagControlFlow::Break | DagControlFlow::Continue => Vec::new(), + } +} + +/// Builds a dependency DAG from a circuit. Returns a newly allocated +/// `CCircuitDag*` (free with `circuit_dag_free`), or NULL on NULL input or +/// build failure. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_from_circuit(ptr: *const CCircuit) -> *mut CCircuitDag { + if ptr.is_null() { + return std::ptr::null_mut(); + } + match CircuitDag::from_circuit(unsafe { &(*ptr).inner }) { + Ok(inner) => Box::into_raw(Box::new(CCircuitDag { inner })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Builds a dependency DAG from explicit qubits and operations. Each +/// element of `qubits` is used directly as a qubit id; every operation +/// must act on registered ids and duplicate ids fail the build. Returns +/// a newly allocated `CCircuitDag*`, or NULL on NULL input or build +/// failure. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_from_operations( + qubits: *const u32, + qubits_len: usize, + ops: *const *const COperation, + ops_len: usize, +) -> *mut CCircuitDag { + if ops_len > 0 && ops.is_null() { + return std::ptr::null_mut(); + } + let mut qubit_list = Vec::with_capacity(qubits_len); + if qubits_len > 0 { + if qubits.is_null() { + return std::ptr::null_mut(); + } + let slice = unsafe { std::slice::from_raw_parts(qubits, qubits_len) }; + for &id in slice { + qubit_list.push(Qubit::new(id)); + } + } + let mut op_list = Vec::with_capacity(ops_len); + if ops_len > 0 { + let slice = unsafe { std::slice::from_raw_parts(ops, ops_len) }; + for op in slice { + if op.is_null() { + return std::ptr::null_mut(); + } + op_list.push(unsafe { (*(*op)).inner.clone() }); + } + } + match CircuitDag::from_operations(qubit_list, &op_list) { + Ok(inner) => Box::into_raw(Box::new(CCircuitDag { inner })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Frees a DAG handle. Passing NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_free(ptr: *mut CCircuitDag) { + if !ptr.is_null() { + unsafe { + drop(Box::from_raw(ptr)); + } + } +} + +/// Rebuilds the source-order circuit from the DAG. Returns a newly +/// allocated `CCircuit*` (free with `circuit_free`), or NULL on NULL input +/// or lowering failure. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_to_circuit(ptr: *const CCircuitDag) -> *mut CCircuit { + if ptr.is_null() { + return std::ptr::null_mut(); + } + match unsafe { (*ptr).inner.to_circuit() } { + Ok(inner) => Box::into_raw(Box::new(CCircuit { inner })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Validates DAG consistency (topological order). Returns 0 on success, -1 +/// on NULL input, -3 on a cyclic or invalid graph. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_validate(ptr: *const CCircuitDag) -> i32 { + if ptr.is_null() { + return -1; + } + unsafe { (*ptr).inner.validate().map_or(-3, |_| 0) } +} + +/// Returns the number of qubits, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_num_qubits(ptr: *const CCircuitDag) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.num_qubits() } +} + +/// Returns the number of operation nodes, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_num_ops(ptr: *const CCircuitDag) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.num_ops() } +} + +/// Returns whether the DAG has no operation nodes: 1 when empty, 0 +/// otherwise, -1 on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_is_empty(ptr: *const CCircuitDag) -> i32 { + if ptr.is_null() { + return -1; + } + unsafe { (*ptr).inner.is_empty() as i32 } +} + +/// Returns the number of qubit ids in the DAG, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_qubits_len(ptr: *const CCircuitDag) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.qubits().len() } +} + +/// Copies the qubit ids in registration order into `buffer`. Returns 0 on +/// success, -1 on NULL input, -8 when `len` is too small. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_qubits(ptr: *const CCircuitDag, buffer: *mut u32, len: usize) -> i32 { + if ptr.is_null() { + return -1; + } + let qubits = unsafe { (*ptr).inner.qubits() }; + if len < qubits.len() { + return -8; + } + if qubits.is_empty() { + return 0; + } + if buffer.is_null() { + return -1; + } + let slice = unsafe { std::slice::from_raw_parts_mut(buffer, qubits.len()) }; + for (slot, qubit) in slice.iter_mut().zip(qubits.iter()) { + *slot = qubit.id(); + } + 0 +} + +/// Returns the number of registered parameters, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_parameters_len(ptr: *const CCircuitDag) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.parameters().len() } +} + +/// Copies the parameter names in registration order into `out`. Each +/// written element is a freshly allocated C string the caller frees with +/// `cqlib_string_free`. Returns 0 on success, -1 on NULL input, -8 when +/// `len` is too small. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_parameters( + ptr: *const CCircuitDag, + out: *mut *mut c_char, + len: usize, +) -> i32 { + if ptr.is_null() { + return -1; + } + let params = unsafe { (*ptr).inner.parameters() }; + if len < params.len() { + return -8; + } + if params.is_empty() { + return 0; + } + if out.is_null() { + return -1; + } + let slice = unsafe { std::slice::from_raw_parts_mut(out, params.len()) }; + for (slot, param) in slice.iter_mut().zip(params.iter()) { + let text = param.as_symbol().unwrap_or_else(|| param.to_string()); + match CString::new(text) { + Ok(text) => *slot = text.into_raw(), + Err(_) => return -3, + } + } + 0 +} + +/// Interns a copy of `param` into the DAG's parameter table, mirroring +/// `Circuit::add_parameter`: the parameter is inserted when absent and all +/// of its symbol names are registered. Writes the table index into +/// `out_index` and 1/0 for "newly inserted" into `out_inserted` unless +/// NULL. Returns 0 on success, -1 on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_add_parameter( + ptr: *mut CCircuitDag, + param: *const CParameter, + out_index: *mut usize, + out_inserted: *mut i32, +) -> i32 { + let dag = if !ptr.is_null() { + unsafe { &mut (*ptr).inner } + } else { + return -1; + }; + if param.is_null() { + return -1; + } + let param = unsafe { (*param).inner.clone() }; + let (index, inserted) = dag.add_parameter(param); + if !out_index.is_null() { + unsafe { *out_index = index }; + } + if !out_inserted.is_null() { + unsafe { *out_inserted = inserted as i32 }; + } + 0 +} + +/// Returns the number of symbols referenced by the DAG, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_symbols_len(ptr: *const CCircuitDag) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.symbols().len() } +} + +/// Copies the symbol names into `out`. Each written element is a freshly +/// allocated C string the caller frees with `cqlib_string_free`. Returns +/// 0 on success, -1 on NULL input, -8 when `len` is too small. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_symbols( + ptr: *const CCircuitDag, + out: *mut *mut c_char, + len: usize, +) -> i32 { + if ptr.is_null() { + return -1; + } + let symbols = unsafe { (*ptr).inner.symbols() }; + if len < symbols.len() { + return -8; + } + if symbols.is_empty() { + return 0; + } + if out.is_null() { + return -1; + } + let slice = unsafe { std::slice::from_raw_parts_mut(out, symbols.len()) }; + for (slot, symbol) in slice.iter_mut().zip(symbols.iter()) { + match CString::new(symbol.as_str()) { + Ok(text) => *slot = text.into_raw(), + Err(_) => return -3, + } + } + 0 +} + +/// Returns the number of wires in the DAG, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_wires_len(ptr: *const CCircuitDag) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.wires().count() } +} + +/// Copies wire tags and payload ids into `tags` and `ids`. Returns 0 on +/// success, -1 on NULL input, -8 when `len` is too small. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_wires( + ptr: *const CCircuitDag, + tags: *mut u32, + ids: *mut u32, + len: usize, +) -> i32 { + if ptr.is_null() { + return -1; + } + let wires: Vec = unsafe { (*ptr).inner.wires().collect() }; + if len < wires.len() { + return -8; + } + if wires.is_empty() { + return 0; + } + if tags.is_null() || ids.is_null() { + return -1; + } + let tag_slice = unsafe { std::slice::from_raw_parts_mut(tags, wires.len()) }; + let id_slice = unsafe { std::slice::from_raw_parts_mut(ids, wires.len()) }; + for (slot, wire) in tag_slice.iter_mut().zip(wires.iter()) { + *slot = wire_tag(wire).0; + } + for (slot, wire) in id_slice.iter_mut().zip(wires.iter()) { + *slot = wire_tag(wire).1; + } + 0 +} + +/// Returns whether the DAG carries the given wire: 1 when present, 0 when +/// not, -1 on NULL input, -8 on an unknown wire tag. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_has_wire(ptr: *const CCircuitDag, tag: u32, id: u32) -> i32 { + if ptr.is_null() { + return -1; + } + match dag_wire_from_tag(tag, id) { + Some(wire) => unsafe { (*ptr).inner.has_wire(wire) as i32 }, + None => -8, + } +} + +// ===== Node identification ===== + +/// Returns whether `node` is an operation node: 1 when it is, 0 when it is +/// a wire sentinel, -1 on NULL input, -2 for an unknown node id. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_is_operation(ptr: *const CCircuitDag, node: u32) -> i32 { + if ptr.is_null() { + return -1; + } + let dag = unsafe { &(*ptr).inner }; + if dag.node_kind(NodeIndex::new(node as usize)).is_none() { + return -2; + } + dag.is_operation(NodeIndex::new(node as usize)) as i32 +} + +/// Returns the kind of `node` as a freshly allocated C string: +/// `"wire_in"`, `"wire_out"`, or `"operation"` (free with +/// `cqlib_string_free`). Returns NULL on NULL input or an unknown node id. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_node_kind(ptr: *const CCircuitDag, node: u32) -> *mut c_char { + if ptr.is_null() { + return std::ptr::null_mut(); + } + match unsafe { &(*ptr).inner }.node_kind(NodeIndex::new(node as usize)) { + Some(kind) => match CString::new(kind) { + Ok(text) => text.into_raw(), + Err(_) => std::ptr::null_mut(), + }, + None => std::ptr::null_mut(), + } +} + +/// Returns a copy of the operation stored at `node` as a newly allocated +/// `COperation*` (free with `operation_free`). Returns NULL on NULL input +/// or when `node` is not an operation node. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_operation(ptr: *const CCircuitDag, node: u32) -> *mut COperation { + if ptr.is_null() { + return std::ptr::null_mut(); + } + match unsafe { &(*ptr).inner }.operation(NodeIndex::new(node as usize)) { + Some(operation) => Box::into_raw(Box::new(COperation { + inner: operation.clone(), + })), + None => std::ptr::null_mut(), + } +} + +// ===== Graph traversal ===== + +/// Returns the number of operation nodes in source order, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_op_nodes_len(ptr: *const CCircuitDag) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { &(*ptr).inner }.op_nodes().count() +} + +/// Copies the operation nodes in source order into `buffer`. Returns 0 on +/// success, -1 on NULL input, -8 when `len` is too small. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_op_nodes( + ptr: *const CCircuitDag, + buffer: *mut u32, + len: usize, +) -> i32 { + if ptr.is_null() { + return -1; + } + let nodes: Vec = unsafe { &(*ptr).inner }.op_nodes().collect(); + copy_node_ids(&nodes, buffer, len) +} + +/// Returns the number of operation nodes in topological order, or 0 for +/// NULL input or a cyclic graph. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_topological_op_nodes_len(ptr: *const CCircuitDag) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { &(*ptr).inner } + .topological_op_nodes() + .map_or(0, |nodes| nodes.len()) +} + +/// Copies the operation nodes in deterministic topological order into +/// `buffer`. Returns 0 on success, -1 on NULL input, -8 when `len` is too +/// small, -3 on a cyclic graph. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_topological_op_nodes( + ptr: *const CCircuitDag, + buffer: *mut u32, + len: usize, +) -> i32 { + if ptr.is_null() { + return -1; + } + match unsafe { &(*ptr).inner }.topological_op_nodes() { + Ok(nodes) => copy_node_ids(&nodes, buffer, len), + Err(_) => -3, + } +} + +/// Returns the number of direct predecessors of `node`, or 0 for NULL +/// input or an unknown node id. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_predecessors_len(ptr: *const CCircuitDag, node: u32) -> usize { + let dag = if !ptr.is_null() { + unsafe { &(*ptr).inner } + } else { + return 0; + }; + if dag.node_kind(NodeIndex::new(node as usize)).is_none() { + return 0; + } + dag.predecessors(NodeIndex::new(node as usize)).count() +} + +/// Copies the direct predecessors of `node` into `buffer`. Returns 0 on +/// success, -1 on NULL input or a NULL buffer, -2 for an unknown node id, +/// -8 when `len` is too small. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_predecessors( + ptr: *const CCircuitDag, + node: u32, + buffer: *mut u32, + len: usize, +) -> i32 { + if ptr.is_null() { + return -1; + } + let dag = unsafe { &(*ptr).inner }; + if dag.node_kind(NodeIndex::new(node as usize)).is_none() { + return -2; + } + let nodes: Vec = dag.predecessors(NodeIndex::new(node as usize)).collect(); + copy_node_ids(&nodes, buffer, len) +} + +/// Returns the number of direct successors of `node`, or 0 for NULL input +/// or an unknown node id. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_successors_len(ptr: *const CCircuitDag, node: u32) -> usize { + let dag = if !ptr.is_null() { + unsafe { &(*ptr).inner } + } else { + return 0; + }; + if dag.node_kind(NodeIndex::new(node as usize)).is_none() { + return 0; + } + dag.successors(NodeIndex::new(node as usize)).count() +} + +/// Copies the direct successors of `node` into `buffer`. Returns 0 on +/// success, -1 on NULL input or a NULL buffer, -2 for an unknown node id, +/// -8 when `len` is too small. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_successors( + ptr: *const CCircuitDag, + node: u32, + buffer: *mut u32, + len: usize, +) -> i32 { + if ptr.is_null() { + return -1; + } + let dag = unsafe { &(*ptr).inner }; + if dag.node_kind(NodeIndex::new(node as usize)).is_none() { + return -2; + } + let nodes: Vec = dag.successors(NodeIndex::new(node as usize)).collect(); + copy_node_ids(&nodes, buffer, len) +} + +/// Returns the number of operation predecessors of `node` connected +/// through `wire`, or 0 for NULL input, an unknown node id, an unknown +/// wire tag, or an invalid combination. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_predecessors_on_wire_len( + ptr: *const CCircuitDag, + node: u32, + tag: u32, + id: u32, +) -> usize { + match dag_wire_neighbors(ptr, node, tag, id) { + Ok(nodes) => nodes.len(), + Err(_) => 0, + } +} + +fn dag_wire_neighbors( + ptr: *const CCircuitDag, + node: u32, + tag: u32, + id: u32, +) -> Result, i32> { + if ptr.is_null() { + return Err(-1); + } + let Some(wire) = dag_wire_from_tag(tag, id) else { + return Err(-8); + }; + let dag = unsafe { &(*ptr).inner }; + if dag.node_kind(NodeIndex::new(node as usize)).is_none() { + return Err(-2); + } + dag.predecessors_on_wire(NodeIndex::new(node as usize), wire) + .map_err(|_| -3) +} + +/// Copies the operation predecessors of `node` connected through `wire` +/// into `buffer`. Returns 0 on success, -1 on NULL input or a NULL buffer +/// with data available, -2 for an unknown node id, -3 for an invalid +/// wire/node combination, -8 for an unknown wire tag or a too-small +/// buffer. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_predecessors_on_wire( + ptr: *const CCircuitDag, + node: u32, + tag: u32, + id: u32, + buffer: *mut u32, + len: usize, +) -> i32 { + match dag_wire_neighbors(ptr, node, tag, id) { + Ok(nodes) => copy_node_ids(&nodes, buffer, len), + Err(code) => code, + } +} + +/// Returns the number of operation successors of `node` connected through +/// `wire`, or 0 for NULL input, an unknown node id, an unknown wire tag, +/// or an invalid combination. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_successors_on_wire_len( + ptr: *const CCircuitDag, + node: u32, + tag: u32, + id: u32, +) -> usize { + match dag_wire_neighbors_out(ptr, node, tag, id) { + Ok(nodes) => nodes.len(), + Err(_) => 0, + } +} + +fn dag_wire_neighbors_out( + ptr: *const CCircuitDag, + node: u32, + tag: u32, + id: u32, +) -> Result, i32> { + if ptr.is_null() { + return Err(-1); + } + let Some(wire) = dag_wire_from_tag(tag, id) else { + return Err(-8); + }; + let dag = unsafe { &(*ptr).inner }; + if dag.node_kind(NodeIndex::new(node as usize)).is_none() { + return Err(-2); + } + dag.successors_on_wire(NodeIndex::new(node as usize), wire) + .map_err(|_| -3) +} + +/// Copies the operation successors of `node` connected through `wire` +/// into `buffer`. Returns 0 on success, -1 on NULL input or a NULL buffer +/// with data available, -2 for an unknown node id, -3 for an invalid +/// wire/node combination, -8 for an unknown wire tag or a too-small +/// buffer. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_successors_on_wire( + ptr: *const CCircuitDag, + node: u32, + tag: u32, + id: u32, + buffer: *mut u32, + len: usize, +) -> i32 { + match dag_wire_neighbors_out(ptr, node, tag, id) { + Ok(nodes) => copy_node_ids(&nodes, buffer, len), + Err(code) => code, + } +} + +enum DagDirection { + QuantumPredecessors, + QuantumSuccessors, + ClassicalPredecessors, + ClassicalSuccessors, +} + +fn node_neighbor_snapshot( + ptr: *const CCircuitDag, + node: u32, + direction: DagDirection, +) -> Result, i32> { + if ptr.is_null() { + return Err(-1); + } + let dag = unsafe { &(*ptr).inner }; + if dag.node_kind(NodeIndex::new(node as usize)).is_none() { + return Err(-2); + } + let result = match direction { + DagDirection::QuantumPredecessors => { + dag.quantum_predecessors(NodeIndex::new(node as usize)) + } + DagDirection::QuantumSuccessors => dag.quantum_successors(NodeIndex::new(node as usize)), + DagDirection::ClassicalPredecessors => { + dag.classical_predecessors(NodeIndex::new(node as usize)) + } + DagDirection::ClassicalSuccessors => { + dag.classical_successors(NodeIndex::new(node as usize)) + } + }; + result.map_err(|_| -3) +} + +/// Returns the number of quantum-wire-connected operation predecessors of +/// `node`, or 0 for NULL input, an unknown node id, or failure. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_quantum_predecessors_len( + ptr: *const CCircuitDag, + node: u32, +) -> usize { + node_neighbor_snapshot(ptr, node, DagDirection::QuantumPredecessors) + .map_or(0, |nodes| nodes.len()) +} + +/// Copies the quantum-wire-connected operation predecessors of `node` +/// into `buffer`. Returns 0 on success, -1 on NULL input, -2 for an +/// unknown node id, -3 on failure, -8 when `len` is too small. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_quantum_predecessors( + ptr: *const CCircuitDag, + node: u32, + buffer: *mut u32, + len: usize, +) -> i32 { + match node_neighbor_snapshot(ptr, node, DagDirection::QuantumPredecessors) { + Ok(nodes) => copy_node_ids(&nodes, buffer, len), + Err(code) => code, + } +} + +/// Returns the number of quantum-wire-connected operation successors of +/// `node`, or 0 for NULL input, an unknown node id, or failure. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_quantum_successors_len(ptr: *const CCircuitDag, node: u32) -> usize { + node_neighbor_snapshot(ptr, node, DagDirection::QuantumSuccessors) + .map_or(0, |nodes| nodes.len()) +} + +/// Copies the quantum-wire-connected operation successors of `node` into +/// `buffer`. Returns 0 on success, -1 on NULL input, -2 for an unknown +/// node id, -3 on failure, -8 when `len` is too small. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_quantum_successors( + ptr: *const CCircuitDag, + node: u32, + buffer: *mut u32, + len: usize, +) -> i32 { + match node_neighbor_snapshot(ptr, node, DagDirection::QuantumSuccessors) { + Ok(nodes) => copy_node_ids(&nodes, buffer, len), + Err(code) => code, + } +} + +/// Returns the number of classical-wire-connected operation predecessors +/// of `node`, or 0 for NULL input, an unknown node id, or failure. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_classical_predecessors_len( + ptr: *const CCircuitDag, + node: u32, +) -> usize { + node_neighbor_snapshot(ptr, node, DagDirection::ClassicalPredecessors) + .map_or(0, |nodes| nodes.len()) +} + +/// Copies the classical-wire-connected operation predecessors of `node` +/// into `buffer`. Returns 0 on success, -1 on NULL input, -2 for an +/// unknown node id, -3 on failure, -8 when `len` is too small. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_classical_predecessors( + ptr: *const CCircuitDag, + node: u32, + buffer: *mut u32, + len: usize, +) -> i32 { + match node_neighbor_snapshot(ptr, node, DagDirection::ClassicalPredecessors) { + Ok(nodes) => copy_node_ids(&nodes, buffer, len), + Err(code) => code, + } +} + +/// Returns the number of classical-wire-connected operation successors of +/// `node`, or 0 for NULL input, an unknown node id, or failure. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_classical_successors_len( + ptr: *const CCircuitDag, + node: u32, +) -> usize { + node_neighbor_snapshot(ptr, node, DagDirection::ClassicalSuccessors) + .map_or(0, |nodes| nodes.len()) +} + +/// Copies the classical-wire-connected operation successors of `node` into +/// `buffer`. Returns 0 on success, -1 on NULL input, -2 for an unknown +/// node id, -3 on failure, -8 when `len` is too small. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_classical_successors( + ptr: *const CCircuitDag, + node: u32, + buffer: *mut u32, + len: usize, +) -> i32 { + match node_neighbor_snapshot(ptr, node, DagDirection::ClassicalSuccessors) { + Ok(nodes) => copy_node_ids(&nodes, buffer, len), + Err(code) => code, + } +} + +// ===== Layering and depth ===== + +/// Returns the number of operation nodes without an operation predecessor, +/// or 0 for NULL input or a cyclic graph. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_front_layer_len(ptr: *const CCircuitDag) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { &(*ptr).inner } + .front_layer() + .map_or(0, |nodes| nodes.len()) +} + +/// Copies the front-layer operation nodes into `buffer`. Returns 0 on +/// success, -1 on NULL input, -3 on a cyclic graph, -8 when `len` is too +/// small. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_front_layer( + ptr: *const CCircuitDag, + buffer: *mut u32, + len: usize, +) -> i32 { + if ptr.is_null() { + return -1; + } + match unsafe { &(*ptr).inner }.front_layer() { + Ok(nodes) => copy_node_ids(&nodes, buffer, len), + Err(_) => -3, + } +} + +/// Returns the number of ASAP layers (the ASAP depth), or 0 for NULL input +/// or a cyclic graph. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_layers_len(ptr: *const CCircuitDag) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { &(*ptr).inner } + .layers() + .map_or(0, |layers| layers.len()) +} + +/// Copies the ASAP layers in CSR style: layer nodes flattened into `nodes` +/// and layer boundaries into `offsets` (`offsets[0] == 0`, the end is +/// `offsets[layers_len]`). `nodes_cap` must hold all operation nodes and +/// `offsets_cap` at least `layers_len + 1`. Returns 0 on success, -1 on +/// NULL input, -3 on a cyclic graph, -8 on undersized capacities. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_layers( + ptr: *const CCircuitDag, + nodes: *mut u32, + nodes_cap: usize, + offsets: *mut usize, + offsets_cap: usize, +) -> i32 { + if ptr.is_null() { + return -1; + } + match unsafe { &(*ptr).inner }.layers() { + Ok(layers) => copy_node_groups(&layers, nodes, nodes_cap, offsets, offsets_cap), + Err(_) => -3, + } +} + +/// Returns the number of per-node layer entries (operation nodes), or 0 +/// for NULL input or a cyclic graph. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_node_layers_len(ptr: *const CCircuitDag) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { &(*ptr).inner } + .node_layers() + .map_or(0, |entries| entries.len()) +} + +/// Copies the ASAP layer index of every operation node as (node, layer) +/// pairs into `nodes` and `layers`. Returns 0 on success, -1 on NULL input +/// or NULL buffers, -3 on a cyclic graph, -8 when `len` is too small. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_node_layers( + ptr: *const CCircuitDag, + nodes: *mut u32, + layers: *mut usize, + len: usize, +) -> i32 { + if ptr.is_null() { + return -1; + } + let entries = match unsafe { &(*ptr).inner }.node_layers() { + Ok(entries) => entries, + Err(_) => return -3, + }; + if len < entries.len() { + return -8; + } + if entries.is_empty() { + return 0; + } + if nodes.is_null() || layers.is_null() { + return -1; + } + let node_slice = unsafe { std::slice::from_raw_parts_mut(nodes, entries.len()) }; + let layer_slice = unsafe { std::slice::from_raw_parts_mut(layers, entries.len()) }; + for ((slot_node, slot_layer), (node, layer)) in node_slice + .iter_mut() + .zip(layer_slice.iter_mut()) + .zip(entries.iter()) + { + *slot_node = node.index() as u32; + *slot_layer = *layer; + } + 0 +} + +/// Returns the ASAP dependency depth: the number of layers. Returns -1 on +/// NULL input, -3 on a cyclic graph. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_depth(ptr: *const CCircuitDag) -> isize { + if ptr.is_null() { + return -1; + } + unsafe { &(*ptr).inner } + .depth() + .map_or(-3, |depth| depth as isize) +} + +// ===== Wire timelines ===== + +/// Returns whether `wire` carries no operation: 1 when idle, 0 when in use, +/// -1 on NULL input, -3 when the wire is not part of this DAG, -8 on an +/// unknown wire tag. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_is_wire_idle(ptr: *const CCircuitDag, tag: u32, id: u32) -> i32 { + let wire = match check_wire(ptr, tag, id) { + Ok(wire) => wire, + Err(code) => return code, + }; + match unsafe { &(*ptr).inner }.is_wire_idle(wire) { + Ok(idle) => idle as i32, + Err(_) => -3, + } +} + +fn check_wire(ptr: *const CCircuitDag, tag: u32, id: u32) -> Result { + if ptr.is_null() { + return Err(-1); + } + dag_wire_from_tag(tag, id).ok_or(-8) +} + +/// Returns the number of operation nodes on `wire` in wire order, or 0 for +/// NULL input, an unknown wire tag, or an invalid wire. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_nodes_on_wire_len( + ptr: *const CCircuitDag, + tag: u32, + id: u32, +) -> usize { + match nodes_on_wire_checked(ptr, tag, id) { + Ok(nodes) => nodes.len(), + Err(_) => 0, + } +} + +fn nodes_on_wire_checked( + ptr: *const CCircuitDag, + tag: u32, + id: u32, +) -> Result, i32> { + let wire = check_wire(ptr, tag, id)?; + unsafe { &(*ptr).inner }.nodes_on_wire(wire).map_err(|_| -3) +} + +/// Copies the operation nodes on `wire` in wire order into `buffer`. +/// Returns 0 on success, -1 on NULL input, -3 when the wire is not part of +/// this DAG, -8 for an unknown wire tag or a too-small buffer. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_nodes_on_wire( + ptr: *const CCircuitDag, + tag: u32, + id: u32, + buffer: *mut u32, + len: usize, +) -> i32 { + match nodes_on_wire_checked(ptr, tag, id) { + Ok(nodes) => copy_node_ids(&nodes, buffer, len), + Err(code) => code, + } +} + +/// Returns the input sentinel node of `wire`, or -1 on NULL input, -3 when +/// the wire is not part of this DAG, -8 on an unknown wire tag. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_wire_in(ptr: *const CCircuitDag, tag: u32, id: u32) -> i32 { + let wire = match check_wire(ptr, tag, id) { + Ok(wire) => wire, + Err(code) => return code, + }; + match unsafe { &(*ptr).inner }.wire_in(wire) { + Some(node) => node.index() as i32, + None => -3, + } +} + +/// Returns the output sentinel node of `wire`, or -1 on NULL input, -3 +/// when the wire is not part of this DAG, -8 on an unknown wire tag. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_wire_out(ptr: *const CCircuitDag, tag: u32, id: u32) -> i32 { + let wire = match check_wire(ptr, tag, id) { + Ok(wire) => wire, + Err(code) => return code, + }; + match unsafe { &(*ptr).inner }.wire_out(wire) { + Some(node) => node.index() as i32, + None => -3, + } +} + +// ===== Control-flow and measurement flags ===== + +/// Returns whether any top-level operation is structured control flow: 1 +/// when true, 0 otherwise, -1 on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_has_control_flow(ptr: *const CCircuitDag) -> i32 { + if ptr.is_null() { + return -1; + } + unsafe { &(*ptr).inner }.has_control_flow() as i32 +} + +/// Returns whether a control-flow body contains structured control flow: 1 +/// when true, 0 otherwise, -1 on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_has_nested_control_flow(ptr: *const CCircuitDag) -> i32 { + if ptr.is_null() { + return -1; + } + unsafe { &(*ptr).inner }.has_nested_control_flow() as i32 +} + +/// Returns whether any top-level operation directly or recursively +/// measures: 1 when true, 0 otherwise, -1 on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_has_measurement(ptr: *const CCircuitDag) -> i32 { + if ptr.is_null() { + return -1; + } + unsafe { &(*ptr).inner }.has_measurement() as i32 +} + +// ===== Operation counting ===== + +/// Returns the number of distinct instruction names among top-level +/// operations, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_operation_count_by_name_len(ptr: *const CCircuitDag) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { &(*ptr).inner }.operation_count_by_name().len() +} + +/// Copies the top-level operation counts into `names` and `counts` in +/// insertion order. Each written name is a freshly allocated C string the +/// caller frees with `cqlib_string_free`. Returns 0 on success, -1 on NULL +/// input or NULL buffers, -8 when `len` is too small. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_operation_count_by_name( + ptr: *const CCircuitDag, + names: *mut *mut c_char, + counts: *mut usize, + len: usize, +) -> i32 { + if ptr.is_null() { + return -1; + } + let totals = unsafe { &(*ptr).inner }.operation_count_by_name(); + copy_name_counts( + totals.iter().map(|(k, v)| (k.clone(), *v)).collect(), + names, + counts, + len, + ) +} + +/// Returns the number of distinct instruction names among top-level and +/// nested control-flow body operations, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_operation_count_by_name_recursive_len( + ptr: *const CCircuitDag, +) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { &(*ptr).inner } + .operation_count_by_name_recursive() + .len() +} + +/// Copies the top-level and nested operation counts into `names` and +/// `counts` in insertion order. Each written name is a freshly allocated C +/// string the caller frees with `cqlib_string_free`. Returns 0 on success, +/// -1 on NULL input or NULL buffers, -8 when `len` is too small. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_operation_count_by_name_recursive( + ptr: *const CCircuitDag, + names: *mut *mut c_char, + counts: *mut usize, + len: usize, +) -> i32 { + if ptr.is_null() { + return -1; + } + let totals = unsafe { &(*ptr).inner }.operation_count_by_name_recursive(); + copy_name_counts( + totals.iter().map(|(k, v)| (k.clone(), *v)).collect(), + names, + counts, + len, + ) +} + +fn copy_name_counts( + totals: Vec<(String, usize)>, + names: *mut *mut c_char, + counts: *mut usize, + len: usize, +) -> i32 { + let n = totals.len(); + if len < n { + return -8; + } + if n == 0 { + return 0; + } + if names.is_null() || counts.is_null() { + return -1; + } + let name_slice = unsafe { std::slice::from_raw_parts_mut(names, n) }; + let count_slice = unsafe { std::slice::from_raw_parts_mut(counts, n) }; + for ((slot_name, slot_count), (name, count)) in name_slice + .iter_mut() + .zip(count_slice.iter_mut()) + .zip(totals.iter()) + { + match CString::new(name.as_str()) { + Ok(text) => *slot_name = text.into_raw(), + Err(_) => return -3, + } + *slot_count = *count; + } + 0 +} + +// ===== Contiguous run collection ===== + +/// Returns the number of contiguous one-qubit gate runs over all qubit +/// wires, or 0 for NULL input or failure. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_collect_1q_runs_len(ptr: *const CCircuitDag) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { &(*ptr).inner } + .collect_1q_runs() + .map_or(0, |runs| runs.len()) +} + +/// Copies the contiguous one-qubit gate runs in CSR style: run nodes +/// flattened into `nodes` and run boundaries into `offsets` +/// (`offsets[0] == 0`). Returns 0 on success, -1 on NULL input, -3 on a +/// broken timeline, -8 on undersized capacities. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_collect_1q_runs( + ptr: *const CCircuitDag, + nodes: *mut u32, + nodes_cap: usize, + offsets: *mut usize, + offsets_cap: usize, +) -> i32 { + if ptr.is_null() { + return -1; + } + match unsafe { &(*ptr).inner }.collect_1q_runs() { + Ok(runs) => copy_node_groups(&runs, nodes, nodes_cap, offsets, offsets_cap), + Err(_) => -3, + } +} + +/// Returns the number of contiguous two-qubit gate runs over all qubit +/// wires, or 0 for NULL input or failure. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_collect_2q_runs_len(ptr: *const CCircuitDag) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { &(*ptr).inner } + .collect_2q_runs() + .map_or(0, |runs| runs.len()) +} + +/// Copies the contiguous two-qubit gate runs in CSR style: run nodes +/// flattened into `nodes` and run boundaries into `offsets` +/// (`offsets[0] == 0`). Returns 0 on success, -1 on NULL input, -3 on a +/// broken timeline, -8 on undersized capacities. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_collect_2q_runs( + ptr: *const CCircuitDag, + nodes: *mut u32, + nodes_cap: usize, + offsets: *mut usize, + offsets_cap: usize, +) -> i32 { + if ptr.is_null() { + return -1; + } + match unsafe { &(*ptr).inner }.collect_2q_runs() { + Ok(runs) => copy_node_groups(&runs, nodes, nodes_cap, offsets, offsets_cap), + Err(_) => -3, + } +} + +/// Returns the number of contiguous gate runs on `wire` matching +/// `criterion` (`DAG_RUN_CRITERION_1Q_GATE` or `DAG_RUN_CRITERION_2Q_GATE`), +/// or 0 for NULL input, an unknown wire tag, an unknown criterion, or +/// failure. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_collect_runs_on_wire_len( + ptr: *const CCircuitDag, + tag: u32, + id: u32, + criterion: u32, +) -> usize { + match wire_runs_checked(ptr, tag, id, criterion) { + Ok(runs) => runs.len(), + Err(_) => 0, + } +} + +fn wire_runs_checked( + ptr: *const CCircuitDag, + tag: u32, + id: u32, + criterion: u32, +) -> Result>, i32> { + let wire = check_wire(ptr, tag, id)?; + let dag = unsafe { &(*ptr).inner }; + match criterion { + DAG_RUN_CRITERION_1Q_GATE => dag + .collect_runs_on_wire(wire, is_one_qubit_gate_c) + .map_err(|_| -3), + DAG_RUN_CRITERION_2Q_GATE => dag + .collect_runs_on_wire(wire, is_two_qubit_gate_c) + .map_err(|_| -3), + _ => Err(-8), + } +} + +/// Copies the contiguous gate runs on `wire` matching `criterion` in CSR +/// style: run nodes flattened into `nodes` and run boundaries into +/// `offsets` (`offsets[0] == 0`). Returns 0 on success, -1 on NULL input, +/// -3 when the wire is invalid or the timeline is broken, -8 for an +/// unknown wire tag or criterion, or undersized capacities. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_collect_runs_on_wire( + ptr: *const CCircuitDag, + tag: u32, + id: u32, + criterion: u32, + nodes: *mut u32, + nodes_cap: usize, + offsets: *mut usize, + offsets_cap: usize, +) -> i32 { + match wire_runs_checked(ptr, tag, id, criterion) { + Ok(runs) => copy_node_groups(&runs, nodes, nodes_cap, offsets, offsets_cap), + Err(code) => code, + } +} + +// ===== Mutating the DAG ===== + +/// Appends a copy of `operation` at the back of the DAG and writes the +/// new node into `out_node` unless NULL. Returns 0 on success, -1 on NULL +/// input, -3 on failure. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_apply_operation_back( + ptr: *mut CCircuitDag, + operation: *const COperation, + out_node: *mut u32, +) -> i32 { + let dag = if !ptr.is_null() { + unsafe { &mut (*ptr).inner } + } else { + return -1; + }; + if operation.is_null() { + return -1; + } + let applied = unsafe { (*operation).inner.clone() }; + match dag.apply_operation_back(applied) { + Ok(node) => { + write_node_out(out_node, node); + 0 + } + Err(_) => -3, + } +} + +/// Lowers and appends a copy of the self-contained value-level operation +/// `operation` at the back of the DAG and writes the new node into +/// `out_node` unless NULL. Returns 0 on success, -1 on NULL input, -3 on +/// failure. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_apply_value_operation_back( + ptr: *mut CCircuitDag, + operation: *const CValueOperation, + out_node: *mut u32, +) -> i32 { + let dag = if !ptr.is_null() { + unsafe { &mut (*ptr).inner } + } else { + return -1; + }; + if operation.is_null() { + return -1; + } + let applied = unsafe { (*operation).inner.clone() }; + match dag.apply_value_operation_back(applied) { + Ok(node) => { + write_node_out(out_node, node); + 0 + } + Err(_) => -3, + } +} + +/// Prepends a copy of `operation` at the front of the DAG and writes the +/// new node into `out_node` unless NULL. Returns 0 on success, -1 on NULL +/// input, -3 on failure. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_apply_operation_front( + ptr: *mut CCircuitDag, + operation: *const COperation, + out_node: *mut u32, +) -> i32 { + let dag = if !ptr.is_null() { + unsafe { &mut (*ptr).inner } + } else { + return -1; + }; + if operation.is_null() { + return -1; + } + let applied = unsafe { (*operation).inner.clone() }; + match dag.apply_operation_front(applied) { + Ok(node) => { + write_node_out(out_node, node); + 0 + } + Err(_) => -3, + } +} + +/// Lowers and prepends a copy of the self-contained value-level operation +/// `operation` at the front of the DAG and writes the new node into +/// `out_node` unless NULL. Returns 0 on success, -1 on NULL input, -3 on +/// failure. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_apply_value_operation_front( + ptr: *mut CCircuitDag, + operation: *const CValueOperation, + out_node: *mut u32, +) -> i32 { + let dag = if !ptr.is_null() { + unsafe { &mut (*ptr).inner } + } else { + return -1; + }; + if operation.is_null() { + return -1; + } + let applied = unsafe { (*operation).inner.clone() }; + match dag.apply_value_operation_front(applied) { + Ok(node) => { + write_node_out(out_node, node); + 0 + } + Err(_) => -3, + } +} + +fn write_node_out(out_node: *mut u32, node: NodeIndex) { + if !out_node.is_null() { + unsafe { *out_node = node.index() as u32 }; + } +} + +/// Removes the operation node `node` and returns the removed operation as +/// a newly allocated `COperation*` (free with `operation_free`). Returns +/// NULL on NULL input or when the node is missing or not an operation +/// node. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_remove_op_node(ptr: *mut CCircuitDag, node: u32) -> *mut COperation { + if ptr.is_null() { + return std::ptr::null_mut(); + } + match unsafe { &mut (*ptr).inner }.remove_op_node(NodeIndex::new(node as usize)) { + Ok(removed) => Box::into_raw(Box::new(COperation { inner: removed })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Replaces the operation node `node` with a copy of `operation`. +/// Returns 0 on success, -1 on NULL input, -2 when `node` is not an +/// operation node, -3 on failure. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_substitute_node( + ptr: *mut CCircuitDag, + node: u32, + operation: *const COperation, +) -> i32 { + let dag = if !ptr.is_null() { + unsafe { &mut (*ptr).inner } + } else { + return -1; + }; + if operation.is_null() { + return -1; + } + if !dag.is_operation(NodeIndex::new(node as usize)) { + return -2; + } + match dag.substitute_node(NodeIndex::new(node as usize), unsafe { + (*operation).inner.clone() + }) { + Ok(()) => 0, + Err(_) => -3, + } +} + +/// Lowers the self-contained value-level operation `operation` and +/// replaces the operation node `node` with it. Returns 0 on success, -1 on +/// NULL input, -2 when `node` is not an operation node, -3 on failure. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_substitute_value_node( + ptr: *mut CCircuitDag, + node: u32, + operation: *const CValueOperation, +) -> i32 { + let dag = if !ptr.is_null() { + unsafe { &mut (*ptr).inner } + } else { + return -1; + }; + if operation.is_null() { + return -1; + } + if !dag.is_operation(NodeIndex::new(node as usize)) { + return -2; + } + match dag.substitute_value_node(NodeIndex::new(node as usize), unsafe { + (*operation).inner.clone() + }) { + Ok(()) => 0, + Err(_) => -3, + } +} + +/// Replaces the operation node `node` with a copy of `replacement`. +/// Returns 0 on success, -1 on NULL input, -2 when `node` is not an +/// operation node, -3 on failure. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_substitute_node_with_dag( + ptr: *mut CCircuitDag, + node: u32, + replacement: *const CCircuitDag, +) -> i32 { + let dag = if !ptr.is_null() { + unsafe { &mut (*ptr).inner } + } else { + return -1; + }; + if replacement.is_null() { + return -1; + } + if !dag.is_operation(NodeIndex::new(node as usize)) { + return -2; + } + let replacement = unsafe { (*replacement).inner.clone() }; + match dag.substitute_node_with_dag(NodeIndex::new(node as usize), replacement) { + Ok(()) => 0, + Err(_) => -3, + } +} + +// ===== Control-flow inspection ===== + +/// Snapshots the recursive control-flow structure attached to `node`. On +/// success writes a freshly allocated `CDagControlFlow*` into `out` (free +/// with `dag_control_flow_free`) and returns 1. Returns 0 when `node` is +/// an ordinary operation, -1 on NULL input, -2 for an unknown or +/// non-operation node. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_dag_control_flow( + ptr: *const CCircuitDag, + node: u32, + out: *mut *mut CDagControlFlow, +) -> i32 { + if ptr.is_null() || out.is_null() { + return -1; + } + let dag = unsafe { &(*ptr).inner }; + unsafe { *out = std::ptr::null_mut() }; + if !dag.is_operation(NodeIndex::new(node as usize)) { + return -2; + } + match dag.control_flow(NodeIndex::new(node as usize)) { + Some(flow) => { + unsafe { + *out = Box::into_raw(Box::new(CDagControlFlow { + inner: flow.clone(), + })); + } + 1 + } + None => 0, + } +} + +/// Returns the control-flow kind tag (`DAG_CONTROL_FLOW_*`), or -1 on NULL +/// input. +#[unsafe(no_mangle)] +pub extern "C" fn dag_control_flow_kind(ptr: *const CDagControlFlow) -> i32 { + if ptr.is_null() { + return -1; + } + control_flow_kind_tag(unsafe { &(*ptr).inner }) as i32 +} + +/// Returns the number of body DAGs: `if` carries its `then` and optional +/// `else` bodies, `while` and `for` one body each, `switch` one per case, +/// and `break`/`continue` none. Returns 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn dag_control_flow_len(ptr: *const CDagControlFlow) -> usize { + if ptr.is_null() { + return 0; + } + control_flow_bodies(unsafe { &(*ptr).inner }).len() +} + +/// Returns a copy of body `index` as a newly allocated `CCircuitDag*` +/// (free with `circuit_dag_free`). Returns NULL on NULL input or +/// out-of-range index. +#[unsafe(no_mangle)] +pub extern "C" fn dag_control_flow_body( + ptr: *const CDagControlFlow, + index: usize, +) -> *mut CCircuitDag { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let bodies = control_flow_bodies(unsafe { &(*ptr).inner }); + match bodies.get(index) { + Some(body) => Box::into_raw(Box::new(CCircuitDag { + inner: (*body).clone(), + })), + None => std::ptr::null_mut(), + } +} + +/// Returns whether the `switch` carries a default body: 1 when present, 0 +/// otherwise, -1 on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn dag_control_flow_has_default(ptr: *const CDagControlFlow) -> i32 { + if ptr.is_null() { + return -1; + } + match unsafe { &(*ptr).inner } { + DagControlFlow::Switch { default, .. } => default.is_some() as i32, + _ => 0, + } +} + +/// Returns a copy of a `switch` default body as a newly allocated +/// `CCircuitDag*` (free with `circuit_dag_free`). Returns NULL on NULL +/// input, when the structure is not a `switch`, or when it has no default. +#[unsafe(no_mangle)] +pub extern "C" fn dag_control_flow_default_body(ptr: *const CDagControlFlow) -> *mut CCircuitDag { + if ptr.is_null() { + return std::ptr::null_mut(); + } + match unsafe { &(*ptr).inner } { + DagControlFlow::Switch { + default: Some(default), + .. + } => Box::into_raw(Box::new(CCircuitDag { + inner: default.as_ref().clone(), + })), + _ => std::ptr::null_mut(), + } +} + +/// Writes the 128-bit case value of switch body `index` into `lo` and +/// `hi`. Returns 0 on success, -1 on NULL input, -8 for an out-of-range +/// index or when the structure is not a `switch`. +#[unsafe(no_mangle)] +pub extern "C" fn dag_control_flow_body_value( + ptr: *const CDagControlFlow, + index: usize, + lo: *mut u64, + hi: *mut u64, +) -> i32 { + if ptr.is_null() || lo.is_null() || hi.is_null() { + return -1; + } + match unsafe { &(*ptr).inner } { + DagControlFlow::Switch { cases, .. } => match cases.get(index) { + Some(case) => { + let value = case.value; + unsafe { + *lo = value as u64; + *hi = (value >> 64) as u64; + } + 0 + } + None => -8, + }, + _ => -8, + } +} + +/// Frees a control-flow snapshot handle. Passing NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn dag_control_flow_free(ptr: *mut CDagControlFlow) { + if !ptr.is_null() { + unsafe { + drop(Box::from_raw(ptr)); + } + } +} diff --git a/crates/binding-c/src/circuit/evolution.rs b/crates/binding-c/src/circuit/evolution.rs new file mode 100644 index 00000000..690e3918 --- /dev/null +++ b/crates/binding-c/src/circuit/evolution.rs @@ -0,0 +1,67 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! C ABI for Pauli evolution gates appended to a circuit. + +use crate::circuit::{CCircuit, check_qubit}; +use crate::error::CqlibError; +use crate::qis::CPauliString; +use cqlib_core::circuit::ParameterValue; +use cqlib_core::qis::evolution::PauliEvolution; + +/// Append the Pauli evolution operator `exp(-i * angle / 2 * P)` to `circuit`. +/// +/// `pauli` is the Pauli string `P` to exponentiate. Its phase must be +/// Hermitian (+/-1). `angle` is a fixed rotation angle. `qubits` lists the +/// circuit positions the Pauli string acts on, in the same order as the +/// Pauli string positions; its length must equal +/// `pauli_string_num_qubits(pauli)`. +/// +/// Returns 0 on success, -1 on NULL arguments, -2 when a qubit index is out +/// of bounds, -3 when the underlying circuit construction fails (for example +/// a position-count mismatch or a non-Hermitian Pauli phase), or -8 when the +/// angle is not finite. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_pauli_evolution( + circuit: *mut CCircuit, + pauli: *const CPauliString, + angle: f64, + qubits: *const u32, + qubits_len: usize, +) -> i32 { + if circuit.is_null() || pauli.is_null() { + return CqlibError::NullPtr as i32; + } + if qubits.is_null() && qubits_len > 0 { + return CqlibError::NullPtr as i32; + } + if !angle.is_finite() { + return CqlibError::InvalidParam as i32; + } + let wrapper = unsafe { &mut *circuit }; + let pauli = unsafe { &(*pauli).inner }; + let mut qubit_list = Vec::with_capacity(qubits_len); + for i in 0..qubits_len { + let id = unsafe { *qubits.add(i) }; + match check_qubit(&wrapper.inner, id) { + Ok(qubit) => qubit_list.push(qubit), + Err(code) => return code, + } + } + match wrapper + .inner + .pauli_evolution(pauli, ParameterValue::Fixed(angle), &qubit_list) + { + Ok(()) => 0, + Err(_) => CqlibError::CircuitError as i32, + } +} diff --git a/crates/binding-c/src/circuit/gates_single.rs b/crates/binding-c/src/circuit/gates_single.rs new file mode 100644 index 00000000..cd33ed8b --- /dev/null +++ b/crates/binding-c/src/circuit/gates_single.rs @@ -0,0 +1,259 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +use crate::circuit::{ + CCircuit, CParameter, apply_param, apply_single, apply_three_params, apply_two_params, +}; +use cqlib_core::circuit::Circuit; + +#[unsafe(no_mangle)] +pub extern "C" fn circuit_h(ptr: *mut CCircuit, qubit: u32) -> i32 { + apply_single(ptr, qubit, Circuit::h) +} + +#[unsafe(no_mangle)] +pub extern "C" fn circuit_x(ptr: *mut CCircuit, qubit: u32) -> i32 { + apply_single(ptr, qubit, Circuit::x) +} + +#[unsafe(no_mangle)] +pub extern "C" fn circuit_y(ptr: *mut CCircuit, qubit: u32) -> i32 { + apply_single(ptr, qubit, Circuit::y) +} + +#[unsafe(no_mangle)] +pub extern "C" fn circuit_z(ptr: *mut CCircuit, qubit: u32) -> i32 { + apply_single(ptr, qubit, Circuit::z) +} + +#[unsafe(no_mangle)] +pub extern "C" fn circuit_rx(ptr: *mut CCircuit, qubit: u32, theta: f64) -> i32 { + if !theta.is_finite() { + return -3; + } + apply_single(ptr, qubit, |circuit, qubit| circuit.rx(qubit, theta)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn circuit_ry(ptr: *mut CCircuit, qubit: u32, theta: f64) -> i32 { + if !theta.is_finite() { + return -3; + } + apply_single(ptr, qubit, |circuit, qubit| circuit.ry(qubit, theta)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn circuit_rz(ptr: *mut CCircuit, qubit: u32, theta: f64) -> i32 { + if !theta.is_finite() { + return -3; + } + apply_single(ptr, qubit, |circuit, qubit| circuit.rz(qubit, theta)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn circuit_measure(ptr: *mut CCircuit, qubit: u32) -> i32 { + apply_single(ptr, qubit, |circuit, qubit| { + circuit.measure(qubit).map(|_| ()) + }) +} + +#[unsafe(no_mangle)] +pub extern "C" fn circuit_reset(ptr: *mut CCircuit, qubit: u32) -> i32 { + apply_single(ptr, qubit, Circuit::reset) +} + +#[unsafe(no_mangle)] +pub extern "C" fn circuit_rx_param( + ptr: *mut CCircuit, + qubit: u32, + param_ptr: *const CParameter, +) -> i32 { + apply_param(ptr, qubit, param_ptr, Circuit::rx) +} + +#[unsafe(no_mangle)] +pub extern "C" fn circuit_ry_param( + ptr: *mut CCircuit, + qubit: u32, + param_ptr: *const CParameter, +) -> i32 { + apply_param(ptr, qubit, param_ptr, Circuit::ry) +} + +#[unsafe(no_mangle)] +pub extern "C" fn circuit_rz_param( + ptr: *mut CCircuit, + qubit: u32, + param_ptr: *const CParameter, +) -> i32 { + apply_param(ptr, qubit, param_ptr, Circuit::rz) +} + +// ===== Section 2.1.1: Single-qubit gates (no parameters) ===== + +#[unsafe(no_mangle)] +pub extern "C" fn circuit_i(ptr: *mut CCircuit, qubit: u32) -> i32 { + apply_single(ptr, qubit, Circuit::i) +} + +#[unsafe(no_mangle)] +pub extern "C" fn circuit_s(ptr: *mut CCircuit, qubit: u32) -> i32 { + apply_single(ptr, qubit, Circuit::s) +} + +#[unsafe(no_mangle)] +pub extern "C" fn circuit_sdg(ptr: *mut CCircuit, qubit: u32) -> i32 { + apply_single(ptr, qubit, Circuit::sdg) +} + +#[unsafe(no_mangle)] +pub extern "C" fn circuit_t(ptr: *mut CCircuit, qubit: u32) -> i32 { + apply_single(ptr, qubit, Circuit::t) +} + +#[unsafe(no_mangle)] +pub extern "C" fn circuit_tdg(ptr: *mut CCircuit, qubit: u32) -> i32 { + apply_single(ptr, qubit, Circuit::tdg) +} + +#[unsafe(no_mangle)] +pub extern "C" fn circuit_x2p(ptr: *mut CCircuit, qubit: u32) -> i32 { + apply_single(ptr, qubit, Circuit::x2p) +} + +#[unsafe(no_mangle)] +pub extern "C" fn circuit_x2m(ptr: *mut CCircuit, qubit: u32) -> i32 { + apply_single(ptr, qubit, Circuit::x2m) +} + +#[unsafe(no_mangle)] +pub extern "C" fn circuit_y2p(ptr: *mut CCircuit, qubit: u32) -> i32 { + apply_single(ptr, qubit, Circuit::y2p) +} + +#[unsafe(no_mangle)] +pub extern "C" fn circuit_y2m(ptr: *mut CCircuit, qubit: u32) -> i32 { + apply_single(ptr, qubit, Circuit::y2m) +} + +// ===== Section 2.1.2: Single-qubit gates (with parameters) ===== + +#[unsafe(no_mangle)] +pub extern "C" fn circuit_phase(ptr: *mut CCircuit, qubit: u32, lambda: f64) -> i32 { + if !lambda.is_finite() { + return -3; + } + apply_single(ptr, qubit, |c, q| c.phase(q, lambda)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn circuit_u( + ptr: *mut CCircuit, + qubit: u32, + theta: f64, + phi: f64, + lambda: f64, +) -> i32 { + if !theta.is_finite() || !phi.is_finite() || !lambda.is_finite() { + return -3; + } + apply_single(ptr, qubit, |c, q| c.u(q, theta, phi, lambda)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn circuit_xy(ptr: *mut CCircuit, qubit: u32, theta: f64) -> i32 { + if !theta.is_finite() { + return -3; + } + apply_single(ptr, qubit, |c, q| c.xy(q, theta)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn circuit_xy2p(ptr: *mut CCircuit, qubit: u32, theta: f64) -> i32 { + if !theta.is_finite() { + return -3; + } + apply_single(ptr, qubit, |c, q| c.xy2p(q, theta)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn circuit_xy2m(ptr: *mut CCircuit, qubit: u32, theta: f64) -> i32 { + if !theta.is_finite() { + return -3; + } + apply_single(ptr, qubit, |c, q| c.xy2m(q, theta)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn circuit_rxy(ptr: *mut CCircuit, qubit: u32, theta: f64, phi: f64) -> i32 { + if !theta.is_finite() || !phi.is_finite() { + return -3; + } + apply_single(ptr, qubit, |c, q| c.rxy(q, theta, phi)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn circuit_phase_param( + ptr: *mut CCircuit, + qubit: u32, + param: *const CParameter, +) -> i32 { + apply_param(ptr, qubit, param, Circuit::phase) +} + +#[unsafe(no_mangle)] +pub extern "C" fn circuit_xy_param( + ptr: *mut CCircuit, + qubit: u32, + param: *const CParameter, +) -> i32 { + apply_param(ptr, qubit, param, Circuit::xy) +} + +#[unsafe(no_mangle)] +pub extern "C" fn circuit_xy2p_param( + ptr: *mut CCircuit, + qubit: u32, + param: *const CParameter, +) -> i32 { + apply_param(ptr, qubit, param, Circuit::xy2p) +} + +#[unsafe(no_mangle)] +pub extern "C" fn circuit_xy2m_param( + ptr: *mut CCircuit, + qubit: u32, + param: *const CParameter, +) -> i32 { + apply_param(ptr, qubit, param, Circuit::xy2m) +} + +#[unsafe(no_mangle)] +pub extern "C" fn circuit_u_param( + ptr: *mut CCircuit, + qubit: u32, + theta: *const CParameter, + phi: *const CParameter, + lambda: *const CParameter, +) -> i32 { + apply_three_params(ptr, qubit, theta, phi, lambda, Circuit::u) +} + +#[unsafe(no_mangle)] +pub extern "C" fn circuit_rxy_param( + ptr: *mut CCircuit, + qubit: u32, + theta: *const CParameter, + phi: *const CParameter, +) -> i32 { + apply_two_params(ptr, qubit, theta, phi, Circuit::rxy) +} diff --git a/crates/binding-c/src/circuit/gates_three.rs b/crates/binding-c/src/circuit/gates_three.rs new file mode 100644 index 00000000..5184270c --- /dev/null +++ b/crates/binding-c/src/circuit/gates_three.rs @@ -0,0 +1,26 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +use crate::circuit::{CCircuit, apply_three}; +use cqlib_core::circuit::Circuit; + +// ===== Section 2.1.5: Three-qubit gate ===== + +#[unsafe(no_mangle)] +pub extern "C" fn circuit_ccx( + ptr: *mut CCircuit, + control1: u32, + control2: u32, + target: u32, +) -> i32 { + apply_three(ptr, control1, control2, target, Circuit::ccx) +} diff --git a/crates/binding-c/src/circuit/gates_two.rs b/crates/binding-c/src/circuit/gates_two.rs new file mode 100644 index 00000000..0473535b --- /dev/null +++ b/crates/binding-c/src/circuit/gates_two.rs @@ -0,0 +1,185 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +use crate::circuit::{ + CCircuit, CParameter, apply_two, apply_two_qubit_param, apply_two_qubit_two_params, +}; +use cqlib_core::circuit::Circuit; + +#[unsafe(no_mangle)] +pub extern "C" fn circuit_cx(ptr: *mut CCircuit, control: u32, target: u32) -> i32 { + apply_two(ptr, control, target, Circuit::cx) +} + +#[unsafe(no_mangle)] +pub extern "C" fn circuit_cz(ptr: *mut CCircuit, control: u32, target: u32) -> i32 { + apply_two(ptr, control, target, Circuit::cz) +} + +// ===== Section 2.1.3: Two-qubit gates (no parameters) ===== + +#[unsafe(no_mangle)] +pub extern "C" fn circuit_cy(ptr: *mut CCircuit, control: u32, target: u32) -> i32 { + apply_two(ptr, control, target, Circuit::cy) +} + +#[unsafe(no_mangle)] +pub extern "C" fn circuit_swap(ptr: *mut CCircuit, a: u32, b: u32) -> i32 { + apply_two(ptr, a, b, Circuit::swap) +} + +// ===== Section 2.1.4: Two-qubit gates (with parameters) ===== + +#[unsafe(no_mangle)] +pub extern "C" fn circuit_rxx(ptr: *mut CCircuit, a: u32, b: u32, theta: f64) -> i32 { + if !theta.is_finite() { + return -3; + } + apply_two(ptr, a, b, |c, a, b| c.rxx(a, b, theta)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn circuit_ryy(ptr: *mut CCircuit, a: u32, b: u32, theta: f64) -> i32 { + if !theta.is_finite() { + return -3; + } + apply_two(ptr, a, b, |c, a, b| c.ryy(a, b, theta)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn circuit_rzz(ptr: *mut CCircuit, a: u32, b: u32, theta: f64) -> i32 { + if !theta.is_finite() { + return -3; + } + apply_two(ptr, a, b, |c, a, b| c.rzz(a, b, theta)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn circuit_rzx(ptr: *mut CCircuit, a: u32, b: u32, theta: f64) -> i32 { + if !theta.is_finite() { + return -3; + } + apply_two(ptr, a, b, |c, a, b| c.rzx(a, b, theta)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn circuit_crx(ptr: *mut CCircuit, control: u32, target: u32, theta: f64) -> i32 { + if !theta.is_finite() { + return -3; + } + apply_two(ptr, control, target, |c, ctl, tgt| c.crx(ctl, tgt, theta)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn circuit_cry(ptr: *mut CCircuit, control: u32, target: u32, theta: f64) -> i32 { + if !theta.is_finite() { + return -3; + } + apply_two(ptr, control, target, |c, ctl, tgt| c.cry(ctl, tgt, theta)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn circuit_crz(ptr: *mut CCircuit, control: u32, target: u32, theta: f64) -> i32 { + if !theta.is_finite() { + return -3; + } + apply_two(ptr, control, target, |c, ctl, tgt| c.crz(ctl, tgt, theta)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn circuit_fsim(ptr: *mut CCircuit, a: u32, b: u32, theta: f64, phi: f64) -> i32 { + if !theta.is_finite() || !phi.is_finite() { + return -3; + } + apply_two(ptr, a, b, |c, a, b| c.fsim(a, b, theta, phi)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn circuit_rxx_param( + ptr: *mut CCircuit, + a: u32, + b: u32, + param: *const CParameter, +) -> i32 { + apply_two_qubit_param(ptr, a, b, param, Circuit::rxx) +} + +#[unsafe(no_mangle)] +pub extern "C" fn circuit_ryy_param( + ptr: *mut CCircuit, + a: u32, + b: u32, + param: *const CParameter, +) -> i32 { + apply_two_qubit_param(ptr, a, b, param, Circuit::ryy) +} + +#[unsafe(no_mangle)] +pub extern "C" fn circuit_rzz_param( + ptr: *mut CCircuit, + a: u32, + b: u32, + param: *const CParameter, +) -> i32 { + apply_two_qubit_param(ptr, a, b, param, Circuit::rzz) +} + +#[unsafe(no_mangle)] +pub extern "C" fn circuit_rzx_param( + ptr: *mut CCircuit, + a: u32, + b: u32, + param: *const CParameter, +) -> i32 { + apply_two_qubit_param(ptr, a, b, param, Circuit::rzx) +} + +#[unsafe(no_mangle)] +pub extern "C" fn circuit_crx_param( + ptr: *mut CCircuit, + control: u32, + target: u32, + param: *const CParameter, +) -> i32 { + apply_two_qubit_param(ptr, control, target, param, Circuit::crx) +} + +#[unsafe(no_mangle)] +pub extern "C" fn circuit_cry_param( + ptr: *mut CCircuit, + control: u32, + target: u32, + param: *const CParameter, +) -> i32 { + apply_two_qubit_param(ptr, control, target, param, Circuit::cry) +} + +#[unsafe(no_mangle)] +pub extern "C" fn circuit_crz_param( + ptr: *mut CCircuit, + control: u32, + target: u32, + param: *const CParameter, +) -> i32 { + apply_two_qubit_param(ptr, control, target, param, Circuit::crz) +} + +#[unsafe(no_mangle)] +pub extern "C" fn circuit_fsim_param( + ptr: *mut CCircuit, + a: u32, + b: u32, + theta: *const CParameter, + phi: *const CParameter, +) -> i32 { + apply_two_qubit_two_params(ptr, a, b, theta, phi, Circuit::fsim) +} diff --git a/crates/binding-c/src/circuit/instructions.rs b/crates/binding-c/src/circuit/instructions.rs new file mode 100644 index 00000000..ad607e75 --- /dev/null +++ b/crates/binding-c/src/circuit/instructions.rs @@ -0,0 +1,33 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +use crate::circuit::CCircuit; +use cqlib_core::circuit::Qubit; + +// ===== Section 2.1.6: Instructions ===== + +/// Insert a barrier over the given qubits. Passing NULL qubits or count=0 +/// creates a global barrier over all circuit qubits. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_barrier(ptr: *mut CCircuit, qubits: *const u32, count: usize) -> i32 { + if ptr.is_null() { + return -1; + } + let wrapper = unsafe { &mut *ptr }; + let qubit_vec: Vec = if qubits.is_null() || count == 0 { + wrapper.inner.qubits() + } else { + let slice = unsafe { std::slice::from_raw_parts(qubits, count) }; + slice.iter().map(|&id| Qubit::new(id)).collect() + }; + wrapper.inner.barrier(qubit_vec).map_or(-3, |_| 0) +} diff --git a/crates/binding-c/src/circuit/lifecycle.rs b/crates/binding-c/src/circuit/lifecycle.rs new file mode 100644 index 00000000..fe13287d --- /dev/null +++ b/crates/binding-c/src/circuit/lifecycle.rs @@ -0,0 +1,261 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! C ABI for circuit lifecycle: construction, structural queries, and +//! checkpoint transactions. + +#![allow(clippy::not_unsafe_ptr_arg_deref)] + +use crate::circuit::CCircuit; +use crate::circuit::advanced::CValueOperation; +use cqlib_core::circuit::Circuit; +use cqlib_core::circuit::{ControlBodyTransaction, Qubit}; + +/// Create a new quantum circuit with `num_qubits` logical qubits. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_new(num_qubits: usize) -> *mut CCircuit { + Box::into_raw(Box::new(CCircuit { + inner: Circuit::new(num_qubits), + })) +} + +/// Free a quantum circuit. Passing NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_free(ptr: *mut CCircuit) { + if !ptr.is_null() { + unsafe { + let _ = Box::from_raw(ptr); + } + } +} + +/// Return the number of qubits in the circuit, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_num_qubits(ptr: *const CCircuit) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.num_qubits() } +} + +/// Return the number of operations in the circuit, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_num_operations(ptr: *const CCircuit) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.operations().len() } +} + +/// Return the number of interned symbolic parameters in the circuit, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_num_parameters(ptr: *const CCircuit) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.parameters().len() } +} + +/// Validate circuit consistency. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_validate(ptr: *const CCircuit) -> i32 { + if ptr.is_null() { + return -1; + } + unsafe { (*ptr).inner.validate().map_or(-3, |_| 0) } +} + +// ===== Structural construction and queries ===== + +/// Creates a circuit from qubits plus a batch of resolved value-level +/// operations (as produced by `circuit_index`), mirroring the core +/// `Circuit::from_operations`. Operations are appended in order through +/// value-level lowering, so qubit membership is validated and symbolic +/// parameters are interned into the new circuit's parameter table. Returns +/// a newly allocated `CCircuit*` (free with `circuit_free`), or NULL on +/// NULL input (including NULL entries inside `operations`), duplicate +/// qubits, or any operation-level failure. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_from_operations( + qubits: *const u32, + qubits_len: usize, + operations: *const *const CValueOperation, + operations_len: usize, +) -> *mut CCircuit { + if qubits_len > 0 && qubits.is_null() { + return std::ptr::null_mut(); + } + if operations_len > 0 && operations.is_null() { + return std::ptr::null_mut(); + } + let qubit_list: Vec = if qubits_len == 0 { + Vec::new() + } else { + unsafe { std::slice::from_raw_parts(qubits, qubits_len) } + .iter() + .map(|&id| Qubit::new(id)) + .collect() + }; + let mut operation_list = Vec::with_capacity(operations_len); + for i in 0..operations_len { + let handle = unsafe { *operations.add(i) }; + if handle.is_null() { + return std::ptr::null_mut(); + } + operation_list.push(unsafe { (*handle).inner.clone() }); + } + match Circuit::from_operations(qubit_list, operation_list, None, None) { + Ok(inner) => Box::into_raw(Box::new(CCircuit { inner })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Appends one resolved value-level operation (as produced by +/// `circuit_index`) to the circuit. Returns 0 on success, -1 on NULL +/// input, -3 on application failure (e.g. the operation references a qubit +/// outside the circuit). +#[unsafe(no_mangle)] +pub extern "C" fn circuit_append_value_operation( + ptr: *mut CCircuit, + op: *const CValueOperation, +) -> i32 { + if ptr.is_null() || op.is_null() { + return -1; + } + let wrapper = unsafe { &mut *ptr }; + let operation = unsafe { (*op).inner.clone() }; + wrapper + .inner + .append_value_operation(operation) + .map_or(-3, |_| 0) +} + +/// Returns whether `qubit` belongs to the circuit: 1 when yes, 0 when no, +/// -1 on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_contains_qubit(ptr: *const CCircuit, qubit: u32) -> i32 { + if ptr.is_null() { + return -1; + } + let qubit = Qubit::new(qubit); + unsafe { (*ptr).inner.qubits().contains(&qubit) as i32 } +} + +/// Returns whether both circuits share the same ordered qubit domain: +/// 1 when yes, 0 when no, -1 on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_has_same_qubits(a: *const CCircuit, b: *const CCircuit) -> i32 { + if a.is_null() || b.is_null() { + return -1; + } + unsafe { ((*a).inner.qubits() == (*b).inner.qubits()) as i32 } +} + +/// Returns whether two circuits are structurally equal, mirroring the +/// core `PartialEq for Circuit`: same ordered qubits, parameter tables, and +/// operations (with parameter indices remapped across the two tables, and +/// circuit-local classical handles compared by structure). Returns 1 when +/// yes, 0 when no, -1 on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_operations_structurally_equal( + a: *const CCircuit, + b: *const CCircuit, +) -> i32 { + if a.is_null() || b.is_null() { + return -1; + } + unsafe { ((*a).inner == (*b).inner) as i32 } +} + +// ===== Checkpoint transactions ===== + +/// Opaque rollback token around a core `ControlBodyTransaction`, produced +/// by `circuit_checkpoint` / `circuit_begin` and consumed exactly once by +/// `circuit_commit`, `circuit_rollback_to`, or +/// `circuit_rollback_control_body_transaction`. Free an unused token with +/// `checkpoint_free`. +pub struct CCheckpoint { + pub inner: ControlBodyTransaction, +} + +/// Captures a checkpoint of the circuit state (operation list, parameter +/// and symbol tables, classical tables, control scopes). The returned +/// token is consumed exactly once by `circuit_commit`, +/// `circuit_rollback_to`, or `circuit_rollback_control_body_transaction`. +/// Returns a newly allocated `CCheckpoint*`, or NULL on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_checkpoint(ptr: *const CCircuit) -> *mut CCheckpoint { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let wrapper = unsafe { &*ptr }; + Box::into_raw(Box::new(CCheckpoint { + inner: wrapper.inner.begin_control_body_transaction(), + })) +} + +/// Starts an externally driven construction transaction; equivalent to +/// [`circuit_checkpoint`]. Returns a newly allocated `CCheckpoint*`, or +/// NULL on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_begin(ptr: *const CCircuit) -> *mut CCheckpoint { + circuit_checkpoint(ptr) +} + +/// Commits the state allocated since the token was captured and consumes +/// the token. Returns 0 on success, -1 on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_commit(ptr: *mut CCircuit, checkpoint: *mut CCheckpoint) -> i32 { + if ptr.is_null() || checkpoint.is_null() { + return -1; + } + let wrapper = unsafe { &mut *ptr }; + let token = unsafe { Box::from_raw(checkpoint) }; + wrapper.inner.commit_control_body_transaction(token.inner); + 0 +} + +/// Rolls back every state allocated since the token was captured and +/// consumes the token. Exposed through the transaction token because the +/// core keeps the raw checkpoint type private. Returns 0 on success, -1 on +/// NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_rollback_to(ptr: *mut CCircuit, checkpoint: *mut CCheckpoint) -> i32 { + circuit_rollback_control_body_transaction(ptr, checkpoint) +} + +/// Rolls back every state allocated since the token was captured and +/// consumes the token (core `rollback_control_body_transaction`). Returns +/// 0 on success, -1 on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_rollback_control_body_transaction( + ptr: *mut CCircuit, + checkpoint: *mut CCheckpoint, +) -> i32 { + if ptr.is_null() || checkpoint.is_null() { + return -1; + } + let wrapper = unsafe { &mut *ptr }; + let token = unsafe { Box::from_raw(checkpoint) }; + wrapper.inner.rollback_control_body_transaction(token.inner); + 0 +} + +/// Frees an unconsumed checkpoint token. Passing NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn checkpoint_free(ptr: *mut CCheckpoint) { + if !ptr.is_null() { + unsafe { + drop(Box::from_raw(ptr)); + } + } +} diff --git a/crates/binding-c/src/circuit/mod.rs b/crates/binding-c/src/circuit/mod.rs new file mode 100644 index 00000000..9dd33de9 --- /dev/null +++ b/crates/binding-c/src/circuit/mod.rs @@ -0,0 +1,340 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! Minimal C ABI for circuit construction and symbolic parameters. + +#![allow(clippy::not_unsafe_ptr_arg_deref)] + +pub mod advanced; +pub mod ansatz; +pub mod cfg; +pub mod classical; +pub mod control_flow; +pub mod dag; +pub mod evolution; +pub mod gates_single; +pub mod gates_three; +pub mod gates_two; +pub mod instructions; +pub mod lifecycle; +pub mod operations; +pub mod parameter; +pub mod properties; +pub mod qubit; +pub mod symbolic_matrix; + +use cqlib_core::circuit::{Circuit, Parameter, ParameterValue, Qubit}; +use std::collections::HashMap; +use std::ffi::CStr; +use std::os::raw::c_char; + +pub struct CCircuit { + pub inner: Circuit, +} + +pub struct CParameter { + pub inner: Parameter, +} + +pub(crate) fn parse_bindings(bindings: *const c_char) -> Option> { + if bindings.is_null() { + return None; + } + + let c_str = unsafe { CStr::from_ptr(bindings) }; + let text = c_str.to_str().ok()?; + let mut map = HashMap::new(); + for pair in text.split(',') { + let pair = pair.trim(); + if pair.is_empty() { + continue; + } + let (name, value) = pair.split_once(':')?; + let value = value.trim().parse::().ok()?; + if !value.is_finite() { + return None; + } + map.insert(name.trim().to_string(), value); + } + Some(map) +} + +pub(crate) fn binding_refs(bindings: &HashMap) -> HashMap<&str, f64> { + bindings + .iter() + .map(|(name, value)| (name.as_str(), *value)) + .collect() +} + +pub(crate) fn check_qubit(circuit: &Circuit, qubit: u32) -> Result { + if qubit as usize >= circuit.num_qubits() { + return Err(-2); + } + Ok(Qubit::new(qubit)) +} + +pub(crate) fn apply_single( + ptr: *mut CCircuit, + qubit: u32, + apply: impl FnOnce(&mut Circuit, Qubit) -> Result<(), cqlib_core::circuit::CircuitError>, +) -> i32 { + if ptr.is_null() { + return -1; + } + let wrapper = unsafe { &mut *ptr }; + let qubit = match check_qubit(&wrapper.inner, qubit) { + Ok(qubit) => qubit, + Err(code) => return code, + }; + apply(&mut wrapper.inner, qubit).map_or(-3, |_| 0) +} + +pub(crate) fn apply_two( + ptr: *mut CCircuit, + first: u32, + second: u32, + apply: impl FnOnce(&mut Circuit, Qubit, Qubit) -> Result<(), cqlib_core::circuit::CircuitError>, +) -> i32 { + if ptr.is_null() { + return -1; + } + let wrapper = unsafe { &mut *ptr }; + let first = match check_qubit(&wrapper.inner, first) { + Ok(qubit) => qubit, + Err(code) => return code, + }; + let second = match check_qubit(&wrapper.inner, second) { + Ok(qubit) => qubit, + Err(code) => return code, + }; + apply(&mut wrapper.inner, first, second).map_or(-3, |_| 0) +} + +pub(crate) fn apply_param( + ptr: *mut CCircuit, + qubit: u32, + param_ptr: *const CParameter, + apply: impl FnOnce( + &mut Circuit, + Qubit, + ParameterValue, + ) -> Result<(), cqlib_core::circuit::CircuitError>, +) -> i32 { + if ptr.is_null() || param_ptr.is_null() { + return -1; + } + let wrapper = unsafe { &mut *ptr }; + let qubit = match check_qubit(&wrapper.inner, qubit) { + Ok(qubit) => qubit, + Err(code) => return code, + }; + let param = unsafe { &(*param_ptr).inner }; + apply( + &mut wrapper.inner, + qubit, + ParameterValue::Param(param.clone()), + ) + .map_or(-3, |_| 0) +} + +/// Apply a single-qubit gate with two symbolic parameters (e.g. `rxy`). +pub(crate) fn apply_two_params( + ptr: *mut CCircuit, + qubit: u32, + p1: *const CParameter, + p2: *const CParameter, + apply: impl FnOnce( + &mut Circuit, + Qubit, + ParameterValue, + ParameterValue, + ) -> Result<(), cqlib_core::circuit::CircuitError>, +) -> i32 { + if ptr.is_null() || p1.is_null() || p2.is_null() { + return -1; + } + let wrapper = unsafe { &mut *ptr }; + let qubit = match check_qubit(&wrapper.inner, qubit) { + Ok(qubit) => qubit, + Err(code) => return code, + }; + let p1 = unsafe { &(*p1).inner }; + let p2 = unsafe { &(*p2).inner }; + apply( + &mut wrapper.inner, + qubit, + ParameterValue::Param(p1.clone()), + ParameterValue::Param(p2.clone()), + ) + .map_or(-3, |_| 0) +} + +/// Apply a single-qubit gate with three symbolic parameters (e.g. `u`). +pub(crate) fn apply_three_params( + ptr: *mut CCircuit, + qubit: u32, + p1: *const CParameter, + p2: *const CParameter, + p3: *const CParameter, + apply: impl FnOnce( + &mut Circuit, + Qubit, + ParameterValue, + ParameterValue, + ParameterValue, + ) -> Result<(), cqlib_core::circuit::CircuitError>, +) -> i32 { + if ptr.is_null() || p1.is_null() || p2.is_null() || p3.is_null() { + return -1; + } + let wrapper = unsafe { &mut *ptr }; + let qubit = match check_qubit(&wrapper.inner, qubit) { + Ok(qubit) => qubit, + Err(code) => return code, + }; + let p1 = unsafe { &(*p1).inner }; + let p2 = unsafe { &(*p2).inner }; + let p3 = unsafe { &(*p3).inner }; + apply( + &mut wrapper.inner, + qubit, + ParameterValue::Param(p1.clone()), + ParameterValue::Param(p2.clone()), + ParameterValue::Param(p3.clone()), + ) + .map_or(-3, |_| 0) +} + +/// Apply a two-qubit gate with one symbolic parameter (e.g. `rxx_param`). +pub(crate) fn apply_two_qubit_param( + ptr: *mut CCircuit, + first: u32, + second: u32, + param_ptr: *const CParameter, + apply: impl FnOnce( + &mut Circuit, + Qubit, + Qubit, + ParameterValue, + ) -> Result<(), cqlib_core::circuit::CircuitError>, +) -> i32 { + if ptr.is_null() || param_ptr.is_null() { + return -1; + } + let wrapper = unsafe { &mut *ptr }; + let first = match check_qubit(&wrapper.inner, first) { + Ok(qubit) => qubit, + Err(code) => return code, + }; + let second = match check_qubit(&wrapper.inner, second) { + Ok(qubit) => qubit, + Err(code) => return code, + }; + let param = unsafe { &(*param_ptr).inner }; + apply( + &mut wrapper.inner, + first, + second, + ParameterValue::Param(param.clone()), + ) + .map_or(-3, |_| 0) +} + +/// Apply a two-qubit gate with two symbolic parameters (e.g. `fsim_param`). +pub(crate) fn apply_two_qubit_two_params( + ptr: *mut CCircuit, + first: u32, + second: u32, + p1: *const CParameter, + p2: *const CParameter, + apply: impl FnOnce( + &mut Circuit, + Qubit, + Qubit, + ParameterValue, + ParameterValue, + ) -> Result<(), cqlib_core::circuit::CircuitError>, +) -> i32 { + if ptr.is_null() || p1.is_null() || p2.is_null() { + return -1; + } + let wrapper = unsafe { &mut *ptr }; + let first = match check_qubit(&wrapper.inner, first) { + Ok(qubit) => qubit, + Err(code) => return code, + }; + let second = match check_qubit(&wrapper.inner, second) { + Ok(qubit) => qubit, + Err(code) => return code, + }; + let p1 = unsafe { &(*p1).inner }; + let p2 = unsafe { &(*p2).inner }; + apply( + &mut wrapper.inner, + first, + second, + ParameterValue::Param(p1.clone()), + ParameterValue::Param(p2.clone()), + ) + .map_or(-3, |_| 0) +} + +/// Apply a three-qubit gate with no parameters (e.g. `ccx`). +#[allow(clippy::too_many_arguments)] +pub(crate) fn apply_three( + ptr: *mut CCircuit, + first: u32, + second: u32, + third: u32, + apply: impl FnOnce( + &mut Circuit, + Qubit, + Qubit, + Qubit, + ) -> Result<(), cqlib_core::circuit::CircuitError>, +) -> i32 { + if ptr.is_null() { + return -1; + } + let wrapper = unsafe { &mut *ptr }; + let first = match check_qubit(&wrapper.inner, first) { + Ok(qubit) => qubit, + Err(code) => return code, + }; + let second = match check_qubit(&wrapper.inner, second) { + Ok(qubit) => qubit, + Err(code) => return code, + }; + let third = match check_qubit(&wrapper.inner, third) { + Ok(qubit) => qubit, + Err(code) => return code, + }; + apply(&mut wrapper.inner, first, second, third).map_or(-3, |_| 0) +} + +pub use advanced::*; +pub use ansatz::*; +pub use cfg::*; +pub use classical::*; +pub use control_flow::*; +pub use dag::*; +pub use evolution::*; +pub use gates_single::*; +pub use gates_three::*; +pub use gates_two::*; +pub use instructions::*; +pub use lifecycle::*; +pub use operations::*; +pub use parameter::*; +pub use properties::*; +pub use qubit::*; +pub use symbolic_matrix::*; diff --git a/crates/binding-c/src/circuit/operations.rs b/crates/binding-c/src/circuit/operations.rs new file mode 100644 index 00000000..2ce1c6c7 --- /dev/null +++ b/crates/binding-c/src/circuit/operations.rs @@ -0,0 +1,513 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! C ABI for standalone circuit operations (instruction + qubits + params). + +#![allow(clippy::not_unsafe_ptr_arg_deref)] + +use crate::circuit::classical::{CClassicalExpr, CClassicalValueInfo, classical_type_parts}; +use crate::device::standard_gate_from_name; +use cqlib_core::circuit::gate::instruction::Instruction; +use cqlib_core::circuit::{CircuitParam, ClassicalExprKind, Directive, Operation, Qubit}; +use std::ffi::{CStr, CString}; +use std::os::raw::c_char; + +/// Opaque handle around an [`Operation`]. +pub struct COperation { + pub inner: Operation, +} + +/// Parameter tag written by [`operation_params`]: value is fixed inline. +pub const OPERATION_PARAM_FIXED: u32 = 0; +/// Parameter tag written by [`operation_params`]: value is an index into +/// the owning circuit's parameter table and cannot be resolved here. +pub const OPERATION_PARAM_INDEX: u32 = 1; + +/// `operation_directive` result tag: the operation carries no directive +/// instruction. +pub const OPERATION_DIRECTIVE_NONE: u32 = 0; +/// `operation_directive` result tag: `Directive::Barrier`. +pub const OPERATION_DIRECTIVE_BARRIER: u32 = 1; +/// `operation_directive` result tag: `Directive::Measure`. +pub const OPERATION_DIRECTIVE_MEASURE: u32 = 2; +/// `operation_directive` result tag: `Directive::Reset`. +pub const OPERATION_DIRECTIVE_RESET: u32 = 3; + +/// Creates a standard-gate operation from a gate name (e.g. "H", "CX", +/// "RZZ"). `params` holds fixed double values. Returns a newly allocated +/// `COperation*` (free with `operation_free`), or NULL on NULL input or +/// unknown gate name. +#[unsafe(no_mangle)] +pub extern "C" fn operation_new( + gate_name: *const c_char, + qubits: *const u32, + qubits_len: usize, + params: *const f64, + params_len: usize, +) -> *mut COperation { + if gate_name.is_null() { + return std::ptr::null_mut(); + } + if qubits_len > 0 && qubits.is_null() { + return std::ptr::null_mut(); + } + if params_len > 0 && params.is_null() { + return std::ptr::null_mut(); + } + let name = unsafe { CStr::from_ptr(gate_name) }; + let Some(gate) = standard_gate_from_name(name.to_string_lossy().as_ref()) else { + return std::ptr::null_mut(); + }; + let qubit_list = if qubits_len == 0 { + Default::default() + } else { + unsafe { std::slice::from_raw_parts(qubits, qubits_len) } + .iter() + .map(|&id| Qubit::new(id)) + .collect() + }; + let param_list = if params_len == 0 { + Default::default() + } else { + unsafe { std::slice::from_raw_parts(params, params_len) } + .iter() + .map(|&value| CircuitParam::Fixed(value)) + .collect() + }; + let inner = Operation { + instruction: Instruction::Standard(gate), + qubits: qubit_list, + params: param_list, + label: None, + }; + Box::into_raw(Box::new(COperation { inner })) +} + +/// Frees an operation handle. Passing NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn operation_free(ptr: *mut COperation) { + if !ptr.is_null() { + unsafe { + drop(Box::from_raw(ptr)); + } + } +} + +/// Returns the instruction name (e.g. "H", "RZZ") as a newly allocated C +/// string (free with `cqlib_string_free`), or NULL on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn operation_name(ptr: *const COperation) -> *mut c_char { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let name = unsafe { (*ptr).inner.instruction.name() }; + match CString::new(name) { + Ok(text) => text.into_raw(), + Err(_) => std::ptr::null_mut(), + } +} + +/// Returns whether the operation is driven by a standard-gate instruction: +/// 1 when yes, 0 when no, -1 on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn operation_is_standard_gate(ptr: *const COperation) -> i32 { + if ptr.is_null() { + return -1; + } + unsafe { (*ptr).inner.standard_gate().is_some() as i32 } +} + +/// Returns the number of qubits the operation acts on, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn operation_num_qubits(ptr: *const COperation) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.qubits.len() } +} + +/// Returns the number of qubits the operation acts on, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn operation_qubits_len(ptr: *const COperation) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.qubits.len() } +} + +/// Copies the qubit ids in application order into `buffer`. Returns 0 on +/// success, -1 on NULL input, -8 when `len` is too small. +#[unsafe(no_mangle)] +pub extern "C" fn operation_qubits(ptr: *const COperation, buffer: *mut u32, len: usize) -> i32 { + if ptr.is_null() { + return -1; + } + let qubits = unsafe { (*ptr).inner.qubits.as_slice() }; + if len < qubits.len() { + return -8; + } + if qubits.is_empty() { + return 0; + } + if buffer.is_null() { + return -1; + } + let slice = unsafe { std::slice::from_raw_parts_mut(buffer, qubits.len()) }; + for (slot, qubit) in slice.iter_mut().zip(qubits.iter()) { + *slot = qubit.id(); + } + 0 +} + +/// Returns the number of parameters carried by the operation, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn operation_params_len(ptr: *const COperation) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.params.len() } +} + +/// Copies the parameters into `tags` and `values`. `tags[i]` is one of +/// `OPERATION_PARAM_*`; `values[i]` is the fixed value, or the parameter +/// table index when tagged `OPERATION_PARAM_INDEX`. Returns 0 on success, +/// -1 on NULL input, -8 when `len` is too small. +#[unsafe(no_mangle)] +pub extern "C" fn operation_params( + ptr: *const COperation, + tags: *mut u32, + values: *mut f64, + len: usize, +) -> i32 { + if ptr.is_null() { + return -1; + } + let params = unsafe { (*ptr).inner.params.as_slice() }; + if len < params.len() { + return -8; + } + if params.is_empty() { + return 0; + } + if tags.is_null() || values.is_null() { + return -1; + } + let tag_slice = unsafe { std::slice::from_raw_parts_mut(tags, params.len()) }; + let value_slice = unsafe { std::slice::from_raw_parts_mut(values, params.len()) }; + for ((tag_slot, value_slot), param) in tag_slice + .iter_mut() + .zip(value_slice.iter_mut()) + .zip(params.iter()) + { + match param { + CircuitParam::Fixed(value) => { + *tag_slot = OPERATION_PARAM_FIXED; + *value_slot = *value; + } + CircuitParam::Index(index) => { + *tag_slot = OPERATION_PARAM_INDEX; + *value_slot = *index as f64; + } + } + } + 0 +} + +/// Returns the optional label as a newly allocated C string (free with +/// `cqlib_string_free`), or NULL when the operation has no label or the +/// handle is NULL. +#[unsafe(no_mangle)] +pub extern "C" fn operation_label(ptr: *const COperation) -> *mut c_char { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let Some(label) = (unsafe { (*ptr).inner.label.as_ref() }) else { + return std::ptr::null_mut(); + }; + match CString::new(&**label) { + Ok(text) => text.into_raw(), + Err(_) => std::ptr::null_mut(), + } +} + +/// Returns the number of complex elements (rows * cols) in the unitary +/// matrix, or 0 on NULL input or when the operation has no numeric matrix +/// (non-unitary instruction or unresolved symbolic parameter). +#[unsafe(no_mangle)] +pub extern "C" fn operation_matrix_len(ptr: *const COperation) -> usize { + if ptr.is_null() { + return 0; + } + match unsafe { (*ptr).inner.matrix() } { + Ok(matrix) => matrix.len(), + Err(_) => 0, + } +} + +/// Writes the matrix shape into `rows` and `cols`. Returns 0 on success, +/// -1 on NULL input, -3 when the operation has no numeric matrix. +#[unsafe(no_mangle)] +pub extern "C" fn operation_matrix_dims( + ptr: *const COperation, + rows: *mut usize, + cols: *mut usize, +) -> i32 { + if ptr.is_null() || rows.is_null() || cols.is_null() { + return -1; + } + match unsafe { (*ptr).inner.matrix() } { + Ok(matrix) => { + let (nrows, ncols) = matrix.dim(); + unsafe { + *rows = nrows; + *cols = ncols; + } + 0 + } + Err(_) => -3, + } +} + +/// Copies the unitary matrix into `out` as interleaved (real, imag) +/// doubles in row-major order. Returns the number of complex elements +/// written, or 0 on error. The buffer must hold at least +/// `2 * operation_matrix_len(...)` doubles. +#[unsafe(no_mangle)] +pub extern "C" fn operation_matrix( + ptr: *const COperation, + out: *mut f64, + buffer_len: usize, +) -> usize { + if ptr.is_null() || out.is_null() { + return 0; + } + match unsafe { (*ptr).inner.matrix() } { + Ok(matrix) => { + let total = matrix.len(); + if buffer_len < total * 2 { + return 0; + } + for (i, value) in matrix.iter().enumerate() { + unsafe { + *out.add(i * 2) = value.re; + *out.add(i * 2 + 1) = value.im; + } + } + total + } + Err(_) => 0, + } +} + +// ===== Instruction-kind discriminants ===== + +/// Returns whether the operation uses a user-defined unitary instruction: +/// 1 when yes, 0 when no, -1 on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn operation_is_unitary(ptr: *const COperation) -> i32 { + if ptr.is_null() { + return -1; + } + unsafe { (*ptr).inner.instruction.is_unitary() as i32 } +} + +/// Returns whether the operation uses a multi-controlled-gate instruction: +/// 1 when yes, 0 when no, -1 on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn operation_is_mcgate(ptr: *const COperation) -> i32 { + if ptr.is_null() { + return -1; + } + unsafe { (*ptr).inner.instruction.is_mcgate() as i32 } +} + +/// Returns whether the operation uses a circuit-backed gate instruction: +/// 1 when yes, 0 when no, -1 on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn operation_is_circuit_gate(ptr: *const COperation) -> i32 { + if ptr.is_null() { + return -1; + } + unsafe { (*ptr).inner.instruction.is_circuit_gate() as i32 } +} + +/// Returns whether the operation uses a non-unitary directive instruction +/// (barrier, measure, reset): 1 when yes, 0 when no, -1 on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn operation_is_directive(ptr: *const COperation) -> i32 { + if ptr.is_null() { + return -1; + } + unsafe { (*ptr).inner.instruction.is_directive() as i32 } +} + +/// Returns whether the operation uses a delay instruction: 1 when yes, +/// 0 when no, -1 on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn operation_is_delay(ptr: *const COperation) -> i32 { + if ptr.is_null() { + return -1; + } + unsafe { (*ptr).inner.instruction.is_delay() as i32 } +} + +/// Returns whether the operation uses a structured classical-control-flow +/// instruction (if / while / for / switch / break / continue): 1 when yes, +/// 0 when no, -1 on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn operation_is_classical_control(ptr: *const COperation) -> i32 { + if ptr.is_null() { + return -1; + } + unsafe { (*ptr).inner.instruction.is_classical_control() as i32 } +} + +/// Returns whether the operation uses a classical-data instruction (store, +/// measure-into-value): 1 when yes, 0 when no, -1 on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn operation_is_classical_data(ptr: *const COperation) -> i32 { + if ptr.is_null() { + return -1; + } + unsafe { (*ptr).inner.instruction.is_classical_data() as i32 } +} + +/// Returns whether the operation is a unitary quantum gate (standard, +/// multi-controlled, user-defined unitary, or circuit-backed gate): 1 when +/// yes, 0 when no, -1 on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn operation_is_quantum_gate(ptr: *const COperation) -> i32 { + if ptr.is_null() { + return -1; + } + unsafe { (*ptr).inner.instruction.is_quantum_gate() as i32 } +} + +/// Returns whether the operation carries a plain instruction rather than +/// structured classical control flow: 1 when yes, 0 for classical-control +/// operations, -1 on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn operation_is_instruction(ptr: *const COperation) -> i32 { + if ptr.is_null() { + return -1; + } + let is_control = matches!( + unsafe { &(*ptr).inner.instruction }, + Instruction::ClassicalControl(_) + ); + (!is_control) as i32 +} + +/// Returns the fixed `(qubit_count, parameter_count)` intrinsic to the +/// instruction, mirroring the core `Instruction::gate_arity`. Returns 0 on +/// success, -1 on NULL input, -3 when the arity is variable +/// (barrier, multi-qubit measurement, classical control). +#[unsafe(no_mangle)] +pub extern "C" fn operation_gate_arity( + ptr: *const COperation, + qubits: *mut usize, + params: *mut usize, +) -> i32 { + if ptr.is_null() || qubits.is_null() || params.is_null() { + return -1; + } + match unsafe { (*ptr).inner.instruction.gate_arity() } { + Some((num_qubits, num_params)) => { + unsafe { + *qubits = num_qubits; + *params = num_params; + } + 0 + } + None => -3, + } +} + +/// Returns an owned clone of the operation when it carries a plain +/// (non-control-flow) instruction, mirroring the core `as_instruction` +/// unwrapping. The result is freed with `operation_free`. Returns NULL on +/// NULL input or when the operation is a classical-control instruction. +#[unsafe(no_mangle)] +pub extern "C" fn operation_as_instruction(ptr: *const COperation) -> *mut COperation { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let inner = unsafe { &(*ptr).inner }; + if matches!(inner.instruction, Instruction::ClassicalControl(_)) { + return std::ptr::null_mut(); + } + Box::into_raw(Box::new(COperation { + inner: inner.clone(), + })) +} + +/// Returns the directive carried by the operation as one of the +/// `OPERATION_DIRECTIVE_*` tags: `OPERATION_DIRECTIVE_NONE` (0) when the +/// operation is not a directive, -1 on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn operation_directive(ptr: *const COperation) -> i32 { + if ptr.is_null() { + return -1; + } + let tag = match unsafe { &(*ptr).inner.instruction } { + Instruction::Directive(Directive::Barrier) => OPERATION_DIRECTIVE_BARRIER, + Instruction::Directive(Directive::Measure) => OPERATION_DIRECTIVE_MEASURE, + Instruction::Directive(Directive::Reset) => OPERATION_DIRECTIVE_RESET, + _ => OPERATION_DIRECTIVE_NONE, + }; + tag as i32 +} + +/// Writes a snapshot of the immutable classical value produced by a +/// measurement operation into `out` (circuit-local value table index plus +/// (tag, width) type parts). Returns 0 on success, -1 on NULL input, -3 +/// when the operation is not a measurement (no classical result). +#[unsafe(no_mangle)] +pub extern "C" fn operation_result(ptr: *const COperation, out: *mut CClassicalValueInfo) -> i32 { + if ptr.is_null() || out.is_null() { + return -1; + } + let value = match unsafe { &(*ptr).inner.instruction } { + Instruction::ClassicalData(operation) => operation.result(), + _ => return -3, + }; + let Some(value) = value else { + return -3; + }; + let (tag, width) = classical_type_parts(value.ty()); + unsafe { + *out = CClassicalValueInfo { + index: value.index(), + tag, + width, + }; + } + 0 +} + +/// Returns whether the operation directly or recursively reads the +/// immutable classical value referenced by `expr`: 1 when yes, 0 when no, +/// -1 on NULL input, -8 when `expr` is not a plain value expression. +#[unsafe(no_mangle)] +pub extern "C" fn operation_reads_value( + ptr: *const COperation, + expr: *const CClassicalExpr, +) -> i32 { + if ptr.is_null() || expr.is_null() { + return -1; + } + let value = match unsafe { (*expr).inner.kind() } { + ClassicalExprKind::Value(value) => value, + _ => return -8, + }; + unsafe { (*ptr).inner.instruction.reads_value(*value) as i32 } +} diff --git a/crates/binding-c/src/circuit/parameter.rs b/crates/binding-c/src/circuit/parameter.rs new file mode 100644 index 00000000..2f9dfa0e --- /dev/null +++ b/crates/binding-c/src/circuit/parameter.rs @@ -0,0 +1,526 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +use crate::circuit::{CCircuit, CParameter, binding_refs, parse_bindings}; +use cqlib_core::circuit::Parameter; +use std::collections::HashMap; +use std::ffi::{CStr, CString}; +use std::os::raw::c_char; + +/// Parse a symbolic parameter expression. +#[unsafe(no_mangle)] +pub extern "C" fn param_parse(expr: *const c_char) -> *mut CParameter { + if expr.is_null() { + return std::ptr::null_mut(); + } + + let c_str = unsafe { CStr::from_ptr(expr) }; + let expr = match c_str.to_str() { + Ok(expr) => expr, + Err(_) => return std::ptr::null_mut(), + }; + + match Parameter::try_from(expr) { + Ok(param) => Box::into_raw(Box::new(CParameter { inner: param })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Free a parameter. Passing NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn param_free(ptr: *mut CParameter) { + if !ptr.is_null() { + unsafe { + let _ = Box::from_raw(ptr); + } + } +} + +/// Evaluate a parameter expression with bindings formatted as "name:value,name2:value2". +#[unsafe(no_mangle)] +pub extern "C" fn param_evaluate(ptr: *const CParameter, bindings: *const c_char) -> f64 { + if ptr.is_null() { + return 0.0; + } + let bindings = parse_bindings(bindings); + let refs = bindings.as_ref().map(binding_refs); + unsafe { (*ptr).inner.evaluate(&refs).unwrap_or(0.0) } +} + +/// Return a new circuit with matching symbolic parameters assigned. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_assign_params( + circuit: *const CCircuit, + bindings: *const c_char, +) -> *mut CCircuit { + if circuit.is_null() { + return std::ptr::null_mut(); + } + + let bindings = parse_bindings(bindings); + let refs = bindings.as_ref().map(binding_refs); + match unsafe { (*circuit).inner.assign_parameters(&refs) } { + Ok(inner) => Box::into_raw(Box::new(CCircuit { inner })), + Err(_) => std::ptr::null_mut(), + } +} + +// ===== Construction helpers ===== + +fn box_param(param: Parameter) -> *mut CParameter { + Box::into_raw(Box::new(CParameter { inner: param })) +} + +fn unary_param( + a: *const CParameter, + build: impl FnOnce(&Parameter) -> Parameter, +) -> *mut CParameter { + if a.is_null() { + return std::ptr::null_mut(); + } + box_param(build(unsafe { &(*a).inner })) +} + +fn binary_param( + a: *const CParameter, + b: *const CParameter, + combine: impl FnOnce(Parameter, Parameter) -> Parameter, +) -> *mut CParameter { + if a.is_null() || b.is_null() { + return std::ptr::null_mut(); + } + let lhs = unsafe { (*a).inner.clone() }; + let rhs = unsafe { (*b).inner.clone() }; + box_param(combine(lhs, rhs)) +} + +// ===== Construction ===== + +/// Create a symbolic parameter for a free variable. Returns NULL on NULL or +/// non-UTF-8 input. +#[unsafe(no_mangle)] +pub extern "C" fn param_symbol(name: *const c_char) -> *mut CParameter { + if name.is_null() { + return std::ptr::null_mut(); + } + match unsafe { CStr::from_ptr(name) }.to_str() { + Ok(name) => box_param(Parameter::symbol(name)), + Err(_) => std::ptr::null_mut(), + } +} + +/// Create the mathematical constant pi as a symbolic parameter. The symbol +/// is resolved automatically during evaluation. +#[unsafe(no_mangle)] +pub extern "C" fn param_pi() -> *mut CParameter { + box_param(Parameter::pi()) +} + +/// Create Euler's number e as a symbolic parameter. The symbol is resolved +/// automatically during evaluation. +#[unsafe(no_mangle)] +pub extern "C" fn param_e() -> *mut CParameter { + box_param(Parameter::e()) +} + +/// Create a numeric parameter from a finite double value. Returns NULL when +/// the value is NaN or infinite. +#[unsafe(no_mangle)] +pub extern "C" fn param_from_double(value: f64) -> *mut CParameter { + if !value.is_finite() { + return std::ptr::null_mut(); + } + box_param(Parameter::from(value)) +} + +// ===== Arithmetic ===== + +/// Return a new parameter computing `a + b`, or NULL on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn param_add(a: *const CParameter, b: *const CParameter) -> *mut CParameter { + binary_param(a, b, |lhs, rhs| lhs + rhs) +} + +/// Return a new parameter computing `a - b`, or NULL on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn param_sub(a: *const CParameter, b: *const CParameter) -> *mut CParameter { + binary_param(a, b, |lhs, rhs| lhs - rhs) +} + +/// Return a new parameter computing `a * b`, or NULL on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn param_mul(a: *const CParameter, b: *const CParameter) -> *mut CParameter { + binary_param(a, b, |lhs, rhs| lhs * rhs) +} + +/// Return a new parameter computing `a / b`, or NULL on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn param_div(a: *const CParameter, b: *const CParameter) -> *mut CParameter { + binary_param(a, b, |lhs, rhs| lhs / rhs) +} + +/// Return a new parameter computing `-a`, or NULL on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn param_neg(a: *const CParameter) -> *mut CParameter { + unary_param(a, |p| -p) +} + +// ===== Mathematical functions ===== + +/// Return a new parameter computing `|a|`, or NULL on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn param_abs(a: *const CParameter) -> *mut CParameter { + unary_param(a, Parameter::abs) +} + +/// Return a new parameter computing `sqrt(a)`, or NULL on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn param_sqrt(a: *const CParameter) -> *mut CParameter { + unary_param(a, Parameter::sqrt) +} + +/// Return a new parameter computing `exp(a)`, or NULL on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn param_exp(a: *const CParameter) -> *mut CParameter { + unary_param(a, Parameter::exp) +} + +/// Return a new parameter computing `ln(a)`, or NULL on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn param_ln(a: *const CParameter) -> *mut CParameter { + unary_param(a, Parameter::ln) +} + +/// Return a new parameter computing `log(a) / log(base)`, or NULL on NULL +/// input. +#[unsafe(no_mangle)] +pub extern "C" fn param_log(a: *const CParameter, base: *const CParameter) -> *mut CParameter { + if base.is_null() { + return std::ptr::null_mut(); + } + let base = unsafe { (*base).inner.clone() }; + unary_param(a, move |p| p.log(base)) +} + +/// Return a new parameter computing `sin(a)`, or NULL on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn param_sin(a: *const CParameter) -> *mut CParameter { + unary_param(a, Parameter::sin) +} + +/// Return a new parameter computing `cos(a)`, or NULL on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn param_cos(a: *const CParameter) -> *mut CParameter { + unary_param(a, Parameter::cos) +} + +/// Return a new parameter computing `tan(a)`, or NULL on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn param_tan(a: *const CParameter) -> *mut CParameter { + unary_param(a, Parameter::tan) +} + +/// Return a new parameter computing `asin(a)`, or NULL on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn param_asin(a: *const CParameter) -> *mut CParameter { + unary_param(a, Parameter::asin) +} + +/// Return a new parameter computing `acos(a)`, or NULL on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn param_acos(a: *const CParameter) -> *mut CParameter { + unary_param(a, Parameter::acos) +} + +/// Return a new parameter computing `atan(a)`, or NULL on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn param_atan(a: *const CParameter) -> *mut CParameter { + unary_param(a, Parameter::atan) +} + +/// Return a new parameter computing `sinh(a)`, or NULL on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn param_sinh(a: *const CParameter) -> *mut CParameter { + unary_param(a, Parameter::sinh) +} + +/// Return a new parameter computing `cosh(a)`, or NULL on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn param_cosh(a: *const CParameter) -> *mut CParameter { + unary_param(a, Parameter::cosh) +} + +/// Return a new parameter computing `tanh(a)`, or NULL on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn param_tanh(a: *const CParameter) -> *mut CParameter { + unary_param(a, Parameter::tanh) +} + +/// Return a new parameter computing `floor(a)`, or NULL on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn param_floor(a: *const CParameter) -> *mut CParameter { + unary_param(a, Parameter::floor) +} + +/// Return a new parameter computing `ceil(a)`, or NULL on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn param_ceil(a: *const CParameter) -> *mut CParameter { + unary_param(a, Parameter::ceil) +} + +/// Return a new parameter computing `round(a)`, or NULL on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn param_round(a: *const CParameter) -> *mut CParameter { + unary_param(a, Parameter::round) +} + +/// Return a new parameter computing `a` raised to the power `exp`, or NULL +/// on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn param_pow(a: *const CParameter, exp: *const CParameter) -> *mut CParameter { + if exp.is_null() { + return std::ptr::null_mut(); + } + let exp = unsafe { (*exp).inner.clone() }; + unary_param(a, move |p| p.pow(exp)) +} + +// ===== Symbol and state queries ===== + +/// Return the number of free symbols (constants pi and e excluded), or 0 +/// for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn param_symbols_len(a: *const CParameter) -> usize { + if a.is_null() { + return 0; + } + unsafe { (*a).inner.get_symbols().len() } +} + +/// Copy the free symbol names, sorted lexicographically, into `out`. Each +/// written element is a freshly allocated C string the caller frees with +/// `cqlib_string_free`. Returns 0 on success, -1 on NULL input, -8 when +/// `len` is too small. +#[unsafe(no_mangle)] +pub extern "C" fn param_symbols(a: *const CParameter, out: *mut *mut c_char, len: usize) -> i32 { + if a.is_null() { + return -1; + } + let mut symbols: Vec = unsafe { (*a).inner.get_symbols().into_iter().collect() }; + symbols.sort(); + if len < symbols.len() { + return -8; + } + if symbols.is_empty() { + return 0; + } + if out.is_null() { + return -1; + } + let slice = unsafe { std::slice::from_raw_parts_mut(out, symbols.len()) }; + for (slot, symbol) in slice.iter_mut().zip(symbols.iter()) { + match CString::new(symbol.as_str()) { + Ok(text) => *slot = text.into_raw(), + Err(_) => return -3, + } + } + 0 +} + +/// Return the symbol name when the parameter is exactly one free symbol. +/// The result is a freshly allocated C string the caller frees with +/// `cqlib_string_free`. Returns NULL for NULL input or when the expression +/// is not a single symbol. +#[unsafe(no_mangle)] +pub extern "C" fn param_as_symbol(a: *const CParameter) -> *mut c_char { + if a.is_null() { + return std::ptr::null_mut(); + } + match unsafe { (*a).inner.as_symbol() } { + Some(symbol) => match CString::new(symbol) { + Ok(text) => text.into_raw(), + Err(_) => std::ptr::null_mut(), + }, + None => std::ptr::null_mut(), + } +} + +/// Return 1 when the parameter has no free symbols, else 0. Returns -1 on +/// NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn param_is_constant(a: *const CParameter) -> i32 { + if a.is_null() { + return -1; + } + unsafe { (*a).inner.is_constant() as i32 } +} + +/// Return 1 when a constant parameter evaluates exactly to zero, else 0. +/// Returns -1 on NULL input, -3 when a constant parameter cannot be +/// evaluated. +#[unsafe(no_mangle)] +pub extern "C" fn param_is_exact_zero(a: *const CParameter) -> i32 { + if a.is_null() { + return -1; + } + unsafe { (*a).inner.is_exact_zero().map_or(-3, |flag| flag as i32) } +} + +/// Return 1 when the parameter evaluates to zero, else 0. Parameters with +/// unbound symbols return 0. Returns -1 on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn param_is_zero(a: *const CParameter) -> i32 { + if a.is_null() { + return -1; + } + unsafe { (*a).inner.is_zero() as i32 } +} + +/// Return 1 when the parameter evaluates to one, else 0. Parameters with +/// unbound symbols return 0. Returns -1 on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn param_is_one(a: *const CParameter) -> i32 { + if a.is_null() { + return -1; + } + unsafe { (*a).inner.is_one() as i32 } +} + +// ===== Simplification ===== + +/// Return a new algebraically simplified parameter, or NULL on NULL input +/// or simplification failure. +#[unsafe(no_mangle)] +pub extern "C" fn param_simplify(a: *const CParameter) -> *mut CParameter { + if a.is_null() { + return std::ptr::null_mut(); + } + match unsafe { (*a).inner.simplify() } { + Ok(inner) => box_param(inner), + Err(_) => std::ptr::null_mut(), + } +} + +/// Return the canonical storage form of the parameter: simplified, with +/// constant expressions evaluated to a finite numeric value. Returns NULL +/// on NULL input or failure. +#[unsafe(no_mangle)] +pub extern "C" fn param_canonicalized(a: *const CParameter) -> *mut CParameter { + if a.is_null() { + return std::ptr::null_mut(); + } + match unsafe { (*a).inner.canonicalized() } { + Ok(inner) => box_param(inner), + Err(_) => std::ptr::null_mut(), + } +} + +// ===== Substitution and differentiation ===== + +/// Substitute every occurrence of `symbol` with `replacement`, returning a +/// new parameter. Returns NULL on NULL input or non-UTF-8 symbol name. +#[unsafe(no_mangle)] +pub extern "C" fn param_replace( + a: *const CParameter, + symbol: *const c_char, + replacement: *const CParameter, +) -> *mut CParameter { + if a.is_null() || symbol.is_null() || replacement.is_null() { + return std::ptr::null_mut(); + } + let symbol = match unsafe { CStr::from_ptr(symbol) }.to_str() { + Ok(symbol) => symbol, + Err(_) => return std::ptr::null_mut(), + }; + let replacement = unsafe { (*replacement).inner.clone() }; + box_param(unsafe { (*a).inner.replace(symbol, replacement) }) +} + +/// Substitute several symbols in one call and simplify the result. +/// `names` and `values` are parallel arrays of length `len`. Returns a new +/// parameter, or NULL on NULL input, non-UTF-8 names or NULL elements. +#[unsafe(no_mangle)] +pub extern "C" fn param_substitute_many( + a: *const CParameter, + names: *const *const c_char, + values: *const *const CParameter, + len: usize, +) -> *mut CParameter { + if a.is_null() { + return std::ptr::null_mut(); + } + let mut bindings: HashMap = HashMap::with_capacity(len); + for i in 0..len { + let name = unsafe { *names.add(i) }; + let value = unsafe { *values.add(i) }; + if name.is_null() || value.is_null() { + return std::ptr::null_mut(); + } + let name = match unsafe { CStr::from_ptr(name) }.to_str() { + Ok(name) => name.to_string(), + Err(_) => return std::ptr::null_mut(), + }; + bindings.insert(name, unsafe { (*value).inner.clone() }); + } + box_param(unsafe { (*a).inner.substitute_many(&bindings) }) +} + +/// Return the symbolic partial derivative with respect to `var`, or NULL +/// on NULL input, non-UTF-8 name or differentiation failure. +#[unsafe(no_mangle)] +pub extern "C" fn param_derivative(a: *const CParameter, var: *const c_char) -> *mut CParameter { + if a.is_null() || var.is_null() { + return std::ptr::null_mut(); + } + let var = match unsafe { CStr::from_ptr(var) }.to_str() { + Ok(var) => var, + Err(_) => return std::ptr::null_mut(), + }; + match unsafe { (*a).inner.derivative(var) } { + Ok(inner) => box_param(inner), + Err(_) => std::ptr::null_mut(), + } +} + +// ===== Conservative equivalence ===== + +/// Conservatively check whether two parameters are provably equal within +/// `tolerance`. Returns 1 when equal, 0 otherwise; -1 on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn param_provably_equal( + a: *const CParameter, + b: *const CParameter, + tolerance: f64, +) -> i32 { + if a.is_null() || b.is_null() { + return -1; + } + unsafe { (*a).inner.provably_equal(&(*b).inner, tolerance) as i32 } +} + +/// Conservatively check whether two parameters are equal modulo `modulus` +/// within `tolerance`. Returns 1 when equal, 0 otherwise; -1 on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn param_provably_equal_modulo( + a: *const CParameter, + b: *const CParameter, + modulus: *const CParameter, + tolerance: f64, +) -> i32 { + if a.is_null() || b.is_null() || modulus.is_null() { + return -1; + } + unsafe { + (*a).inner + .provably_equal_modulo(&(*b).inner, &(*modulus).inner, tolerance) as i32 + } +} diff --git a/crates/binding-c/src/circuit/properties.rs b/crates/binding-c/src/circuit/properties.rs new file mode 100644 index 00000000..309c9fb8 --- /dev/null +++ b/crates/binding-c/src/circuit/properties.rs @@ -0,0 +1,223 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +use crate::circuit::CCircuit; +use cqlib_core::circuit::{Parameter, Qubit}; + +// ===== Section 2.1.7: Circuit properties and auxiliary operations ===== + +/// Return the circuit depth. Negative values indicate errors. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_depth(ptr: *const CCircuit, recurse: bool) -> isize { + if ptr.is_null() { + return -1; + } + unsafe { (*ptr).inner.depth(recurse).map_or(-3, |d| d as isize) } +} + +/// Return the circuit width (number of qubits), or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_width(ptr: *const CCircuit) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.width() } +} + +/// Return the number of qubits in the circuit (for array allocation), or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_qubits_len(ptr: *const CCircuit) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.qubits().len() } +} + +/// Copy qubit identifiers into `out`. Returns the total number of qubits. +/// If `out` is NULL or `len` is too small, only the count is returned (no copy). +#[unsafe(no_mangle)] +pub extern "C" fn circuit_qubits(ptr: *const CCircuit, out: *mut u32, len: usize) -> usize { + if ptr.is_null() { + return 0; + } + let qubits = unsafe { (*ptr).inner.qubits() }; + let total = qubits.len(); + if !out.is_null() { + let count = total.min(len); + for (i, q) in qubits.iter().take(count).enumerate() { + unsafe { + *out.add(i) = q.id(); + } + } + } + total +} + +/// Remove the operation at `index`. Returns 0 on success, negative on error. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_remove_operation(ptr: *mut CCircuit, index: usize) -> i32 { + if ptr.is_null() { + return -1; + } + let wrapper = unsafe { &mut *ptr }; + wrapper.inner.remove_operation(index).map_or(-3, |_| 0) +} + +/// Return a new circuit that is the inverse of the input, or NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_inverse(ptr: *const CCircuit) -> *mut CCircuit { + if ptr.is_null() { + return std::ptr::null_mut(); + } + match unsafe { (*ptr).inner.inverse() } { + Ok(inner) => Box::into_raw(Box::new(CCircuit { inner })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Return a decomposed copy of the circuit, or NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_decompose(ptr: *const CCircuit) -> *mut CCircuit { + if ptr.is_null() { + return std::ptr::null_mut(); + } + match unsafe { (*ptr).inner.decompose() } { + Ok(inner) => Box::into_raw(Box::new(CCircuit { inner })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Return the number of complex elements (rows * cols) in the unitary matrix, +/// or 0 on error. Pass `qubits_order` = NULL to use default ordering. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_to_matrix_len( + ptr: *const CCircuit, + qubits_order: *const usize, + order_len: usize, +) -> usize { + if ptr.is_null() { + return 0; + } + let order = if qubits_order.is_null() || order_len == 0 { + None + } else { + let slice = unsafe { std::slice::from_raw_parts(qubits_order, order_len) }; + Some(slice) + }; + match unsafe { (*ptr).inner.to_matrix(order) } { + Ok(matrix) => matrix.len(), + Err(_) => 0, + } +} + +/// Copy the unitary matrix into `out` as interleaved (real, imag) doubles. +/// Returns the number of complex elements written, or 0 on error. +/// Buffer must hold at least `2 * circuit_to_matrix_len(...)` doubles. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_to_matrix( + ptr: *const CCircuit, + qubits_order: *const usize, + order_len: usize, + out: *mut f64, + buffer_len: usize, +) -> usize { + if ptr.is_null() || out.is_null() { + return 0; + } + let order = if qubits_order.is_null() || order_len == 0 { + None + } else { + let slice = unsafe { std::slice::from_raw_parts(qubits_order, order_len) }; + Some(slice) + }; + match unsafe { (*ptr).inner.to_matrix(order) } { + Ok(matrix) => { + let total = matrix.len(); + if buffer_len < total * 2 { + return 0; + } + for (i, c) in matrix.iter().enumerate() { + unsafe { + *out.add(i * 2) = c.re; + *out.add(i * 2 + 1) = c.im; + } + } + total + } + Err(_) => 0, + } +} + +/// Compose another circuit into this one. `qubits_map` maps `other`'s qubits +/// to this circuit's qubits. NULL map uses identity mapping (merge by ID). +#[unsafe(no_mangle)] +pub extern "C" fn circuit_compose( + ptr: *mut CCircuit, + other: *const CCircuit, + qubits_map: *const u32, + map_len: usize, +) -> i32 { + if ptr.is_null() || other.is_null() { + return -1; + } + let wrapper = unsafe { &mut *ptr }; + let other_inner = unsafe { &(*other).inner }; + let result = if qubits_map.is_null() || map_len == 0 { + wrapper.inner.compose(other_inner, None) + } else { + let slice = unsafe { std::slice::from_raw_parts(qubits_map, map_len) }; + let qubits: Vec = slice.iter().map(|&id| Qubit::new(id)).collect(); + wrapper.inner.compose(other_inner, Some(qubits.as_slice())) + }; + result.map_or(-3, |_| 0) +} + +/// Append new qubits to the circuit. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_add_qubits(ptr: *mut CCircuit, qubits: *const u32, count: usize) -> i32 { + if ptr.is_null() || qubits.is_null() { + return -1; + } + let wrapper = unsafe { &mut *ptr }; + let slice = unsafe { std::slice::from_raw_parts(qubits, count) }; + let new_qubits: Vec = slice.iter().map(|&id| Qubit::new(id)).collect(); + wrapper.inner.add_qubits(new_qubits).map_or(-3, |_| 0) +} + +/// Set the global phase to a numeric value. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_set_global_phase(ptr: *mut CCircuit, phase: f64) -> i32 { + if ptr.is_null() { + return -1; + } + if !phase.is_finite() { + return -3; + } + let wrapper = unsafe { &mut *ptr }; + wrapper.inner.set_global_phase(Parameter::from(phase)); + 0 +} + +/// Set the global phase to a symbolic parameter. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_set_global_phase_param( + ptr: *mut CCircuit, + param: *const crate::circuit::CParameter, +) -> i32 { + if ptr.is_null() || param.is_null() { + return -1; + } + let wrapper = unsafe { &mut *ptr }; + let param = unsafe { (*param).inner.clone() }; + wrapper.inner.set_global_phase(param); + 0 +} diff --git a/crates/binding-c/src/circuit/qubit.rs b/crates/binding-c/src/circuit/qubit.rs new file mode 100644 index 00000000..b42b65ee --- /dev/null +++ b/crates/binding-c/src/circuit/qubit.rs @@ -0,0 +1,116 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating that +// they have been altered from the originals. + +//! C ABI for the `Qubit` logical-qubit identifier. +//! +//! `Qubit` is a lightweight, copyable handle wrapping a numeric ID. It does +//! not carry a circuit identity or a state-vector position; the owning +//! circuit and the conversion interfaces determine those meanings. + +use crate::error::CqlibError; +use cqlib_core::circuit::Qubit; +use std::ffi::CString; +use std::os::raw::c_char; + +/// Logical qubit identifier: a value handle wrapping a numeric ID, passed by +/// value. Compare and hash via the numeric ID. +#[repr(C)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct CQubit { + /// Numeric identifier of the logical qubit. + pub id: u32, +} + +impl From for CQubit { + fn from(qubit: Qubit) -> Self { + Self { id: qubit.id() } + } +} + +impl From for Qubit { + fn from(qubit: CQubit) -> Self { + Qubit::new(qubit.id) + } +} + +/// Creates a logical qubit with the supplied numeric identifier. +#[unsafe(no_mangle)] +pub extern "C" fn qubit_new(id: u32) -> CQubit { + CQubit { id } +} + +/// Returns the raw numeric identifier of the logical qubit. +#[unsafe(no_mangle)] +pub extern "C" fn qubit_id(qubit: CQubit) -> u32 { + Qubit::from(qubit).id() +} + +/// Returns the numeric identifier as a `uintptr_t` for index-style use. The +/// value does not map the qubit to its position in any circuit or simulator. +#[unsafe(no_mangle)] +pub extern "C" fn qubit_index(qubit: CQubit) -> usize { + Qubit::from(qubit).index() +} + +/// Checked conversion from a signed integer: writes the qubit to `*out` and +/// returns 0, -1 when `out` is NULL, or -8 when `value` is negative or +/// exceeds the `u32` ID range. +#[unsafe(no_mangle)] +pub extern "C" fn qubit_try_from_i64(value: i64, out: *mut CQubit) -> i32 { + if out.is_null() { + return CqlibError::NullPtr as i32; + } + if value < 0 || value > u32::MAX as i64 { + return CqlibError::InvalidParam as i32; + } + unsafe { *out = CQubit { id: value as u32 } }; + CqlibError::Ok as i32 +} + +/// Checked conversion from an unsigned integer: writes the qubit to `*out` +/// and returns 0, -1 when `out` is NULL, or -8 when `value` exceeds the +/// `u32` ID range. +#[unsafe(no_mangle)] +pub extern "C" fn qubit_try_from_u64(value: u64, out: *mut CQubit) -> i32 { + if out.is_null() { + return CqlibError::NullPtr as i32; + } + if value > u32::MAX as u64 { + return CqlibError::InvalidParam as i32; + } + unsafe { *out = CQubit { id: value as u32 } }; + CqlibError::Ok as i32 +} + +/// Returns 1 when both qubits carry the same numeric identifier, else 0. +#[unsafe(no_mangle)] +pub extern "C" fn qubit_equal(a: CQubit, b: CQubit) -> i32 { + Qubit::from(a).eq(&Qubit::from(b)) as i32 +} + +/// Compares two qubits by numeric identifier: -1 when `a < b`, 0 when equal, +/// 1 when `a > b`. +#[unsafe(no_mangle)] +pub extern "C" fn qubit_compare(a: CQubit, b: CQubit) -> i32 { + match Qubit::from(a).cmp(&Qubit::from(b)) { + std::cmp::Ordering::Less => -1, + std::cmp::Ordering::Equal => 0, + std::cmp::Ordering::Greater => 1, + } +} + +/// Formats the qubit identifier as `Q` (e.g. `Q12`); free with +/// `cqlib_string_free`. Returns NULL on allocation failure. +#[unsafe(no_mangle)] +pub extern "C" fn qubit_to_string(qubit: CQubit) -> *mut c_char { + CString::new(format!("{}", Qubit::from(qubit))).map_or(std::ptr::null_mut(), |cs| cs.into_raw()) +} diff --git a/crates/binding-c/src/circuit/symbolic_matrix.rs b/crates/binding-c/src/circuit/symbolic_matrix.rs new file mode 100644 index 00000000..148a0113 --- /dev/null +++ b/crates/binding-c/src/circuit/symbolic_matrix.rs @@ -0,0 +1,1057 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! C ABI for symbolic matrices and global-phase equivalence checks. + +#![allow(clippy::not_unsafe_ptr_arg_deref)] + +use crate::circuit::{CCircuit, CParameter, binding_refs, parse_bindings}; +use crate::device::standard_gate_from_name; +use cqlib_core::circuit::Parameter; +use cqlib_core::circuit::symbolic_matrix::gate::{ + apply_gate_to_matrix as core_apply_gate_to_matrix, + apply_gate_to_matrix_num as core_apply_gate_to_matrix_num, + apply_general_gate as core_apply_general_gate, + apply_general_gate_num as core_apply_general_gate_num, + apply_single_qubit_gate as core_apply_single_qubit_gate, + apply_single_qubit_gate_num as core_apply_single_qubit_gate_num, + apply_standard_gate_to_matrix as core_apply_standard_gate_to_matrix, + apply_two_qubit_gate as core_apply_two_qubit_gate, + apply_two_qubit_gate_num as core_apply_two_qubit_gate_num, control_matrix, +}; +use cqlib_core::circuit::symbolic_matrix::matrix::{ + apply_numeric_diagonal_gate as core_apply_numeric_diagonal_gate, + apply_numeric_permutation_gate as core_apply_numeric_permutation_gate, + apply_symbolic_diagonal_gate as core_apply_symbolic_diagonal_gate, + apply_symbolic_permutation_gate as core_apply_symbolic_permutation_gate, simplify_matrix, +}; +use cqlib_core::circuit::symbolic_matrix::{ + SymbolicComplex, SymbolicMatrix, circuit_to_symbolic_matrix as core_circuit_to_symbolic_matrix, + circuits_equivalent as core_circuits_equivalent, evaluate_symbolic_matrix, + standard_gate_symbolic_matrix as core_standard_gate_symbolic_matrix, + substitute_symbolic_matrix, symbolic_eye as core_symbolic_eye, + symbolic_matrices_equivalent as core_symbolic_matrices_equivalent, +}; +use ndarray::Array2; +use num_complex::Complex64; +use std::collections::HashMap; +use std::ffi::{CStr, CString}; +use std::os::raw::c_char; + +/// Opaque handle around a symbolic matrix ([`SymbolicMatrix`]). +pub struct CSymbolicMatrix { + pub inner: SymbolicMatrix, +} + +/// Creates a `dim x dim` symbolic identity matrix. Returns NULL only on +/// allocation failure. +#[unsafe(no_mangle)] +pub extern "C" fn symbolic_eye(dim: usize) -> *mut CSymbolicMatrix { + Box::into_raw(Box::new(CSymbolicMatrix { + inner: core_symbolic_eye(dim), + })) +} + +/// Frees a symbolic matrix handle. Passing NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn symbolic_matrix_free(ptr: *mut CSymbolicMatrix) { + if !ptr.is_null() { + unsafe { + drop(Box::from_raw(ptr)); + } + } +} + +/// Returns the row count, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn symbolic_matrix_rows(ptr: *const CSymbolicMatrix) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.nrows() } +} + +/// Returns the column count, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn symbolic_matrix_cols(ptr: *const CSymbolicMatrix) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.ncols() } +} + +/// Formats one matrix element as a human-readable parameter expression +/// (e.g. `"cos(theta*0.5)"`). Returns a newly allocated C string (free with +/// `cqlib_string_free`), or NULL on NULL input or out-of-bounds indices. +#[unsafe(no_mangle)] +pub extern "C" fn symbolic_matrix_element_str( + ptr: *const CSymbolicMatrix, + row: usize, + col: usize, +) -> *mut c_char { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let inner = unsafe { &(*ptr).inner }; + match inner.get([row, col]) { + Some(value) => match CString::new(value.to_string()) { + Ok(text) => text.into_raw(), + Err(_) => std::ptr::null_mut(), + }, + None => std::ptr::null_mut(), + } +} + +/// Simplifies every element of the matrix. Returns a newly allocated +/// `CSymbolicMatrix*` (free with `symbolic_matrix_free`), or NULL on NULL +/// input or simplification failure. +#[unsafe(no_mangle)] +pub extern "C" fn symbolic_matrix_simplify(ptr: *const CSymbolicMatrix) -> *mut CSymbolicMatrix { + if ptr.is_null() { + return std::ptr::null_mut(); + } + match simplify_matrix(unsafe { &(*ptr).inner }) { + Ok(inner) => Box::into_raw(Box::new(CSymbolicMatrix { inner })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Returns the flattened element count (`rows * cols`), or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn symbolic_matrix_evaluate_len(ptr: *const CSymbolicMatrix) -> usize { + if ptr.is_null() { + return 0; + } + let inner = unsafe { &(*ptr).inner }; + inner.nrows().saturating_mul(inner.ncols()) +} + +/// Evaluates every element under parameter bindings into `buffer` in +/// row-major order. `bindings` is a `"name:value,name:value"` string (NULL +/// means no bindings). `len` must be at least `symbolic_matrix_evaluate_len`. +/// Returns 0 on success, -1 on NULL input, -3 on unevaluable symbols, +/// -4 on a malformed bindings string, -8 when `len` is too small. +#[unsafe(no_mangle)] +pub extern "C" fn symbolic_matrix_evaluate( + ptr: *const CSymbolicMatrix, + bindings: *const c_char, + buffer: *mut Complex64, + len: usize, +) -> i32 { + if ptr.is_null() { + return -1; + } + let mut bindings_owned = HashMap::new(); + let has_bindings = !bindings.is_null(); + if has_bindings { + match parse_bindings(bindings) { + Some(map) => bindings_owned = map, + None => return -4, + } + } + let bindings_map = if has_bindings { + Some(binding_refs(&bindings_owned)) + } else { + None + }; + match evaluate_symbolic_matrix(unsafe { &(*ptr).inner }, &bindings_map) { + Ok(out) => { + if buffer.is_null() { + return -1; + } + let count = out.len(); + if len < count { + return -8; + } + let slice = unsafe { std::slice::from_raw_parts_mut(buffer, count) }; + for (slot, value) in slice.iter_mut().zip(out.iter()) { + *slot = *value; + } + 0 + } + Err(_) => -3, + } +} + +/// Simultaneously substitutes symbols in the matrix. `names[i]` is a symbol +/// name and `params[i]` the replacement expression. Returns a newly allocated +/// `CSymbolicMatrix*` (free with `symbolic_matrix_free`), or NULL on NULL +/// input, invalid UTF-8 or a substitution error (e.g. reserved prefix). +#[unsafe(no_mangle)] +pub extern "C" fn symbolic_matrix_substitute( + ptr: *const CSymbolicMatrix, + names: *const *const c_char, + params: *const *const CParameter, + len: usize, +) -> *mut CSymbolicMatrix { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let mut replacements: HashMap = HashMap::new(); + if len > 0 { + if names.is_null() || params.is_null() { + return std::ptr::null_mut(); + } + let names = unsafe { std::slice::from_raw_parts(names, len) }; + let params = unsafe { std::slice::from_raw_parts(params, len) }; + for (name, param) in names.iter().zip(params.iter()) { + if name.is_null() || param.is_null() { + return std::ptr::null_mut(); + } + let name = match unsafe { CStr::from_ptr(*name) }.to_str() { + Ok(name) => name, + Err(_) => return std::ptr::null_mut(), + }; + let param = unsafe { (*(*param)).inner.clone() }; + replacements.insert(name.to_string(), param); + } + } + let matrix = unsafe { (*ptr).inner.clone() }; + match substitute_symbolic_matrix(matrix, &replacements) { + Ok(inner) => Box::into_raw(Box::new(CSymbolicMatrix { inner })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Builds the symbolic unitary matrix of a standard gate selected by name +/// (e.g. `"RX"`, `"RZZ"`; same name set as `circuit_multi_control`). +/// Returns a newly allocated `CSymbolicMatrix*`, or NULL on NULL input, +/// invalid UTF-8, an unknown gate name or a parameter-count mismatch. +#[unsafe(no_mangle)] +pub extern "C" fn standard_gate_symbolic_matrix( + gate_name: *const c_char, + params: *const *const CParameter, + len: usize, +) -> *mut CSymbolicMatrix { + if gate_name.is_null() { + return std::ptr::null_mut(); + } + let name = match unsafe { CStr::from_ptr(gate_name) }.to_str() { + Ok(name) => name, + Err(_) => return std::ptr::null_mut(), + }; + let gate = match standard_gate_from_name(name) { + Some(gate) => gate, + None => return std::ptr::null_mut(), + }; + let mut values: Vec = Vec::with_capacity(len); + if len > 0 { + if params.is_null() { + return std::ptr::null_mut(); + } + let params = unsafe { std::slice::from_raw_parts(params, len) }; + for param in params { + if param.is_null() { + return std::ptr::null_mut(); + } + values.push(unsafe { (*(*param)).inner.clone() }); + } + } + match core_standard_gate_symbolic_matrix(gate, &values) { + Ok(inner) => Box::into_raw(Box::new(CSymbolicMatrix { inner })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Computes the symbolic unitary matrix of a circuit. `order` optionally +/// lists the qubit ids from least-significant to most-significant bit (NULL +/// sorts by qubit index). Returns a newly allocated `CSymbolicMatrix*`, or +/// NULL on NULL input, an order/qubit-set mismatch or non-unitary operations. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_to_symbolic_matrix( + ptr: *const CCircuit, + order: *const u32, + order_len: usize, +) -> *mut CSymbolicMatrix { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let order = match read_order(order, order_len) { + Ok(order) => order, + Err(_) => return std::ptr::null_mut(), + }; + match core_circuit_to_symbolic_matrix(unsafe { &(*ptr).inner }, order.as_deref()) { + Ok(inner) => Box::into_raw(Box::new(CSymbolicMatrix { inner })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Returns whether two symbolic matrices are equivalent up to a global +/// phase: 1 when equivalent, 0 when not, -1 on NULL input, -3 on +/// simplification failure. +#[unsafe(no_mangle)] +pub extern "C" fn symbolic_matrices_equivalent( + lhs: *const CSymbolicMatrix, + rhs: *const CSymbolicMatrix, +) -> i32 { + if lhs.is_null() || rhs.is_null() { + return -1; + } + match core_symbolic_matrices_equivalent(unsafe { &(*lhs).inner }, unsafe { &(*rhs).inner }) { + Ok(equivalent) => equivalent as i32, + Err(_) => -3, + } +} + +/// Compares two circuits up to a global phase in the requested qubit order. +/// Returns 1 when equivalent, 0 when not, -1 on NULL input, -3 on matrix +/// construction failure. +#[unsafe(no_mangle)] +pub extern "C" fn circuits_equivalent( + lhs: *const CCircuit, + rhs: *const CCircuit, + order: *const u32, + order_len: usize, +) -> i32 { + if lhs.is_null() || rhs.is_null() { + return -1; + } + let order = match read_order(order, order_len) { + Ok(order) => order, + Err(_) => return -8, + }; + match core_circuits_equivalent( + unsafe { &(*lhs).inner }, + unsafe { &(*rhs).inner }, + order.as_deref(), + ) { + Ok(equivalent) => equivalent as i32, + Err(_) => -3, + } +} + +/// Embeds `base` into the bottom-right block of a larger identity matrix +/// for `num_ctrls` control qubits. Returns a newly allocated +/// `CSymbolicMatrix*`, or NULL on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn symbolic_matrix_controlled( + base: *const CSymbolicMatrix, + num_ctrls: usize, +) -> *mut CSymbolicMatrix { + if base.is_null() { + return std::ptr::null_mut(); + } + let inner = control_matrix(unsafe { &(*base).inner }, num_ctrls); + Box::into_raw(Box::new(CSymbolicMatrix { inner })) +} + +// --------------------------------------------------------------------------- +// Symbolic complex values +// --------------------------------------------------------------------------- + +/// Opaque handle around a symbolic complex value ([`SymbolicComplex`]): both +/// the real and the imaginary part are parameter expressions. +pub struct CSymbolicComplex { + pub inner: SymbolicComplex, +} + +fn box_complex(value: SymbolicComplex) -> *mut CSymbolicComplex { + Box::into_raw(Box::new(CSymbolicComplex { inner: value })) +} + +fn box_param_clone(param: Parameter) -> *mut CParameter { + Box::into_raw(Box::new(CParameter { inner: param })) +} + +/// Creates a symbolic complex value from real and imaginary expressions. +/// Returns NULL on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn symbolic_complex_new( + re: *const CParameter, + im: *const CParameter, +) -> *mut CSymbolicComplex { + if re.is_null() || im.is_null() { + return std::ptr::null_mut(); + } + let re = unsafe { (*re).inner.clone() }; + let im = unsafe { (*im).inner.clone() }; + box_complex(SymbolicComplex::new(re, im)) +} + +/// Returns the additive identity `0 + 0i`. +#[unsafe(no_mangle)] +pub extern "C" fn symbolic_complex_zero() -> *mut CSymbolicComplex { + box_complex(SymbolicComplex::zero()) +} + +/// Returns the multiplicative identity `1 + 0i`. +#[unsafe(no_mangle)] +pub extern "C" fn symbolic_complex_one() -> *mut CSymbolicComplex { + box_complex(SymbolicComplex::one()) +} + +/// Returns the imaginary unit `0 + 1i`. +#[unsafe(no_mangle)] +pub extern "C" fn symbolic_complex_i() -> *mut CSymbolicComplex { + box_complex(SymbolicComplex::i()) +} + +/// Creates a symbolic complex value with zero imaginary part from the given +/// real expression. Returns NULL on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn symbolic_complex_from_real(re: *const CParameter) -> *mut CSymbolicComplex { + if re.is_null() { + return std::ptr::null_mut(); + } + box_complex(SymbolicComplex::from_real(unsafe { (*re).inner.clone() })) +} + +/// Creates a symbolic complex constant from a raw `Complex64` pair. Returns +/// NULL when either component is not finite. +#[unsafe(no_mangle)] +pub extern "C" fn symbolic_complex_from_complex(re: f64, im: f64) -> *mut CSymbolicComplex { + if !re.is_finite() || !im.is_finite() { + return std::ptr::null_mut(); + } + box_complex(SymbolicComplex::from_complex(Complex64::new(re, im))) +} + +/// Builds `exp(i*theta)`, i.e. `cos(theta) + i*sin(theta)` (free via +/// `symbolic_complex_free`). Returns NULL on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn symbolic_complex_exp_i(theta: *const CParameter) -> *mut CSymbolicComplex { + if theta.is_null() { + return std::ptr::null_mut(); + } + box_complex(SymbolicComplex::exp_i(unsafe { (*theta).inner.clone() })) +} + +/// Frees a symbolic complex handle. Passing NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn symbolic_complex_free(ptr: *mut CSymbolicComplex) { + if !ptr.is_null() { + unsafe { + drop(Box::from_raw(ptr)); + } + } +} + +/// Returns a clone of the real part (free with `param_free`); NULL on NULL. +#[unsafe(no_mangle)] +pub extern "C" fn symbolic_complex_re(ptr: *const CSymbolicComplex) -> *mut CParameter { + if ptr.is_null() { + return std::ptr::null_mut(); + } + box_param_clone(unsafe { (*ptr).inner.re.clone() }) +} + +/// Returns a clone of the imaginary part (free with `param_free`); NULL on +/// NULL. +#[unsafe(no_mangle)] +pub extern "C" fn symbolic_complex_im(ptr: *const CSymbolicComplex) -> *mut CParameter { + if ptr.is_null() { + return std::ptr::null_mut(); + } + box_param_clone(unsafe { (*ptr).inner.im.clone() }) +} + +/// Evaluates both parts under `bindings` (`"name:value,..."`; NULL means no +/// bindings) and stores the `Complex64` result in `out`. Returns 0 on +/// success, -1 on NULL input, -3 on unevaluable symbols, -4 on a malformed +/// bindings string. +#[unsafe(no_mangle)] +pub extern "C" fn symbolic_complex_evaluate( + ptr: *const CSymbolicComplex, + bindings: *const c_char, + out: *mut Complex64, +) -> i32 { + if ptr.is_null() || out.is_null() { + return -1; + } + let mut bindings_owned = HashMap::new(); + let has_bindings = !bindings.is_null(); + if has_bindings { + match parse_bindings(bindings) { + Some(map) => bindings_owned = map, + None => return -4, + } + } + let bindings_map = if has_bindings { + Some(binding_refs(&bindings_owned)) + } else { + None + }; + match unsafe { (*ptr).inner.evaluate(&bindings_map) } { + Ok(value) => { + unsafe { *out = value }; + 0 + } + Err(_) => -3, + } +} + +/// Simplifies both parts (free with `symbolic_complex_free`). Returns NULL +/// on NULL input or simplification failure. +#[unsafe(no_mangle)] +pub extern "C" fn symbolic_complex_simplify(ptr: *const CSymbolicComplex) -> *mut CSymbolicComplex { + if ptr.is_null() { + return std::ptr::null_mut(); + } + match unsafe { (*ptr).inner.simplify() } { + Ok(inner) => box_complex(inner), + Err(_) => std::ptr::null_mut(), + } +} + +/// Replaces every occurrence of `symbol` in both parts with `replacement` +/// (free with `symbolic_complex_free`). Returns NULL on NULL input or a +/// non-UTF-8 symbol name. +#[unsafe(no_mangle)] +pub extern "C" fn symbolic_complex_replace( + ptr: *const CSymbolicComplex, + symbol: *const c_char, + replacement: *const CParameter, +) -> *mut CSymbolicComplex { + if ptr.is_null() || symbol.is_null() || replacement.is_null() { + return std::ptr::null_mut(); + } + let symbol = match unsafe { CStr::from_ptr(symbol) }.to_str() { + Ok(name) => name, + Err(_) => return std::ptr::null_mut(), + }; + let value = unsafe { (*replacement).inner.clone() }; + box_complex(unsafe { (*ptr).inner.replace(symbol, value) }) +} + +/// Returns whether both parts are exactly zero: 1/0, or -1 on NULL. +#[unsafe(no_mangle)] +pub extern "C" fn symbolic_complex_is_zero_exact(ptr: *const CSymbolicComplex) -> i32 { + if ptr.is_null() { + return -1; + } + unsafe { (*ptr).inner.is_zero_exact() as i32 } +} + +/// Returns whether the real part is one and the imaginary part zero: +/// 1/0, or -1 on NULL. +#[unsafe(no_mangle)] +pub extern "C" fn symbolic_complex_is_one_exact(ptr: *const CSymbolicComplex) -> i32 { + if ptr.is_null() { + return -1; + } + unsafe { (*ptr).inner.is_one_exact() as i32 } +} + +/// Returns whether this value simplifies to exactly zero: 1 when it does, +/// 0 when not, -1 on NULL, -3 on simplification failure. +#[unsafe(no_mangle)] +pub extern "C" fn symbolic_complex_simplifies_to_zero(ptr: *const CSymbolicComplex) -> i32 { + if ptr.is_null() { + return -1; + } + match unsafe { (*ptr).inner.simplifies_to_zero() } { + Ok(value) => value as i32, + Err(_) => -3, + } +} + +// --------------------------------------------------------------------------- +// Matrix element access and construction +// --------------------------------------------------------------------------- + +/// Builds a `rows x cols` symbolic matrix from `rows * cols` element handles +/// given in row-major order (free with `symbolic_matrix_free`). Returns NULL +/// on NULL elements, a length/shape mismatch or a zero dimension. +#[unsafe(no_mangle)] +pub extern "C" fn symbolic_matrix_new( + rows: usize, + cols: usize, + elements: *const *const CSymbolicComplex, + len: usize, +) -> *mut CSymbolicMatrix { + if rows == 0 || cols == 0 || elements.is_null() || rows.checked_mul(cols) != Some(len) { + return std::ptr::null_mut(); + } + let handles = unsafe { std::slice::from_raw_parts(elements, len) }; + let mut values = Vec::with_capacity(len); + for handle in handles { + if handle.is_null() { + return std::ptr::null_mut(); + } + values.push(unsafe { (**handle).inner.clone() }); + } + match SymbolicMatrix::from_shape_vec((rows, cols), values) { + Ok(inner) => Box::into_raw(Box::new(CSymbolicMatrix { inner })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Returns a clone of the `[row, col]` element (free with +/// `symbolic_complex_free`); NULL on NULL input or out-of-bounds indices. +#[unsafe(no_mangle)] +pub extern "C" fn symbolic_matrix_element( + ptr: *const CSymbolicMatrix, + row: usize, + col: usize, +) -> *mut CSymbolicComplex { + if ptr.is_null() { + return std::ptr::null_mut(); + } + match unsafe { (*ptr).inner.get([row, col]) } { + Some(value) => box_complex(value.clone()), + None => std::ptr::null_mut(), + } +} + +// --------------------------------------------------------------------------- +// In-place gate application +// --------------------------------------------------------------------------- + +/// Reads the target-bit array. `Err(())` marks a NULL/empty buffer, a bit +/// outside the matrix dimension (rows must be a power of two) or duplicate +/// bits. +fn read_target_bits(bits: *const usize, len: usize, dim: usize) -> Result, ()> { + if len == 0 || bits.is_null() || !dim.is_power_of_two() { + return Err(()); + } + let slice = unsafe { std::slice::from_raw_parts(bits, len) }; + for (pos, &bit) in slice.iter().enumerate() { + if bit >= dim.trailing_zeros() as usize || slice[..pos].contains(&bit) { + return Err(()); + } + } + Ok(slice.to_vec()) +} + +/// Builds a square `gate_dim x gate_dim` numeric matrix from `gate_dim * +/// gate_dim` row-major `Complex64` values. Returns None on NULL input. +fn numeric_gate_from_raw(gate: *const Complex64, gate_dim: usize) -> Option> { + if gate.is_null() || gate_dim == 0 { + return None; + } + let count = gate_dim.checked_mul(gate_dim)?; + let values = unsafe { std::slice::from_raw_parts(gate, count) }.to_vec(); + Array2::from_shape_vec((gate_dim, gate_dim), values).ok() +} + +/// Applies `1 << bits_len` as a checked power of two; 0 marks an overflow. +fn gate_dim_for(bits_len: usize) -> usize { + (1usize).checked_shl(bits_len as u32).unwrap_or(0) +} + +/// Applies a named standard gate (same name set as `circuit_multi_control`) +/// with symbolic parameters to the target bits, mutating the matrix in +/// place. Returns 0 on success, -1 on NULL input, -3 on a gate/matrix +/// mismatch, -4 on a non-UTF-8 gate name, -8 on an unknown gate name or +/// invalid bits. +#[unsafe(no_mangle)] +pub extern "C" fn symbolic_matrix_apply_standard_gate( + ptr: *mut CSymbolicMatrix, + gate_name: *const c_char, + bits: *const usize, + bits_len: usize, + params: *const *const CParameter, + params_len: usize, +) -> i32 { + if ptr.is_null() || gate_name.is_null() { + return -1; + } + let name = match unsafe { CStr::from_ptr(gate_name) }.to_str() { + Ok(name) => name, + Err(_) => return -4, + }; + let gate = match standard_gate_from_name(name) { + Some(gate) => gate, + None => return -8, + }; + let bits = match read_target_bits(bits, bits_len, unsafe { (*ptr).inner.nrows() }) { + Ok(bits) => bits, + Err(()) => return -8, + }; + let mut values: Vec = Vec::with_capacity(params_len); + if params_len > 0 { + if params.is_null() { + return -1; + } + let handles = unsafe { std::slice::from_raw_parts(params, params_len) }; + for handle in handles { + if handle.is_null() { + return -1; + } + values.push(unsafe { (**handle).inner.clone() }); + } + } + match core_apply_standard_gate_to_matrix(unsafe { &mut (*ptr).inner }, gate, &bits, &values) { + Ok(()) => 0, + Err(_) => -3, + } +} + +/// Applies a symbolic gate matrix (dimension `1 << bits_len`) to the target +/// bits, mutating the matrix in place. Returns 0 on success, -1 on NULL, +/// -3 on a gate/matrix mismatch, -8 on invalid bits. +#[unsafe(no_mangle)] +pub extern "C" fn symbolic_matrix_apply_gate( + ptr: *mut CSymbolicMatrix, + gate: *const CSymbolicMatrix, + bits: *const usize, + bits_len: usize, +) -> i32 { + if ptr.is_null() || gate.is_null() { + return -1; + } + let bits = match read_target_bits(bits, bits_len, unsafe { (*ptr).inner.nrows() }) { + Ok(bits) => bits, + Err(()) => return -8, + }; + match core_apply_gate_to_matrix( + unsafe { &mut (*ptr).inner }, + unsafe { &(*gate).inner }, + &bits, + ) { + Ok(()) => 0, + Err(_) => -3, + } +} + +/// Numeric variant of `symbolic_matrix_apply_gate`: `gate` points at +/// `gate_dim * gate_dim` row-major `Complex64` values and `gate_dim` must +/// equal `1 << bits_len`. +#[unsafe(no_mangle)] +pub extern "C" fn symbolic_matrix_apply_gate_num( + ptr: *mut CSymbolicMatrix, + gate: *const Complex64, + gate_dim: usize, + bits: *const usize, + bits_len: usize, +) -> i32 { + if ptr.is_null() { + return -1; + } + let bits = match read_target_bits(bits, bits_len, unsafe { (*ptr).inner.nrows() }) { + Ok(bits) => bits, + Err(()) => return -8, + }; + if gate_dim == 0 || gate_dim != gate_dim_for(bits_len) { + return -8; + } + let matrix = match numeric_gate_from_raw(gate, gate_dim) { + Some(matrix) => matrix, + None => return -1, + }; + match core_apply_gate_to_matrix_num(unsafe { &mut (*ptr).inner }, &matrix, &bits) { + Ok(()) => 0, + Err(_) => -3, + } +} + +/// Applies a 2x2 symbolic gate to the target bit in place. Returns 0 on +/// success, -1 on NULL, -8 when the gate is not 2x2 or the bit is invalid. +#[unsafe(no_mangle)] +pub extern "C" fn symbolic_matrix_apply_single_qubit_gate( + ptr: *mut CSymbolicMatrix, + gate: *const CSymbolicMatrix, + bit: usize, +) -> i32 { + if ptr.is_null() || gate.is_null() { + return -1; + } + let inner = unsafe { &mut (*ptr).inner }; + let target = unsafe { &(*gate).inner }; + if target.nrows() != 2 + || target.ncols() != 2 + || !inner.nrows().is_power_of_two() + || bit >= inner.nrows().trailing_zeros() as usize + { + return -8; + } + core_apply_single_qubit_gate(inner, target, bit); + 0 +} + +/// Numeric variant of `symbolic_matrix_apply_single_qubit_gate`: `gate` +/// points at 4 row-major `Complex64` values. +#[unsafe(no_mangle)] +pub extern "C" fn symbolic_matrix_apply_single_qubit_gate_num( + ptr: *mut CSymbolicMatrix, + gate: *const Complex64, + bit: usize, +) -> i32 { + if ptr.is_null() || gate.is_null() { + return -1; + } + let inner = unsafe { &mut (*ptr).inner }; + if !inner.nrows().is_power_of_two() || bit >= inner.nrows().trailing_zeros() as usize { + return -8; + } + let values = unsafe { std::slice::from_raw_parts(gate, 4) }.to_vec(); + let target = match Array2::from_shape_vec((2, 2), values) { + Ok(target) => target, + Err(_) => return -3, + }; + core_apply_single_qubit_gate_num(inner, &target, bit); + 0 +} + +/// Applies a 4x4 symbolic gate to the two target bits in place. Returns 0 +/// on success, -1 on NULL, -8 when the gate is not 4x4 or the bits are +/// invalid/duplicated. +#[unsafe(no_mangle)] +pub extern "C" fn symbolic_matrix_apply_two_qubit_gate( + ptr: *mut CSymbolicMatrix, + gate: *const CSymbolicMatrix, + b0: usize, + b1: usize, +) -> i32 { + if ptr.is_null() || gate.is_null() { + return -1; + } + let inner = unsafe { &mut (*ptr).inner }; + let target = unsafe { &(*gate).inner }; + if target.nrows() != 4 || target.ncols() != 4 || !inner.nrows().is_power_of_two() { + return -8; + } + let limit = inner.nrows().trailing_zeros() as usize; + if b0 >= limit || b1 >= limit || b0 == b1 { + return -8; + } + core_apply_two_qubit_gate(inner, target, b0, b1); + 0 +} + +/// Numeric variant of `symbolic_matrix_apply_two_qubit_gate`: `gate` points +/// at 16 row-major `Complex64` values. +#[unsafe(no_mangle)] +pub extern "C" fn symbolic_matrix_apply_two_qubit_gate_num( + ptr: *mut CSymbolicMatrix, + gate: *const Complex64, + b0: usize, + b1: usize, +) -> i32 { + if ptr.is_null() || gate.is_null() { + return -1; + } + let inner = unsafe { &mut (*ptr).inner }; + if !inner.nrows().is_power_of_two() { + return -8; + } + let limit = inner.nrows().trailing_zeros() as usize; + if b0 >= limit || b1 >= limit || b0 == b1 { + return -8; + } + let values = unsafe { std::slice::from_raw_parts(gate, 16) }.to_vec(); + let target = match Array2::from_shape_vec((4, 4), values) { + Ok(target) => target, + Err(_) => return -3, + }; + core_apply_two_qubit_gate_num(inner, &target, b0, b1); + 0 +} + +/// Applies a symbolic gate of arbitrary arity (dimension `1 << bits_len`) +/// to the target bits in place. Returns 0 on success, -1 on NULL, -8 when +/// the gate shape or bits are invalid. +#[unsafe(no_mangle)] +pub extern "C" fn symbolic_matrix_apply_general_gate( + ptr: *mut CSymbolicMatrix, + gate: *const CSymbolicMatrix, + bits: *const usize, + bits_len: usize, +) -> i32 { + if ptr.is_null() || gate.is_null() { + return -1; + } + let inner = unsafe { &mut (*ptr).inner }; + let target = unsafe { &(*gate).inner }; + let bits = match read_target_bits(bits, bits_len, inner.nrows()) { + Ok(bits) => bits, + Err(()) => return -8, + }; + let gate_dim = gate_dim_for(bits_len); + if gate_dim == 0 + || !target.nrows().is_power_of_two() + || target.nrows() != target.ncols() + || target.nrows() != gate_dim + { + return -8; + } + core_apply_general_gate(inner, target, &bits); + 0 +} + +/// Numeric variant of `symbolic_matrix_apply_general_gate`: `gate` points at +/// `1 << bits_len` squared row-major `Complex64` values. +#[unsafe(no_mangle)] +pub extern "C" fn symbolic_matrix_apply_general_gate_num( + ptr: *mut CSymbolicMatrix, + gate: *const Complex64, + bits: *const usize, + bits_len: usize, +) -> i32 { + if ptr.is_null() { + return -1; + } + let inner = unsafe { &mut (*ptr).inner }; + let bits = match read_target_bits(bits, bits_len, inner.nrows()) { + Ok(bits) => bits, + Err(()) => return -8, + }; + let gate_dim = gate_dim_for(bits_len); + if gate_dim == 0 { + return -8; + } + let target = match numeric_gate_from_raw(gate, gate_dim) { + Some(target) => target, + None => return -1, + }; + core_apply_general_gate_num(inner, &target, &bits); + 0 +} + +/// Applies a symbolic permutation gate in place: `permutation[i] = +/// (indices[i], values[i])` means output local row `i` is `values[i]` times +/// input row `indices[i]`; `len` must equal `1 << bits_len` and indices must +/// be within the gate dimension. Returns 0 on success, -1 on NULL, -8 on a +/// length/shape mismatch or invalid bits. +#[unsafe(no_mangle)] +pub extern "C" fn symbolic_matrix_apply_permutation_gate( + ptr: *mut CSymbolicMatrix, + indices: *const usize, + values: *const *const CSymbolicComplex, + len: usize, + bits: *const usize, + bits_len: usize, +) -> i32 { + if ptr.is_null() { + return -1; + } + let bits = match read_target_bits(bits, bits_len, unsafe { (*ptr).inner.nrows() }) { + Ok(bits) => bits, + Err(()) => return -8, + }; + let gate_dim = gate_dim_for(bits_len); + if gate_dim == 0 || len != gate_dim || indices.is_null() || values.is_null() { + return -8; + } + let idx = unsafe { std::slice::from_raw_parts(indices, len) }; + let handles = unsafe { std::slice::from_raw_parts(values, len) }; + let mut permutation: Vec<(usize, SymbolicComplex)> = Vec::with_capacity(len); + for (source, handle) in idx.iter().zip(handles.iter()) { + if *source >= gate_dim || handle.is_null() { + return -1; + } + permutation.push((*source, unsafe { (**handle).inner.clone() })); + } + core_apply_symbolic_permutation_gate(unsafe { &mut (*ptr).inner }, &permutation, &bits); + 0 +} + +/// Numeric variant of `symbolic_matrix_apply_permutation_gate`: `values` +/// points at `len` raw `Complex64` factors. +#[unsafe(no_mangle)] +pub extern "C" fn symbolic_matrix_apply_permutation_gate_num( + ptr: *mut CSymbolicMatrix, + indices: *const usize, + values: *const Complex64, + len: usize, + bits: *const usize, + bits_len: usize, +) -> i32 { + if ptr.is_null() || values.is_null() { + return -1; + } + let bits = match read_target_bits(bits, bits_len, unsafe { (*ptr).inner.nrows() }) { + Ok(bits) => bits, + Err(()) => return -8, + }; + let gate_dim = gate_dim_for(bits_len); + if gate_dim == 0 || len != gate_dim || indices.is_null() { + return -8; + } + let idx = unsafe { std::slice::from_raw_parts(indices, len) }; + let vals = unsafe { std::slice::from_raw_parts(values, len) }; + let permutation: Vec<(usize, Complex64)> = idx + .iter() + .zip(vals.iter()) + .map(|(source, value)| (*source, *value)) + .collect(); + for (source, _) in &permutation { + if *source >= gate_dim { + return -8; + } + } + core_apply_numeric_permutation_gate(unsafe { &mut (*ptr).inner }, &permutation, &bits); + 0 +} + +/// Applies a symbolic diagonal gate in place: `diagonal[i]` scales local row +/// `i`; `len` must equal `1 << bits_len`. Returns 0 on success, -1 on +/// NULL, -8 on a length/shape mismatch or invalid bits. +#[unsafe(no_mangle)] +pub extern "C" fn symbolic_matrix_apply_diagonal_gate( + ptr: *mut CSymbolicMatrix, + diagonal: *const *const CSymbolicComplex, + len: usize, + bits: *const usize, + bits_len: usize, +) -> i32 { + if ptr.is_null() { + return -1; + } + let bits = match read_target_bits(bits, bits_len, unsafe { (*ptr).inner.nrows() }) { + Ok(bits) => bits, + Err(()) => return -8, + }; + let gate_dim = gate_dim_for(bits_len); + if gate_dim == 0 || len != gate_dim || diagonal.is_null() { + return -8; + } + let handles = unsafe { std::slice::from_raw_parts(diagonal, len) }; + let mut values: Vec = Vec::with_capacity(len); + for handle in handles { + if handle.is_null() { + return -1; + } + values.push(unsafe { (**handle).inner.clone() }); + } + core_apply_symbolic_diagonal_gate(unsafe { &mut (*ptr).inner }, &values, &bits); + 0 +} + +/// Numeric variant of `symbolic_matrix_apply_diagonal_gate`: `diagonal` +/// points at `len` raw `Complex64` values. +#[unsafe(no_mangle)] +pub extern "C" fn symbolic_matrix_apply_diagonal_gate_num( + ptr: *mut CSymbolicMatrix, + diagonal: *const Complex64, + len: usize, + bits: *const usize, + bits_len: usize, +) -> i32 { + if ptr.is_null() || diagonal.is_null() { + return -1; + } + let bits = match read_target_bits(bits, bits_len, unsafe { (*ptr).inner.nrows() }) { + Ok(bits) => bits, + Err(()) => return -8, + }; + let gate_dim = gate_dim_for(bits_len); + if gate_dim == 0 || len != gate_dim { + return -8; + } + let values = unsafe { std::slice::from_raw_parts(diagonal, len) }; + core_apply_numeric_diagonal_gate(unsafe { &mut (*ptr).inner }, values, &bits); + 0 +} + +/// Reads an optional qubit-order array. `Err(())` marks a NULL buffer with a +/// non-zero length. +fn read_order(order: *const u32, order_len: usize) -> Result>, ()> { + if order_len == 0 { + return Ok(None); + } + if order.is_null() { + return Err(()); + } + let slice = unsafe { std::slice::from_raw_parts(order, order_len) }; + Ok(Some(slice.iter().map(|&q| q as usize).collect())) +} diff --git a/crates/binding-c/src/compile/commutation.rs b/crates/binding-c/src/compile/commutation.rs new file mode 100644 index 00000000..edec3f63 --- /dev/null +++ b/crates/binding-c/src/compile/commutation.rs @@ -0,0 +1,371 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! C ABI for gate-commutation proofs: shared builtin and algebraic-only +//! checks plus configurable reusable checkers. +//! +//! Gates are described by a standard-gate name (as accepted elsewhere by the +//! C ABI, e.g. `"H"`, `"RZ"`, `"CX"`), a qubit-id array, and a numeric +//! parameter array. Proof results are conservative: tag 0 only means the +//! configured proof sources could not establish commutation. + +use crate::compile::knowledge::CKnowledgeLibrary; +use crate::device::standard_gate_from_name; +use crate::error::CqlibError; +use cqlib_core::circuit::{Instruction, Parameter, Qubit}; +use cqlib_core::compile::commutation::{ + Commutation, CommutationChecker, CommutationConfig, CommutationResult, algebraic_commutation, + check_commutation, +}; +use std::ffi::CStr; +use std::os::raw::c_char; + +/// Commutation proof tag. +/// | Value | Proof | +/// |-------|------------------------------------------------| +/// | 0 | Not provable with the configured proof sources | +/// | 1 | Exact commutation | +/// | 2 | Commutation up to a global phase | +pub const COMMUTATION_RESULT_UNPROVEN: i32 = 0; +pub const COMMUTATION_RESULT_EXACT: i32 = 1; +pub const COMMUTATION_RESULT_UP_TO_GLOBAL_PHASE: i32 = 2; + +/// C form of [`CommutationConfig`]. +#[repr(C)] +pub struct CCommutationConfig { + /// 1 enables matching explicit knowledge-base commutation rules. + pub enable_rule_oracle: u8, + /// 1 enables small local matrix comparison as a fallback. + pub enable_matrix_fallback: u8, + /// Reserved padding to keep the struct layout stable. + pub _pad: [u8; 6], + /// Maximum union-support size for the matrix fallback. + pub max_matrix_qubits: usize, + /// Reserved padding to keep the struct layout stable. + pub _reserved: [u64; 2], +} + +/// Opaque handle around a [`CommutationChecker`]. +pub struct CCommutationChecker { + pub inner: CommutationChecker, +} + +// ===== Internal helpers ===== + +/// A parsed gate application (instruction, qubits, numeric parameters). +struct GateApplication { + instruction: Instruction, + qubits: Vec, + params: Vec, +} + +/// Parses one gate application from its C arguments. +/// +/// Returns -1 for NULL input, -4 for invalid UTF-8, and -8 for an unknown +/// standard-gate name. +fn parse_gate_application( + gate_name: *const c_char, + qubits: *const u32, + qubits_len: usize, + params: *const f64, + params_len: usize, +) -> Result { + if gate_name.is_null() { + return Err(CqlibError::NullPtr as i32); + } + let name = match unsafe { CStr::from_ptr(gate_name) }.to_str() { + Ok(name) => name, + Err(_) => return Err(CqlibError::ParseError as i32), + }; + let gate = match standard_gate_from_name(name) { + Some(gate) => gate, + None => return Err(CqlibError::InvalidParam as i32), + }; + let mut qubit_list = Vec::with_capacity(qubits_len); + if qubits_len > 0 { + if qubits.is_null() { + return Err(CqlibError::NullPtr as i32); + } + for &id in unsafe { std::slice::from_raw_parts(qubits, qubits_len) } { + qubit_list.push(Qubit::new(id)); + } + } + let mut param_list = Vec::with_capacity(params_len); + if params_len > 0 { + if params.is_null() { + return Err(CqlibError::NullPtr as i32); + } + for &value in unsafe { std::slice::from_raw_parts(params, params_len) } { + param_list.push(Parameter::from(value)); + } + } + Ok(GateApplication { + instruction: Instruction::Standard(gate), + qubits: qubit_list, + params: param_list, + }) +} + +/// Writes `value` to `phase` when it is non-NULL. +fn write_phase(phase: *mut f64, value: f64) { + if !phase.is_null() { + unsafe { *phase = value }; + } +} + +/// Converts a commutation proof into its `COMMUTATION_RESULT_*` tag and, +/// when `phase` is non-NULL, writes the proof's global phase (0.0 for exact +/// proofs). A symbolic phase that cannot be evaluated numerically surfaces as +/// NaN. +fn commutation_to_tag(result: CommutationResult, phase: *mut f64) -> i32 { + match result { + None => COMMUTATION_RESULT_UNPROVEN, + Some(Commutation::Exact) => { + write_phase(phase, 0.0); + COMMUTATION_RESULT_EXACT + } + Some(Commutation::UpToGlobalPhase(parameter)) => { + let value = parameter.evaluate(&None).unwrap_or(f64::NAN); + write_phase(phase, value); + COMMUTATION_RESULT_UP_TO_GLOBAL_PHASE + } + } +} + +/// Converts a C commutation configuration into the core form. +fn commutation_config_from_c(raw: &CCommutationConfig) -> CommutationConfig { + CommutationConfig { + enable_rule_oracle: raw.enable_rule_oracle != 0, + enable_matrix_fallback: raw.enable_matrix_fallback != 0, + max_matrix_qubits: raw.max_matrix_qubits, + } +} + +// ===== One-shot checks ===== + +/// Checks whether two concrete gate applications commute using the shared +/// builtin checker (knowledge rules and matrix fallback enabled). +/// +/// Each side is described by a standard-gate name, a qubit-id array, and a +/// numeric parameter array. When the proof is `UpToGlobalPhase`, the phase is +/// written to `phase` (may be NULL). Returns one of +/// `COMMUTATION_RESULT_*`, -1 on NULL input, -4 on invalid UTF-8, and -8 on +/// an unknown gate name. +#[allow(clippy::too_many_arguments)] +#[unsafe(no_mangle)] +pub extern "C" fn commutation_check( + lhs_gate: *const c_char, + lhs_qubits: *const u32, + lhs_qubits_len: usize, + lhs_params: *const f64, + lhs_params_len: usize, + rhs_gate: *const c_char, + rhs_qubits: *const u32, + rhs_qubits_len: usize, + rhs_params: *const f64, + rhs_params_len: usize, + phase: *mut f64, +) -> i32 { + let lhs = match parse_gate_application( + lhs_gate, + lhs_qubits, + lhs_qubits_len, + lhs_params, + lhs_params_len, + ) { + Ok(lhs) => lhs, + Err(code) => return code, + }; + let rhs = match parse_gate_application( + rhs_gate, + rhs_qubits, + rhs_qubits_len, + rhs_params, + rhs_params_len, + ) { + Ok(rhs) => rhs, + Err(code) => return code, + }; + commutation_to_tag( + check_commutation( + &lhs.instruction, + &lhs.qubits, + &lhs.params, + &rhs.instruction, + &rhs.qubits, + &rhs.params, + ), + phase, + ) +} + +/// Checks commutation using only the symbolic algebraic proof sources +/// (structural facts, diagonal/axis algebra, controlled-axis and symmetric +/// two-qubit families). +/// +/// Arguments and return values match [`commutation_check`]; tag 0 keeps the +/// result conservative so rule or matrix fallbacks can still run. +#[allow(clippy::too_many_arguments)] +#[unsafe(no_mangle)] +pub extern "C" fn commutation_check_algebraic( + lhs_gate: *const c_char, + lhs_qubits: *const u32, + lhs_qubits_len: usize, + lhs_params: *const f64, + lhs_params_len: usize, + rhs_gate: *const c_char, + rhs_qubits: *const u32, + rhs_qubits_len: usize, + rhs_params: *const f64, + rhs_params_len: usize, + phase: *mut f64, +) -> i32 { + let lhs = match parse_gate_application( + lhs_gate, + lhs_qubits, + lhs_qubits_len, + lhs_params, + lhs_params_len, + ) { + Ok(lhs) => lhs, + Err(code) => return code, + }; + let rhs = match parse_gate_application( + rhs_gate, + rhs_qubits, + rhs_qubits_len, + rhs_params, + rhs_params_len, + ) { + Ok(rhs) => rhs, + Err(code) => return code, + }; + commutation_to_tag( + algebraic_commutation( + &lhs.instruction, + &lhs.qubits, + &lhs.params, + &rhs.instruction, + &rhs.qubits, + &rhs.params, + ), + phase, + ) +} + +// ===== Reusable checker handles ===== + +/// Returns the default commutation configuration by value. +#[unsafe(no_mangle)] +pub extern "C" fn commutation_config_default() -> CCommutationConfig { + let config = CommutationConfig::default(); + CCommutationConfig { + enable_rule_oracle: config.enable_rule_oracle as u8, + enable_matrix_fallback: config.enable_matrix_fallback as u8, + _pad: [0; 6], + max_matrix_qubits: config.max_matrix_qubits, + _reserved: [0; 2], + } +} + +/// Creates a reusable checker with builtin commutation rules and an explicit +/// configuration. Free with `commutation_checker_free`. +#[unsafe(no_mangle)] +pub extern "C" fn commutation_checker_new(config: CCommutationConfig) -> *mut CCommutationChecker { + let inner = CommutationChecker::with_config(commutation_config_from_c(&config)); + Box::into_raw(Box::new(CCommutationChecker { inner })) +} + +/// Creates a reusable checker that draws commutation rules from an already +/// loaded knowledge library. +/// +/// Returns a heap-allocated `CCommutationChecker*` (free with +/// `commutation_checker_free`), or NULL on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn commutation_checker_from_library( + library: *const CKnowledgeLibrary, + config: CCommutationConfig, +) -> *mut CCommutationChecker { + if library.is_null() { + return std::ptr::null_mut(); + } + let library = unsafe { &(*library).inner }; + let inner = CommutationChecker::from_library(library, commutation_config_from_c(&config)); + Box::into_raw(Box::new(CCommutationChecker { inner })) +} + +/// Frees a commutation checker. NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn commutation_checker_free(ptr: *mut CCommutationChecker) { + if !ptr.is_null() { + unsafe { + let _ = Box::from_raw(ptr); + } + } +} + +/// Checks whether two concrete gate applications commute using this checker. +/// +/// Arguments and return values match [`commutation_check`]; the checker's +/// configuration selects which proof sources participate. +#[allow(clippy::too_many_arguments)] +#[unsafe(no_mangle)] +pub extern "C" fn commutation_checker_check( + ptr: *const CCommutationChecker, + lhs_gate: *const c_char, + lhs_qubits: *const u32, + lhs_qubits_len: usize, + lhs_params: *const f64, + lhs_params_len: usize, + rhs_gate: *const c_char, + rhs_qubits: *const u32, + rhs_qubits_len: usize, + rhs_params: *const f64, + rhs_params_len: usize, + phase: *mut f64, +) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + let lhs = match parse_gate_application( + lhs_gate, + lhs_qubits, + lhs_qubits_len, + lhs_params, + lhs_params_len, + ) { + Ok(lhs) => lhs, + Err(code) => return code, + }; + let rhs = match parse_gate_application( + rhs_gate, + rhs_qubits, + rhs_qubits_len, + rhs_params, + rhs_params_len, + ) { + Ok(rhs) => rhs, + Err(code) => return code, + }; + let checker = unsafe { &(*ptr).inner }; + commutation_to_tag( + checker.check( + &lhs.instruction, + &lhs.qubits, + &lhs.params, + &rhs.instruction, + &rhs.qubits, + &rhs.params, + ), + phase, + ) +} diff --git a/crates/binding-c/src/compile/decompose.rs b/crates/binding-c/src/compile/decompose.rs new file mode 100644 index 00000000..143acea2 --- /dev/null +++ b/crates/binding-c/src/compile/decompose.rs @@ -0,0 +1,1145 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! C ABI for gate decomposition, numeric unitary synthesis, two-qubit block +//! resynthesis, and target-basis / device lowering. + +use crate::circuit::CCircuit; +use crate::compile::resource::CResourcePolicy; +use crate::compile::transform::{standard_instructions_from_names, write_name_list}; +use crate::device::{CDevice, standard_gate_from_name}; +use crate::error::CqlibError; +use cqlib_core::circuit::{Instruction, ParameterValue, Qubit, StandardGate, ValueOperation}; +use cqlib_core::compile::commutation::CommutationConfig; +use cqlib_core::compile::resource::{ResourceLimits, ResourcePolicy}; +use cqlib_core::compile::transform::decompose::mc_gate::decompose_mc_gates_for_device as core_decompose_mc_gates_for_device; +use cqlib_core::compile::transform::decompose::unitary::{ + KakDecomposition, TwoQubitSynthesisTarget, TwoQubitUnitaryDecomposeBasis, + TwoQubitUnitarySynthesisResult, kak_decompose as core_kak_decompose, + synthesize_numeric_1q_unitary as core_synthesize_numeric_1q_unitary, + synthesize_numeric_2q_unitary as core_synthesize_numeric_2q_unitary, +}; +use cqlib_core::compile::transform::decompose::{ + DecompositionRuleStats, McGateDecomposeConfig, UnitaryDecomposeConfig, + decompose_mc_gates_with_rule_stats as core_decompose_mc_gates_with_rule_stats, + decompose_unitaries_with_rule_stats as core_decompose_unitaries_with_rule_stats, + expand_definitions as core_expand_definitions, +}; +use cqlib_core::compile::transform::resynthesis::{ + TwoQubitBlockResynthesisConfig, resynthesize_two_qubit_blocks as core_resynthesize_blocks, +}; +use cqlib_core::compile::transform::target_basis::TargetBasisLowerer; +use cqlib_core::compile::transform::target_basis::{TargetBasisCost, TargetBasisCostModel}; +use cqlib_core::compile::transform::{DeviceLowerer, Transformer}; +use ndarray::Array2; +use num_complex::Complex64; +use std::ffi::{CStr, CString}; +use std::os::raw::c_char; + +/// Two-qubit synthesis basis tag. +/// | Value | Basis | +/// |-------|------------------------------------------| +/// | 0 | Local `U` + `RXX`/`RYY`/`RZZ` | +/// | 1 | Local `U` + `CX` templates | +/// | 2 | Local `U` + `CY` templates | +/// | 3 | Local `U` + `CZ` templates | +/// | 4 | Local `U` + `RZZ` | +pub const TWO_QUBIT_BASIS_PAULI_ROTATIONS: u8 = 0; +pub const TWO_QUBIT_BASIS_CX: u8 = 1; +pub const TWO_QUBIT_BASIS_CY: u8 = 2; +pub const TWO_QUBIT_BASIS_CZ: u8 = 3; +pub const TWO_QUBIT_BASIS_RZZ: u8 = 4; + +/// KAK local-factor tag. +/// | Value | Factor | +/// |-------|-------------------------------------------| +/// | 0 | `k1l` (left, after the interaction) | +/// | 1 | `k1r` (right, after the interaction) | +/// | 2 | `k2l` (left, before the interaction) | +/// | 3 | `k2r` (right, before the interaction) | +pub const KAK_FACTOR_K1L: u8 = 0; +pub const KAK_FACTOR_K1R: u8 = 1; +pub const KAK_FACTOR_K2L: u8 = 2; +pub const KAK_FACTOR_K2R: u8 = 3; + +/// C snapshot of pass-local decomposition-rule cache statistics. +#[repr(C)] +pub struct CDecompositionRuleStats { + /// Cache hits during the run. + pub hits: usize, + /// Cache misses during the run. + pub misses: usize, + /// Cache writes during the run. + pub inserts: usize, +} + +/// C form of [`UnitaryDecomposeConfig`]. +/// +/// The two-qubit synthesis target is fixed to the unconstrained default +/// (neutral Pauli-rotation fallback); building a constrained target needs an +/// `Instruction` list, which the C ABI does not parse. +#[repr(C)] +pub struct CUnitaryDecomposeConfig { + /// 1 recurses into control-flow bodies, 0 does not. + pub recurse_control_flow: u8, + /// Reserved padding to keep the struct layout stable. + pub _reserved: [u8; 7], +} + +/// C form of [`McGateDecomposeConfig`]. +#[repr(C)] +pub struct CMcGateDecomposeConfig { + /// Total clean logical ancillas the pass may create before layout. + pub max_pre_layout_clean_ancillas: usize, + /// 1 allows borrowing input qubits under the dirty contract. + pub allow_dirty_borrowing: u8, + /// 1 enforces `max_total_qubits`, 0 leaves the limit unset. + pub has_max_total_qubits: u8, + /// Reserved padding to keep the struct layout stable. + pub _pad: [u8; 6], + /// Hard limit on total logical qubits (used when `has_max_total_qubits != 0`). + pub max_total_qubits: usize, + /// Reserved padding to keep the struct layout stable. + pub _reserved: [u64; 2], +} + +/// C form of [`TwoQubitBlockResynthesisConfig`]. +/// +/// The two-qubit synthesis target is fixed to the unconstrained default; the +/// embedded commutation-engine configuration is exposed as scalar fields. +#[repr(C)] +pub struct CResynthesisConfig { + /// Maximum source operations in one bounded candidate block. + pub max_block_ops: usize, + /// Maximum non-block operations crossed while collecting a block. + pub max_crossed_ops: usize, + /// Collection budget per side of a two-qubit anchor. + pub max_scan_span: usize, + /// 1 treats labeled operations as hard boundaries. + pub skip_labeled_ops: u8, + /// 1 recurses into structured classical-control bodies. + pub recurse_control_flow: u8, + /// 1 enables the knowledge-rule commutation oracle. + pub enable_rule_oracle: u8, + /// 1 enables the local matrix fallback in the commutation engine. + pub enable_matrix_fallback: u8, + /// Reserved padding to keep the struct layout stable. + pub _pad: [u8; 4], + /// Maximum union-support size for the matrix fallback. + pub max_matrix_qubits: usize, + /// Reserved padding to keep the struct layout stable. + pub _reserved: [u64; 2], +} + +/// C snapshot of a one-qubit numeric synthesis result. The represented +/// matrix is `exp(i * global_phase) * U(theta, phi, lambda)`. +#[repr(C)] +pub struct COneQubitUnitaryDecomposition { + /// Polar rotation angle. + pub theta: f64, + /// First azimuthal angle. + pub phi: f64, + /// Second azimuthal angle. + pub lambda: f64, + /// Scalar phase multiplying the synthesized gate. + pub global_phase: f64, +} + +/// Opaque handle around a [`TwoQubitUnitarySynthesisResult`]. +pub struct CTwoQubitUnitarySynthesis { + pub inner: TwoQubitUnitarySynthesisResult, +} + +/// Opaque handle around a [`KakDecomposition`]. +pub struct CKakDecomposition { + pub inner: KakDecomposition, +} + +/// Opaque handle around a [`TargetBasisLowerer`]. +pub struct CTargetBasisLowerer { + pub inner: TargetBasisLowerer, +} + +/// Returns the default unitary-decompose configuration by value. +#[unsafe(no_mangle)] +pub extern "C" fn decompose_unitary_config_default() -> CUnitaryDecomposeConfig { + CUnitaryDecomposeConfig { + recurse_control_flow: 1, + _reserved: [0; 7], + } +} + +/// Returns the default MC-gate decompose configuration by value. +#[unsafe(no_mangle)] +pub extern "C" fn mc_gate_config_default() -> CMcGateDecomposeConfig { + CMcGateDecomposeConfig { + max_pre_layout_clean_ancillas: 0, + allow_dirty_borrowing: 0, + has_max_total_qubits: 0, + _pad: [0; 6], + max_total_qubits: 0, + _reserved: [0; 2], + } +} + +/// Returns the normal-budget resynthesis configuration by value. +#[unsafe(no_mangle)] +pub extern "C" fn resynthesis_config_normal() -> CResynthesisConfig { + resynthesis_config_from_core(TwoQubitBlockResynthesisConfig::normal(Default::default())) +} + +/// Returns the enhanced-budget resynthesis configuration by value. +#[unsafe(no_mangle)] +pub extern "C" fn resynthesis_config_enhanced() -> CResynthesisConfig { + resynthesis_config_from_core(TwoQubitBlockResynthesisConfig::enhanced(Default::default())) +} + +/// Converts a core resynthesis configuration into the C snapshot form. +fn resynthesis_config_from_core(config: TwoQubitBlockResynthesisConfig) -> CResynthesisConfig { + CResynthesisConfig { + max_block_ops: config.max_block_ops, + max_crossed_ops: config.max_crossed_ops, + max_scan_span: config.max_scan_span, + skip_labeled_ops: config.skip_labeled_ops as u8, + recurse_control_flow: config.recurse_control_flow as u8, + enable_rule_oracle: config.commutation.enable_rule_oracle as u8, + enable_matrix_fallback: config.commutation.enable_matrix_fallback as u8, + _pad: [0; 4], + max_matrix_qubits: config.commutation.max_matrix_qubits, + _reserved: [0; 2], + } +} + +/// Converts a C resynthesis configuration into the core form. +fn resynthesis_config_to_core(raw: &CResynthesisConfig) -> TwoQubitBlockResynthesisConfig { + TwoQubitBlockResynthesisConfig { + two_qubit_target: Default::default(), + max_block_ops: raw.max_block_ops, + max_crossed_ops: raw.max_crossed_ops, + max_scan_span: raw.max_scan_span, + skip_labeled_ops: raw.skip_labeled_ops != 0, + recurse_control_flow: raw.recurse_control_flow != 0, + commutation: CommutationConfig { + enable_rule_oracle: raw.enable_rule_oracle != 0, + enable_matrix_fallback: raw.enable_matrix_fallback != 0, + max_matrix_qubits: raw.max_matrix_qubits, + }, + } +} + +/// Converts a C MC-gate decompose configuration into the core form. +fn mc_gate_config_to_core(raw: &CMcGateDecomposeConfig) -> McGateDecomposeConfig { + McGateDecomposeConfig { + resource_policy: ResourcePolicy { + max_pre_layout_clean_ancillas: raw.max_pre_layout_clean_ancillas, + allow_dirty_borrowing: raw.allow_dirty_borrowing != 0, + }, + resource_limits: ResourceLimits { + max_total_qubits: if raw.has_max_total_qubits != 0 { + Some(raw.max_total_qubits) + } else { + None + }, + }, + } +} + +// ===== Circuit-level decomposition passes ===== + +/// Expands circuit-gate definitions of `circuit` and returns the rebuilt +/// circuit as an owned `CCircuit*` (free with `circuit_free`). +/// +/// Returns NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn decompose_expand_definitions(circuit: *const CCircuit) -> *mut CCircuit { + if circuit.is_null() { + return std::ptr::null_mut(); + } + let circuit = unsafe { &(*circuit).inner }; + match core_expand_definitions(circuit) { + Ok(inner) => Box::into_raw(Box::new(CCircuit { inner })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Synthesizes matrix-backed unitary gates of `circuit` and returns the +/// rebuilt circuit as an owned `CCircuit*` (free with `circuit_free`). +/// +/// Returns NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn decompose_unitaries( + circuit: *const CCircuit, + config: CUnitaryDecomposeConfig, +) -> *mut CCircuit { + decompose_unitaries_with_rule_stats(circuit, config, std::ptr::null_mut()) +} + +/// Diagnostic form of [`decompose_unitaries`]: additionally writes the +/// pass-local rule-cache statistics to `stats` when it is non-NULL. +/// +/// Returns the rebuilt circuit as an owned `CCircuit*` (free with +/// `circuit_free`), or NULL on error (`stats` is then left untouched). +#[unsafe(no_mangle)] +pub extern "C" fn decompose_unitaries_with_rule_stats( + circuit: *const CCircuit, + config: CUnitaryDecomposeConfig, + stats: *mut CDecompositionRuleStats, +) -> *mut CCircuit { + if circuit.is_null() { + return std::ptr::null_mut(); + } + let config = UnitaryDecomposeConfig { + two_qubit_target: Default::default(), + recurse_control_flow: config.recurse_control_flow != 0, + }; + let circuit = unsafe { &(*circuit).inner }; + match core_decompose_unitaries_with_rule_stats(circuit, config) { + Ok((outcome, rule_stats)) => { + write_rule_stats(stats, &rule_stats); + Box::into_raw(Box::new(CCircuit { + inner: outcome.into_circuit(circuit), + })) + } + Err(_) => std::ptr::null_mut(), + } +} + +/// Rewrites multi-controlled gates of `circuit` and returns the rebuilt +/// circuit as an owned `CCircuit*` (free with `circuit_free`). +/// +/// Returns NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn decompose_mc_gates( + circuit: *const CCircuit, + config: CMcGateDecomposeConfig, +) -> *mut CCircuit { + decompose_mc_gates_with_rule_stats(circuit, config, std::ptr::null_mut()) +} + +/// Diagnostic form of [`decompose_mc_gates`]: additionally writes the +/// pass-local rule-cache statistics to `stats` when it is non-NULL. +/// +/// Returns the rebuilt circuit as an owned `CCircuit*` (free with +/// `circuit_free`), or NULL on error (`stats` is then left untouched). +#[unsafe(no_mangle)] +pub extern "C" fn decompose_mc_gates_with_rule_stats( + circuit: *const CCircuit, + config: CMcGateDecomposeConfig, + stats: *mut CDecompositionRuleStats, +) -> *mut CCircuit { + if circuit.is_null() { + return std::ptr::null_mut(); + } + let config = mc_gate_config_to_core(&config); + let circuit = unsafe { &(*circuit).inner }; + match core_decompose_mc_gates_with_rule_stats(circuit, config) { + Ok((outcome, rule_stats)) => { + write_rule_stats(stats, &rule_stats); + Box::into_raw(Box::new(CCircuit { + inner: outcome.into_circuit(circuit), + })) + } + Err(_) => std::ptr::null_mut(), + } +} + +/// Rewrites multi-controlled gates of `circuit` within the usable-qubit +/// capacity of `device` (a pre-layout logical transform). +/// +/// `policy` may be NULL to select the default resource policy. Returns the +/// rebuilt circuit as an owned `CCircuit*` (free with `circuit_free`), or +/// NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn decompose_mc_gates_for_device( + circuit: *const CCircuit, + device: *const CDevice, + policy: *const CResourcePolicy, +) -> *mut CCircuit { + if circuit.is_null() || device.is_null() { + return std::ptr::null_mut(); + } + let policy = if policy.is_null() { + ResourcePolicy::default() + } else { + let raw = unsafe { &*policy }; + ResourcePolicy { + max_pre_layout_clean_ancillas: raw.max_pre_layout_clean_ancillas, + allow_dirty_borrowing: raw.allow_dirty_borrowing != 0, + } + }; + let device = unsafe { &(*device).inner }; + let circuit = unsafe { &(*circuit).inner }; + match core_decompose_mc_gates_for_device(circuit, device, policy) { + Ok(outcome) => Box::into_raw(Box::new(CCircuit { + inner: outcome.into_circuit(circuit), + })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Writes a rule-stats snapshot to `stats` when it is non-NULL. +fn write_rule_stats(stats: *mut CDecompositionRuleStats, rule_stats: &DecompositionRuleStats) { + if !stats.is_null() { + unsafe { + *stats = CDecompositionRuleStats { + hits: rule_stats.hits, + misses: rule_stats.misses, + inserts: rule_stats.inserts, + }; + } + } +} + +// ===== Two-qubit block resynthesis ===== + +/// Resynthesizes two-qubit blocks of `circuit` and returns the rebuilt +/// circuit as an owned `CCircuit*` (free with `circuit_free`). +/// +/// Returns NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn resynthesize_two_qubit_blocks( + circuit: *const CCircuit, + config: CResynthesisConfig, +) -> *mut CCircuit { + if circuit.is_null() { + return std::ptr::null_mut(); + } + let config = resynthesis_config_to_core(&config); + let circuit = unsafe { &(*circuit).inner }; + match core_resynthesize_blocks(circuit, config) { + Ok(outcome) => Box::into_raw(Box::new(CCircuit { + inner: outcome.into_circuit(circuit), + })), + Err(_) => std::ptr::null_mut(), + } +} + +// ===== Numeric unitary synthesis primitives ===== + +/// Synthesizes a 2x2 unitary matrix into Cqlib's `U` convention. +/// +/// `matrix` points at 4 row-major `Complex64` elements. Returns 0 on +/// success, -1 on NULL arguments, -6 when the matrix is non-finite, +/// non-unitary, or otherwise rejected by the synthesizer. +#[unsafe(no_mangle)] +pub extern "C" fn synthesize_numeric_1q_unitary( + matrix: *const Complex64, + out: *mut COneQubitUnitaryDecomposition, +) -> i32 { + if matrix.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + let flat = unsafe { std::slice::from_raw_parts(matrix, 4) }.to_vec(); + let matrix = match Array2::from_shape_vec((2, 2), flat) { + Ok(matrix) => matrix, + Err(_) => return CqlibError::InvalidParam as i32, + }; + match core_synthesize_numeric_1q_unitary(&matrix) { + Ok(decomposition) => { + unsafe { + *out = COneQubitUnitaryDecomposition { + theta: decomposition.theta, + phi: decomposition.phi, + lambda: decomposition.lambda, + global_phase: decomposition.global_phase, + }; + } + CqlibError::Ok as i32 + } + Err(_) => CqlibError::CompilerError as i32, + } +} + +/// Synthesizes a 4x4 unitary matrix into standard-gate operations. +/// +/// `matrix` points at 16 row-major `Complex64` elements; `first` and +/// `second` are the target qubits; `basis` is one of `TWO_QUBIT_BASIS_*`. +/// Returns a heap-allocated `CTwoQubitUnitarySynthesis*` (free with +/// `two_qubit_synthesis_free`), or NULL on NULL input, an unknown basis tag, +/// or a synthesis failure. +#[unsafe(no_mangle)] +pub extern "C" fn synthesize_numeric_2q_unitary( + matrix: *const Complex64, + first: u32, + second: u32, + basis: u8, +) -> *mut CTwoQubitUnitarySynthesis { + if matrix.is_null() { + return std::ptr::null_mut(); + } + let basis = match basis { + TWO_QUBIT_BASIS_PAULI_ROTATIONS => TwoQubitUnitaryDecomposeBasis::PauliRotations, + TWO_QUBIT_BASIS_CX => TwoQubitUnitaryDecomposeBasis::Cx, + TWO_QUBIT_BASIS_CY => TwoQubitUnitaryDecomposeBasis::Cy, + TWO_QUBIT_BASIS_CZ => TwoQubitUnitaryDecomposeBasis::Cz, + TWO_QUBIT_BASIS_RZZ => TwoQubitUnitaryDecomposeBasis::Rzz, + _ => return std::ptr::null_mut(), + }; + let flat = unsafe { std::slice::from_raw_parts(matrix, 16) }.to_vec(); + let matrix = match Array2::from_shape_vec((4, 4), flat) { + Ok(matrix) => matrix, + Err(_) => return std::ptr::null_mut(), + }; + match core_synthesize_numeric_2q_unitary( + &matrix, + [Qubit::new(first), Qubit::new(second)], + basis, + ) { + Ok(inner) => Box::into_raw(Box::new(CTwoQubitUnitarySynthesis { inner })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Frees a two-qubit synthesis result. NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn two_qubit_synthesis_free(ptr: *mut CTwoQubitUnitarySynthesis) { + if !ptr.is_null() { + unsafe { + let _ = Box::from_raw(ptr); + } + } +} + +/// Returns the number of emitted operations, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn two_qubit_synthesis_num_operations( + ptr: *const CTwoQubitUnitarySynthesis, +) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.operations.len() } +} + +/// Returns the global phase multiplying the emitted sequence, or 0.0 for +/// NULL. +#[unsafe(no_mangle)] +pub extern "C" fn two_qubit_synthesis_global_phase(ptr: *const CTwoQubitUnitarySynthesis) -> f64 { + if ptr.is_null() { + return 0.0; + } + unsafe { (*ptr).inner.global_phase } +} + +/// Returns the instruction name of operation `index` as a heap-allocated C +/// string (free with `cqlib_string_free`). Returns NULL on NULL input or +/// out-of-bounds index. +#[unsafe(no_mangle)] +pub extern "C" fn two_qubit_synthesis_operation_name( + ptr: *const CTwoQubitUnitarySynthesis, + index: usize, +) -> *mut c_char { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let result = unsafe { &(*ptr).inner }; + match result.operations.get(index) { + Some(operation) => match CString::new(operation.instruction.name()) { + Ok(name) => name.into_raw(), + Err(_) => std::ptr::null_mut(), + }, + None => std::ptr::null_mut(), + } +} + +/// Copies the qubits of operation `index` into `out` (two-step pattern; +/// call `two_qubit_synthesis_operation_qubits_len` first). Returns the total +/// number of qubits, or 0 for NULL input or an out-of-bounds index. +#[unsafe(no_mangle)] +pub extern "C" fn two_qubit_synthesis_operation_qubits( + ptr: *const CTwoQubitUnitarySynthesis, + index: usize, + out: *mut u32, + len: usize, +) -> usize { + if ptr.is_null() { + return 0; + } + let result = unsafe { &(*ptr).inner }; + match result.operations.get(index) { + Some(operation) => { + let qubits: Vec = operation.qubits.iter().map(|qubit| qubit.id()).collect(); + crate::device::write_u32_list(&qubits, out, len) + } + None => 0, + } +} + +/// Returns the number of qubits of operation `index`, or 0 for NULL input or +/// an out-of-bounds index. +#[unsafe(no_mangle)] +pub extern "C" fn two_qubit_synthesis_operation_qubits_len( + ptr: *const CTwoQubitUnitarySynthesis, + index: usize, +) -> usize { + if ptr.is_null() { + return 0; + } + let result = unsafe { &(*ptr).inner }; + result + .operations + .get(index) + .map(|operation| operation.qubits.len()) + .unwrap_or(0) +} + +// ===== KAK decomposition ===== + +/// Decomposes a 4x4 unitary matrix into its KAK (Weyl) coordinates and local +/// factors. +/// +/// `matrix` points at 16 row-major `Complex64` elements. Returns a +/// heap-allocated `CKakDecomposition*` (free with `kak_decomposition_free`), +/// or NULL on NULL input or a decomposition failure. +#[unsafe(no_mangle)] +pub extern "C" fn kak_decompose(matrix: *const Complex64) -> *mut CKakDecomposition { + if matrix.is_null() { + return std::ptr::null_mut(); + } + let flat = unsafe { std::slice::from_raw_parts(matrix, 16) }.to_vec(); + let matrix = match Array2::from_shape_vec((4, 4), flat) { + Ok(matrix) => matrix, + Err(_) => return std::ptr::null_mut(), + }; + match core_kak_decompose(&matrix) { + Ok(inner) => Box::into_raw(Box::new(CKakDecomposition { inner })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Frees a KAK decomposition. NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn kak_decomposition_free(ptr: *mut CKakDecomposition) { + if !ptr.is_null() { + unsafe { + let _ = Box::from_raw(ptr); + } + } +} + +/// Returns the scalar phase multiplying the complete decomposition, or 0.0 +/// for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn kak_decomposition_global_phase(ptr: *const CKakDecomposition) -> f64 { + if ptr.is_null() { + return 0.0; + } + unsafe { (*ptr).inner.global_phase } +} + +/// Writes the canonical Pauli-XX/YY/ZZ interaction coordinates `a`, `b`, +/// `c` to the given out pointers. +/// +/// Returns 0 on success or -1 on NULL arguments. +#[unsafe(no_mangle)] +pub extern "C" fn kak_decomposition_coordinates( + ptr: *const CKakDecomposition, + a: *mut f64, + b: *mut f64, + c: *mut f64, +) -> i32 { + if ptr.is_null() || a.is_null() || b.is_null() || c.is_null() { + return CqlibError::NullPtr as i32; + } + let decomposition = unsafe { &(*ptr).inner }; + unsafe { + *a = decomposition.a; + *b = decomposition.b; + *c = decomposition.c; + } + CqlibError::Ok as i32 +} + +/// Copies one 2x2 local factor (selected by a `KAK_FACTOR_*` tag) into `out` +/// as 4 row-major `Complex64` elements. +/// +/// Returns 0 on success, -1 on NULL input, -8 on an unknown factor tag. +#[unsafe(no_mangle)] +pub extern "C" fn kak_decomposition_local_factor( + ptr: *const CKakDecomposition, + factor: u8, + out: *mut Complex64, +) -> i32 { + if ptr.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + let decomposition = unsafe { &(*ptr).inner }; + let matrix = match factor { + KAK_FACTOR_K1L => &decomposition.k1l, + KAK_FACTOR_K1R => &decomposition.k1r, + KAK_FACTOR_K2L => &decomposition.k2l, + KAK_FACTOR_K2R => &decomposition.k2r, + _ => return CqlibError::InvalidParam as i32, + }; + let slice = unsafe { std::slice::from_raw_parts_mut(out, 4) }; + for (slot, value) in slice.iter_mut().zip(matrix.iter()) { + *slot = *value; + } + CqlibError::Ok as i32 +} + +// ===== Target-basis and device lowering ===== + +/// Creates a target-basis lowerer from standard-gate names. +/// +/// Each entry of `gate_names` must be a standard-gate name accepted elsewhere +/// by the C ABI (e.g. `"H"`, `"RZ"`, `"CX"`); the names become the target +/// basis. Returns a heap-allocated `CTargetBasisLowerer*` (free with +/// `target_basis_lowerer_free`), or NULL on NULL input, invalid UTF-8, an +/// unknown gate name, or an empty basis. +#[unsafe(no_mangle)] +pub extern "C" fn target_basis_lowerer_new( + gate_names: *const *const c_char, + len: usize, +) -> *mut CTargetBasisLowerer { + if gate_names.is_null() && len > 0 { + return std::ptr::null_mut(); + } + let mut basis = Vec::with_capacity(len); + if len > 0 { + let names = unsafe { std::slice::from_raw_parts(gate_names, len) }; + for name in names { + if name.is_null() { + return std::ptr::null_mut(); + } + let name = match unsafe { CStr::from_ptr(*name) }.to_str() { + Ok(name) => name, + Err(_) => return std::ptr::null_mut(), + }; + let gate = match standard_gate_from_name(name) { + Some(gate) => gate, + None => return std::ptr::null_mut(), + }; + basis.push(Instruction::Standard(gate)); + } + } + match TargetBasisLowerer::new(basis) { + Ok(inner) => Box::into_raw(Box::new(CTargetBasisLowerer { inner })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Frees a target-basis lowerer. NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn target_basis_lowerer_free(ptr: *mut CTargetBasisLowerer) { + if !ptr.is_null() { + unsafe { + let _ = Box::from_raw(ptr); + } + } +} + +/// Returns the number of gates in the target basis, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn target_basis_lowerer_num_gates(ptr: *const CTargetBasisLowerer) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.target_basis().len() } +} + +/// Returns 1 when translating `circuit` to the target basis can change or +/// reject its gate-like operations, 0 otherwise, or -1 on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn target_basis_lowerer_requires_lowering( + ptr: *const CTargetBasisLowerer, + circuit: *const CCircuit, +) -> i32 { + if ptr.is_null() || circuit.is_null() { + return CqlibError::NullPtr as i32; + } + let lowerer = unsafe { &(*ptr).inner }; + let circuit = unsafe { &(*circuit).inner }; + lowerer.requires_lowering(circuit) as i32 +} + +/// Lowers `circuit` to the target basis and returns the rebuilt circuit as +/// an owned `CCircuit*` (free with `circuit_free`). +/// +/// Returns NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn target_basis_lowerer_apply( + ptr: *const CTargetBasisLowerer, + circuit: *const CCircuit, +) -> *mut CCircuit { + if ptr.is_null() || circuit.is_null() { + return std::ptr::null_mut(); + } + let lowerer = unsafe { &(*ptr).inner }; + let circuit = unsafe { &(*circuit).inner }; + match lowerer.transform(circuit, None) { + Ok(outcome) => Box::into_raw(Box::new(CCircuit { + inner: outcome.into_circuit(circuit), + })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Lowers `circuit` to the native instruction set of `device` and returns +/// the rebuilt circuit as an owned `CCircuit*` (free with `circuit_free`). +/// +/// Returns NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn decompose_lower_to_device( + circuit: *const CCircuit, + device: *const CDevice, +) -> *mut CCircuit { + if circuit.is_null() || device.is_null() { + return std::ptr::null_mut(); + } + let device = unsafe { &(*device).inner }; + let circuit = unsafe { &(*circuit).inner }; + match DeviceLowerer::new(device).transform(circuit, None) { + Ok(outcome) => Box::into_raw(Box::new(CCircuit { + inner: outcome.into_circuit(circuit), + })), + Err(_) => std::ptr::null_mut(), + } +} + +// ===== Unitary-decompose two-qubit synthesis target ===== + +/// C snapshot of a target-basis lowering cost. +#[repr(C)] +pub struct CTargetBasisCost { + /// Two-qubit operations in the lowered sequence. + pub two_qubit_ops: usize, + /// Critical-path depth of the lowered sequence. + pub depth: usize, + /// Total operation count of the lowered sequence (global phase excluded). + pub total_ops: usize, + /// Operations carrying parameters in the lowered sequence. + pub parameterized_ops: usize, +} + +/// Opaque handle around a core [`TwoQubitSynthesisTarget`]: the two-qubit +/// synthesis capability plugged into the unitary-decompose configuration +/// (`UnitaryDecomposeConfig::two_qubit_target`). +pub struct CDecomposeUnitaryTarget { + pub inner: TwoQubitSynthesisTarget, +} + +/// Extracts the standard gates from parsed instructions (the shared parser +/// only ever produces standard instructions). +fn standard_gates_from_instructions(instructions: Vec) -> Vec { + instructions + .into_iter() + .filter_map(|instruction| match instruction { + Instruction::Standard(gate) => Some(gate), + _ => None, + }) + .collect() +} + +/// Collects the native gate names of `target` (one-qubit gates followed by +/// two-qubit gates). +fn target_gate_names(target: &TwoQubitSynthesisTarget) -> Vec { + target + .native_1q() + .iter() + .chain(target.native_2q().iter()) + .map(|gate| Instruction::Standard(*gate).name()) + .collect() +} + +/// Creates an unconstrained synthesis target: no native-basis restrictions +/// and the neutral exact Pauli-rotation fallback. +/// +/// Returns a heap-allocated `CDecomposeUnitaryTarget*` (free with +/// `decompose_unitary_target_free`). +#[unsafe(no_mangle)] +pub extern "C" fn decompose_unitary_target_unconstrained() -> *mut CDecomposeUnitaryTarget { + Box::into_raw(Box::new(CDecomposeUnitaryTarget { + inner: TwoQubitSynthesisTarget::unconstrained(), + })) +} + +/// Creates a synthesis target from a workflow-style target-basis list of +/// standard-gate names (e.g. `"H"`, `"RZ"`, `"CX"`): one-qubit names become +/// the native one-qubit basis, two-qubit names the native two-qubit basis, +/// and the exact Pauli-rotation fallback stays enabled. +/// +/// Returns a heap-allocated `CDecomposeUnitaryTarget*` (free with +/// `decompose_unitary_target_free`), or NULL on NULL input, invalid UTF-8, +/// an unknown gate name, or when the core rejects the basis (e.g. empty). +#[unsafe(no_mangle)] +pub extern "C" fn decompose_unitary_target_from_instructions( + gate_names: *const *const c_char, + len: usize, +) -> *mut CDecomposeUnitaryTarget { + let instructions = match standard_instructions_from_names(gate_names, len) { + Ok(instructions) => instructions, + Err(_) => return std::ptr::null_mut(), + }; + match TwoQubitSynthesisTarget::from_instructions(Some(&instructions)) { + Ok(inner) => Box::into_raw(Box::new(CDecomposeUnitaryTarget { inner })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Creates a synthesis target from explicit native one-qubit and two-qubit +/// standard-gate name lists and a Pauli-rotation fallback flag. +/// +/// Every name in `native_1q_names` must be a one-qubit standard gate and +/// every name in `native_2q_names` a two-qubit standard gate. Returns a +/// heap-allocated `CDecomposeUnitaryTarget*` (free with +/// `decompose_unitary_target_free`), or NULL on NULL input, invalid UTF-8, +/// an unknown gate name, a wrong-arity gate, or an empty combined basis. +#[unsafe(no_mangle)] +pub extern "C" fn decompose_unitary_target_from_standard_gates( + native_1q_names: *const *const c_char, + num_1q: usize, + native_2q_names: *const *const c_char, + num_2q: usize, + fallback_pauli: u8, +) -> *mut CDecomposeUnitaryTarget { + let native_1q = match standard_instructions_from_names(native_1q_names, num_1q) { + Ok(instructions) => standard_gates_from_instructions(instructions), + Err(_) => return std::ptr::null_mut(), + }; + let native_2q = match standard_instructions_from_names(native_2q_names, num_2q) { + Ok(instructions) => standard_gates_from_instructions(instructions), + Err(_) => return std::ptr::null_mut(), + }; + match TwoQubitSynthesisTarget::from_standard_gates(native_1q, native_2q, fallback_pauli != 0) { + Ok(inner) => Box::into_raw(Box::new(CDecomposeUnitaryTarget { inner })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Frees a unitary-decompose synthesis target. NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn decompose_unitary_target_free(ptr: *mut CDecomposeUnitaryTarget) { + if !ptr.is_null() { + unsafe { + let _ = Box::from_raw(ptr); + } + } +} + +/// Returns 1 when Pauli-rotation fallback is permitted for `target`, 0 +/// otherwise, or -1 on NULL. +#[unsafe(no_mangle)] +pub extern "C" fn decompose_unitary_target_fallback_pauli( + ptr: *const CDecomposeUnitaryTarget, +) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + unsafe { (*ptr).inner.fallback_pauli() as i32 } +} + +/// Sets whether Pauli-rotation fallback is permitted for `target`. +/// +/// The target is rebuilt around its native gate lists, which requires a +/// non-empty basis. Returns 0 on success, -1 on NULL, or -8 when the target +/// is unconstrained (no native gates), in which case the target is left +/// unchanged. +#[unsafe(no_mangle)] +pub extern "C" fn decompose_unitary_target_set_fallback_pauli( + ptr: *mut CDecomposeUnitaryTarget, + value: u8, +) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + let wrapper = unsafe { &mut *ptr }; + let native_1q = wrapper.inner.native_1q().to_vec(); + let native_2q = wrapper.inner.native_2q().to_vec(); + match TwoQubitSynthesisTarget::from_standard_gates(native_1q, native_2q, value != 0) { + Ok(inner) => { + wrapper.inner = inner; + CqlibError::Ok as i32 + } + Err(_) => CqlibError::InvalidParam as i32, + } +} + +/// Returns the number of native one-qubit gates of `target`, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn decompose_unitary_target_native_1q_len( + ptr: *const CDecomposeUnitaryTarget, +) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.native_1q().len() } +} + +/// Copies the native one-qubit gate names of `target` into `out` (two-step +/// pattern; pair with `decompose_unitary_target_native_1q_len`). Each written +/// entry is a freshly allocated C string freed with `cqlib_string_free`. +/// Returns the total number of gates, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn decompose_unitary_target_native_1q( + ptr: *const CDecomposeUnitaryTarget, + out: *mut *mut c_char, + len: usize, +) -> usize { + if ptr.is_null() { + return 0; + } + let names: Vec = unsafe { (*ptr).inner.native_1q() } + .iter() + .map(|gate| Instruction::Standard(*gate).name()) + .collect(); + write_name_list(&names, out, len) +} + +/// Returns the number of native two-qubit gates of `target`, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn decompose_unitary_target_native_2q_len( + ptr: *const CDecomposeUnitaryTarget, +) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.native_2q().len() } +} + +/// Copies the native two-qubit gate names of `target` into `out` (two-step +/// pattern; pair with `decompose_unitary_target_native_2q_len`). Each written +/// entry is a freshly allocated C string freed with `cqlib_string_free`. +/// Returns the total number of gates, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn decompose_unitary_target_native_2q( + ptr: *const CDecomposeUnitaryTarget, + out: *mut *mut c_char, + len: usize, +) -> usize { + if ptr.is_null() { + return 0; + } + let names: Vec = unsafe { (*ptr).inner.native_2q() } + .iter() + .map(|gate| Instruction::Standard(*gate).name()) + .collect(); + write_name_list(&names, out, len) +} + +/// Returns the number of native basis gates of `target` (one-qubit gates +/// followed by two-qubit gates), or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn decompose_unitary_target_basis_len(ptr: *const CDecomposeUnitaryTarget) -> usize { + if ptr.is_null() { + return 0; + } + let target = unsafe { &(*ptr).inner }; + target.native_1q().len() + target.native_2q().len() +} + +/// Copies the native basis gate names of `target` into `out` (two-step +/// pattern; pair with `decompose_unitary_target_basis_len`). Each written +/// entry is a freshly allocated C string freed with `cqlib_string_free`. +/// Returns the total number of gates, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn decompose_unitary_target_basis( + ptr: *const CDecomposeUnitaryTarget, + out: *mut *mut c_char, + len: usize, +) -> usize { + if ptr.is_null() { + return 0; + } + let names = target_gate_names(unsafe { &(*ptr).inner }); + write_name_list(&names, out, len) +} + +/// Computes the exact target-basis cost of a fixed standard-gate operation +/// sequence under the native basis of `target`. +/// +/// The sequence is given as `num_ops` gate names (`gate_names`), with each +/// operation's qubit IDs flattened into `qubit_ids` and per-operation counts +/// in `qubit_counts`. Parameters are treated as fixed zeros. Requires a +/// constrained target (an unconstrained target has no basis to cost +/// against). +/// +/// Writes the cost snapshot to `*out`. Returns 0 on success, -1 on NULL, +/// -4 on invalid UTF-8 or an unknown gate name, -8 when the target is +/// unconstrained, or -6 when the core rejects the sequence. +#[unsafe(no_mangle)] +pub extern "C" fn decompose_unitary_target_cost_of_fixed_operations( + ptr: *const CDecomposeUnitaryTarget, + gate_names: *const *const c_char, + qubit_ids: *const u32, + qubit_counts: *const u32, + num_ops: usize, + out: *mut CTargetBasisCost, +) -> i32 { + if ptr.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + let target = unsafe { &(*ptr).inner }; + let basis: Vec = target + .native_1q() + .iter() + .chain(target.native_2q().iter()) + .map(|gate| Instruction::Standard(*gate)) + .collect(); + let model = match TargetBasisCostModel::new(basis) { + Ok(model) => model, + Err(_) => return CqlibError::InvalidParam as i32, + }; + + let cost = if num_ops == 0 { + TargetBasisCost::default() + } else { + if gate_names.is_null() || qubit_counts.is_null() { + return CqlibError::NullPtr as i32; + } + let names = unsafe { std::slice::from_raw_parts(gate_names, num_ops) }; + let counts = unsafe { std::slice::from_raw_parts(qubit_counts, num_ops) }; + let mut operations: Vec = Vec::with_capacity(num_ops); + let mut qubits: Vec = Vec::new(); + let mut cursor = 0usize; + for (name, &count) in names.iter().zip(counts) { + if name.is_null() { + return CqlibError::NullPtr as i32; + } + let name = match unsafe { CStr::from_ptr(*name) }.to_str() { + Ok(name) => name, + Err(_) => return CqlibError::ParseError as i32, + }; + let Some(gate) = standard_gate_from_name(name) else { + return CqlibError::ParseError as i32; + }; + let mut op_qubits = Vec::with_capacity(count as usize); + for k in 0..count as usize { + if qubit_ids.is_null() { + return CqlibError::NullPtr as i32; + } + let id = unsafe { *qubit_ids.add(cursor + k) }; + let qubit = Qubit::new(id); + if !qubits.contains(&qubit) { + qubits.push(qubit); + } + op_qubits.push(qubit); + } + cursor += count as usize; + let params = (0..gate.num_params()).map(|_| ParameterValue::Fixed(0.0)); + operations.push(ValueOperation::from_standard(gate, op_qubits, params)); + } + match model.cost_of_fixed_operations(qubits, operations) { + Ok(cost) => cost, + Err(_) => return CqlibError::CompilerError as i32, + } + }; + unsafe { + *out = CTargetBasisCost { + two_qubit_ops: cost.two_qubit_ops, + depth: cost.depth, + total_ops: cost.total_ops, + parameterized_ops: cost.parameterized_ops, + }; + } + CqlibError::Ok as i32 +} diff --git a/crates/binding-c/src/compile/knowledge.rs b/crates/binding-c/src/compile/knowledge.rs new file mode 100644 index 00000000..65db0a30 --- /dev/null +++ b/crates/binding-c/src/compile/knowledge.rs @@ -0,0 +1,1374 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! C ABI for the compiler knowledge base: rule-library construction (empty, +//! builtin, DSL text, DSL file), single-rule parsing, rule insertion, rule +//! metadata queries, and the matching engine (transactional pattern +//! matching, condition checks, item equivalence, candidate and +//! instruction-key filtering, rule extension, and rewrite-target +//! instantiation). + +use crate::circuit::COperation; +use crate::device::standard_gate_from_name; +use crate::error::CqlibError; +use cqlib_core::circuit::{CircuitParam, Instruction, Parameter, ParameterValue}; +use cqlib_core::compile::knowledge::matcher::{ + ConcreteOperationView, MatchBindings, MatchError, MatchedReplacement, conditions_hold, + instantiate_target, match_rule_item, +}; +use cqlib_core::compile::knowledge::rule::Rule; +use cqlib_core::compile::knowledge::rule_dsl::load::LoadError; +use cqlib_core::compile::knowledge::{RuleKind, RuleLibrary, RuleLibraryError, RuleMetadata}; +use std::collections::HashSet; +use std::ffi::{CStr, CString}; +use std::os::raw::c_char; + +/// Rule-kind tag. +/// | Value | Kind | +/// |-------|----------------| +/// | 0 | Simplify | +/// | 1 | Cancel | +/// | 2 | Merge | +/// | 3 | Commute | +/// | 4 | Decompose | +/// | 5 | Canonicalize | +/// | 6 | HardwareNative | +/// | 7 | Other | +pub const RULE_KIND_SIMPLIFY: u8 = 0; +pub const RULE_KIND_CANCEL: u8 = 1; +pub const RULE_KIND_MERGE: u8 = 2; +pub const RULE_KIND_COMMUTE: u8 = 3; +pub const RULE_KIND_DECOMPOSE: u8 = 4; +pub const RULE_KIND_CANONICALIZE: u8 = 5; +pub const RULE_KIND_HARDWARE_NATIVE: u8 = 6; +pub const RULE_KIND_OTHER: u8 = 7; + +/// Opaque handle around a [`RuleLibrary`]. +pub struct CKnowledgeLibrary { + pub inner: RuleLibrary, +} + +/// Opaque handle around a single knowledge [`Rule`]. +/// +/// Handles are produced by [`knowledge_rule_from_dsl`] and consumed by +/// [`knowledge_library_add_rule`]. +pub struct CKnowledgeRule { + pub inner: Rule, +} + +/// Opaque handle around the mutable bindings produced while matching a rule +/// instance (`MatchBindings`). +pub struct CKnowledgeBindings { + pub inner: MatchBindings, +} + +/// Opaque handle around the rewrite-target instantiation result (a list of +/// `MatchedReplacement` items). +pub struct CKnowledgeReplacements { + pub inner: Vec, +} + +// ===== Kind and error mapping helpers ===== + +/// Converts a `RULE_KIND_*` tag into the core form. +fn rule_kind_from_tag(tag: u8) -> Option { + Some(match tag { + RULE_KIND_SIMPLIFY => RuleKind::Simplify, + RULE_KIND_CANCEL => RuleKind::Cancel, + RULE_KIND_MERGE => RuleKind::Merge, + RULE_KIND_COMMUTE => RuleKind::Commute, + RULE_KIND_DECOMPOSE => RuleKind::Decompose, + RULE_KIND_CANONICALIZE => RuleKind::Canonicalize, + RULE_KIND_HARDWARE_NATIVE => RuleKind::HardwareNative, + RULE_KIND_OTHER => RuleKind::Other, + _ => return None, + }) +} + +/// Converts a core rule kind into its `RULE_KIND_*` tag. +fn rule_kind_to_tag(kind: RuleKind) -> u8 { + match kind { + RuleKind::Simplify => RULE_KIND_SIMPLIFY, + RuleKind::Cancel => RULE_KIND_CANCEL, + RuleKind::Merge => RULE_KIND_MERGE, + RuleKind::Commute => RULE_KIND_COMMUTE, + RuleKind::Decompose => RULE_KIND_DECOMPOSE, + RuleKind::Canonicalize => RULE_KIND_CANONICALIZE, + RuleKind::HardwareNative => RULE_KIND_HARDWARE_NATIVE, + RuleKind::Other => RULE_KIND_OTHER, + } +} + +/// Maps a rule-library error onto the C error codes. +/// +/// DSL parse/lowering failures, duplicate rule names, and structural +/// validation failures map to -4; file-level I/O failures map to -5; +/// instructions that cannot be indexed by the matcher map to -6. +fn library_error_code(error: &RuleLibraryError) -> i32 { + match error { + RuleLibraryError::Load(LoadError::Io(_)) => CqlibError::IoError as i32, + RuleLibraryError::Load(_) + | RuleLibraryError::InvalidRule { .. } + | RuleLibraryError::DuplicateRuleName(_) => CqlibError::ParseError as i32, + RuleLibraryError::UnsupportedInstruction { .. } => CqlibError::CompilerError as i32, + } +} + +/// Resolves the precomputed metadata of the rule at `index`. +/// +/// Rule ids equal the insertion index and rule names are unique, so the +/// metadata is recovered through the rule's name. +fn metadata_for_index(library: &RuleLibrary, index: usize) -> Option<&RuleMetadata> { + let name = library.rules().get(index)?.name.as_str(); + let id = library.id_by_name(name)?; + library.metadata(id) +} + +// ===== Library construction ===== + +/// Creates an empty rule library. Free with `knowledge_library_free`. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_library_new() -> *mut CKnowledgeLibrary { + Box::into_raw(Box::new(CKnowledgeLibrary { + inner: RuleLibrary::new(), + })) +} + +/// Creates a library containing the builtin compiler rules. Free with +/// `knowledge_library_free`. Returns NULL when the builtin sources fail to +/// validate. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_library_builtin() -> *mut CKnowledgeLibrary { + match RuleLibrary::builtin_rules() { + Ok(library) => Box::into_raw(Box::new(CKnowledgeLibrary { + inner: library.clone(), + })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Parses and validates rules from a DSL source string. +/// +/// `kind` is one of `RULE_KIND_*` and classifies every parsed rule. On success +/// writes a heap-allocated `CKnowledgeLibrary*` (free with +/// `knowledge_library_free`) to `out` and returns 0. Returns -1 on NULL input, +/// -8 on an unknown kind tag, and -4 on DSL parse, lowering, or validation +/// failures. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_library_from_dsl_str( + source: *const c_char, + kind: u8, + out: *mut *mut CKnowledgeLibrary, +) -> i32 { + if source.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + let Some(kind) = rule_kind_from_tag(kind) else { + return CqlibError::InvalidParam as i32; + }; + let source = match unsafe { CStr::from_ptr(source) }.to_str() { + Ok(source) => source, + Err(_) => return CqlibError::ParseError as i32, + }; + match RuleLibrary::from_dsl_str(source, kind) { + Ok(inner) => { + unsafe { *out = Box::into_raw(Box::new(CKnowledgeLibrary { inner })) }; + CqlibError::Ok as i32 + } + Err(err) => library_error_code(&err), + } +} + +/// Loads, parses, and validates rules from a DSL file. +/// +/// `kind` is one of `RULE_KIND_*`. On success writes a heap-allocated +/// `CKnowledgeLibrary*` (free with `knowledge_library_free`) to `out` and +/// returns 0. Returns -1 on NULL input, -8 on an unknown kind tag, -4 on DSL +/// parse, lowering, or validation failures, and -5 when the file cannot be +/// read. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_library_from_dsl_file( + path: *const c_char, + kind: u8, + out: *mut *mut CKnowledgeLibrary, +) -> i32 { + if path.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + let Some(kind) = rule_kind_from_tag(kind) else { + return CqlibError::InvalidParam as i32; + }; + let path = match unsafe { CStr::from_ptr(path) }.to_str() { + Ok(path) => path, + Err(_) => return CqlibError::ParseError as i32, + }; + match RuleLibrary::from_dsl_file(path, kind) { + Ok(inner) => { + unsafe { *out = Box::into_raw(Box::new(CKnowledgeLibrary { inner })) }; + CqlibError::Ok as i32 + } + Err(err) => library_error_code(&err), + } +} + +/// Frees a rule library. NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_library_free(ptr: *mut CKnowledgeLibrary) { + if !ptr.is_null() { + unsafe { + let _ = Box::from_raw(ptr); + } + } +} + +// ===== Library queries ===== + +/// Returns the number of rules in the library, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_library_len(ptr: *const CKnowledgeLibrary) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.len() } +} + +/// Returns 1 when the library contains no rules, 0 otherwise, or -1 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_library_is_empty(ptr: *const CKnowledgeLibrary) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + unsafe { (*ptr).inner.is_empty() as i32 } +} + +/// Returns 1 when a rule with the given name exists, 0 otherwise, or -1 on +/// NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_library_contains( + ptr: *const CKnowledgeLibrary, + name: *const c_char, +) -> i32 { + if ptr.is_null() || name.is_null() { + return CqlibError::NullPtr as i32; + } + let name = match unsafe { CStr::from_ptr(name) }.to_str() { + Ok(name) => name, + Err(_) => return CqlibError::ParseError as i32, + }; + unsafe { (*ptr).inner.contains(name) as i32 } +} + +/// Looks up the rule id (insertion index) of `name`. +/// +/// Returns the id as a non-negative value, or -1 on NULL input or when no +/// rule with that name exists. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_library_id_by_name( + ptr: *const CKnowledgeLibrary, + name: *const c_char, +) -> i64 { + if ptr.is_null() || name.is_null() { + return CqlibError::NullPtr as i32 as i64; + } + let name = match unsafe { CStr::from_ptr(name) }.to_str() { + Ok(name) => name, + Err(_) => return CqlibError::ParseError as i32 as i64, + }; + match unsafe { (*ptr).inner.id_by_name(name) } { + Some(id) => id.as_usize() as i64, + None => -1, + } +} + +/// Returns the number of rules registered under `kind` (one of +/// `RULE_KIND_*`), or 0 for NULL input or an unknown kind tag. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_library_rules_by_kind_len( + ptr: *const CKnowledgeLibrary, + kind: u8, +) -> usize { + let Some(kind) = rule_kind_from_tag(kind) else { + return 0; + }; + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.rules_by_kind(kind).len() } +} + +/// Copies the rule ids registered under `kind` into `out` (two-step pattern; +/// call `knowledge_library_rules_by_kind_len` first). Returns the total number +/// of rules of that kind, or 0 for NULL input or an unknown kind tag. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_library_rules_by_kind( + ptr: *const CKnowledgeLibrary, + kind: u8, + out: *mut u32, + len: usize, +) -> usize { + let Some(kind) = rule_kind_from_tag(kind) else { + return 0; + }; + if ptr.is_null() { + return 0; + } + let ids: Vec = unsafe { (*ptr).inner.rules_by_kind(kind) } + .iter() + .map(|id| id.as_usize() as u32) + .collect(); + crate::device::write_u32_list(&ids, out, len) +} + +// ===== Rule metadata accessors (by insertion index) ===== + +/// Returns the name of the rule at `index` as a heap-allocated C string (free +/// with `cqlib_string_free`). Returns NULL on NULL input or an out-of-bounds +/// index. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_library_rule_name( + ptr: *const CKnowledgeLibrary, + index: usize, +) -> *mut c_char { + if ptr.is_null() { + return std::ptr::null_mut(); + } + match unsafe { (*ptr).inner.rules() }.get(index) { + Some(rule) => match CString::new(rule.name.as_str()) { + Ok(name) => name.into_raw(), + Err(_) => std::ptr::null_mut(), + }, + None => std::ptr::null_mut(), + } +} + +/// Returns the kind tag (one of `RULE_KIND_*`) of the rule at `index`, or -1 +/// on NULL input and -8 on an out-of-bounds index. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_library_rule_kind(ptr: *const CKnowledgeLibrary, index: usize) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + match metadata_for_index(unsafe { &(*ptr).inner }, index) { + Some(metadata) => rule_kind_to_tag(metadata.kind) as i32, + None => CqlibError::InvalidParam as i32, + } +} + +/// Returns the instruction name starting the rule's match pattern as a +/// heap-allocated C string (free with `cqlib_string_free`). Returns NULL on +/// NULL input or an out-of-bounds index. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_library_rule_first_instruction( + ptr: *const CKnowledgeLibrary, + index: usize, +) -> *mut c_char { + if ptr.is_null() { + return std::ptr::null_mut(); + } + match metadata_for_index(unsafe { &(*ptr).inner }, index) { + Some(metadata) => match CString::new(metadata.first_instruction.to_string()) { + Ok(name) => name.into_raw(), + Err(_) => std::ptr::null_mut(), + }, + None => std::ptr::null_mut(), + } +} + +/// Returns the number of operations in the rule's match pattern, or 0 for +/// NULL input or an out-of-bounds index. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_library_rule_pattern_len( + ptr: *const CKnowledgeLibrary, + index: usize, +) -> usize { + if ptr.is_null() { + return 0; + } + match metadata_for_index(unsafe { &(*ptr).inner }, index) { + Some(metadata) => metadata.pattern_len, + None => 0, + } +} + +/// Returns the number of operations emitted by the rule's rewrite target, or +/// 0 for NULL input or an out-of-bounds index. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_library_rule_rewrite_len( + ptr: *const CKnowledgeLibrary, + index: usize, +) -> usize { + if ptr.is_null() { + return 0; + } + match metadata_for_index(unsafe { &(*ptr).inner }, index) { + Some(metadata) => metadata.rewrite_len, + None => 0, + } +} + +/// Returns the number of distinct rule-local qubit labels used by the rule, +/// or 0 for NULL input or an out-of-bounds index. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_library_rule_qubit_count( + ptr: *const CKnowledgeLibrary, + index: usize, +) -> usize { + if ptr.is_null() { + return 0; + } + match metadata_for_index(unsafe { &(*ptr).inner }, index) { + Some(metadata) => metadata.qubit_count, + None => 0, + } +} + +/// Returns the static operation-count delta (`rewrite_len - pattern_len`) of +/// the rule, or 0 for NULL input or an out-of-bounds index. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_library_rule_cost_delta( + ptr: *const CKnowledgeLibrary, + index: usize, +) -> i64 { + if ptr.is_null() { + return 0; + } + match metadata_for_index(unsafe { &(*ptr).inner }, index) { + Some(metadata) => metadata.cost_delta as i64, + None => 0, + } +} + +/// Returns 1 when the rule carries non-empty parameter conditions, 0 +/// otherwise, or -1 on NULL input and -8 on an out-of-bounds index. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_library_rule_has_conditions( + ptr: *const CKnowledgeLibrary, + index: usize, +) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + match metadata_for_index(unsafe { &(*ptr).inner }, index) { + Some(metadata) => metadata.has_conditions as i32, + None => CqlibError::InvalidParam as i32, + } +} + +// ===== Single-rule parsing and insertion ===== + +/// Parses a DSL source string that must define exactly one rule. +/// +/// On success writes a heap-allocated `CKnowledgeRule*` (free with +/// `knowledge_rule_free`) to `out` and returns 0. Returns -1 on NULL input, +/// -4 on DSL parse, lowering, or validation failures, and -8 when the source +/// does not define exactly one rule. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_rule_from_dsl( + source: *const c_char, + out: *mut *mut CKnowledgeRule, +) -> i32 { + if source.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + let source = match unsafe { CStr::from_ptr(source) }.to_str() { + Ok(source) => source, + Err(_) => return CqlibError::ParseError as i32, + }; + match RuleLibrary::from_dsl_str(source, RuleKind::Other) { + Ok(library) if library.len() == 1 => { + let rule = library.rules()[0].clone(); + unsafe { *out = Box::into_raw(Box::new(CKnowledgeRule { inner: rule })) }; + CqlibError::Ok as i32 + } + Ok(_) => CqlibError::InvalidParam as i32, + Err(err) => library_error_code(&err), + } +} + +/// Frees a single-rule handle. NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_rule_free(ptr: *mut CKnowledgeRule) { + if !ptr.is_null() { + unsafe { + let _ = Box::from_raw(ptr); + } + } +} + +/// Adds a parsed rule to the library under `kind` (one of `RULE_KIND_*`) +/// and returns its assigned id. +/// +/// The rule is structurally validated before insertion. Returns -1 on NULL +/// input, -8 on an unknown kind tag, -4 on a validation failure or duplicate +/// name, and -6 when the rule's first instruction cannot be indexed. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_library_add_rule( + ptr: *mut CKnowledgeLibrary, + rule: *const CKnowledgeRule, + kind: u8, +) -> i64 { + if ptr.is_null() || rule.is_null() { + return CqlibError::NullPtr as i32 as i64; + } + let Some(kind) = rule_kind_from_tag(kind) else { + return CqlibError::InvalidParam as i32 as i64; + }; + let library = unsafe { &mut *ptr }; + let rule = unsafe { (*rule).inner.clone() }; + match library.inner.add_rule(rule, kind, true) { + Ok(id) => id.as_usize() as i64, + Err(err) => library_error_code(&err) as i64, + } +} + +// ===== Single-rule accessors ===== + +/// Returns the rule's name as a heap-allocated C string (free with +/// `cqlib_string_free`). Returns NULL on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_rule_name(ptr: *const CKnowledgeRule) -> *mut c_char { + if ptr.is_null() { + return std::ptr::null_mut(); + } + match CString::new(unsafe { (*ptr).inner.name.as_str() }) { + Ok(name) => name.into_raw(), + Err(_) => std::ptr::null_mut(), + } +} + +/// Returns the number of qubit labels the rule involves, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_rule_num_qubits(ptr: *const CKnowledgeRule) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.num_qubits() } +} + +/// Returns the number of operations in the rule's match pattern, or 0 for +/// NULL. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_rule_pattern_len(ptr: *const CKnowledgeRule) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.operations.len() } +} + +/// Returns the number of operations emitted by the rule's rewrite target, or +/// 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_rule_rewrite_len(ptr: *const CKnowledgeRule) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.target.len() } +} + +/// Returns 1 when the rule carries non-empty parameter conditions, 0 +/// otherwise, or -1 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_rule_has_conditions(ptr: *const CKnowledgeRule) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + let rule = unsafe { &(*ptr).inner }; + rule.conditions + .as_ref() + .is_some_and(|conditions| !conditions.is_empty()) as i32 +} + +/// Runs the rule's structural validation. +/// +/// Returns 0 when the rule is well formed, -1 on NULL input, and -4 when +/// validation fails. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_rule_validate(ptr: *const CKnowledgeRule) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + match unsafe { (*ptr).inner.validate() } { + Ok(()) => CqlibError::Ok as i32, + Err(_) => CqlibError::ParseError as i32, + } +} + +// ===== Matching helpers ===== + +/// Maps a matcher error onto the C error codes: instructions the matcher +/// cannot represent map to -6, unbound rewrite qubits map to -2, and unbound +/// rewrite symbols map to -8. +fn match_error_code(error: &MatchError) -> i32 { + match error { + MatchError::UnsupportedRuleInstruction { .. } => CqlibError::CompilerError as i32, + MatchError::UnboundRewriteQubit { .. } => CqlibError::QubitOutOfBounds as i32, + MatchError::UnboundRewriteSymbol { .. } => CqlibError::InvalidParam as i32, + } +} + +/// Converts an operation's parameters into core parameters. Indexed +/// parameters reference a circuit-local table the matcher cannot resolve, so +/// they are rejected. +fn operation_params_as_parameters(op: &COperation) -> Result, ()> { + op.inner + .params + .iter() + .map(|param| match param { + CircuitParam::Fixed(value) => Ok(Parameter::from(*value)), + CircuitParam::Index(_) => Err(()), + }) + .collect() +} + +/// Resolves a gate name into a standard-gate instruction. +fn instruction_from_name(name: &str) -> Option { + standard_gate_from_name(name).map(Instruction::Standard) +} + +/// Resolves a C array of gate names into standard-gate instructions. +/// Returns `None` on a NULL entry or an unknown gate name. +fn instructions_from_names(names: *const *const c_char, len: usize) -> Option> { + if len > 0 && names.is_null() { + return None; + } + let mut result = Vec::with_capacity(len); + for i in 0..len { + let ptr = unsafe { *names.add(i) }; + if ptr.is_null() { + return None; + } + let name = unsafe { CStr::from_ptr(ptr) }.to_str().ok()?; + result.push(instruction_from_name(name)?); + } + Some(result) +} + +/// Writes a list of strings into a caller buffer of C string pointers +/// (two-step pattern). Returns the total number of available strings; only +/// the first `min(total, len)` entries are written. +fn write_cstring_list(items: &[String], out: *mut *mut c_char, len: usize) -> usize { + let total = items.len(); + if !out.is_null() { + for (i, item) in items.iter().take(total.min(len)).enumerate() { + if let Ok(text) = CString::new(item.as_str()) { + unsafe { *out.add(i) = text.into_raw() }; + } + } + } + total +} + +/// Resolves the candidate rule ids for a first-instruction gate name. +/// Unknown names, invalid UTF-8, and NULL input yield an empty list. +fn candidate_ids(library: &CKnowledgeLibrary, name: *const c_char) -> Vec { + if name.is_null() { + return Vec::new(); + } + let Ok(name) = unsafe { CStr::from_ptr(name) }.to_str() else { + return Vec::new(); + }; + let Some(instruction) = instruction_from_name(name) else { + return Vec::new(); + }; + match library.inner.candidates_for_first_instruction(&instruction) { + Ok(ids) => ids.iter().map(|id| id.as_usize() as u32).collect(), + Err(_) => Vec::new(), + } +} + +/// Resolves the rules whose match-side and rewrite-side instruction keys +/// cover the requested gate names. Invalid input yields an empty list. +fn filtered_rule_ids( + library: &CKnowledgeLibrary, + ops: *const *const c_char, + ops_len: usize, + targets: *const *const c_char, + targets_len: usize, +) -> Vec { + let Some(op_instructions) = instructions_from_names(ops, ops_len) else { + return Vec::new(); + }; + let Some(target_instructions) = instructions_from_names(targets, targets_len) else { + return Vec::new(); + }; + match library + .inner + .filter_rule_ids_by_instruction_keys(&op_instructions, &target_instructions) + { + Ok(ids) => ids.iter().map(|id| id.as_usize() as u32).collect(), + Err(_) => Vec::new(), + } +} + +/// Collects the rule's free symbols in sorted order. +fn sorted_free_symbols(rule: &Rule) -> Vec { + let mut symbols: Vec = rule.collect_free_symbols().into_iter().collect(); + symbols.sort(); + symbols +} + +/// Collects a rule symbol set in sorted order. +fn sorted_symbols(symbols: HashSet) -> Vec { + let mut symbols: Vec = symbols.into_iter().collect(); + symbols.sort(); + symbols +} + +/// Evaluates a replacement parameter to a fixed double. Symbolic values are +/// evaluated without free bindings; unresolvable expressions yield NaN. +fn replacement_param_value(value: &ParameterValue) -> f64 { + match value { + ParameterValue::Fixed(value) => *value, + ParameterValue::Param(parameter) => parameter.evaluate(&None).unwrap_or(f64::NAN), + } +} + +// ===== Match bindings ===== + +/// Creates empty match bindings. Free with `knowledge_bindings_free`. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_bindings_new() -> *mut CKnowledgeBindings { + Box::into_raw(Box::new(CKnowledgeBindings { + inner: MatchBindings::new(), + })) +} + +/// Frees match bindings. NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_bindings_free(ptr: *mut CKnowledgeBindings) { + if !ptr.is_null() { + unsafe { + drop(Box::from_raw(ptr)); + } + } +} + +// ===== Rule listing and lookup ===== + +/// Returns the number of rules in the library, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_rules_len(ptr: *const CKnowledgeLibrary) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.len() } +} + +/// Copies the rule ids (insertion indices) into `out` (two-step pattern; +/// call `knowledge_rules_len` first). Returns the total number of rules, or +/// 0 for NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_rules( + ptr: *const CKnowledgeLibrary, + out: *mut u32, + len: usize, +) -> usize { + if ptr.is_null() { + return 0; + } + let total = unsafe { (*ptr).inner.len() }; + let ids: Vec = (0..total).map(|i| i as u32).collect(); + crate::device::write_u32_list(&ids, out, len) +} + +/// Looks up a rule by name and returns a cloned handle (free with +/// `knowledge_rule_free`). Returns NULL on NULL input or when no rule with +/// that name exists. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_get_by_name( + ptr: *const CKnowledgeLibrary, + name: *const c_char, +) -> *mut CKnowledgeRule { + if ptr.is_null() || name.is_null() { + return std::ptr::null_mut(); + } + let Ok(name) = unsafe { CStr::from_ptr(name) }.to_str() else { + return std::ptr::null_mut(); + }; + match unsafe { (*ptr).inner.get_by_name(name) } { + Some(rule) => Box::into_raw(Box::new(CKnowledgeRule { + inner: rule.clone(), + })), + None => std::ptr::null_mut(), + } +} + +// ===== Matching ===== + +/// Matches the rule's match-pattern item at `item_index` against a concrete +/// operation and records the bound qubits and parameters into `bindings`. +/// +/// The match is transactional: when the item does not match, the bindings +/// are left unchanged. Returns 1 on a match, 0 on no match, -1 on NULL +/// input, -2 on an out-of-bounds `item_index`, -8 when the operation carries +/// indexed (table-resolved) parameters, and -6 when the rule item uses an +/// instruction the matcher cannot represent. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_match_rule_item( + ptr: *const CKnowledgeRule, + item_index: usize, + op: *const COperation, + bindings: *mut CKnowledgeBindings, +) -> i32 { + if ptr.is_null() || op.is_null() || bindings.is_null() { + return CqlibError::NullPtr as i32; + } + let Some(item) = unsafe { &(*ptr).inner }.operations.get(item_index) else { + return CqlibError::QubitOutOfBounds as i32; + }; + let operation = unsafe { &*op }; + let params = match operation_params_as_parameters(operation) { + Ok(params) => params, + Err(_) => return CqlibError::InvalidParam as i32, + }; + let concrete = ConcreteOperationView::new( + &operation.inner.instruction, + &operation.inner.qubits, + ¶ms, + ); + let bindings = unsafe { &mut *bindings }; + match match_rule_item(item, concrete, &mut bindings.inner) { + Ok(true) => 1, + Ok(false) => 0, + Err(err) => match_error_code(&err), + } +} + +/// Matches the rule's full match pattern against `operations_len` adjacent +/// operations. Parameter conditions are not evaluated here; check them +/// separately with `knowledge_conditions_hold` on the returned bindings. On +/// success writes a new `CKnowledgeBindings*` (free with +/// `knowledge_bindings_free`) to `out` and returns 1. Returns 0 when the +/// pattern does not match (with `out` left untouched), -1 on NULL input, and +/// -8 when `operations_len` differs from the pattern length or an operation +/// carries indexed parameters. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_rule_matches_operations( + ptr: *const CKnowledgeRule, + operations: *const *const COperation, + operations_len: usize, + out: *mut *mut CKnowledgeBindings, +) -> i32 { + if ptr.is_null() || out.is_null() || (operations_len > 0 && operations.is_null()) { + return CqlibError::NullPtr as i32; + } + let rule = unsafe { &(*ptr).inner }; + if rule.operations.len() != operations_len { + return CqlibError::InvalidParam as i32; + } + // Parameter tables are built first and never resized afterwards so the + // borrowed views below stay valid. + let mut param_tables = Vec::with_capacity(operations_len); + for i in 0..operations_len { + let handle = unsafe { *operations.add(i) }; + if handle.is_null() { + return CqlibError::NullPtr as i32; + } + match operation_params_as_parameters(unsafe { &*handle }) { + Ok(params) => param_tables.push(params), + Err(_) => return CqlibError::InvalidParam as i32, + } + } + let mut views = Vec::with_capacity(operations_len); + for (i, params) in param_tables.iter().enumerate() { + let operation = unsafe { &(*(*operations.add(i))).inner }; + views.push(ConcreteOperationView::new( + &operation.instruction, + &operation.qubits, + params, + )); + } + // Match every pattern item without evaluating conditions so callers can + // inspect and report condition failures through `knowledge_conditions_hold`. + let mut bindings = MatchBindings::new(); + for (item, view) in rule.operations.iter().zip(views.iter()) { + match match_rule_item(item, *view, &mut bindings) { + Ok(true) => {} + Ok(false) => return 0, + Err(err) => return match_error_code(&err), + } + } + unsafe { + *out = Box::into_raw(Box::new(CKnowledgeBindings { inner: bindings })); + } + 1 +} + +/// Returns 1 when all of the rule's parameter conditions hold under the +/// bindings, 0 when any fails, or -1 on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_conditions_hold( + ptr: *const CKnowledgeRule, + bindings: *const CKnowledgeBindings, +) -> i32 { + if ptr.is_null() || bindings.is_null() { + return CqlibError::NullPtr as i32; + } + let rule = unsafe { &(*ptr).inner }; + let bindings = unsafe { &(*bindings).inner }; + conditions_hold(rule.conditions.as_deref(), bindings) as i32 +} + +/// Returns 1 when the match-pattern item at `lhs_item` of `lhs` is +/// equivalent to the item at `rhs_item` of `rhs`, 0 when not, -1 on NULL +/// input, and -2 on an out-of-bounds item index. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_equivalent_to( + lhs: *const CKnowledgeRule, + lhs_item: usize, + rhs: *const CKnowledgeRule, + rhs_item: usize, +) -> i32 { + if lhs.is_null() || rhs.is_null() { + return CqlibError::NullPtr as i32; + } + let Some(left) = unsafe { &(*lhs).inner }.operations.get(lhs_item) else { + return CqlibError::QubitOutOfBounds as i32; + }; + let Some(right) = unsafe { &(*rhs).inner }.operations.get(rhs_item) else { + return CqlibError::QubitOutOfBounds as i32; + }; + left.equivalent_to(right) as i32 +} + +// ===== Candidate and instruction-key filtering ===== + +/// Returns the number of rules whose first match instruction has the same +/// matcher key as the gate `name`, or 0 for NULL input or an unknown gate +/// name. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_candidates_for_first_instruction_len( + ptr: *const CKnowledgeLibrary, + name: *const c_char, +) -> usize { + if ptr.is_null() { + return 0; + } + candidate_ids(unsafe { &*ptr }, name).len() +} + +/// Copies the candidate rule ids for the first-instruction gate `name` into +/// `out` (two-step pattern; call +/// `knowledge_candidates_for_first_instruction_len` first). Returns the +/// total number of candidates, or 0 for NULL input or an unknown gate name. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_candidates_for_first_instruction( + ptr: *const CKnowledgeLibrary, + name: *const c_char, + out: *mut u32, + len: usize, +) -> usize { + if ptr.is_null() { + return 0; + } + let ids = candidate_ids(unsafe { &*ptr }, name); + crate::device::write_u32_list(&ids, out, len) +} + +/// Returns the number of distinct free symbols in the rule, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_collect_free_symbols_len(ptr: *const CKnowledgeRule) -> usize { + if ptr.is_null() { + return 0; + } + sorted_free_symbols(unsafe { &(*ptr).inner }).len() +} + +/// Copies the rule's free symbols (sorted) into `out` as newly allocated C +/// strings (two-step pattern; call `knowledge_collect_free_symbols_len` +/// first). Each string must be released with `cqlib_string_free`. Returns +/// the total number of symbols, or 0 for NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_collect_free_symbols( + ptr: *const CKnowledgeRule, + out: *mut *mut c_char, + len: usize, +) -> usize { + if ptr.is_null() { + return 0; + } + let symbols = sorted_free_symbols(unsafe { &(*ptr).inner }); + write_cstring_list(&symbols, out, len) +} + +// ===== Rule symbol queries ===== + +/// Returns the number of distinct symbols bound by the rule's match block, +/// or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_rule_operation_symbols_len(ptr: *const CKnowledgeRule) -> usize { + if ptr.is_null() { + return 0; + } + sorted_symbols(unsafe { &(*ptr).inner }.operation_symbols()).len() +} + +/// Copies the symbols bound by the rule's match block (sorted) into `out` as +/// newly allocated C strings (two-step pattern; call +/// `knowledge_rule_operation_symbols_len` first). Each string must be +/// released with `cqlib_string_free`. Returns 0 on success, -1 on NULL +/// input, and -8 when `len` does not match the symbol count. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_rule_operation_symbols( + ptr: *const CKnowledgeRule, + out: *mut *mut c_char, + len: usize, +) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + let symbols = sorted_symbols(unsafe { &(*ptr).inner }.operation_symbols()); + if symbols.len() != len { + return CqlibError::InvalidParam as i32; + } + if len > 0 && out.is_null() { + return CqlibError::NullPtr as i32; + } + write_cstring_list(&symbols, out, len); + CqlibError::Ok as i32 +} + +/// Returns the number of distinct symbols referenced by the rule's rewrite +/// target, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_rule_target_symbols_len(ptr: *const CKnowledgeRule) -> usize { + if ptr.is_null() { + return 0; + } + sorted_symbols(unsafe { &(*ptr).inner }.target_symbols()).len() +} + +/// Copies the symbols referenced by the rule's rewrite target (sorted) into +/// `out` as newly allocated C strings (two-step pattern; call +/// `knowledge_rule_target_symbols_len` first). Each string must be released +/// with `cqlib_string_free`. Returns 0 on success, -1 on NULL input, and -8 +/// when `len` does not match the symbol count. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_rule_target_symbols( + ptr: *const CKnowledgeRule, + out: *mut *mut c_char, + len: usize, +) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + let symbols = sorted_symbols(unsafe { &(*ptr).inner }.target_symbols()); + if symbols.len() != len { + return CqlibError::InvalidParam as i32; + } + if len > 0 && out.is_null() { + return CqlibError::NullPtr as i32; + } + write_cstring_list(&symbols, out, len); + CqlibError::Ok as i32 +} + +/// Returns the number of distinct symbols referenced by the rule's +/// parameter conditions, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_rule_condition_symbols_len(ptr: *const CKnowledgeRule) -> usize { + if ptr.is_null() { + return 0; + } + sorted_symbols(unsafe { &(*ptr).inner }.condition_symbols()).len() +} + +/// Copies the symbols referenced by the rule's parameter conditions (sorted) +/// into `out` as newly allocated C strings (two-step pattern; call +/// `knowledge_rule_condition_symbols_len` first). Each string must be +/// released with `cqlib_string_free`. Returns 0 on success, -1 on NULL +/// input, and -8 when `len` does not match the symbol count. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_rule_condition_symbols( + ptr: *const CKnowledgeRule, + out: *mut *mut c_char, + len: usize, +) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + let symbols = sorted_symbols(unsafe { &(*ptr).inner }.condition_symbols()); + if symbols.len() != len { + return CqlibError::InvalidParam as i32; + } + if len > 0 && out.is_null() { + return CqlibError::NullPtr as i32; + } + write_cstring_list(&symbols, out, len); + CqlibError::Ok as i32 +} + +/// Returns the number of rules whose match pattern covers every gate in +/// `ops` and whose rewrite target covers every gate in `targets` (both as +/// arrays of gate names), or 0 for NULL input or an unknown gate name. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_filter_rule_ids_by_instruction_keys_len( + ptr: *const CKnowledgeLibrary, + ops: *const *const c_char, + ops_len: usize, + targets: *const *const c_char, + targets_len: usize, +) -> usize { + if ptr.is_null() { + return 0; + } + filtered_rule_ids(unsafe { &*ptr }, ops, ops_len, targets, targets_len).len() +} + +/// Copies the rule ids filtered by match-side (`ops`) and rewrite-side +/// (`targets`) gate names into `out` (two-step pattern; call +/// `knowledge_filter_rule_ids_by_instruction_keys_len` first). Returns the +/// total number of matching rules, or 0 for NULL input or an unknown gate +/// name. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_filter_rule_ids_by_instruction_keys( + ptr: *const CKnowledgeLibrary, + ops: *const *const c_char, + ops_len: usize, + targets: *const *const c_char, + targets_len: usize, + out: *mut u32, + len: usize, +) -> usize { + if ptr.is_null() { + return 0; + } + let ids = filtered_rule_ids(unsafe { &*ptr }, ops, ops_len, targets, targets_len); + crate::device::write_u32_list(&ids, out, len) +} + +// ===== Rule extension ===== + +/// Adds several parsed rules to the library under `kind` (one of +/// `RULE_KIND_*`) atomically and writes the assigned rule ids into `out` +/// (which must have room for `rules_len` entries). If any rule fails, the +/// library is left unchanged. Returns 0 on success, -1 on NULL input, -8 on +/// an unknown kind tag or an `out` buffer smaller than `rules_len`, and -4 +/// on a validation failure or duplicate rule name. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_extend_rules( + ptr: *mut CKnowledgeLibrary, + rules: *const *const CKnowledgeRule, + rules_len: usize, + kind: u8, + out: *mut u32, + out_len: usize, +) -> i32 { + if ptr.is_null() || (rules_len > 0 && rules.is_null()) { + return CqlibError::NullPtr as i32; + } + let Some(kind) = rule_kind_from_tag(kind) else { + return CqlibError::InvalidParam as i32; + }; + if out_len < rules_len { + return CqlibError::InvalidParam as i32; + } + let mut handles = Vec::with_capacity(rules_len); + for i in 0..rules_len { + let handle = unsafe { *rules.add(i) }; + if handle.is_null() { + return CqlibError::NullPtr as i32; + } + handles.push(unsafe { (*handle).inner.clone() }); + } + match unsafe { &mut *ptr }.inner.extend_rules(handles, kind) { + Ok(ids) => { + let values: Vec = ids.iter().map(|id| id.as_usize() as u32).collect(); + crate::device::write_u32_list(&values, out, out_len); + CqlibError::Ok as i32 + } + Err(err) => library_error_code(&err), + } +} + +// ===== Target queries and instantiation ===== + +/// Returns the number of distinct rule-local qubit labels used by the +/// rule's rewrite target, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_target_qubits_len(ptr: *const CKnowledgeRule) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.target_qubits().len() } +} + +/// Copies the rule-local qubit labels (sorted) used by the rule's rewrite +/// target into `out` (two-step pattern; call `knowledge_target_qubits_len` +/// first). Returns the total number of labels, or 0 for NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_target_qubits( + ptr: *const CKnowledgeRule, + out: *mut u32, + len: usize, +) -> usize { + if ptr.is_null() { + return 0; + } + let qubits: Vec = unsafe { (*ptr).inner.target_qubits() } + .into_iter() + .collect(); + crate::device::write_u32_list(&qubits, out, len) +} + +/// Instantiates the rule's rewrite target under the match bindings. On +/// success writes a new `CKnowledgeReplacements*` (free with +/// `knowledge_replacements_free`) to `out` and returns 0. Returns -1 on NULL +/// input, -2 when the target references a qubit not bound by the match, -8 +/// when the target references a symbol not bound by the match, and -6 when +/// the target uses an instruction the matcher cannot represent. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_instantiate_target( + ptr: *const CKnowledgeRule, + bindings: *const CKnowledgeBindings, + out: *mut *mut CKnowledgeReplacements, +) -> i32 { + if ptr.is_null() || bindings.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + let rule = unsafe { &(*ptr).inner }; + let bindings = unsafe { &(*bindings).inner }; + match instantiate_target(&rule.target, bindings) { + Ok(replacements) => { + unsafe { + *out = Box::into_raw(Box::new(CKnowledgeReplacements { + inner: replacements, + })); + } + CqlibError::Ok as i32 + } + Err(err) => match_error_code(&err), + } +} + +/// Returns the number of instantiated replacement items, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_replacements_len(ptr: *const CKnowledgeReplacements) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.len() } +} + +/// Returns the instruction name (e.g. "RZ") of the replacement item at +/// `index` as a newly allocated C string (free with `cqlib_string_free`). +/// Returns NULL on NULL input or an out-of-bounds index. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_replacement_instruction( + ptr: *const CKnowledgeReplacements, + index: usize, +) -> *mut c_char { + if ptr.is_null() { + return std::ptr::null_mut(); + } + match unsafe { &(*ptr).inner }.get(index) { + Some(replacement) => match CString::new(replacement.instruction.name()) { + Ok(text) => text.into_raw(), + Err(_) => std::ptr::null_mut(), + }, + None => std::ptr::null_mut(), + } +} + +/// Returns the number of concrete qubits of the replacement item at +/// `index`, or 0 for NULL input or an out-of-bounds index. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_replacement_qubits_len( + ptr: *const CKnowledgeReplacements, + index: usize, +) -> usize { + if ptr.is_null() { + return 0; + } + match unsafe { &(*ptr).inner }.get(index) { + Some(replacement) => replacement.qubits.len(), + None => 0, + } +} + +/// Copies the concrete qubits of the replacement item at `index` into `out` +/// (two-step pattern; call `knowledge_replacement_qubits_len` first). +/// Returns the total number of qubits, or 0 for NULL input or an +/// out-of-bounds index. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_replacement_qubits( + ptr: *const CKnowledgeReplacements, + index: usize, + out: *mut u32, + len: usize, +) -> usize { + if ptr.is_null() { + return 0; + } + let Some(replacement) = unsafe { &(*ptr).inner }.get(index) else { + return 0; + }; + let qubits: Vec = replacement.qubits.iter().map(|qubit| qubit.id()).collect(); + crate::device::write_u32_list(&qubits, out, len) +} + +/// Returns the number of evaluated parameters of the replacement item at +/// `index`, or 0 for NULL input or an out-of-bounds index. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_replacement_params_len( + ptr: *const CKnowledgeReplacements, + index: usize, +) -> usize { + if ptr.is_null() { + return 0; + } + match unsafe { &(*ptr).inner }.get(index) { + Some(replacement) => replacement.params.len(), + None => 0, + } +} + +/// Copies the evaluated double parameters of the replacement item at +/// `index` into `out` (two-step pattern; call +/// `knowledge_replacement_params_len` first). Returns the total number of +/// parameters, or 0 for NULL input or an out-of-bounds index. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_replacement_params( + ptr: *const CKnowledgeReplacements, + index: usize, + out: *mut f64, + len: usize, +) -> usize { + if ptr.is_null() { + return 0; + } + let Some(replacement) = unsafe { &(*ptr).inner }.get(index) else { + return 0; + }; + let values: Vec = replacement + .params + .iter() + .map(replacement_param_value) + .collect(); + let total = values.len(); + if !out.is_null() { + for (i, value) in values.iter().take(total.min(len)).enumerate() { + unsafe { *out.add(i) = *value }; + } + } + total +} + +/// Frees a rewrite-target instantiation result. NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_replacements_free(ptr: *mut CKnowledgeReplacements) { + if !ptr.is_null() { + unsafe { + drop(Box::from_raw(ptr)); + } + } +} diff --git a/crates/binding-c/src/compile/layout.rs b/crates/binding-c/src/compile/layout.rs new file mode 100644 index 00000000..c2ad1d5f --- /dev/null +++ b/crates/binding-c/src/compile/layout.rs @@ -0,0 +1,1031 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! C ABI for initial-layout algorithms and layout analysis tools. + +use crate::circuit::CCircuit; +use crate::compile::sabre::{SabreConfigC, resolve_sabre_config}; +use crate::device::{CDevice, CLayout, standard_gate_from_name, write_u32_list}; +use crate::error::CqlibError; +use cqlib_core::compile::CompilerError; +use cqlib_core::compile::physical_target::PhysicalLayoutGraph; +use cqlib_core::compile::transform::layout::analyze_circuit_for_layout as core_analyze_circuit_for_layout; +use cqlib_core::compile::transform::layout::{ + CircuitLayoutAnalysis, Interaction, LayoutObjective, LayoutResult, LayoutScore, + PreparedSabreCircuit, PreparedSabreTarget, Vf2EdgeRequirement, Vf2LayoutConfig, +}; +use cqlib_core::compile::transform::layout::{ + greedy_layout as core_greedy_layout, greedy_layout_prepared as core_greedy_layout_prepared, +}; +use cqlib_core::compile::transform::layout::{ + prepare_sabre_circuit as core_prepare_sabre_circuit, + prepare_sabre_device_target as core_prepare_sabre_device_target, +}; +use cqlib_core::compile::transform::layout::{ + sabre_layout as core_sabre_layout, sabre_layout_prepared as core_sabre_layout_prepared, +}; +use cqlib_core::compile::transform::layout::{ + trivial_layout as core_trivial_layout, trivial_layout_prepared as core_trivial_layout_prepared, +}; +use cqlib_core::compile::transform::layout::{ + vf2_perfect_layout as core_vf2_perfect_layout, + vf2_perfect_layout_prepared as core_vf2_perfect_layout_prepared, +}; +use cqlib_core::device::{Device, PhysicalQubit}; +use std::ffi::CString; +use std::os::raw::c_char; + +/// Opaque handle around a [`LayoutResult`]. +pub struct CLayoutResult { + pub inner: LayoutResult, +} + +/// Opaque handle around a [`CircuitLayoutAnalysis`]. +pub struct CCircuitLayoutAnalysis { + pub inner: CircuitLayoutAnalysis, +} + +/// Opaque handle around a [`PhysicalLayoutGraph`]. +pub struct CPhysicalLayoutGraph { + pub inner: PhysicalLayoutGraph, +} + +/// Opaque handle around a [`PreparedSabreCircuit`]. +pub struct CPreparedSabreCircuit { + pub inner: PreparedSabreCircuit, +} + +/// Opaque handle around a [`PreparedSabreTarget`]. +pub struct CPreparedSabreTarget { + pub inner: PreparedSabreTarget, +} + +/// C snapshot of a [`LayoutScore`]. +#[repr(C)] +pub struct CLayoutScore { + /// Weighted total according to the objective. + pub total: f64, + /// Raw weighted-distance component. + pub distance: f64, + /// Raw direction-mismatch component. + pub direction: f64, + /// Unweighted effective two-qubit placement cost. + pub two_qubit_error: f64, + /// Raw readout error component. + pub readout_error: f64, + /// 1 when the objective used fidelity terms. + pub used_fidelity: u8, + /// Reserved padding to keep the struct layout stable. + pub _reserved: [u8; 7], +} + +/// C snapshot of one logical-qubit interaction. +#[repr(C)] +pub struct CInteraction { + /// Lower-sorted logical qubit endpoint. + pub left: u32, + /// Higher-sorted logical qubit endpoint. + pub right: u32, + /// Total interaction weight for this unordered pair. + pub weight: f64, + /// Weight observed in operation order `left -> right`. + pub directed_weight_left_to_right: f64, + /// Weight observed in operation order `right -> left`. + pub directed_weight_right_to_left: f64, + /// First appearance index in operation order. + pub first_seen_order: usize, +} + +/// Layout objective tag. +/// | Value | Objective | +/// |-------|--------------------------------------------------| +/// | 0 | Topology only (distance + direction mismatch) | +/// | 1 | Fidelity aware (adds error-rate terms) | +/// | 2 | Auto from the device's calibration data | +/// | 3 | Fidelity required (errors without calibration) | +pub const LAYOUT_OBJECTIVE_TOPOLOGY_ONLY: u8 = 0; +pub const LAYOUT_OBJECTIVE_FIDELITY_AWARE: u8 = 1; +pub const LAYOUT_OBJECTIVE_AUTO: u8 = 2; +pub const LAYOUT_OBJECTIVE_FIDELITY_REQUIRED: u8 = 3; + +/// VF2 edge-requirement tag. +/// | Value | Requirement | +/// |-------|------------------------------| +/// | 0 | Positive interactions only | +/// | 1 | All interactions | +pub const VF2_EDGE_REQUIREMENT_POSITIVE_INTERACTIONS: u8 = 0; +pub const VF2_EDGE_REQUIREMENT_ALL_INTERACTIONS: u8 = 1; + +/// Sentinel for `CVf2LayoutConfig::call_limit` meaning "no explicit limit". +pub const VF2_CALL_LIMIT_NONE: usize = usize::MAX; + +/// C form of [`Vf2LayoutConfig`]. +#[repr(C)] +pub struct CVf2LayoutConfig { + /// Maximum number of complete perfect candidates to score. + pub candidate_limit: usize, + /// Maximum number of partial mapping extensions attempted + /// (`VF2_CALL_LIMIT_NONE` means no explicit limit). + pub call_limit: usize, + /// One of `VF2_EDGE_REQUIREMENT_*`. + pub edge_requirement: u8, + /// Reserved padding to keep the struct layout stable. + pub _reserved: [u8; 7], +} + +/// Resolves a layout-objective tag against an optional prepared physical +/// graph and/or device. +/// +/// `LAYOUT_OBJECTIVE_AUTO` resolves `LayoutObjective::auto_from_physical` +/// against the physical graph (derived from `device` when no prepared graph +/// is supplied). `LAYOUT_OBJECTIVE_FIDELITY_REQUIRED` resolves +/// `LayoutObjective::fidelity_required` and errors when the target has no +/// usable calibration data. Returns `Ok(None)` for an unknown tag or when the +/// physical graph cannot be derived from the device. +pub(crate) fn objective_from_tag( + tag: u8, + physical: Option<&PhysicalLayoutGraph>, + device: Option<&Device>, +) -> Result, CompilerError> { + match tag { + LAYOUT_OBJECTIVE_TOPOLOGY_ONLY => Ok(Some(LayoutObjective::topology_only())), + LAYOUT_OBJECTIVE_FIDELITY_AWARE => Ok(Some(LayoutObjective::fidelity_aware())), + LAYOUT_OBJECTIVE_AUTO => { + let physical = match physical { + Some(physical) => Some(physical.clone()), + None => device.and_then(|device| PhysicalLayoutGraph::from_device(device).ok()), + }; + Ok(physical.map(|physical| LayoutObjective::auto_from_physical(&physical))) + } + LAYOUT_OBJECTIVE_FIDELITY_REQUIRED => { + let physical = match physical { + Some(physical) => physical.clone(), + None => { + let Some(physical) = + device.and_then(|device| PhysicalLayoutGraph::from_device(device).ok()) + else { + return Ok(None); + }; + physical + } + }; + LayoutObjective::fidelity_required(&physical).map(Some) + } + _ => Ok(None), + } +} + +/// Converts a C VF2 layout configuration pointer into the core form. +/// A NULL pointer selects the core default. Returns None for an unknown +/// edge-requirement tag. +fn vf2_config_from_c(config: *const CVf2LayoutConfig) -> Option { + if config.is_null() { + return Some(Vf2LayoutConfig::default()); + } + let raw = unsafe { &*config }; + let edge_requirement = match raw.edge_requirement { + VF2_EDGE_REQUIREMENT_POSITIVE_INTERACTIONS => Vf2EdgeRequirement::PositiveInteractions, + VF2_EDGE_REQUIREMENT_ALL_INTERACTIONS => Vf2EdgeRequirement::AllInteractions, + _ => return None, + }; + Some(Vf2LayoutConfig { + candidate_limit: raw.candidate_limit, + call_limit: if raw.call_limit == VF2_CALL_LIMIT_NONE { + None + } else { + Some(raw.call_limit) + }, + edge_requirement, + }) +} + +/// Returns the default VF2 layout configuration by value. +#[unsafe(no_mangle)] +pub extern "C" fn vf2_layout_config_default() -> CVf2LayoutConfig { + CVf2LayoutConfig { + candidate_limit: 10, + call_limit: VF2_CALL_LIMIT_NONE, + edge_requirement: VF2_EDGE_REQUIREMENT_POSITIVE_INTERACTIONS, + _reserved: [0; 7], + } +} + +// ===== Layout entry points ===== + +/// Maps logical qubits to usable physical qubits in their existing order. +/// +/// `objective` is one of the `LAYOUT_OBJECTIVE_*` tags. Returns a +/// heap-allocated `CLayoutResult*`, or NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn trivial_layout( + circuit: *const CCircuit, + device: *const CDevice, + objective: u8, +) -> *mut CLayoutResult { + if circuit.is_null() || device.is_null() { + return std::ptr::null_mut(); + } + let device = unsafe { &(*device).inner }; + let Ok(Some(objective)) = objective_from_tag(objective, None, Some(device)) else { + return std::ptr::null_mut(); + }; + let circuit = unsafe { &(*circuit).inner }; + match core_trivial_layout(circuit, device, &objective) { + Ok(result) => Box::into_raw(Box::new(CLayoutResult { inner: result })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Builds a greedy layout by placing qubits in interaction-strength order. +/// +/// `objective` is one of the `LAYOUT_OBJECTIVE_*` tags. Returns a +/// heap-allocated `CLayoutResult*`, or NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn greedy_layout( + circuit: *const CCircuit, + device: *const CDevice, + objective: u8, +) -> *mut CLayoutResult { + if circuit.is_null() || device.is_null() { + return std::ptr::null_mut(); + } + let device = unsafe { &(*device).inner }; + let Ok(Some(objective)) = objective_from_tag(objective, None, Some(device)) else { + return std::ptr::null_mut(); + }; + let circuit = unsafe { &(*circuit).inner }; + match core_greedy_layout(circuit, device, &objective) { + Ok(result) => Box::into_raw(Box::new(CLayoutResult { inner: result })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Searches for a perfect initial layout using non-induced VF2++ matching. +/// +/// `objective` is one of the `LAYOUT_OBJECTIVE_*` tags; `config` may be NULL +/// to use the default. Returns a heap-allocated `CLayoutResult*`, or NULL +/// when no perfect mapping exists or on error. +#[unsafe(no_mangle)] +pub extern "C" fn vf2_perfect_layout( + circuit: *const CCircuit, + device: *const CDevice, + objective: u8, + config: *const CVf2LayoutConfig, +) -> *mut CLayoutResult { + if circuit.is_null() || device.is_null() { + return std::ptr::null_mut(); + } + let device = unsafe { &(*device).inner }; + let Ok(Some(objective)) = objective_from_tag(objective, None, Some(device)) else { + return std::ptr::null_mut(); + }; + let Some(config) = vf2_config_from_c(config) else { + return std::ptr::null_mut(); + }; + let circuit = unsafe { &(*circuit).inner }; + match core_vf2_perfect_layout(circuit, device, &objective, &config) { + Ok(result) => Box::into_raw(Box::new(CLayoutResult { inner: result })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Selects an initial layout using SABRE layout refinement. +/// +/// `objective` is one of the `LAYOUT_OBJECTIVE_*` tags; `config` may be NULL +/// to use the default. Returns a heap-allocated `CLayoutResult*`, or NULL on +/// error. +#[unsafe(no_mangle)] +pub extern "C" fn sabre_layout( + circuit: *const CCircuit, + device: *const CDevice, + objective: u8, + config: *const SabreConfigC, +) -> *mut CLayoutResult { + if circuit.is_null() || device.is_null() { + return std::ptr::null_mut(); + } + let device = unsafe { &(*device).inner }; + let Ok(Some(objective)) = objective_from_tag(objective, None, Some(device)) else { + return std::ptr::null_mut(); + }; + let config = match resolve_sabre_config(config) { + Ok(config) => config, + Err(_) => return std::ptr::null_mut(), + }; + let circuit = unsafe { &(*circuit).inner }; + match core_sabre_layout(circuit, device, &objective, &config) { + Ok(result) => Box::into_raw(Box::new(CLayoutResult { inner: result })), + Err(_) => std::ptr::null_mut(), + } +} + +// ===== Prepared entry points ===== + +/// Analyzes a circuit for layout planning (weighted logical interactions). +/// +/// Returns a heap-allocated `CCircuitLayoutAnalysis*`, or NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn analyze_circuit_for_layout( + circuit: *const CCircuit, +) -> *mut CCircuitLayoutAnalysis { + if circuit.is_null() { + return std::ptr::null_mut(); + } + let circuit = unsafe { &(*circuit).inner }; + match core_analyze_circuit_for_layout(circuit) { + Ok(analysis) => Box::into_raw(Box::new(CCircuitLayoutAnalysis { inner: analysis })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Frees a circuit layout analysis. NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_layout_analysis_free(ptr: *mut CCircuitLayoutAnalysis) { + if !ptr.is_null() { + unsafe { + let _ = Box::from_raw(ptr); + } + } +} + +/// Returns the number of logical qubits in the analysis, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_layout_analysis_num_logical(ptr: *const CCircuitLayoutAnalysis) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.logical_qubits.len() } +} + +/// Copies the logical qubit IDs in source-circuit order into `out` +/// (two-step pattern; pair with `circuit_layout_analysis_num_logical`). +/// Returns the total count. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_layout_analysis_logical_qubits( + ptr: *const CCircuitLayoutAnalysis, + out: *mut u32, + len: usize, +) -> usize { + if ptr.is_null() { + return 0; + } + let items: Vec = unsafe { (*ptr).inner.logical_qubits.iter().map(|q| q.id()).collect() }; + write_u32_list(&items, out, len) +} + +/// Returns the number of weighted logical interactions, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_layout_analysis_interactions_len( + ptr: *const CCircuitLayoutAnalysis, +) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.interactions.interactions().len() } +} + +/// Writes the interaction at `index` to `*out`. +/// +/// Returns 0 on success, -1 on NULL arguments, or -8 when `index` is out of +/// bounds. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_layout_analysis_interaction( + ptr: *const CCircuitLayoutAnalysis, + index: usize, + out: *mut CInteraction, +) -> i32 { + if ptr.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + let analysis = unsafe { &(*ptr).inner }; + match analysis.interactions.interactions().get(index) { + Some(interaction) => { + write_interaction(interaction, out); + CqlibError::Ok as i32 + } + None => CqlibError::InvalidParam as i32, + } +} + +/// Writes one interaction snapshot. +fn write_interaction(interaction: &Interaction, out: *mut CInteraction) { + unsafe { + *out = CInteraction { + left: interaction.left.id(), + right: interaction.right.id(), + weight: interaction.weight, + directed_weight_left_to_right: interaction.directed_weight_left_to_right, + directed_weight_right_to_left: interaction.directed_weight_right_to_left, + first_seen_order: interaction.first_seen_order, + }; + } +} + +// ===== Physical layout graph ===== + +/// Builds the compiler-local physical topology and calibration view of a +/// device. Returns a heap-allocated `CPhysicalLayoutGraph*`, or NULL on +/// error. +#[unsafe(no_mangle)] +pub extern "C" fn physical_layout_graph_from_device( + device: *const CDevice, +) -> *mut CPhysicalLayoutGraph { + if device.is_null() { + return std::ptr::null_mut(); + } + let device = unsafe { &(*device).inner }; + match PhysicalLayoutGraph::from_device(device) { + Ok(graph) => Box::into_raw(Box::new(CPhysicalLayoutGraph { inner: graph })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Frees a physical layout graph. NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn physical_layout_graph_free(ptr: *mut CPhysicalLayoutGraph) { + if !ptr.is_null() { + unsafe { + let _ = Box::from_raw(ptr); + } + } +} + +/// Returns the number of usable physical qubits, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn physical_layout_graph_num_physical(ptr: *const CPhysicalLayoutGraph) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.physical_qubits().len() } +} + +/// Copies the usable physical qubit IDs in ascending order into `out` +/// (two-step pattern; pair with `physical_layout_graph_num_physical`). +/// Returns the total count. +#[unsafe(no_mangle)] +pub extern "C" fn physical_layout_graph_physical_qubits( + ptr: *const CPhysicalLayoutGraph, + out: *mut u32, + len: usize, +) -> usize { + if ptr.is_null() { + return 0; + } + let items: Vec = unsafe { (*ptr).inner.physical_qubits() } + .iter() + .map(|q| q.id()) + .collect(); + write_u32_list(&items, out, len) +} + +/// Returns the undirected shortest-path distance between two physical +/// qubits, or `UINT32_MAX` when the pair is disconnected or unknown. +#[unsafe(no_mangle)] +pub extern "C" fn physical_layout_graph_distance( + ptr: *const CPhysicalLayoutGraph, + a: u32, + b: u32, +) -> u32 { + if ptr.is_null() { + return u32::MAX; + } + let graph = unsafe { &(*ptr).inner }; + graph + .distance(PhysicalQubit::new(a), PhysicalQubit::new(b)) + .unwrap_or(u32::MAX) +} + +/// Returns 1 when the two physical qubits are adjacent (undirected), 0 +/// otherwise, or -1 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn physical_layout_graph_is_adjacent_undirected( + ptr: *const CPhysicalLayoutGraph, + a: u32, + b: u32, +) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + let graph = unsafe { &(*ptr).inner }; + graph.is_adjacent_undirected(PhysicalQubit::new(a), PhysicalQubit::new(b)) as i32 +} + +/// Returns the readout error rate of one physical qubit. +/// +/// Returns 0 with the value in `*out`, or -8 when no readout data is +/// recorded for the qubit. +#[unsafe(no_mangle)] +pub extern "C" fn physical_layout_graph_readout_error( + ptr: *const CPhysicalLayoutGraph, + qubit: u32, + out: *mut f64, +) -> i32 { + if ptr.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + let graph = unsafe { &(*ptr).inner }; + match graph.readout_error(PhysicalQubit::new(qubit)) { + Some(error) => { + unsafe { *out = error }; + CqlibError::Ok as i32 + } + None => CqlibError::InvalidParam as i32, + } +} + +/// Returns 1 when the directed coupling `control -> target` exists, 0 +/// otherwise, or -1 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn physical_layout_graph_supports_directed_coupling( + ptr: *const CPhysicalLayoutGraph, + control: u32, + target: u32, +) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + let graph = unsafe { &(*ptr).inner }; + graph.supports_directed_coupling(PhysicalQubit::new(control), PhysicalQubit::new(target)) as i32 +} + +/// Returns 1 when `gate_name` is a native capability on the directed edge +/// `control -> target`, 0 when not, -1 on NULL, or -4 for an unknown gate +/// name. +#[unsafe(no_mangle)] +pub extern "C" fn physical_layout_graph_supports_two_qubit_gate_directed( + ptr: *const CPhysicalLayoutGraph, + gate_name: *const c_char, + control: u32, + target: u32, +) -> i32 { + let gate = match parse_gate_name(gate_name) { + Ok(gate) => gate, + Err(code) => return code, + }; + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + let graph = unsafe { &(*ptr).inner }; + graph.supports_two_qubit_gate_directed( + PhysicalQubit::new(control), + PhysicalQubit::new(target), + gate, + ) as i32 +} + +/// Returns the calibrated error rate of `gate_name` on the directed edge +/// `control -> target`. +/// +/// Returns 0 with the value in `*out`, -8 when the gate is unsupported or +/// uncalibrated on the edge, -1 on NULL, or -4 for an unknown gate name. +#[unsafe(no_mangle)] +pub extern "C" fn physical_layout_graph_two_qubit_gate_error_directed( + ptr: *const CPhysicalLayoutGraph, + gate_name: *const c_char, + control: u32, + target: u32, + out: *mut f64, +) -> i32 { + let gate = match parse_gate_name(gate_name) { + Ok(gate) => gate, + Err(code) => return code, + }; + if ptr.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + let graph = unsafe { &(*ptr).inner }; + match graph.two_qubit_gate_error_directed( + PhysicalQubit::new(control), + PhysicalQubit::new(target), + gate, + ) { + Some(error) => { + unsafe { *out = error }; + CqlibError::Ok as i32 + } + None => CqlibError::InvalidParam as i32, + } +} + +/// Returns 1 when any readout or two-qubit calibration data is available, 0 +/// otherwise, or -1 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn physical_layout_graph_has_fidelity_data(ptr: *const CPhysicalLayoutGraph) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + unsafe { (*ptr).inner.has_fidelity_data() as i32 } +} + +/// Returns 1 when readout-error data is available, 0 otherwise, or -1 for +/// NULL. +#[unsafe(no_mangle)] +pub extern "C" fn physical_layout_graph_has_readout_error_data( + ptr: *const CPhysicalLayoutGraph, +) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + unsafe { (*ptr).inner.has_readout_error_data() as i32 } +} + +/// Returns 1 when two-qubit error data is available, 0 otherwise, or -1 for +/// NULL. +#[unsafe(no_mangle)] +pub extern "C" fn physical_layout_graph_has_two_qubit_error_data( + ptr: *const CPhysicalLayoutGraph, +) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + unsafe { (*ptr).inner.has_two_qubit_error_data() as i32 } +} + +/// Resolves a two-qubit standard-gate name to a `StandardGate`. +fn parse_gate_name( + gate_name: *const c_char, +) -> Result { + if gate_name.is_null() { + return Err(CqlibError::NullPtr as i32); + } + let name = match unsafe { std::ffi::CStr::from_ptr(gate_name) }.to_str() { + Ok(name) => name, + Err(_) => return Err(CqlibError::ParseError as i32), + }; + standard_gate_from_name(name).ok_or(CqlibError::ParseError as i32) +} + +// ===== Prepared SABRE circuit/target ===== + +/// Prepares the circuit-side analysis and dependency models used by SABRE. +/// +/// Returns a heap-allocated `CPreparedSabreCircuit*`, or NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn prepare_sabre_circuit(circuit: *const CCircuit) -> *mut CPreparedSabreCircuit { + if circuit.is_null() { + return std::ptr::null_mut(); + } + let circuit = unsafe { &(*circuit).inner }; + match core_prepare_sabre_circuit(circuit) { + Ok(prepared) => Box::into_raw(Box::new(CPreparedSabreCircuit { inner: prepared })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Frees a prepared SABRE circuit. NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn prepared_sabre_circuit_free(ptr: *mut CPreparedSabreCircuit) { + if !ptr.is_null() { + unsafe { + let _ = Box::from_raw(ptr); + } + } +} + +/// Returns the number of logical qubits of the prepared circuit, or 0 for +/// NULL. +#[unsafe(no_mangle)] +pub extern "C" fn prepared_sabre_circuit_logical_qubits_len( + ptr: *const CPreparedSabreCircuit, +) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.logical_qubits().len() } +} + +/// Copies the prepared circuit's logical qubit IDs in source-circuit order +/// into `out` (two-step pattern; pair with +/// `prepared_sabre_circuit_logical_qubits_len`). Returns the total count. +#[unsafe(no_mangle)] +pub extern "C" fn prepared_sabre_circuit_logical_qubits( + ptr: *const CPreparedSabreCircuit, + out: *mut u32, + len: usize, +) -> usize { + if ptr.is_null() { + return 0; + } + let items: Vec = unsafe { (*ptr).inner.logical_qubits() } + .iter() + .map(|q| q.id()) + .collect(); + write_u32_list(&items, out, len) +} + +/// Prepares exact device-native SABRE data for a prepared circuit. +/// +/// Returns a heap-allocated `CPreparedSabreTarget*`, or NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn prepare_sabre_device_target( + prepared: *const CPreparedSabreCircuit, + device: *const CDevice, +) -> *mut CPreparedSabreTarget { + if prepared.is_null() || device.is_null() { + return std::ptr::null_mut(); + } + let prepared = unsafe { &(*prepared).inner }; + let device = unsafe { &(*device).inner }; + match core_prepare_sabre_device_target(prepared, device) { + Ok(target) => Box::into_raw(Box::new(CPreparedSabreTarget { inner: target })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Frees a prepared SABRE target. NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn prepared_sabre_target_free(ptr: *mut CPreparedSabreTarget) { + if !ptr.is_null() { + unsafe { + let _ = Box::from_raw(ptr); + } + } +} + +/// Returns the physical graph used by layout objective scoring, as an owned +/// `CPhysicalLayoutGraph*` (free with `physical_layout_graph_free`). +/// Returns NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn prepared_sabre_target_physical( + ptr: *const CPreparedSabreTarget, +) -> *mut CPhysicalLayoutGraph { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let target = unsafe { &*ptr }; + Box::into_raw(Box::new(CPhysicalLayoutGraph { + inner: target.inner.physical().clone(), + })) +} + +// ===== Prepared layout algorithms ===== + +/// Trivial layout over a prepared analysis and physical graph. +/// +/// `objective` is one of the `LAYOUT_OBJECTIVE_*` tags. Returns a +/// heap-allocated `CLayoutResult*`, or NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn trivial_layout_prepared( + analysis: *const CCircuitLayoutAnalysis, + physical: *const CPhysicalLayoutGraph, + objective: u8, +) -> *mut CLayoutResult { + if analysis.is_null() || physical.is_null() { + return std::ptr::null_mut(); + } + let physical = unsafe { &(*physical).inner }; + let Ok(Some(objective)) = objective_from_tag(objective, Some(physical), None) else { + return std::ptr::null_mut(); + }; + let analysis = unsafe { &(*analysis).inner }; + match core_trivial_layout_prepared(analysis, physical, &objective) { + Ok(result) => Box::into_raw(Box::new(CLayoutResult { inner: result })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Greedy layout over a prepared analysis and physical graph. +#[unsafe(no_mangle)] +pub extern "C" fn greedy_layout_prepared( + analysis: *const CCircuitLayoutAnalysis, + physical: *const CPhysicalLayoutGraph, + objective: u8, +) -> *mut CLayoutResult { + if analysis.is_null() || physical.is_null() { + return std::ptr::null_mut(); + } + let physical = unsafe { &(*physical).inner }; + let Ok(Some(objective)) = objective_from_tag(objective, Some(physical), None) else { + return std::ptr::null_mut(); + }; + let analysis = unsafe { &(*analysis).inner }; + match core_greedy_layout_prepared(analysis, physical, &objective) { + Ok(result) => Box::into_raw(Box::new(CLayoutResult { inner: result })), + Err(_) => std::ptr::null_mut(), + } +} + +/// VF2 perfect layout over a prepared analysis and physical graph. +/// +/// `config` may be NULL to use the default. +#[unsafe(no_mangle)] +pub extern "C" fn vf2_perfect_layout_prepared( + analysis: *const CCircuitLayoutAnalysis, + physical: *const CPhysicalLayoutGraph, + objective: u8, + config: *const CVf2LayoutConfig, +) -> *mut CLayoutResult { + if analysis.is_null() || physical.is_null() { + return std::ptr::null_mut(); + } + let physical = unsafe { &(*physical).inner }; + let Ok(Some(objective)) = objective_from_tag(objective, Some(physical), None) else { + return std::ptr::null_mut(); + }; + let Some(config) = vf2_config_from_c(config) else { + return std::ptr::null_mut(); + }; + let analysis = unsafe { &(*analysis).inner }; + match core_vf2_perfect_layout_prepared(analysis, physical, &objective, &config) { + Ok(result) => Box::into_raw(Box::new(CLayoutResult { inner: result })), + Err(_) => std::ptr::null_mut(), + } +} + +/// SABRE layout over a prepared circuit and prepared target. +/// +/// `config` may be NULL to use the default. +#[unsafe(no_mangle)] +pub extern "C" fn sabre_layout_prepared( + prepared: *const CPreparedSabreCircuit, + prepared_target: *const CPreparedSabreTarget, + objective: u8, + config: *const SabreConfigC, +) -> *mut CLayoutResult { + if prepared.is_null() || prepared_target.is_null() { + return std::ptr::null_mut(); + } + let prepared = unsafe { &(*prepared).inner }; + let prepared_target = unsafe { &(*prepared_target).inner }; + let Ok(Some(objective)) = objective_from_tag(objective, Some(prepared_target.physical()), None) + else { + return std::ptr::null_mut(); + }; + let config = match resolve_sabre_config(config) { + Ok(config) => config, + Err(_) => return std::ptr::null_mut(), + }; + match core_sabre_layout_prepared(prepared, prepared_target, &objective, &config) { + Ok(result) => Box::into_raw(Box::new(CLayoutResult { inner: result })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Scores a candidate layout against circuit and physical-graph analysis. +/// +/// Returns 0 with the score written to `*out`, -1 on NULL arguments, -8 for +/// an unknown objective tag, or -6 when the core scoring fails. +#[unsafe(no_mangle)] +pub extern "C" fn score_layout( + objective: u8, + analysis: *const CCircuitLayoutAnalysis, + physical: *const CPhysicalLayoutGraph, + layout: *const CLayout, + out: *mut CLayoutScore, +) -> i32 { + if analysis.is_null() || physical.is_null() || layout.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + let physical = unsafe { &(*physical).inner }; + let objective = match objective_from_tag(objective, Some(physical), None) { + Ok(Some(objective)) => objective, + Ok(None) => return CqlibError::InvalidParam as i32, + Err(CompilerError::InvalidInput(_)) => return CqlibError::InvalidParam as i32, + Err(_) => return CqlibError::CompilerError as i32, + }; + let analysis = unsafe { &(*analysis).inner }; + let layout = unsafe { &(*layout).inner }; + match objective.score_layout(analysis, physical, layout) { + Ok(score) => { + write_layout_score(&score, out); + CqlibError::Ok as i32 + } + Err(_) => CqlibError::CompilerError as i32, + } +} + +/// Writes one layout-score snapshot. +pub(crate) fn write_layout_score(score: &LayoutScore, out: *mut CLayoutScore) { + unsafe { + *out = CLayoutScore { + total: score.total, + distance: score.distance, + direction: score.direction, + two_qubit_error: score.two_qubit_error, + readout_error: score.readout_error, + used_fidelity: score.used_fidelity as u8, + _reserved: [0; 7], + }; + } +} + +// ===== LayoutResult accessors ===== + +/// Frees a layout result. NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn layout_result_free(ptr: *mut CLayoutResult) { + if !ptr.is_null() { + unsafe { + let _ = Box::from_raw(ptr); + } + } +} + +/// Returns the selected logical-to-physical mapping as an owned `CLayout*` +/// (free with `layout_free`). Returns NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn layout_result_layout(ptr: *const CLayoutResult) -> *mut CLayout { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let result = unsafe { &*ptr }; + Box::into_raw(Box::new(CLayout { + inner: result.inner.layout.clone(), + })) +} + +/// Returns 1 when the result carries an observed score, 0 otherwise, or -1 +/// for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn layout_result_has_score(ptr: *const CLayoutResult) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + unsafe { (*ptr).inner.score.is_some() as i32 } +} + +/// Writes the layout-score snapshot to `*out`. +/// +/// Returns 0 on success, -1 on NULL arguments, or -8 when the result has no +/// score. +#[unsafe(no_mangle)] +pub extern "C" fn layout_result_score(ptr: *const CLayoutResult, out: *mut CLayoutScore) -> i32 { + if ptr.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + let result = unsafe { &*ptr }; + match &result.inner.score { + Some(score) => { + write_layout_score(score, out); + CqlibError::Ok as i32 + } + None => CqlibError::InvalidParam as i32, + } +} + +/// Returns 1 when every positive-weight interaction lands on an adjacent +/// hardware edge, 0 otherwise, or -1 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn layout_result_is_perfect(ptr: *const CLayoutResult) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + unsafe { (*ptr).inner.diagnostics.is_perfect as i32 } +} + +/// Returns the number of candidate layouts considered by the method, or 0 +/// for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn layout_result_candidates_evaluated(ptr: *const CLayoutResult) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.diagnostics.candidates_evaluated } +} + +/// Returns 1 when fidelity data contributed to the selected score, 0 +/// otherwise, or -1 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn layout_result_used_fidelity(ptr: *const CLayoutResult) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + unsafe { (*ptr).inner.diagnostics.used_fidelity as i32 } +} + +/// Returns the number of diagnostic notes, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn layout_result_notes_len(ptr: *const CLayoutResult) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.diagnostics.notes.len() } +} + +/// Returns the diagnostic note at `index` as a heap-allocated C string. +/// +/// Caller must free with `cqlib_string_free`. Returns NULL on out-of-bounds +/// or error. +#[unsafe(no_mangle)] +pub extern "C" fn layout_result_note(ptr: *const CLayoutResult, index: usize) -> *mut c_char { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let result = unsafe { &*ptr }; + match result.inner.diagnostics.notes.get(index) { + Some(note) => match CString::new(note.as_str()) { + Ok(cs) => cs.into_raw(), + Err(_) => std::ptr::null_mut(), + }, + None => std::ptr::null_mut(), + } +} diff --git a/crates/binding-c/src/compile/mod.rs b/crates/binding-c/src/compile/mod.rs new file mode 100644 index 00000000..9ea8947a --- /dev/null +++ b/crates/binding-c/src/compile/mod.rs @@ -0,0 +1,84 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! C ABI for the compile module. + +#![allow(clippy::not_unsafe_ptr_arg_deref)] + +pub mod commutation; +pub mod decompose; +pub mod knowledge; +pub mod layout; +pub mod pipeline; +pub mod resource; +pub mod routing; +pub mod sabre; +pub mod transform; +pub mod workflow; + +use cqlib_core::compile::CompileResult; + +/// Opaque handle around a [`CompileResult`]. +pub struct CCompileResult { + pub inner: CompileResult, +} + +/// Compile mode tag exposed to C. +/// | Value | Mode | +/// |-------|----------| +/// | 0 | Normal | +/// | 1 | Enhanced | +pub const COMPILE_MODE_NORMAL: u8 = 0; +pub const COMPILE_MODE_ENHANCED: u8 = 1; + +/// Target tag exposed to C. +/// | Value | Target | +/// |-------|---------------| +/// | 0 | Logical | +/// | 1 | Basis | +/// | 2 | Device | +/// | 3 | TopologyBasis | +pub const COMPILE_TARGET_LOGICAL: u8 = 0; +pub const COMPILE_TARGET_BASIS: u8 = 1; +pub const COMPILE_TARGET_DEVICE: u8 = 2; +pub const COMPILE_TARGET_TOPOLOGY_BASIS: u8 = 3; + +/// C-side compile configuration. +/// +/// For `Logical` targets, only `mode` is consulted. +/// For `Basis` targets, the Rust side falls back to logical compilation +/// (the binding does not parse `Instruction` from C; users should construct +/// such targets from Rust or build them out-of-band). All fields are kept +/// so the struct can be extended without breaking ABI. +#[repr(C)] +pub struct CompileConfigC { + /// One of `COMPILE_MODE_*`. + pub mode: u8, + /// One of `COMPILE_TARGET_*`. + pub target: u8, + /// Reserved for future use (resource policy flags). + pub allow_dirty_ancilla: u8, + pub allow_clean_ancilla: u8, + /// Reserved padding to keep the struct layout stable. + pub _reserved: [u8; 4], +} + +pub use commutation::*; +pub use decompose::*; +pub use knowledge::*; +pub use layout::*; +pub use pipeline::*; +pub use resource::*; +pub use routing::*; +pub use sabre::*; +pub use transform::*; +pub use workflow::*; diff --git a/crates/binding-c/src/compile/pipeline.rs b/crates/binding-c/src/compile/pipeline.rs new file mode 100644 index 00000000..f91246c2 --- /dev/null +++ b/crates/binding-c/src/compile/pipeline.rs @@ -0,0 +1,305 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! C ABI entry point for the compiler pipeline. + +use crate::circuit::CCircuit; +use crate::compile::{CCompileResult, COMPILE_MODE_ENHANCED, COMPILE_MODE_NORMAL, CompileConfigC}; +use crate::device::{CDevice, CLayout}; +use crate::error::CqlibError; +use cqlib_core::compile::resource::ResourcePolicy; +use cqlib_core::compile::{ + CompileConfig, CompileMode, CompileTarget, DeviceCompileTarget, compile as cqlib_compile, +}; +use std::ffi::{CStr, CString}; +use std::os::raw::c_char; + +/// Run the configured compiler workflow over `circuit`. +/// +/// `config.mode` selects `Normal` or `Enhanced`. `config.target` selects +/// `Logical`; other targets fall back to logical compilation (the C ABI +/// does not parse `Instruction` lists or `Device` objects). +/// +/// Returns a heap-allocated `CCompileResult*`, or NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn compile(circuit: *const CCircuit, config: CompileConfigC) -> *mut CCompileResult { + if circuit.is_null() { + return std::ptr::null_mut(); + } + let mode = match config.mode { + 0 => CompileMode::Normal, + 1 => CompileMode::Enhanced, + _ => return std::ptr::null_mut(), + }; + let target = match config.target { + 0 => Some(CompileTarget::Logical), + _ => None, + }; + let target = target.unwrap_or(CompileTarget::Logical); + let cfg = CompileConfig { + mode, + target, + resource_policy: ResourcePolicy::default(), + }; + let circuit = unsafe { &(*circuit).inner }; + match cqlib_compile(circuit, cfg) { + Ok(result) => Box::into_raw(Box::new(CCompileResult { inner: result })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Free a compile result. NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn compile_result_free(ptr: *mut CCompileResult) { + if !ptr.is_null() { + unsafe { + let _ = Box::from_raw(ptr); + } + } +} + +/// Wrap a `Circuit*` around the optimized circuit stored in `result`. +/// +/// The returned circuit is a deep clone of the result's circuit, so the +/// caller can free `result` independently. Returns NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn compile_result_circuit(ptr: *const CCompileResult) -> *mut CCircuit { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let result = unsafe { &*ptr }; + Box::into_raw(Box::new(CCircuit { + inner: result.inner.circuit.clone(), + })) +} + +/// Return 1 if the workflow changed the circuit, 0 otherwise, or -1 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn compile_result_changed(ptr: *const CCompileResult) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + let result = unsafe { &*ptr }; + if result.inner.changed { 1 } else { 0 } +} + +/// Return the compile mode (0 = Normal, 1 = Enhanced), or -1 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn compile_result_mode(ptr: *const CCompileResult) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + let result = unsafe { &*ptr }; + match result.inner.mode { + CompileMode::Normal => 0, + CompileMode::Enhanced => 1, + } +} + +/// Return the number of step reports in the result, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn compile_result_num_steps(ptr: *const CCompileResult) -> usize { + if ptr.is_null() { + return 0; + } + let result = unsafe { &*ptr }; + result.inner.steps.len() +} + +/// Return the name of step `index` as a heap-allocated C string. +/// Caller must free with `cqlib_string_free`. Returns NULL on out-of-bounds. +#[unsafe(no_mangle)] +pub extern "C" fn compile_result_step_name( + ptr: *const CCompileResult, + index: usize, +) -> *mut c_char { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let result = unsafe { &*ptr }; + match result.inner.steps.get(index) { + Some(step) => match CString::new(step.name) { + Ok(cs) => cs.into_raw(), + Err(_) => std::ptr::null_mut(), + }, + None => std::ptr::null_mut(), + } +} + +/// Return 1 if step `index` changed the circuit, 0 otherwise, or -1 on error. +#[unsafe(no_mangle)] +pub extern "C" fn compile_result_step_changed(ptr: *const CCompileResult, index: usize) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + let result = unsafe { &*ptr }; + match result.inner.steps.get(index) { + Some(step) => { + if step.changed && !step.skipped { + 1 + } else { + 0 + } + } + None => CqlibError::InvalidParam as i32, + } +} + +/// Return the optional skip or configuration note of step `index` as a +/// heap-allocated C string (free with `cqlib_string_free`). +/// +/// Returns NULL when the step record carries no reason, on NULL input, or on +/// an out-of-bounds index. +#[unsafe(no_mangle)] +pub extern "C" fn compile_result_step_reason( + ptr: *const CCompileResult, + index: usize, +) -> *mut c_char { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let result = unsafe { &*ptr }; + match result + .inner + .steps + .get(index) + .and_then(|step| step.reason.as_deref()) + { + Some(reason) => match CString::new(reason) { + Ok(text) => text.into_raw(), + Err(_) => std::ptr::null_mut(), + }, + None => std::ptr::null_mut(), + } +} + +/// Find the first workflow step report named `name` (the C mirror of +/// `CompileResult::step`). +/// +/// Writes the step's raw `changed` and `skipped` flags to the out pointers. +/// Returns 1 when a matching step exists, 0 when no step matches, or -1 on +/// NULL arguments or invalid UTF-8. +#[unsafe(no_mangle)] +pub extern "C" fn compile_result_step( + ptr: *const CCompileResult, + name: *const c_char, + out_changed: *mut i32, + out_skipped: *mut i32, +) -> i32 { + if ptr.is_null() || name.is_null() || out_changed.is_null() || out_skipped.is_null() { + return CqlibError::NullPtr as i32; + } + let name = match unsafe { CStr::from_ptr(name) }.to_str() { + Ok(name) => name, + Err(_) => return CqlibError::ParseError as i32, + }; + let result = unsafe { &*ptr }; + match result.inner.step(name) { + Some(step) => { + unsafe { + *out_changed = step.changed as i32; + *out_skipped = step.skipped as i32; + } + 1 + } + None => 0, + } +} + +/// Run the configured compiler workflow for a concrete device target. +/// +/// This is the device-target counterpart of [`compile`]: the circuit is +/// routed on the device topology and lowered to the device's ordered native +/// capabilities, so the returned circuit is always accepted by `device`. +/// +/// - `mode` is one of the `COMPILE_MODE_*` tags. +/// - `initial_layout` optionally pins the initial logical-to-physical +/// mapping (NULL lets the workflow choose one). +/// - `seed` negative means "no seed" (heuristic default); otherwise it is +/// used as the deterministic device layout/routing seed. +/// +/// Returns a heap-allocated `CCompileResult*`, or NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn compile_with_device( + circuit: *const CCircuit, + mode: u8, + device: *const CDevice, + initial_layout: *const CLayout, + seed: i64, +) -> *mut CCompileResult { + if circuit.is_null() || device.is_null() { + return std::ptr::null_mut(); + } + let mode = match mode { + COMPILE_MODE_NORMAL => CompileMode::Normal, + COMPILE_MODE_ENHANCED => CompileMode::Enhanced, + _ => return std::ptr::null_mut(), + }; + let initial_layout = if initial_layout.is_null() { + None + } else { + Some(unsafe { (*initial_layout).inner.clone() }) + }; + let target = CompileTarget::Device(DeviceCompileTarget { + device: unsafe { (*device).inner.clone() }, + initial_layout, + seed: u32::try_from(seed).ok(), + }); + let cfg = CompileConfig { + mode, + target, + resource_policy: ResourcePolicy::default(), + }; + let circuit = unsafe { &(*circuit).inner }; + match cqlib_compile(circuit, cfg) { + Ok(result) => Box::into_raw(Box::new(CCompileResult { inner: result })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Return the initial logical-to-physical layout recorded by a device-target +/// compilation, as an owned `CLayout*` (free with `layout_free`). +/// +/// Returns NULL when the result carries no device metadata (for example +/// after a logical-target compile) or on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn compile_result_initial_layout(ptr: *const CCompileResult) -> *mut CLayout { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let result = unsafe { &*ptr }; + match &result.inner.device_metadata { + Some(metadata) => Box::into_raw(Box::new(CLayout { + inner: metadata.initial_layout.clone(), + })), + None => std::ptr::null_mut(), + } +} + +/// Return the final logical-to-physical layout recorded by a device-target +/// compilation, as an owned `CLayout*` (free with `layout_free`). +/// +/// Returns NULL when the result carries no device metadata (for example +/// after a logical-target compile) or on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn compile_result_final_layout(ptr: *const CCompileResult) -> *mut CLayout { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let result = unsafe { &*ptr }; + match &result.inner.device_metadata { + Some(metadata) => Box::into_raw(Box::new(CLayout { + inner: metadata.final_layout.clone(), + })), + None => std::ptr::null_mut(), + } +} diff --git a/crates/binding-c/src/compile/resource.rs b/crates/binding-c/src/compile/resource.rs new file mode 100644 index 00000000..9d814dc2 --- /dev/null +++ b/crates/binding-c/src/compile/resource.rs @@ -0,0 +1,419 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! C ABI for ancillary-resource management: policies, limits, preview/commit +//! leases, and manager consistency checks. + +use crate::circuit::CCircuit; +use crate::error::CqlibError; +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::compile::resource::{ + AncillaRequirement, ResourceLease, ResourceLimits, ResourceManager, ResourcePlan, + ResourcePolicy, ResourceRequest, +}; +use std::collections::BTreeSet; + +/// Ancilla-requirement tag. +/// | Value | Requirement | +/// |-------|----------------------------------------------------| +/// | 0 | Clean zero (enter and leave in `\|0>`) | +/// | 1 | Dirty (unknown input state, exact restore) | +pub const RESOURCE_REQUIREMENT_CLEAN_ZERO: u8 = 0; +pub const RESOURCE_REQUIREMENT_DIRTY: u8 = 1; + +/// C form of [`ResourcePolicy`]. +#[repr(C)] +pub struct CResourcePolicy { + /// Total clean logical ancillas the compiler may create before layout. + pub max_pre_layout_clean_ancillas: usize, + /// 1 allows borrowing input qubits under the dirty contract. + pub allow_dirty_borrowing: u8, + /// Reserved padding to keep the struct layout stable. + pub _reserved: [u8; 7], +} + +/// C form of [`ResourceLimits`]. +#[repr(C)] +pub struct CResourceLimits { + /// 1 enforces `max_total_qubits`, 0 leaves the limit unset. + pub has_max_total_qubits: u8, + /// Reserved padding to keep the struct layout stable. + pub _pad: [u8; 7], + /// Hard limit on total logical qubits (used when `has_max_total_qubits != 0`). + pub max_total_qubits: usize, + /// Reserved padding to keep the struct layout stable. + pub _reserved: [u64; 2], +} + +/// Opaque handle around a [`ResourceManager`] plus its synchronized working +/// circuit. +/// +/// The manager must evolve together with its circuit, so the C handle owns a +/// private clone of the source circuit. Use [`resource_manager_circuit`] to +/// take out the current working circuit. +pub struct CResourceManager { + /// Working copy of the managed circuit (mutated by `commit`). + pub circuit: Circuit, + /// The resource manager bound to `circuit`. + pub inner: ResourceManager, +} + +/// Opaque handle around a [`ResourcePlan`]. +pub struct CResourcePlan { + pub inner: ResourcePlan, +} + +/// Opaque handle around a [`ResourceLease`]. +pub struct CResourceLease { + pub inner: ResourceLease, +} + +/// Returns the default resource policy by value. +#[unsafe(no_mangle)] +pub extern "C" fn resource_policy_default() -> CResourcePolicy { + CResourcePolicy { + max_pre_layout_clean_ancillas: 0, + allow_dirty_borrowing: 0, + _reserved: [0; 7], + } +} + +/// Returns the default resource limits by value (no total-qubit bound). +#[unsafe(no_mangle)] +pub extern "C" fn resource_limits_default() -> CResourceLimits { + CResourceLimits { + has_max_total_qubits: 0, + _pad: [0; 7], + max_total_qubits: 0, + _reserved: [0; 2], + } +} + +/// Converts a C resource policy into the core form. +fn resource_policy_from_c(raw: &CResourcePolicy) -> ResourcePolicy { + ResourcePolicy { + max_pre_layout_clean_ancillas: raw.max_pre_layout_clean_ancillas, + allow_dirty_borrowing: raw.allow_dirty_borrowing != 0, + } +} + +/// Converts a C resource-limits struct into the core form. +fn resource_limits_from_c(raw: &CResourceLimits) -> ResourceLimits { + ResourceLimits { + max_total_qubits: if raw.has_max_total_qubits != 0 { + Some(raw.max_total_qubits) + } else { + None + }, + } +} + +/// Creates a resource manager synchronized with a private clone of `circuit`. +/// +/// `policy` and `limits` may be NULL to select the core defaults. Returns a +/// heap-allocated `CResourceManager*`, or NULL on NULL input or when the +/// circuit already exceeds the configured limits. +#[unsafe(no_mangle)] +pub extern "C" fn resource_manager_from_circuit( + circuit: *const CCircuit, + policy: *const CResourcePolicy, + limits: *const CResourceLimits, +) -> *mut CResourceManager { + if circuit.is_null() { + return std::ptr::null_mut(); + } + let policy = if policy.is_null() { + ResourcePolicy::default() + } else { + resource_policy_from_c(unsafe { &*policy }) + }; + let limits = if limits.is_null() { + ResourceLimits::default() + } else { + resource_limits_from_c(unsafe { &*limits }) + }; + let circuit = unsafe { &(*circuit).inner }; + match ResourceManager::from_circuit(circuit, policy, limits) { + Ok(inner) => Box::into_raw(Box::new(CResourceManager { + circuit: circuit.clone(), + inner, + })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Frees a resource manager. NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn resource_manager_free(ptr: *mut CResourceManager) { + if !ptr.is_null() { + unsafe { + let _ = Box::from_raw(ptr); + } + } +} + +/// Returns the manager's current working circuit as an owned `CCircuit*` +/// (free with `circuit_free`). The manager keeps its own copy, so the +/// returned handle is independent. Returns NULL on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn resource_manager_circuit(ptr: *const CResourceManager) -> *mut CCircuit { + if ptr.is_null() { + return std::ptr::null_mut(); + } + Box::into_raw(Box::new(CCircuit { + inner: unsafe { (*ptr).circuit.clone() }, + })) +} + +/// Previews an ancilla request against the manager without reserving +/// resources or mutating the working circuit. +/// +/// `requirement` is one of `RESOURCE_REQUIREMENT_*`; `excluded` lists qubits +/// that must not be consumed as ancillary resources (may be NULL when +/// `excluded_len` is 0). Returns a heap-allocated `CResourcePlan*` (free with +/// `resource_plan_free`), or NULL on NULL input, an invalid requirement tag, +/// or an unsatisfiable request. +#[unsafe(no_mangle)] +pub extern "C" fn resource_manager_preview( + ptr: *const CResourceManager, + requirement: u8, + count: usize, + excluded: *const u32, + excluded_len: usize, +) -> *mut CResourcePlan { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let requirement = match requirement { + RESOURCE_REQUIREMENT_CLEAN_ZERO => AncillaRequirement::CleanZero, + RESOURCE_REQUIREMENT_DIRTY => AncillaRequirement::Dirty, + _ => return std::ptr::null_mut(), + }; + let mut excluded_set = BTreeSet::new(); + if excluded_len > 0 { + if excluded.is_null() { + return std::ptr::null_mut(); + } + let slice = unsafe { std::slice::from_raw_parts(excluded, excluded_len) }; + for &id in slice { + excluded_set.insert(Qubit::new(id)); + } + } + let manager = unsafe { &(*ptr).inner }; + let request = ResourceRequest { + requirement, + count, + excluded: excluded_set, + }; + match manager.preview(&request) { + Ok(inner) => Box::into_raw(Box::new(CResourcePlan { inner })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Frees a resource plan. NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn resource_plan_free(ptr: *mut CResourcePlan) { + if !ptr.is_null() { + unsafe { + let _ = Box::from_raw(ptr); + } + } +} + +/// Returns the number of planned qubits, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn resource_plan_qubits_len(ptr: *const CResourcePlan) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.qubits().len() } +} + +/// Copies the planned qubits into `out` (two-step pattern; call +/// `resource_plan_qubits_len` first). Returns the total number of planned +/// qubits, or 0 for NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn resource_plan_qubits( + ptr: *const CResourcePlan, + out: *mut u32, + len: usize, +) -> usize { + if ptr.is_null() { + return 0; + } + let plan = unsafe { &(*ptr).inner }; + let qubits: Vec = plan.qubits().iter().map(|qubit| qubit.id()).collect(); + crate::device::write_u32_list(&qubits, out, len) +} + +/// Returns the number of new logical qubits the plan introduces when +/// committed, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn resource_plan_num_new_qubits(ptr: *const CResourcePlan) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.num_new_qubits() } +} + +/// Returns the plan's ancilla requirement (one of `RESOURCE_REQUIREMENT_*`), +/// or -1 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn resource_plan_requirement(ptr: *const CResourcePlan) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + match unsafe { (*ptr).inner.requirement() } { + AncillaRequirement::CleanZero => RESOURCE_REQUIREMENT_CLEAN_ZERO as i32, + AncillaRequirement::Dirty => RESOURCE_REQUIREMENT_DIRTY as i32, + } +} + +/// Commits a previewed plan: reserves its qubits, mutates the manager's +/// working circuit, and returns the active lease. +/// +/// The plan handle is consumed (freed) by this call. Returns a +/// heap-allocated `CResourceLease*` (free with `resource_lease_free` after +/// `resource_manager_release`), or NULL on NULL input or a stale/invalid +/// plan. +#[unsafe(no_mangle)] +pub extern "C" fn resource_manager_commit( + ptr: *mut CResourceManager, + plan: *mut CResourcePlan, +) -> *mut CResourceLease { + if ptr.is_null() || plan.is_null() { + return std::ptr::null_mut(); + } + let manager = unsafe { &mut *ptr }; + let plan = unsafe { Box::from_raw(plan) }; + match manager.inner.commit(&mut manager.circuit, plan.inner) { + Ok(inner) => Box::into_raw(Box::new(CResourceLease { inner })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Frees a resource lease. NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn resource_lease_free(ptr: *mut CResourceLease) { + if !ptr.is_null() { + unsafe { + let _ = Box::from_raw(ptr); + } + } +} + +/// Returns the number of leased qubits, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn resource_lease_qubits_len(ptr: *const CResourceLease) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.qubits().len() } +} + +/// Copies the leased qubits into `out` (two-step pattern; call +/// `resource_lease_qubits_len` first). Returns the total number of leased +/// qubits, or 0 for NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn resource_lease_qubits( + ptr: *const CResourceLease, + out: *mut u32, + len: usize, +) -> usize { + if ptr.is_null() { + return 0; + } + let lease = unsafe { &(*ptr).inner }; + let qubits: Vec = lease.qubits().iter().map(|qubit| qubit.id()).collect(); + crate::device::write_u32_list(&qubits, out, len) +} + +/// Returns the lease's ancilla requirement (one of `RESOURCE_REQUIREMENT_*`), +/// or -1 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn resource_lease_requirement(ptr: *const CResourceLease) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + match unsafe { (*ptr).inner.requirement() } { + AncillaRequirement::CleanZero => RESOURCE_REQUIREMENT_CLEAN_ZERO as i32, + AncillaRequirement::Dirty => RESOURCE_REQUIREMENT_DIRTY as i32, + } +} + +/// Releases an active lease, returning its qubits to the resource pool. +/// +/// The consuming transform must have restored the lease's state contract +/// before this call. The lease handle stays valid for inspection until freed +/// with `resource_lease_free`. Returns 0 on success, -1 on NULL input, -6 on +/// an unknown or already-released lease. +#[unsafe(no_mangle)] +pub extern "C" fn resource_manager_release( + ptr: *mut CResourceManager, + lease: *const CResourceLease, +) -> i32 { + if ptr.is_null() || lease.is_null() { + return CqlibError::NullPtr as i32; + } + let manager = unsafe { &mut *ptr }; + let lease = unsafe { &(*lease).inner }; + match manager.inner.release(lease) { + Ok(()) => CqlibError::Ok as i32, + Err(_) => CqlibError::CompilerError as i32, + } +} + +/// Moves the manager into the post-layout phase (no fresh logical ancillas). +/// +/// Returns 0 on success, -1 on NULL input, -6 on a consistency failure. +#[unsafe(no_mangle)] +pub extern "C" fn resource_manager_enter_post_layout(ptr: *mut CResourceManager) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + let manager = unsafe { &mut *ptr }; + match manager.inner.enter_post_layout(&manager.circuit) { + Ok(()) => CqlibError::Ok as i32, + Err(_) => CqlibError::CompilerError as i32, + } +} + +/// Checks that the manager still agrees with its working circuit. +/// +/// Returns 0 on success, -1 on NULL input, -6 on a mismatch. +#[unsafe(no_mangle)] +pub extern "C" fn resource_manager_verify_consistency(ptr: *const CResourceManager) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + let manager = unsafe { &(*ptr) }; + match manager.inner.verify_consistency(&manager.circuit) { + Ok(()) => CqlibError::Ok as i32, + Err(_) => CqlibError::CompilerError as i32, + } +} + +/// Checks that every lease has been released and the pool is idle. +/// +/// Returns 0 on success, -1 on NULL input, -6 when leases are still active. +#[unsafe(no_mangle)] +pub extern "C" fn resource_manager_verify_idle(ptr: *const CResourceManager) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + let manager = unsafe { &(*ptr) }; + match manager.inner.verify_idle(&manager.circuit) { + Ok(()) => CqlibError::Ok as i32, + Err(_) => CqlibError::CompilerError as i32, + } +} diff --git a/crates/binding-c/src/compile/routing.rs b/crates/binding-c/src/compile/routing.rs new file mode 100644 index 00000000..dfdeb0fd --- /dev/null +++ b/crates/binding-c/src/compile/routing.rs @@ -0,0 +1,387 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// use this file except in compliance with the License. You may obtain +// a copy of the License in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! C ABI for transform-level routing entry points: `route_with_layout` and +//! `route_sabre`, plus accessors over the routed circuit and the full +//! SABRE layout-plus-route result. + +use crate::circuit::CCircuit; +use crate::compile::layout::{CLayoutScore, objective_from_tag, write_layout_score}; +use crate::compile::sabre::{ + CSabreRoutingDiagnostics, SabreConfigC, resolve_sabre_config, write_sabre_routing_diagnostics, +}; +use crate::device::{CDevice, CLayout}; +use crate::error::CqlibError; +use cqlib_core::compile::transform::routing::{RoutedCircuit, SabreRouteResult}; +use cqlib_core::compile::transform::routing::{ + route_sabre as core_route_sabre, route_with_layout as core_route_with_layout, +}; +use std::ffi::CString; +use std::os::raw::c_char; + +/// Opaque handle around a [`RoutedCircuit`]. +pub struct CRoutedCircuit { + pub inner: RoutedCircuit, +} + +/// Opaque handle around a [`SabreRouteResult`]. +pub struct CSabreRouteResult { + pub inner: SabreRouteResult, +} + +// ===== Routing entry points ===== + +/// Routes `circuit` on `device` from a caller-supplied initial layout. +/// +/// This is the low-level entry point for callers that already have a layout +/// (for example from `vf2_perfect_layout`, `greedy_layout`, or a previous +/// SABRE run). It does not perform layout refinement or scoring. `config` may +/// be NULL to use the core default. Returns a heap-allocated +/// `CRoutedCircuit*`, or NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn route_with_layout( + circuit: *const CCircuit, + device: *const CDevice, + initial_layout: *const CLayout, + config: *const SabreConfigC, +) -> *mut CRoutedCircuit { + if circuit.is_null() || device.is_null() || initial_layout.is_null() { + return std::ptr::null_mut(); + } + let Ok(config) = resolve_sabre_config(config) else { + return std::ptr::null_mut(); + }; + let circuit = unsafe { &(*circuit).inner }; + let device = unsafe { &(*device).inner }; + let layout = unsafe { &(*initial_layout).inner }; + match core_route_with_layout(circuit, device, layout, &config) { + Ok(routed) => Box::into_raw(Box::new(CRoutedCircuit { inner: routed })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Selects a SABRE initial layout for `circuit` on `device`, then routes the +/// original forward circuit from the selected layout. +/// +/// `objective` is one of the `LAYOUT_OBJECTIVE_*` tags. `config` may be NULL +/// to use the core default. Returns a heap-allocated `CSabreRouteResult*`, or +/// NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn route_sabre( + circuit: *const CCircuit, + device: *const CDevice, + objective: u8, + config: *const SabreConfigC, +) -> *mut CSabreRouteResult { + if circuit.is_null() || device.is_null() { + return std::ptr::null_mut(); + } + let device = unsafe { &(*device).inner }; + let Ok(Some(objective)) = objective_from_tag(objective, None, Some(device)) else { + return std::ptr::null_mut(); + }; + let Ok(config) = resolve_sabre_config(config) else { + return std::ptr::null_mut(); + }; + let circuit = unsafe { &(*circuit).inner }; + match core_route_sabre(circuit, device, &objective, &config) { + Ok(result) => Box::into_raw(Box::new(CSabreRouteResult { inner: result })), + Err(_) => std::ptr::null_mut(), + } +} + +// ===== RoutedCircuit accessors ===== + +/// Frees a routed circuit. NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn routed_circuit_free(ptr: *mut CRoutedCircuit) { + if !ptr.is_null() { + unsafe { + let _ = Box::from_raw(ptr); + } + } +} + +/// Returns the routed physical circuit as an owned `CCircuit*` (free with +/// `circuit_free`). Returns NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn routed_circuit_circuit(ptr: *const CRoutedCircuit) -> *mut CCircuit { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let routed = unsafe { &*ptr }; + Box::into_raw(Box::new(CCircuit { + inner: routed.inner.circuit().clone(), + })) +} + +/// Returns the initial logical-to-physical layout used for routing, as an +/// owned `CLayout*` (free with `layout_free`). Returns NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn routed_circuit_initial_layout(ptr: *const CRoutedCircuit) -> *mut CLayout { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let routed = unsafe { &*ptr }; + Box::into_raw(Box::new(CLayout { + inner: routed.inner.initial_layout().clone(), + })) +} + +/// Returns the final logical-to-physical layout after all routed operations, +/// as an owned `CLayout*` (free with `layout_free`). Returns NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn routed_circuit_final_layout(ptr: *const CRoutedCircuit) -> *mut CLayout { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let routed = unsafe { &*ptr }; + Box::into_raw(Box::new(CLayout { + inner: routed.inner.final_layout().clone(), + })) +} + +/// Returns the number of inserted SWAP operations, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn routed_circuit_swap_count(ptr: *const CRoutedCircuit) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.swap_count() } +} + +/// Writes the routing-diagnostics snapshot to `*out`. +/// +/// Returns 0 on success or -1 on NULL arguments. +#[unsafe(no_mangle)] +pub extern "C" fn routed_circuit_diagnostics( + ptr: *const CRoutedCircuit, + out: *mut CSabreRoutingDiagnostics, +) -> i32 { + if ptr.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + let routed = unsafe { &*ptr }; + write_sabre_routing_diagnostics(routed.inner.diagnostics(), out); + CqlibError::Ok as i32 +} + +/// Returns whether routing observably changed `original`. +/// +/// Returns 1 when changed, 0 when unchanged, or -1 on NULL arguments. +#[unsafe(no_mangle)] +pub extern "C" fn routed_circuit_changed( + ptr: *const CRoutedCircuit, + original: *const CCircuit, +) -> i32 { + if ptr.is_null() || original.is_null() { + return CqlibError::NullPtr as i32; + } + let routed = unsafe { &*ptr }; + let original = unsafe { &(*original).inner }; + routed.inner.changed(original) as i32 +} + +// ===== SabreRouteResult accessors ===== + +/// Frees a SABRE route result. NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn sabre_route_result_free(ptr: *mut CSabreRouteResult) { + if !ptr.is_null() { + unsafe { + let _ = Box::from_raw(ptr); + } + } +} + +/// Returns the routed circuit and routing metadata as an owned +/// `CRoutedCircuit*` (free with `routed_circuit_free`). Returns NULL on +/// error. +#[unsafe(no_mangle)] +pub extern "C" fn sabre_route_result_routed(ptr: *const CSabreRouteResult) -> *mut CRoutedCircuit { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let result = unsafe { &*ptr }; + Box::into_raw(Box::new(CRoutedCircuit { + inner: result.inner.routed().clone(), + })) +} + +/// Returns the routed physical circuit as an owned `CCircuit*` (free with +/// `circuit_free`). Returns NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn sabre_route_result_circuit(ptr: *const CSabreRouteResult) -> *mut CCircuit { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let result = unsafe { &*ptr }; + Box::into_raw(Box::new(CCircuit { + inner: result.inner.circuit().clone(), + })) +} + +/// Returns the initial logical-to-physical layout used for routing, as an +/// owned `CLayout*` (free with `layout_free`). Returns NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn sabre_route_result_initial_layout(ptr: *const CSabreRouteResult) -> *mut CLayout { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let result = unsafe { &*ptr }; + Box::into_raw(Box::new(CLayout { + inner: result.inner.initial_layout().clone(), + })) +} + +/// Returns the final logical-to-physical layout after routing, as an owned +/// `CLayout*` (free with `layout_free`). Returns NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn sabre_route_result_final_layout(ptr: *const CSabreRouteResult) -> *mut CLayout { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let result = unsafe { &*ptr }; + Box::into_raw(Box::new(CLayout { + inner: result.inner.final_layout().clone(), + })) +} + +/// Returns the number of inserted SWAP operations, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn sabre_route_result_swap_count(ptr: *const CSabreRouteResult) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.swap_count() } +} + +/// Writes the routing-diagnostics snapshot to `*out`. +/// +/// Returns 0 on success or -1 on NULL arguments. +#[unsafe(no_mangle)] +pub extern "C" fn sabre_route_result_diagnostics( + ptr: *const CSabreRouteResult, + out: *mut CSabreRoutingDiagnostics, +) -> i32 { + if ptr.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + let result = unsafe { &*ptr }; + write_sabre_routing_diagnostics(result.inner.diagnostics(), out); + CqlibError::Ok as i32 +} + +/// Returns whether routing observably changed `original`. +/// +/// Returns 1 when changed, 0 when unchanged, or -1 on NULL arguments. +#[unsafe(no_mangle)] +pub extern "C" fn sabre_route_result_changed( + ptr: *const CSabreRouteResult, + original: *const CCircuit, +) -> i32 { + if ptr.is_null() || original.is_null() { + return CqlibError::NullPtr as i32; + } + let result = unsafe { &*ptr }; + let original = unsafe { &(*original).inner }; + result.inner.changed(original) as i32 +} + +// ===== SabreRouteResult layout metadata ===== + +/// Writes the layout-score snapshot of the selected initial layout to `*out`. +/// +/// Returns 0 on success, -1 on NULL arguments, or -8 when the result carries +/// no layout score. +#[unsafe(no_mangle)] +pub extern "C" fn sabre_route_result_layout_score( + ptr: *const CSabreRouteResult, + out: *mut CLayoutScore, +) -> i32 { + if ptr.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + let result = unsafe { &*ptr }; + match result.inner.layout_score() { + Some(score) => { + write_layout_score(score, out); + CqlibError::Ok as i32 + } + None => CqlibError::InvalidParam as i32, + } +} + +/// Returns 1 when every positive-weight interaction lands on an adjacent +/// hardware edge, 0 otherwise, or -1 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn sabre_route_result_layout_is_perfect(ptr: *const CSabreRouteResult) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + unsafe { (*ptr).inner.layout_diagnostics().is_perfect as i32 } +} + +/// Returns the number of candidate layouts considered while selecting the +/// initial layout, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn sabre_route_result_layout_candidates_evaluated( + ptr: *const CSabreRouteResult, +) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.layout_diagnostics().candidates_evaluated } +} + +/// Returns 1 when fidelity data contributed to the selected layout score, 0 +/// otherwise, or -1 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn sabre_route_result_layout_used_fidelity(ptr: *const CSabreRouteResult) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + unsafe { (*ptr).inner.layout_diagnostics().used_fidelity as i32 } +} + +/// Returns the number of layout diagnostic notes, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn sabre_route_result_layout_notes_len(ptr: *const CSabreRouteResult) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.layout_diagnostics().notes.len() } +} + +/// Returns the layout diagnostic note at `index` as a heap-allocated C +/// string. +/// +/// Caller must free with `cqlib_string_free`. Returns NULL on out-of-bounds or +/// error. +#[unsafe(no_mangle)] +pub extern "C" fn sabre_route_result_layout_note( + ptr: *const CSabreRouteResult, + index: usize, +) -> *mut c_char { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let result = unsafe { &*ptr }; + match result.inner.layout_diagnostics().notes.get(index) { + Some(note) => match CString::new(note.as_str()) { + Ok(cs) => cs.into_raw(), + Err(_) => std::ptr::null_mut(), + }, + None => std::ptr::null_mut(), + } +} diff --git a/crates/binding-c/src/compile/sabre.rs b/crates/binding-c/src/compile/sabre.rs new file mode 100644 index 00000000..fb3424c0 --- /dev/null +++ b/crates/binding-c/src/compile/sabre.rs @@ -0,0 +1,452 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! C ABI for the low-level SABRE routing core: `sabre_route`, initial-layout +//! normalization, reachability validation, and the SABRE configuration. + +use crate::circuit::CCircuit; +use crate::device::{CDevice, CLayout}; +use crate::error::CqlibError; +use cqlib_core::compile::sabre::normalize_initial_layout as core_normalize_initial_layout; +use cqlib_core::compile::sabre::sabre_route as core_sabre_route; +use cqlib_core::compile::sabre::validate_reachable_interactions as core_validate_reachable_interactions; +use cqlib_core::compile::sabre::{ + SabreConfig, SabreHeuristicConfig, SabreRoutingDiagnostics, SabreRoutingResult, + SabreVf2PrepassConfig, +}; +use cqlib_core::device::LogicalQubit; + +/// Maximum number of lookahead weights representable in +/// `SabreHeuristicConfigC`. +pub const SABRE_MAX_LOOKAHEAD_WEIGHTS: usize = 8; + +/// Opaque handle around a [`SabreRoutingResult`]. +pub struct CSabreRoutingResult { + pub inner: SabreRoutingResult, +} + +/// Bounded VF2 prepass configuration used to seed SABRE layout candidates. +#[repr(C)] +pub struct SabreVf2PrepassConfigC { + /// Maximum number of complete perfect mappings scored by VF2. + pub candidate_limit: usize, + /// Maximum number of partial mapping extensions attempted by VF2. + pub call_limit: usize, + /// Reserved padding to keep the struct layout stable. + pub _reserved: [u64; 2], +} + +/// SABRE swap-selection heuristic configuration. +/// +/// `decay_enabled == 0` disables congestion control (the core +/// `decay_increment: None` case). `lookahead_weights` carries +/// `num_lookahead_weights` leading entries; the rest is padding. +#[repr(C)] +pub struct SabreHeuristicConfigC { + /// Weight of the current front-layer total distance. + pub basic_weight: f64, + /// Weights of each active-layer-scaled lookahead-layer total. + pub lookahead_weights: [f64; SABRE_MAX_LOOKAHEAD_WEIGHTS], + /// Number of leading `lookahead_weights` entries in use. + pub num_lookahead_weights: usize, + /// Congestion multiplier increment applied after a heuristic SWAP. + pub decay_increment: f64, + /// 1 enables congestion control, 0 disables it. + pub decay_enabled: u8, + /// Number of heuristic SWAP attempts before decay values reset. + pub decay_reset: usize, + /// Heuristic SWAPs allowed without routing a front-layer node before + /// the shortest-path fallback. + pub attempt_limit: usize, + /// Floating-point tolerance for treating candidate scores as tied. + pub best_epsilon: f64, + /// Reserved padding to keep the struct layout stable. + pub _reserved: [u8; 8], +} + +/// SABRE layout-refinement and routing configuration. +/// +/// `vf2_prepass_enabled == 0` disables the prepass (`vf2_prepass` is then +/// ignored). `has_seed == 0` selects the unseeded default; otherwise `seed` +/// fixes a deterministic configuration. +#[repr(C)] +pub struct SabreConfigC { + /// Randomized starting layouts added to deterministic candidates. + pub layout_trials: usize, + /// Maximum layout component-assignment states explored. + pub layout_assignment_budget: usize, + /// 1 enables the bounded VF2 prepass, 0 disables it. + pub vf2_prepass_enabled: u8, + /// VF2 prepass limits (used when `vf2_prepass_enabled != 0`). + pub vf2_prepass: SabreVf2PrepassConfigC, + /// Forward/backward refinement iterations per layout trial. + pub refinement_iterations: usize, + /// Complete routing trials per fully refined layout. + pub routing_trials: usize, + /// Deterministic seed (used when `has_seed != 0`). + pub seed: u64, + /// 1 uses `seed`, 0 leaves the seed unset. + pub has_seed: u8, + /// Swap-selection heuristic configuration. + pub heuristic: SabreHeuristicConfigC, + /// Reserved padding to keep the struct layout stable. + pub _reserved: [u64; 4], +} + +/// C snapshot of SABRE routing diagnostics. +#[repr(C)] +pub struct CSabreRoutingDiagnostics { + /// Number of routing trials evaluated. + pub trials_evaluated: usize, + /// Zero-based index of the selected routing trial. + pub selected_trial_index: usize, + /// Number of times the shortest-path fallback was used. + pub fallback_count: usize, + /// Number of recursively routed control-flow blocks. + pub control_flow_blocks_routed: usize, + /// ASAP two-qubit depth of the selected routed operation stream. + pub two_qubit_depth: usize, + /// Total number of operations in the selected routed stream. + pub operation_count: usize, + /// Predicted native two-qubit operation count after device lowering. + pub native_two_qubit_count: usize, + /// ASAP two-qubit depth using the selected exact-qargs native plans. + pub native_two_qubit_depth: usize, + /// ASAP total depth using the selected exact-qargs native plans. + pub native_total_depth: usize, + /// Predicted total native operation count after device lowering. + pub native_operation_count: usize, + /// Dynamic `for`/`while` loops whose total execution cost is unknown. + pub unknown_loop_count: usize, + /// Number of distinct unary/pair requirement signatures prepared. + pub requirement_signature_count: usize, + /// Pair-placement lower-bound states retained eagerly by the target. + pub eager_pair_state_count: usize, + /// Lazy pair-state lower-bound probes made by the selected trial. + pub lazy_pair_l1_lookup_count: usize, + /// Selected-trial lazy pair-state probes served by its local L1 cache. + pub lazy_pair_l1_hit_count: usize, + /// Pair-state entries retained by the selected trial's bounded L1 cache. + pub lazy_pair_l1_cached_count: usize, +} + +/// Converts a C SABRE configuration into the core form. +/// +/// Returns `Err(-8)` when `num_lookahead_weights` exceeds +/// `SABRE_MAX_LOOKAHEAD_WEIGHTS`. +pub(crate) fn sabre_config_from_c(raw: &SabreConfigC) -> Result { + if raw.heuristic.num_lookahead_weights > SABRE_MAX_LOOKAHEAD_WEIGHTS { + return Err(CqlibError::InvalidParam as i32); + } + let lookahead_weights = + raw.heuristic.lookahead_weights[..raw.heuristic.num_lookahead_weights].to_vec(); + let vf2_prepass = if raw.vf2_prepass_enabled != 0 { + Some(SabreVf2PrepassConfig { + candidate_limit: raw.vf2_prepass.candidate_limit, + call_limit: raw.vf2_prepass.call_limit, + }) + } else { + None + }; + Ok(SabreConfig { + layout_trials: raw.layout_trials, + layout_assignment_budget: raw.layout_assignment_budget, + vf2_prepass, + refinement_iterations: raw.refinement_iterations, + routing_trials: raw.routing_trials, + seed: if raw.has_seed != 0 { + Some(raw.seed) + } else { + None + }, + heuristic: SabreHeuristicConfig { + basic_weight: raw.heuristic.basic_weight, + lookahead_weights, + decay_increment: if raw.heuristic.decay_enabled != 0 { + Some(raw.heuristic.decay_increment) + } else { + None + }, + decay_reset: raw.heuristic.decay_reset, + attempt_limit: raw.heuristic.attempt_limit, + best_epsilon: raw.heuristic.best_epsilon, + }, + }) +} + +/// Converts a core SABRE configuration into the C form. +fn sabre_config_to_c(config: &SabreConfig) -> SabreConfigC { + let mut lookahead_weights = [0.0f64; SABRE_MAX_LOOKAHEAD_WEIGHTS]; + let num = config + .heuristic + .lookahead_weights + .len() + .min(SABRE_MAX_LOOKAHEAD_WEIGHTS); + lookahead_weights[..num].copy_from_slice(&config.heuristic.lookahead_weights[..num]); + SabreConfigC { + layout_trials: config.layout_trials, + layout_assignment_budget: config.layout_assignment_budget, + vf2_prepass_enabled: config.vf2_prepass.is_some() as u8, + vf2_prepass: SabreVf2PrepassConfigC { + candidate_limit: config.vf2_prepass.map(|c| c.candidate_limit).unwrap_or(0), + call_limit: config.vf2_prepass.map(|c| c.call_limit).unwrap_or(0), + _reserved: [0; 2], + }, + refinement_iterations: config.refinement_iterations, + routing_trials: config.routing_trials, + seed: config.seed.unwrap_or(0), + has_seed: config.seed.is_some() as u8, + heuristic: SabreHeuristicConfigC { + basic_weight: config.heuristic.basic_weight, + lookahead_weights, + num_lookahead_weights: num, + decay_increment: config.heuristic.decay_increment.unwrap_or(0.0), + decay_enabled: config.heuristic.decay_increment.is_some() as u8, + decay_reset: config.heuristic.decay_reset, + attempt_limit: config.heuristic.attempt_limit, + best_epsilon: config.heuristic.best_epsilon, + _reserved: [0; 8], + }, + _reserved: [0; 4], + } +} + +/// Resolves a C SABRE configuration pointer, falling back to the core +/// default when `config` is NULL. +pub(crate) fn resolve_sabre_config(config: *const SabreConfigC) -> Result { + if config.is_null() { + Ok(SabreConfig::default()) + } else { + sabre_config_from_c(unsafe { &*config }) + } +} + +/// Writes a routing-diagnostics snapshot to `out`. +pub(crate) fn write_sabre_routing_diagnostics( + diagnostics: &SabreRoutingDiagnostics, + out: *mut CSabreRoutingDiagnostics, +) { + unsafe { + *out = CSabreRoutingDiagnostics { + trials_evaluated: diagnostics.trials_evaluated, + selected_trial_index: diagnostics.selected_trial_index, + fallback_count: diagnostics.fallback_count, + control_flow_blocks_routed: diagnostics.control_flow_blocks_routed, + two_qubit_depth: diagnostics.two_qubit_depth, + operation_count: diagnostics.operation_count, + native_two_qubit_count: diagnostics.native_two_qubit_count, + native_two_qubit_depth: diagnostics.native_two_qubit_depth, + native_total_depth: diagnostics.native_total_depth, + native_operation_count: diagnostics.native_operation_count, + unknown_loop_count: diagnostics.unknown_loop_count, + requirement_signature_count: diagnostics.requirement_signature_count, + eager_pair_state_count: diagnostics.eager_pair_state_count, + lazy_pair_l1_lookup_count: diagnostics.lazy_pair_l1_lookup_count, + lazy_pair_l1_hit_count: diagnostics.lazy_pair_l1_hit_count, + lazy_pair_l1_cached_count: diagnostics.lazy_pair_l1_cached_count, + }; + } +} + +/// Returns the default SABRE configuration by value. +#[unsafe(no_mangle)] +pub extern "C" fn sabre_config_default() -> SabreConfigC { + sabre_config_to_c(&SabreConfig::default()) +} + +/// Returns a compact deterministic SABRE configuration for reproducible +/// runs, fixing the random seed to `seed`. +#[unsafe(no_mangle)] +pub extern "C" fn sabre_config_deterministic_seeded(seed: u64) -> SabreConfigC { + sabre_config_to_c(&SabreConfig::deterministic_seeded(seed)) +} + +/// Validates the routing fields of a C SABRE configuration. +/// +/// Returns 0 when valid, -1 on NULL, -8 when the configuration cannot be +/// represented, or -6 when the core validation rejects it. +#[unsafe(no_mangle)] +pub extern "C" fn sabre_config_validate(config: *const SabreConfigC) -> i32 { + if config.is_null() { + return CqlibError::NullPtr as i32; + } + match sabre_config_from_c(unsafe { &*config }) { + Ok(config) => match config.validate() { + Ok(()) => CqlibError::Ok as i32, + Err(_) => CqlibError::CompilerError as i32, + }, + Err(code) => code, + } +} + +/// Routes `circuit` on `device` from a caller-supplied initial layout. +/// +/// `config` may be NULL to use the core default. Returns a heap-allocated +/// `CSabreRoutingResult*`, or NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn sabre_route( + circuit: *const CCircuit, + device: *const CDevice, + initial_layout: *const CLayout, + config: *const SabreConfigC, +) -> *mut CSabreRoutingResult { + if circuit.is_null() || device.is_null() || initial_layout.is_null() { + return std::ptr::null_mut(); + } + let config = match resolve_sabre_config(config) { + Ok(config) => config, + Err(_) => return std::ptr::null_mut(), + }; + let circuit = unsafe { &(*circuit).inner }; + let device = unsafe { &(*device).inner }; + let layout = unsafe { &(*initial_layout).inner }; + match core_sabre_route(circuit, device, layout, &config) { + Ok(result) => Box::into_raw(Box::new(CSabreRoutingResult { inner: result })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Frees a SABRE routing result. NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn sabre_routing_result_free(ptr: *mut CSabreRoutingResult) { + if !ptr.is_null() { + unsafe { + let _ = Box::from_raw(ptr); + } + } +} + +/// Returns the routed physical circuit as an owned `CCircuit*` (free with +/// `circuit_free`). Returns NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn sabre_routing_result_circuit(ptr: *const CSabreRoutingResult) -> *mut CCircuit { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let result = unsafe { &*ptr }; + Box::into_raw(Box::new(CCircuit { + inner: result.inner.circuit.clone(), + })) +} + +/// Returns the initial logical-to-physical layout used by the selected +/// trial, as an owned `CLayout*` (free with `layout_free`). Returns NULL on +/// error. +#[unsafe(no_mangle)] +pub extern "C" fn sabre_routing_result_initial_layout( + ptr: *const CSabreRoutingResult, +) -> *mut CLayout { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let result = unsafe { &*ptr }; + Box::into_raw(Box::new(CLayout { + inner: result.inner.initial_layout.clone(), + })) +} + +/// Returns the final logical-to-physical layout after all routed +/// operations, as an owned `CLayout*` (free with `layout_free`). Returns +/// NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn sabre_routing_result_final_layout( + ptr: *const CSabreRoutingResult, +) -> *mut CLayout { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let result = unsafe { &*ptr }; + Box::into_raw(Box::new(CLayout { + inner: result.inner.final_layout.clone(), + })) +} + +/// Returns the number of inserted SWAP operations (including control-flow +/// epilogues), or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn sabre_routing_result_swap_count(ptr: *const CSabreRoutingResult) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.swap_count } +} + +/// Writes the routing-diagnostics snapshot to `*out`. +/// +/// Returns 0 on success or -1 on NULL arguments. +#[unsafe(no_mangle)] +pub extern "C" fn sabre_routing_result_diagnostics( + ptr: *const CSabreRoutingResult, + out: *mut CSabreRoutingDiagnostics, +) -> i32 { + if ptr.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + let result = unsafe { &*ptr }; + write_sabre_routing_diagnostics(&result.inner.diagnostics, out); + CqlibError::Ok as i32 +} + +/// Normalizes a full logical-to-physical layout onto the device's usable +/// qubits. +/// +/// `logical_qubits` is an array of `num_logical` u32 logical qubit IDs. +/// Returns an owned `CLayout*` (free with `layout_free`), or NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn normalize_initial_layout( + logical_qubits: *const u32, + num_logical: usize, + device: *const CDevice, + initial_layout: *const CLayout, +) -> *mut CLayout { + if device.is_null() || initial_layout.is_null() || (num_logical > 0 && logical_qubits.is_null()) + { + return std::ptr::null_mut(); + } + let logical: Vec = if num_logical == 0 { + Vec::new() + } else { + unsafe { std::slice::from_raw_parts(logical_qubits, num_logical) } + .iter() + .map(|&q| LogicalQubit::new(q)) + .collect() + }; + let device = unsafe { &(*device).inner }; + let layout = unsafe { &(*initial_layout).inner }; + match core_normalize_initial_layout(&logical, device, layout) { + Ok(layout) => Box::into_raw(Box::new(CLayout { inner: layout })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Validates that every circuit interaction is reachable from +/// `initial_layout` without executing routing. +/// +/// Returns 0 when reachable, -1 on NULL arguments, or -6 when the core +/// validation fails. +#[unsafe(no_mangle)] +pub extern "C" fn validate_reachable_interactions( + circuit: *const CCircuit, + device: *const CDevice, + initial_layout: *const CLayout, +) -> i32 { + if circuit.is_null() || device.is_null() || initial_layout.is_null() { + return CqlibError::NullPtr as i32; + } + let circuit = unsafe { &(*circuit).inner }; + let device = unsafe { &(*device).inner }; + let layout = unsafe { &(*initial_layout).inner }; + match core_validate_reachable_interactions(circuit, device, layout) { + Ok(()) => CqlibError::Ok as i32, + Err(_) => CqlibError::CompilerError as i32, + } +} diff --git a/crates/binding-c/src/compile/transform.rs b/crates/binding-c/src/compile/transform.rs new file mode 100644 index 00000000..988f0be5 --- /dev/null +++ b/crates/binding-c/src/compile/transform.rs @@ -0,0 +1,1085 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// use this file except in compliance with the License. You may obtain +// a copy of the License in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! C ABI for circuit transforms: structural analysis, canonicalization, +//! knowledge rewrite, and standalone pass wrappers returning new circuits. + +use crate::circuit::CCircuit; +use crate::device::standard_gate_from_name; +use crate::error::CqlibError; +use cqlib_core::circuit::Instruction; +use cqlib_core::compile::CompilerError; +use cqlib_core::compile::knowledge::RuleKind; +use cqlib_core::compile::transform::canonicalize_circuit as core_canonicalize_circuit; +use cqlib_core::compile::transform::rewrite_circuit as core_rewrite_circuit; +use cqlib_core::compile::transform::{ + CanonicalizeConfig, CanonicalizeResult, Canonicalizer, CircuitAnalysis, + CommutativeCancellation, KnowledgeRewriteResult, KnowledgeRewriter, LowerToRoutingBasis, + OptimizeOneQubitRuns, RewriteConfig, RewriteMode, Transformer, +}; +use std::ffi::{CStr, CString}; +use std::os::raw::c_char; + +/// C snapshot of structural circuit facts. Every flag is 0 or 1. +#[repr(C)] +pub struct CCircuitAnalysis { + /// Circuit contains classical-data operations. + pub has_classical_data: u8, + /// Circuit contains classical control flow. + pub has_classical_control: u8, + /// Circuit contains measurement operations. + pub has_measurement: u8, + /// Circuit registers classical values. + pub has_classical_values: u8, + /// Circuit registers classical variables. + pub has_classical_vars: u8, + /// Circuit uses any runtime classical construct. + pub has_runtime_classical: u8, + /// Rewrites must preserve classical handles for this circuit. + pub needs_classical_handle_preservation: u8, + /// Circuit contains unexpanded circuit-backed gate definitions. + pub has_circuit_gate_definitions: u8, + /// Circuit contains unitary definitions with circuit bodies. + pub has_unitary_circuit_definitions: u8, + /// Circuit contains matrix-backed unitary gates. + pub has_unitary_gates: u8, + /// Circuit contains multi-controlled gates. + pub has_mc_gates: u8, +} + +/// Opaque handle around a [`CanonicalizeResult`]. +pub struct CCanonicalizeResult { + pub inner: CanonicalizeResult, +} + +/// Opaque handle around a [`KnowledgeRewriteResult`]. +pub struct CKnowledgeRewriteResult { + pub inner: KnowledgeRewriteResult, +} + +/// Rewrite mode tag. +/// | Value | Mode | +/// |-------|----------------------------------------| +/// | 0 | Conservative optimization (production) | +/// | 1 | Explicit knowledge-based lowering | +pub const REWRITE_MODE_OPTIMIZE: u8 = 0; +pub const REWRITE_MODE_LOWERING: u8 = 1; + +/// C form of [`RewriteConfig`]. +/// +/// `mode` selects the rule-set base (`REWRITE_MODE_OPTIMIZE` uses production +/// defaults, `REWRITE_MODE_LOWERING` adds decomposition and hardware-native +/// rules); the remaining fields override the base values. Boolean fields use +/// 0/1. +#[repr(C)] +pub struct CRewriteConfig { + /// One of `REWRITE_MODE_*`. + pub mode: u8, + /// Fixpoint round limit. + pub max_rounds: u8, + /// 1 recurses into control-flow bodies, 0 does not. + pub recurse_control_flow: u8, + /// 1 skips labeled operations, 0 does not. + pub skip_labeled_ops: u8, + /// 1 preserves undirected two-qubit connectivity, 0 does not. + pub preserve_two_qubit_connectivity: u8, + /// Reserved padding to keep the struct layout stable. + pub _pad: [u8; 3], + /// Matching-window operation limit. + pub max_window_ops: usize, + /// Maximum matched pattern length. + pub max_pattern_len: usize, + /// Borrowed target-basis gate names (each a NUL-terminated string), set + /// with `rewrite_config_with_target_instructions`; NULL when no target + /// basis is configured. The array and its strings are borrowed and must + /// stay valid until the rewrite call that consumes this configuration. + pub target_gate_names: *const *const c_char, + /// Number of entries in `target_gate_names`. + pub target_gate_names_len: usize, +} + +/// C snapshot of knowledge-rewrite run statistics. +#[repr(C)] +pub struct CKnowledgeRewriteStats { + /// Fixpoint rounds actually executed. + pub rounds_executed: u8, + /// 1 when a stable round was observed before `max_rounds`. + pub reached_fixpoint: u8, + /// Reserved padding to keep the struct layout stable. + pub _pad: [u8; 6], + /// Rule patches emitted into rebuilt sequences. + pub rules_applied: usize, + /// Operation sequences whose patch set was non-empty. + pub changed_sequences: usize, +} + +/// C snapshot of knowledge-rewrite performance diagnostics. +#[repr(C)] +pub struct CKnowledgeRewriteDiagnostics { + /// Rewrite phases skipped by a reusable session fixpoint proof. + pub direct_reuses: usize, + /// Anchors presented by verified dirty ranges before density fallback. + pub dirty_anchors: usize, + /// Incremental executions conservatively promoted to full scans. + pub full_scan_fallbacks: usize, + /// Cached rule-condition results reused. + pub condition_cache_hits: usize, + /// Cacheable rule-condition bindings not found in the local cache. + pub condition_cache_misses: usize, + /// Conditions evaluated by the conservative symbolic path. + pub symbolic_fallbacks: usize, +} + +/// Converts a C rewrite configuration into the core form. +fn rewrite_config_from_c(raw: &CRewriteConfig) -> Result { + let config = match raw.mode { + REWRITE_MODE_OPTIMIZE => RewriteConfig::production(), + REWRITE_MODE_LOWERING => RewriteConfig::lowering(), + _ => return Err(CqlibError::InvalidParam as i32), + }; + let mut config = config + .with_max_rounds(raw.max_rounds) + .with_max_window_ops(raw.max_window_ops) + .with_max_pattern_len(raw.max_pattern_len) + .recurse_control_flow(raw.recurse_control_flow != 0) + .skip_labeled_ops(raw.skip_labeled_ops != 0) + .with_preserve_two_qubit_connectivity(raw.preserve_two_qubit_connectivity != 0); + if raw.target_gate_names_len > 0 { + let instructions = + standard_instructions_from_names(raw.target_gate_names, raw.target_gate_names_len)?; + config = match config.try_with_target_instructions(instructions) { + Ok(config) => config, + Err(CompilerError::InvalidInput(_)) => return Err(CqlibError::InvalidParam as i32), + Err(_) => return Err(CqlibError::CompilerError as i32), + }; + } + Ok(config) +} + +/// Returns the production rewrite configuration by value. +#[unsafe(no_mangle)] +pub extern "C" fn rewrite_config_default() -> CRewriteConfig { + CRewriteConfig { + mode: REWRITE_MODE_OPTIMIZE, + max_rounds: 8, + recurse_control_flow: 1, + skip_labeled_ops: 1, + preserve_two_qubit_connectivity: 0, + _pad: [0; 3], + max_window_ops: 16, + max_pattern_len: 8, + target_gate_names: std::ptr::null(), + target_gate_names_len: 0, + } +} + +/// Returns the explicit lowering rewrite configuration by value. +#[unsafe(no_mangle)] +pub extern "C" fn rewrite_config_lowering() -> CRewriteConfig { + let mut config = rewrite_config_default(); + config.mode = REWRITE_MODE_LOWERING; + config +} + +/// Restricts a by-value rewrite configuration to an explicit standard-gate +/// target instruction basis, parsed from `len` gate names (e.g. `"H"`, +/// `"CX"`). +/// +/// The name array is borrowed and must remain valid until the rewrite call +/// that consumes `config`. The target basis only takes effect when `config` +/// uses `REWRITE_MODE_LOWERING`. Returns 0 on success, -1 on NULL pointers, +/// -4 on invalid UTF-8 or an unknown gate name, or -8 for an empty basis +/// (the configuration is then left unchanged). +#[unsafe(no_mangle)] +pub extern "C" fn rewrite_config_with_target_instructions( + config: *mut CRewriteConfig, + gate_names: *const *const c_char, + len: usize, +) -> i32 { + if config.is_null() { + return CqlibError::NullPtr as i32; + } + if len == 0 { + return CqlibError::InvalidParam as i32; + } + // Validate eagerly so bad names are reported at set time; the parsed + // instructions are rebuilt lazily in `rewrite_config_from_c`. + if let Err(code) = standard_instructions_from_names(gate_names, len) { + return code; + } + unsafe { + (*config).target_gate_names = gate_names; + (*config).target_gate_names_len = len; + } + CqlibError::Ok as i32 +} + +/// Writes a structural-analysis snapshot of `circuit` to `*out`. +/// +/// Returns 0 on success or -1 on NULL arguments. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_analyze(circuit: *const CCircuit, out: *mut CCircuitAnalysis) -> i32 { + if circuit.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + let circuit = unsafe { &(*circuit).inner }; + let analysis = CircuitAnalysis::analyze(circuit); + unsafe { + *out = CCircuitAnalysis { + has_classical_data: analysis.has_classical_data as u8, + has_classical_control: analysis.has_classical_control as u8, + has_measurement: analysis.has_measurement as u8, + has_classical_values: analysis.has_classical_values as u8, + has_classical_vars: analysis.has_classical_vars as u8, + has_runtime_classical: analysis.has_runtime_classical as u8, + needs_classical_handle_preservation: analysis.needs_classical_handle_preservation as u8, + has_circuit_gate_definitions: analysis.has_circuit_gate_definitions as u8, + has_unitary_circuit_definitions: analysis.has_unitary_circuit_definitions as u8, + has_unitary_gates: analysis.has_unitary_gates as u8, + has_mc_gates: analysis.has_mc_gates as u8, + }; + } + CqlibError::Ok as i32 +} + +// ===== Canonicalization ===== + +/// Canonicalizes `circuit` with production defaults. +/// +/// Returns a heap-allocated `CCanonicalizeResult*`, or NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn canonicalize_circuit(circuit: *const CCircuit) -> *mut CCanonicalizeResult { + if circuit.is_null() { + return std::ptr::null_mut(); + } + let circuit = unsafe { &(*circuit).inner }; + match core_canonicalize_circuit(circuit) { + Ok(result) => Box::into_raw(Box::new(CCanonicalizeResult { inner: result })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Frees a canonicalize result. NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn canonicalize_result_free(ptr: *mut CCanonicalizeResult) { + if !ptr.is_null() { + unsafe { + let _ = Box::from_raw(ptr); + } + } +} + +/// Returns the canonicalized circuit as an owned `CCircuit*` (free with +/// `circuit_free`). Returns NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn canonicalize_result_circuit(ptr: *const CCanonicalizeResult) -> *mut CCircuit { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let result = unsafe { &*ptr }; + Box::into_raw(Box::new(CCircuit { + inner: result.inner.circuit.clone(), + })) +} + +/// Returns 1 when the output differs from the input representation, 0 +/// otherwise, or -1 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn canonicalize_result_changed(ptr: *const CCanonicalizeResult) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + unsafe { (*ptr).inner.changed as i32 } +} + +/// Returns the number of canonicalization rounds executed, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn canonicalize_result_rounds(ptr: *const CCanonicalizeResult) -> u8 { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.rounds } +} + +// ===== Canonicalize configuration ===== + +/// Opaque handle around a [`CanonicalizeConfig`]. +pub struct CCanonicalizeConfig { + pub inner: CanonicalizeConfig, +} + +/// Creates a canonicalize config with production defaults (round limit 8, +/// all optional behaviors enabled). +/// +/// Returns a heap-allocated `CCanonicalizeConfig*`, or NULL on allocation +/// failure. Free with `canonicalize_config_free`. +#[unsafe(no_mangle)] +pub extern "C" fn canonicalize_config_default() -> *mut CCanonicalizeConfig { + Box::into_raw(Box::new(CCanonicalizeConfig { + inner: CanonicalizeConfig::production(), + })) +} + +/// Frees a canonicalize config. Passing NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn canonicalize_config_free(ptr: *mut CCanonicalizeConfig) { + if !ptr.is_null() { + unsafe { + drop(Box::from_raw(ptr)); + } + } +} + +/// Sets the maximum number of canonicalization rounds. Returns 0, or -1 on +/// NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn canonicalize_config_with_round_limit( + ptr: *mut CCanonicalizeConfig, + round_limit: u8, +) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + unsafe { + (*ptr).inner = (*ptr).inner.clone().with_round_limit(round_limit); + } + CqlibError::Ok as i32 +} + +fn canonicalize_config_set_flag( + ptr: *mut CCanonicalizeConfig, + enabled: i32, + apply: impl FnOnce(CanonicalizeConfig, bool) -> CanonicalizeConfig, +) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + unsafe { + let current = (*ptr).inner.clone(); + (*ptr).inner = apply(current, enabled != 0); + } + CqlibError::Ok as i32 +} + +fn canonicalize_config_get_flag( + ptr: *const CCanonicalizeConfig, + read: impl FnOnce(&CanonicalizeConfig) -> bool, +) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + unsafe { read(&(*ptr).inner) as i32 } +} + +/// Sets whether control-flow bodies are recursively canonicalized (1) or not +/// (0). Returns 0, or -1 on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn canonicalize_config_with_recurse_control_flow( + ptr: *mut CCanonicalizeConfig, + enabled: i32, +) -> i32 { + canonicalize_config_set_flag(ptr, enabled, |cfg, on| cfg.recurse_control_flow(on)) +} + +/// Sets whether `GPhase` operations are folded into scope-local phase. +/// Returns 0, or -1 on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn canonicalize_config_with_fold_gphase( + ptr: *mut CCanonicalizeConfig, + enabled: i32, +) -> i32 { + canonicalize_config_set_flag(ptr, enabled, |cfg, on| cfg.fold_gphase(on)) +} + +/// Sets whether `McGate` forms are collapsed into existing standard gates. +/// Returns 0, or -1 on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn canonicalize_config_with_canonicalize_instruction_form( + ptr: *mut CCanonicalizeConfig, + enabled: i32, +) -> i32 { + canonicalize_config_set_flag(ptr, enabled, |cfg, on| { + cfg.canonicalize_instruction_form(on) + }) +} + +/// Sets whether strict no-op removal is performed. Returns 0, or -1 on NULL +/// input. +#[unsafe(no_mangle)] +pub extern "C" fn canonicalize_config_with_drop_noops( + ptr: *mut CCanonicalizeConfig, + enabled: i32, +) -> i32 { + canonicalize_config_set_flag(ptr, enabled, |cfg, on| cfg.drop_noops(on)) +} + +/// Sets whether barrier scopes are canonicalized and adjacent barriers +/// merged. Returns 0, or -1 on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn canonicalize_config_with_canonicalize_barriers( + ptr: *mut CCanonicalizeConfig, + enabled: i32, +) -> i32 { + canonicalize_config_set_flag(ptr, enabled, |cfg, on| cfg.canonicalize_barriers(on)) +} + +/// Returns the configured round limit, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn canonicalize_config_round_limit(ptr: *const CCanonicalizeConfig) -> u8 { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.round_limit() } +} + +/// Returns 1 when control-flow recursion is enabled, 0 otherwise, or -1 for +/// NULL. +#[unsafe(no_mangle)] +pub extern "C" fn canonicalize_config_recurse_control_flow(ptr: *const CCanonicalizeConfig) -> i32 { + canonicalize_config_get_flag(ptr, |cfg| cfg.recurses_control_flow()) +} + +/// Returns 1 when `GPhase` folding is enabled, 0 otherwise, or -1 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn canonicalize_config_fold_gphase(ptr: *const CCanonicalizeConfig) -> i32 { + canonicalize_config_get_flag(ptr, |cfg| cfg.folds_gphase()) +} + +/// Returns 1 when instruction form canonicalization is enabled, 0 otherwise, +/// or -1 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn canonicalize_config_canonicalize_instruction_form( + ptr: *const CCanonicalizeConfig, +) -> i32 { + canonicalize_config_get_flag(ptr, |cfg| cfg.canonicalizes_instruction_form()) +} + +/// Returns 1 when strict no-op removal is enabled, 0 otherwise, or -1 for +/// NULL. +#[unsafe(no_mangle)] +pub extern "C" fn canonicalize_config_drop_noops(ptr: *const CCanonicalizeConfig) -> i32 { + canonicalize_config_get_flag(ptr, |cfg| cfg.drops_noops()) +} + +/// Returns 1 when barrier canonicalization is enabled, 0 otherwise, or -1 +/// for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn canonicalize_config_canonicalize_barriers( + ptr: *const CCanonicalizeConfig, +) -> i32 { + canonicalize_config_get_flag(ptr, |cfg| cfg.canonicalizes_barriers()) +} + +/// Canonicalizes `circuit` using the supplied configuration. The config is +/// not consumed and remains owned by the caller. +/// +/// Returns a heap-allocated `CCanonicalizeResult*`, or NULL on error or NULL +/// input. +#[unsafe(no_mangle)] +pub extern "C" fn canonicalize_circuit_with_config( + circuit: *const CCircuit, + config: *const CCanonicalizeConfig, +) -> *mut CCanonicalizeResult { + if circuit.is_null() || config.is_null() { + return std::ptr::null_mut(); + } + let circuit = unsafe { &(*circuit).inner }; + let canonicalizer = Canonicalizer::new(unsafe { &(*config).inner }.clone()); + match canonicalizer.run(circuit) { + Ok(result) => Box::into_raw(Box::new(CCanonicalizeResult { inner: result })), + Err(_) => std::ptr::null_mut(), + } +} + +// ===== Knowledge rewrite ===== + +/// Applies the knowledge rewriter to `circuit` with the supplied +/// configuration. +/// +/// Returns a heap-allocated `CKnowledgeRewriteResult*`, or NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn rewrite_circuit( + circuit: *const CCircuit, + config: CRewriteConfig, +) -> *mut CKnowledgeRewriteResult { + if circuit.is_null() { + return std::ptr::null_mut(); + } + let Ok(config) = rewrite_config_from_c(&config) else { + return std::ptr::null_mut(); + }; + let circuit = unsafe { &(*circuit).inner }; + match core_rewrite_circuit(circuit, config) { + Ok(result) => Box::into_raw(Box::new(CKnowledgeRewriteResult { inner: result })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Frees a knowledge-rewrite result. NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_rewrite_result_free(ptr: *mut CKnowledgeRewriteResult) { + if !ptr.is_null() { + unsafe { + let _ = Box::from_raw(ptr); + } + } +} + +/// Returns the rewritten circuit as an owned `CCircuit*` (free with +/// `circuit_free`). Returns NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_rewrite_result_circuit( + ptr: *const CKnowledgeRewriteResult, +) -> *mut CCircuit { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let result = unsafe { &*ptr }; + Box::into_raw(Box::new(CCircuit { + inner: result.inner.circuit.clone(), + })) +} + +/// Returns 1 when the output differs from the input representation, 0 +/// otherwise, or -1 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_rewrite_result_changed(ptr: *const CKnowledgeRewriteResult) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + unsafe { (*ptr).inner.changed as i32 } +} + +/// Writes the rewrite-statistics snapshot to `*out`. +/// +/// Returns 0 on success or -1 on NULL arguments. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_rewrite_result_stats( + ptr: *const CKnowledgeRewriteResult, + out: *mut CKnowledgeRewriteStats, +) -> i32 { + if ptr.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + let result = unsafe { &*ptr }; + let stats = &result.inner.stats; + unsafe { + *out = CKnowledgeRewriteStats { + rounds_executed: stats.rounds_executed, + reached_fixpoint: stats.reached_fixpoint as u8, + _pad: [0; 6], + rules_applied: stats.rules_applied, + changed_sequences: stats.changed_sequences, + }; + } + CqlibError::Ok as i32 +} + +/// Writes the rewrite-diagnostics snapshot to `*out`. +/// +/// Returns 0 on success or -1 on NULL arguments. +#[unsafe(no_mangle)] +pub extern "C" fn knowledge_rewrite_result_diagnostics( + ptr: *const CKnowledgeRewriteResult, + out: *mut CKnowledgeRewriteDiagnostics, +) -> i32 { + if ptr.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + let result = unsafe { &*ptr }; + let diagnostics = &result.inner.diagnostics; + unsafe { + *out = CKnowledgeRewriteDiagnostics { + direct_reuses: diagnostics.direct_reuses, + dirty_anchors: diagnostics.dirty_anchors, + full_scan_fallbacks: diagnostics.full_scan_fallbacks, + condition_cache_hits: diagnostics.condition_cache_hits, + condition_cache_misses: diagnostics.condition_cache_misses, + symbolic_fallbacks: diagnostics.symbolic_fallbacks, + }; + } + CqlibError::Ok as i32 +} + +// ===== Standalone pass wrappers ===== + +/// Runs one `Transformer` pass and returns the resolved circuit. +fn run_transform_pass(pass: &T, circuit: *const CCircuit) -> *mut CCircuit { + if circuit.is_null() { + return std::ptr::null_mut(); + } + let circuit = unsafe { &(*circuit).inner }; + match pass.transform(circuit, None) { + Ok(outcome) => Box::into_raw(Box::new(CCircuit { + inner: outcome.into_circuit(circuit), + })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Canonicalizes `circuit` with production defaults and returns the rebuilt +/// circuit as an owned `CCircuit*` (free with `circuit_free`). +/// +/// Returns NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn transform_canonicalize(circuit: *const CCircuit) -> *mut CCircuit { + run_transform_pass(&Canonicalizer::production(), circuit) +} + +/// Applies the knowledge rewriter to `circuit` and returns the rebuilt circuit +/// as an owned `CCircuit*` (free with `circuit_free`). +/// +/// Returns NULL on error (including an invalid `config.mode`). +#[unsafe(no_mangle)] +pub extern "C" fn transform_knowledge_rewrite( + circuit: *const CCircuit, + config: CRewriteConfig, +) -> *mut CCircuit { + if circuit.is_null() { + return std::ptr::null_mut(); + } + let Ok(config) = rewrite_config_from_c(&config) else { + return std::ptr::null_mut(); + }; + run_transform_pass(&KnowledgeRewriter::new(config), circuit) +} + +/// Fuses fixed numeric one-qubit runs through exact synthesis (target-neutral +/// logical cost) and returns the rebuilt circuit as an owned `CCircuit*` (free +/// with `circuit_free`). +/// +/// Returns NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn transform_optimize_one_qubit_runs(circuit: *const CCircuit) -> *mut CCircuit { + run_transform_pass(&OptimizeOneQubitRuns::logical(), circuit) +} + +/// Cancels self-inverse gate pairs through global commutation analysis and +/// returns the rebuilt circuit as an owned `CCircuit*` (free with +/// `circuit_free`). +/// +/// Returns NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn transform_commutative_cancellation(circuit: *const CCircuit) -> *mut CCircuit { + run_transform_pass(&CommutativeCancellation::new(), circuit) +} + +/// Lowers `circuit` to the routing basis and returns the rebuilt circuit as +/// an owned `CCircuit*` (free with `circuit_free`). +/// +/// Returns NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn transform_lower_to_routing_basis(circuit: *const CCircuit) -> *mut CCircuit { + run_transform_pass(&LowerToRoutingBasis::new(None), circuit) +} + +// ===== Rewrite configuration handle ===== + +/// Rewrite rule-category tag used by `transform_config_with_enabled_kinds` +/// and `transform_config_enabled_kinds`. +/// | Value | Kind | +/// |-------|---------------------------| +/// | 0 | Algebraic simplification | +/// | 1 | Inverse/repeat cancellation | +/// | 2 | Neighbor-gate merge | +/// | 3 | Explicit commutation | +/// | 4 | Decomposition / lowering | +/// | 5 | Canonical representation | +/// | 6 | Hardware-native rewriting | +/// | 7 | Other | +pub const REWRITE_KIND_SIMPLIFY: u8 = 0; +pub const REWRITE_KIND_CANCEL: u8 = 1; +pub const REWRITE_KIND_MERGE: u8 = 2; +pub const REWRITE_KIND_COMMUTE: u8 = 3; +pub const REWRITE_KIND_DECOMPOSE: u8 = 4; +pub const REWRITE_KIND_CANONICALIZE: u8 = 5; +pub const REWRITE_KIND_HARDWARE_NATIVE: u8 = 6; +pub const REWRITE_KIND_OTHER: u8 = 7; + +/// Opaque handle around a core [`RewriteConfig`] (the knowledge-rewrite / +/// transform configuration). +pub struct CTransformConfig { + pub inner: RewriteConfig, +} + +/// Maps a `REWRITE_KIND_*` tag to the core rule kind. +fn rule_kind_from_tag(tag: u8) -> Option { + Some(match tag { + REWRITE_KIND_SIMPLIFY => RuleKind::Simplify, + REWRITE_KIND_CANCEL => RuleKind::Cancel, + REWRITE_KIND_MERGE => RuleKind::Merge, + REWRITE_KIND_COMMUTE => RuleKind::Commute, + REWRITE_KIND_DECOMPOSE => RuleKind::Decompose, + REWRITE_KIND_CANONICALIZE => RuleKind::Canonicalize, + REWRITE_KIND_HARDWARE_NATIVE => RuleKind::HardwareNative, + REWRITE_KIND_OTHER => RuleKind::Other, + _ => return None, + }) +} + +/// Maps a core rule kind to its `REWRITE_KIND_*` tag. +fn rule_kind_tag(kind: RuleKind) -> u8 { + match kind { + RuleKind::Simplify => REWRITE_KIND_SIMPLIFY, + RuleKind::Cancel => REWRITE_KIND_CANCEL, + RuleKind::Merge => REWRITE_KIND_MERGE, + RuleKind::Commute => REWRITE_KIND_COMMUTE, + RuleKind::Decompose => REWRITE_KIND_DECOMPOSE, + RuleKind::Canonicalize => REWRITE_KIND_CANONICALIZE, + RuleKind::HardwareNative => REWRITE_KIND_HARDWARE_NATIVE, + RuleKind::Other => REWRITE_KIND_OTHER, + } +} + +/// Parses a C array of standard-gate names into core instructions. +/// +/// Returns -1 on NULL entries, -4 on invalid UTF-8 or an unknown gate name. +pub(crate) fn standard_instructions_from_names( + names: *const *const c_char, + len: usize, +) -> Result, i32> { + if len == 0 { + return Ok(Vec::new()); + } + if names.is_null() { + return Err(CqlibError::NullPtr as i32); + } + let raw = unsafe { std::slice::from_raw_parts(names, len) }; + let mut instructions = Vec::with_capacity(len); + for name in raw { + if name.is_null() { + return Err(CqlibError::NullPtr as i32); + } + let name = match unsafe { CStr::from_ptr(*name) }.to_str() { + Ok(name) => name, + Err(_) => return Err(CqlibError::ParseError as i32), + }; + let Some(gate) = standard_gate_from_name(name) else { + return Err(CqlibError::ParseError as i32); + }; + instructions.push(Instruction::Standard(gate)); + } + Ok(instructions) +} + +/// Copies a name list into `out` (two-step pattern). Each written entry is a +/// freshly allocated C string the caller frees with `cqlib_string_free`. +/// Returns the total number of available entries. +pub(crate) fn write_name_list(names: &[String], out: *mut *mut c_char, len: usize) -> usize { + let total = names.len(); + if !out.is_null() { + for (i, name) in names.iter().take(total.min(len)).enumerate() { + let written = CString::new(name.as_str()) + .map(|s| s.into_raw()) + .unwrap_or(std::ptr::null_mut()); + unsafe { *out.add(i) = written }; + } + } + total +} + +/// Creates a transform configuration with production (conservative +/// optimization) defaults. +/// +/// Returns a heap-allocated `CTransformConfig*` (free with +/// `transform_config_free`), or NULL on allocation failure. +#[unsafe(no_mangle)] +pub extern "C" fn transform_config_default() -> *mut CTransformConfig { + Box::into_raw(Box::new(CTransformConfig { + inner: RewriteConfig::production(), + })) +} + +/// Creates a transform configuration with explicit knowledge-based lowering +/// defaults. +/// +/// Returns a heap-allocated `CTransformConfig*` (free with +/// `transform_config_free`), or NULL on allocation failure. +#[unsafe(no_mangle)] +pub extern "C" fn transform_config_lowering() -> *mut CTransformConfig { + Box::into_raw(Box::new(CTransformConfig { + inner: RewriteConfig::lowering(), + })) +} + +/// Frees a transform configuration. NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn transform_config_free(ptr: *mut CTransformConfig) { + if !ptr.is_null() { + unsafe { + let _ = Box::from_raw(ptr); + } + } +} + +/// Sets the rewrite mode of `config` in place. +/// +/// `mode` must be one of `REWRITE_MODE_*`. Returns 0 on success, -1 on NULL, +/// or -8 on an unknown mode tag. +#[unsafe(no_mangle)] +pub extern "C" fn transform_config_with_mode(ptr: *mut CTransformConfig, mode: u8) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + let mode = match mode { + REWRITE_MODE_OPTIMIZE => RewriteMode::Optimize, + REWRITE_MODE_LOWERING => RewriteMode::Lowering, + _ => return CqlibError::InvalidParam as i32, + }; + unsafe { + (*ptr).inner = (*ptr).inner.clone().with_mode(mode); + } + CqlibError::Ok as i32 +} + +/// Sets the fixpoint round limit of `config` in place. +/// +/// Returns 0 on success or -1 on NULL. +#[unsafe(no_mangle)] +pub extern "C" fn transform_config_with_max_rounds( + ptr: *mut CTransformConfig, + max_rounds: u8, +) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + unsafe { + (*ptr).inner = (*ptr).inner.clone().with_max_rounds(max_rounds); + } + CqlibError::Ok as i32 +} + +/// Sets the matching-window operation limit of `config` in place. +/// +/// Returns 0 on success or -1 on NULL. +#[unsafe(no_mangle)] +pub extern "C" fn transform_config_with_max_window_ops( + ptr: *mut CTransformConfig, + max_window_ops: usize, +) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + unsafe { + (*ptr).inner = (*ptr).inner.clone().with_max_window_ops(max_window_ops); + } + CqlibError::Ok as i32 +} + +/// Sets the maximum matched pattern length of `config` in place. +/// +/// Returns 0 on success or -1 on NULL. +#[unsafe(no_mangle)] +pub extern "C" fn transform_config_with_max_pattern_len( + ptr: *mut CTransformConfig, + max_pattern_len: usize, +) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + unsafe { + (*ptr).inner = (*ptr).inner.clone().with_max_pattern_len(max_pattern_len); + } + CqlibError::Ok as i32 +} + +/// Replaces the enabled rule categories of `config` in place. +/// +/// `kinds` points at `len` `REWRITE_KIND_*` tags (NULL when `len` is 0, which +/// disables every rule category). Returns 0 on success, -1 on NULL, or -8 on +/// an unknown kind tag (the configuration is then left unchanged). +#[unsafe(no_mangle)] +pub extern "C" fn transform_config_with_enabled_kinds( + ptr: *mut CTransformConfig, + kinds: *const u8, + len: usize, +) -> i32 { + if ptr.is_null() || (len > 0 && kinds.is_null()) { + return CqlibError::NullPtr as i32; + } + let raw = if len == 0 { + &[] + } else { + unsafe { std::slice::from_raw_parts(kinds, len) } + }; + let mut parsed = Vec::with_capacity(len); + for tag in raw { + match rule_kind_from_tag(*tag) { + Some(kind) => parsed.push(kind), + None => return CqlibError::InvalidParam as i32, + } + } + unsafe { + (*ptr).inner = (*ptr).inner.clone().with_enabled_kinds(parsed); + } + CqlibError::Ok as i32 +} + +/// Restricts the transform configuration to an explicit standard-gate target +/// instruction basis, parsed from `len` gate names (e.g. `"H"`, `"CX"`). +/// +/// An empty basis is rejected. Returns 0 on success, -1 on NULL, -4 on +/// invalid UTF-8 or an unknown gate name, -8 for an empty basis, or -6 when +/// the core rejects the basis (the configuration is then left unchanged). +#[unsafe(no_mangle)] +pub extern "C" fn transform_config_with_target_instructions( + ptr: *mut CTransformConfig, + gate_names: *const *const c_char, + len: usize, +) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + let instructions = match standard_instructions_from_names(gate_names, len) { + Ok(instructions) => instructions, + Err(code) => return code, + }; + let config = unsafe { &mut (*ptr).inner }; + match config.clone().try_with_target_instructions(instructions) { + Ok(updated) => { + *config = updated; + CqlibError::Ok as i32 + } + Err(CompilerError::InvalidInput(_)) => CqlibError::InvalidParam as i32, + Err(_) => CqlibError::CompilerError as i32, + } +} + +/// Returns the `REWRITE_MODE_*` mode tag of `config`, or -1 on NULL. +#[unsafe(no_mangle)] +pub extern "C" fn transform_config_mode(ptr: *const CTransformConfig) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + match unsafe { (*ptr).inner.mode() } { + RewriteMode::Optimize => REWRITE_MODE_OPTIMIZE as i32, + RewriteMode::Lowering => REWRITE_MODE_LOWERING as i32, + } +} + +/// Returns the fixpoint round limit of `config`, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn transform_config_max_rounds(ptr: *const CTransformConfig) -> u8 { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.max_rounds() } +} + +/// Returns the matching-window operation limit of `config`, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn transform_config_max_window_ops(ptr: *const CTransformConfig) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.max_window_ops() } +} + +/// Returns the maximum matched pattern length of `config`, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn transform_config_max_pattern_len(ptr: *const CTransformConfig) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.max_pattern_len() } +} + +/// Returns 1 when `config` recurses into control-flow bodies, 0 otherwise, +/// or -1 on NULL. +#[unsafe(no_mangle)] +pub extern "C" fn transform_config_recurse_control_flow(ptr: *const CTransformConfig) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + unsafe { (*ptr).inner.recurses_control_flow() as i32 } +} + +/// Returns 1 when `config` skips labeled operations, 0 otherwise, or -1 on +/// NULL. +#[unsafe(no_mangle)] +pub extern "C" fn transform_config_skip_labeled_ops(ptr: *const CTransformConfig) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + unsafe { (*ptr).inner.skips_labeled_ops() as i32 } +} + +/// Returns the number of enabled rule categories of `config`, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn transform_config_enabled_kinds_len(ptr: *const CTransformConfig) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.enabled_kinds().len() } +} + +/// Copies the enabled rule categories of `config` into `out` as +/// `REWRITE_KIND_*` tags (two-step pattern; pair with +/// `transform_config_enabled_kinds_len`). Returns the total number of +/// categories, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn transform_config_enabled_kinds( + ptr: *const CTransformConfig, + out: *mut u8, + len: usize, +) -> usize { + if ptr.is_null() { + return 0; + } + let kinds = unsafe { (*ptr).inner.enabled_kinds() }; + let total = kinds.len(); + if !out.is_null() { + for (i, kind) in kinds.iter().take(total.min(len)).enumerate() { + unsafe { *out.add(i) = rule_kind_tag(*kind) }; + } + } + total +} + +/// Returns the number of configured target-basis instructions of `config` +/// (0 when no target basis is set or on NULL). +#[unsafe(no_mangle)] +pub extern "C" fn transform_config_target_instruction_basis_len( + ptr: *const CTransformConfig, +) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { + (*ptr) + .inner + .target_instruction_basis() + .map_or(0, |basis| basis.len()) + } +} + +/// Copies the configured target-basis gate names of `config` into `out` +/// (two-step pattern; pair with +/// `transform_config_target_instruction_basis_len`). Each written entry is a +/// freshly allocated C string freed with `cqlib_string_free`. Returns the +/// total number of instructions, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn transform_config_target_instruction_basis( + ptr: *const CTransformConfig, + out: *mut *mut c_char, + len: usize, +) -> usize { + if ptr.is_null() { + return 0; + } + let Some(basis) = (unsafe { (*ptr).inner.target_instruction_basis() }) else { + return 0; + }; + let names: Vec = basis.iter().map(|instruction| instruction.name()).collect(); + write_name_list(&names, out, len) +} diff --git a/crates/binding-c/src/compile/workflow.rs b/crates/binding-c/src/compile/workflow.rs new file mode 100644 index 00000000..6ee65190 --- /dev/null +++ b/crates/binding-c/src/compile/workflow.rs @@ -0,0 +1,215 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! C ABI for the reusable compiler workflow and device compilation metadata. + +use crate::circuit::CCircuit; +use crate::compile::{ + CCompileResult, COMPILE_MODE_ENHANCED, COMPILE_MODE_NORMAL, COMPILE_TARGET_LOGICAL, + CompileConfigC, +}; +use crate::device::CLayout; +use crate::error::CqlibError; +use cqlib_core::compile::resource::ResourcePolicy; +use cqlib_core::compile::{CompileConfig, CompileMode, CompileTarget, CompilerWorkflow}; + +/// Opaque handle around a [`CompilerWorkflow`]. +pub struct CCompilerWorkflow { + pub inner: CompilerWorkflow, +} + +/// Create a reusable compiler workflow from a C configuration. +/// +/// `config.mode` selects `Normal` or `Enhanced`. `config.target` only +/// supports `COMPILE_TARGET_LOGICAL`; other targets fall back to NULL +/// because the C ABI does not parse `Instruction` lists or `Device` +/// objects. Target preparation is validated eagerly (the mirror of +/// `CompilerWorkflow::try_new`), so an invalid configuration returns NULL. +#[unsafe(no_mangle)] +pub extern "C" fn compiler_workflow_new(config: CompileConfigC) -> *mut CCompilerWorkflow { + let mode = match config.mode { + COMPILE_MODE_NORMAL => CompileMode::Normal, + COMPILE_MODE_ENHANCED => CompileMode::Enhanced, + _ => return std::ptr::null_mut(), + }; + let target = match config.target { + COMPILE_TARGET_LOGICAL => CompileTarget::Logical, + _ => return std::ptr::null_mut(), + }; + let cfg = CompileConfig { + mode, + target, + resource_policy: ResourcePolicy::default(), + }; + match CompilerWorkflow::try_new(cfg) { + Ok(workflow) => Box::into_raw(Box::new(CCompilerWorkflow { inner: workflow })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Free a compiler workflow. NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn compiler_workflow_free(ptr: *mut CCompilerWorkflow) { + if !ptr.is_null() { + unsafe { + let _ = Box::from_raw(ptr); + } + } +} + +/// Run the workflow over `circuit` and return a heap-allocated compile +/// result. +/// +/// The workflow can be run repeatedly on different circuits; the input +/// circuit is never modified. Returns a `CCompileResult*` (free with +/// `compile_result_free`), or NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn compiler_workflow_run( + workflow: *const CCompilerWorkflow, + circuit: *const CCircuit, +) -> *mut CCompileResult { + if workflow.is_null() || circuit.is_null() { + return std::ptr::null_mut(); + } + let workflow = unsafe { &(*workflow).inner }; + let circuit = unsafe { &(*circuit).inner }; + match workflow.run(circuit) { + Ok(result) => Box::into_raw(Box::new(CCompileResult { inner: result })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Return 1 when the result carries device compilation metadata (a +/// device-topology compile), 0 when it does not (for example after a +/// logical-target compile), or -1 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn compile_result_has_device_metadata(ptr: *const CCompileResult) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + let result = unsafe { &*ptr }; + if result.inner.device_metadata.is_some() { + 1 + } else { + 0 + } +} + +/// Return the initial logical-to-physical layout recorded by a +/// device-target compilation, as an owned `CLayout*` (free with +/// `layout_free`). +/// +/// Returns NULL when the result carries no device metadata (for example +/// after a logical-target compile) or on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn compile_result_device_metadata_initial_layout( + ptr: *const CCompileResult, +) -> *mut CLayout { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let result = unsafe { &*ptr }; + match &result.inner.device_metadata { + Some(metadata) => Box::into_raw(Box::new(CLayout { + inner: metadata.initial_layout.clone(), + })), + None => std::ptr::null_mut(), + } +} + +/// Return the final logical-to-physical layout recorded by a device-target +/// compilation, as an owned `CLayout*` (free with `layout_free`). +/// +/// Returns NULL when the result carries no device metadata (for example +/// after a logical-target compile) or on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn compile_result_device_metadata_final_layout( + ptr: *const CCompileResult, +) -> *mut CLayout { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let result = unsafe { &*ptr }; + match &result.inner.device_metadata { + Some(metadata) => Box::into_raw(Box::new(CLayout { + inner: metadata.final_layout.clone(), + })), + None => std::ptr::null_mut(), + } +} + +/// Return the number of entries in the recorded virtual output permutation +/// (original output qubit -> rewritten output qubit), or 0 for NULL input +/// or a result without device metadata. +#[unsafe(no_mangle)] +pub extern "C" fn compile_result_device_metadata_permutation_len( + ptr: *const CCompileResult, +) -> usize { + if ptr.is_null() { + return 0; + } + let result = unsafe { &*ptr }; + match &result.inner.device_metadata { + Some(metadata) => metadata + .virtual_permutation + .original_output_to_rewritten_output() + .len(), + None => 0, + } +} + +/// Copy the virtual output permutation into the `original` and `rewritten` +/// buffers as `uint32_t` qubit IDs, in ascending original-output order. +/// +/// Each index `i` records that the state of original output +/// `original[i]` is carried by rewritten output `rewritten[i]`. `len` must +/// equal `compile_result_device_metadata_permutation_len(ptr)`. +/// +/// Returns 0 on success (including a no-metadata result with len 0), -1 on +/// NULL arguments, or -8 on a length mismatch. +#[unsafe(no_mangle)] +pub extern "C" fn compile_result_device_metadata_permutation( + ptr: *const CCompileResult, + original: *mut u32, + rewritten: *mut u32, + len: usize, +) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + let result = unsafe { &*ptr }; + let permutation = match &result.inner.device_metadata { + Some(metadata) => metadata + .virtual_permutation + .original_output_to_rewritten_output(), + None => { + return if len == 0 { + 0 + } else { + CqlibError::InvalidParam as i32 + }; + } + }; + if permutation.len() != len { + return CqlibError::InvalidParam as i32; + } + if len > 0 && (original.is_null() || rewritten.is_null()) { + return CqlibError::NullPtr as i32; + } + for (i, (from, to)) in permutation.iter().enumerate() { + unsafe { + *original.add(i) = from.id(); + *rewritten.add(i) = to.id(); + } + } + 0 +} diff --git a/crates/binding-c/src/device/layout.rs b/crates/binding-c/src/device/layout.rs new file mode 100644 index 00000000..ddf44a26 --- /dev/null +++ b/crates/binding-c/src/device/layout.rs @@ -0,0 +1,433 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! C ABI for `Layout` (logical-to-physical qubit mapping). + +use crate::compile::layout::{CCircuitLayoutAnalysis, CPhysicalLayoutGraph, CPreparedSabreCircuit}; +use crate::device::{CLayout, write_u32_list, write_u32_pairs}; +use crate::error::CqlibError; +use cqlib_core::device::error::LayoutError; +use cqlib_core::device::{Layout, LogicalQubit, PhysicalQubit}; + +/// Maps a `LayoutError` to a C error code: unknown physical qubits map to +/// -2, conflicting or missing bindings map to -8. +fn layout_error_code(error: &LayoutError) -> i32 { + match error { + LayoutError::InvalidPhysicalQubit(_) => CqlibError::QubitOutOfBounds as i32, + _ => CqlibError::InvalidParam as i32, + } +} + +/// Creates a layout mapping logical qubits `0..num_logical` onto the supplied +/// physical qubits in order. +/// +/// `logical` and `physical` are arrays of u32 qubit IDs; both must have the +/// same non-zero length. Returns NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn layout_new( + logical: *const u32, + num_logical: usize, + physical: *const u32, + num_physical: usize, +) -> *mut CLayout { + if num_logical == 0 || logical.is_null() || num_physical != num_logical || physical.is_null() { + return std::ptr::null_mut(); + } + let logical: Vec<_> = unsafe { std::slice::from_raw_parts(logical, num_logical) } + .iter() + .map(|&q| LogicalQubit::new(q)) + .collect(); + let physical: Vec<_> = unsafe { std::slice::from_raw_parts(physical, num_physical) } + .iter() + .map(|&q| PhysicalQubit::new(q)) + .collect(); + match Layout::new(logical, physical, None) { + Ok(layout) => Box::into_raw(Box::new(CLayout { inner: layout })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Creates a layout from `(logical, physical)` pairs. +/// +/// `pairs` points to `2 * num_pairs` u32 values laid out as consecutive +/// `(logical, physical)` pairs. `physical_count` defines the total number of +/// physical qubits (`0..physical_count`); unreferenced ones stay vacant. +/// Returns NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn layout_from_pairs( + pairs: *const u32, + num_pairs: usize, + physical_count: u32, +) -> *mut CLayout { + if num_pairs == 0 || pairs.is_null() { + return std::ptr::null_mut(); + } + let raw = unsafe { std::slice::from_raw_parts(pairs, num_pairs * 2) }; + let mapped: Vec<(u32, u32)> = raw.chunks_exact(2).map(|c| (c[0], c[1])).collect(); + match Layout::from_pairs(&mapped, physical_count) { + Ok(layout) => Box::into_raw(Box::new(CLayout { inner: layout })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Frees a layout. NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn layout_free(ptr: *mut CLayout) { + if !ptr.is_null() { + unsafe { + let _ = Box::from_raw(ptr); + } + } +} + +/// Returns the physical qubit mapped to `logical`, or `u32::MAX` when +/// unmapped / on error. +#[unsafe(no_mangle)] +pub extern "C" fn layout_get(ptr: *const CLayout, logical: u32) -> u32 { + if ptr.is_null() { + return u32::MAX; + } + let layout = unsafe { &*ptr }; + match layout.inner.get_physical(LogicalQubit::new(logical)) { + Some(physical) => physical.id(), + None => u32::MAX, + } +} + +/// Returns the number of mapped logical qubits, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn layout_num_logical(ptr: *const CLayout) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.num_logical() } +} + +/// Returns the total number of physical qubits, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn layout_num_physical(ptr: *const CLayout) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.num_physical() } +} + +// ===== Section 3.2: binding, reverse lookup, and map introspection ===== + +/// Binds an unmapped `logical` qubit to a vacant `physical` qubit. +/// +/// Returns 0 on success, -1 on NULL, -2 when `physical` does not belong to +/// the layout, or -8 when either side already participates in a mapping. +#[unsafe(no_mangle)] +pub extern "C" fn layout_bind(ptr: *mut CLayout, logical: u32, physical: u32) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + let wrapper = unsafe { &mut *ptr }; + match wrapper + .inner + .bind(LogicalQubit::new(logical), PhysicalQubit::new(physical)) + { + Ok(()) => CqlibError::Ok as i32, + Err(e) => layout_error_code(&e), + } +} + +/// Removes the mapping for `logical` and writes the released physical qubit +/// ID to `*out_physical`. +/// +/// Returns 0 on success, -1 on NULL, or -8 when `logical` is not bound. +#[unsafe(no_mangle)] +pub extern "C" fn layout_unbind(ptr: *mut CLayout, logical: u32, out_physical: *mut u32) -> i32 { + if ptr.is_null() || out_physical.is_null() { + return CqlibError::NullPtr as i32; + } + let wrapper = unsafe { &mut *ptr }; + match wrapper.inner.unbind(LogicalQubit::new(logical)) { + Ok(physical) => { + unsafe { *out_physical = physical.id() }; + CqlibError::Ok as i32 + } + Err(e) => layout_error_code(&e), + } +} + +/// Returns the logical qubit carried by `physical` (reverse lookup of +/// `layout_get`). +/// +/// Returns: +/// - `uint32_t`: logical qubit ID; `physical` **vacant** or unknown returns +/// `UINT32_MAX` (`u32::MAX`), which callers must treat as a sentinel. +#[unsafe(no_mangle)] +pub extern "C" fn layout_get_logical(ptr: *const CLayout, physical: u32) -> u32 { + if ptr.is_null() { + return u32::MAX; + } + let layout = unsafe { &*ptr }; + match layout.inner.get_logical(PhysicalQubit::new(physical)) { + Some(logical) => logical.id(), + None => u32::MAX, + } +} + +/// Returns 1 when `physical` belongs to the layout and is vacant, 0 when it +/// is occupied or unknown, or -1 on NULL. +#[unsafe(no_mangle)] +pub extern "C" fn layout_is_physical_vacant(ptr: *const CLayout, physical: u32) -> i32 { + if ptr.is_null() { + return -1; + } + unsafe { + (*ptr) + .inner + .is_physical_vacant(PhysicalQubit::new(physical)) as i32 + } +} + +/// Returns the number of logical-to-physical mapping entries, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn layout_l2p_map_len(ptr: *const CLayout) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.l2p_map().len() } +} + +/// Copies the logical-to-physical mapping into `out` as consecutive +/// `(logical, physical)` u32 pairs sorted by logical ID (two-step pattern; +/// pair with `layout_l2p_map_len`). `out` must have room for `2 * len` +/// entries. Returns the total number of pairs. +#[unsafe(no_mangle)] +pub extern "C" fn layout_l2p_map(ptr: *const CLayout, out: *mut u32, len: usize) -> usize { + if ptr.is_null() { + return 0; + } + let pairs: Vec<(u32, u32)> = unsafe { + (*ptr) + .inner + .l2p_map() + .iter() + .map(|(l, p)| (l.id(), p.id())) + .collect() + }; + write_u32_pairs(&pairs, out, len) +} + +/// Returns the number of physical-to-logical mapping entries, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn layout_p2l_map_len(ptr: *const CLayout) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.p2l_map().len() } +} + +/// Copies the physical-to-logical mapping into `out` as consecutive +/// `(physical, logical)` u32 pairs sorted by physical ID (two-step pattern; +/// pair with `layout_p2l_map_len`). `out` must have room for `2 * len` +/// entries. Returns the total number of pairs. +#[unsafe(no_mangle)] +pub extern "C" fn layout_p2l_map(ptr: *const CLayout, out: *mut u32, len: usize) -> usize { + if ptr.is_null() { + return 0; + } + let pairs: Vec<(u32, u32)> = unsafe { + (*ptr) + .inner + .p2l_map() + .iter() + .map(|(p, l)| (p.id(), l.id())) + .collect() + }; + write_u32_pairs(&pairs, out, len) +} + +/// Returns the number of mapped logical qubit IDs, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn layout_logical_qubits_len(ptr: *const CLayout) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.logical_qubits().count() } +} + +/// Copies the mapped logical qubit IDs in ascending order into `out` +/// (two-step pattern; pair with `layout_logical_qubits_len`). Returns the +/// total count. +#[unsafe(no_mangle)] +pub extern "C" fn layout_logical_qubits(ptr: *const CLayout, out: *mut u32, len: usize) -> usize { + if ptr.is_null() { + return 0; + } + let items: Vec = unsafe { (*ptr).inner.logical_qubits() } + .map(|q| q.id()) + .collect(); + write_u32_list(&items, out, len) +} + +/// Returns the number of physical qubit IDs available to the layout +/// (including vacant ones), or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn layout_physical_qubits_len(ptr: *const CLayout) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.physical_qubits().count() } +} + +/// Copies all physical qubit IDs available to the layout (including vacant +/// ones) in ascending order into `out` (two-step pattern; pair with +/// `layout_physical_qubits_len`). Returns the total count. +#[unsafe(no_mangle)] +pub extern "C" fn layout_physical_qubits(ptr: *const CLayout, out: *mut u32, len: usize) -> usize { + if ptr.is_null() { + return 0; + } + let items: Vec = unsafe { (*ptr).inner.physical_qubits() } + .map(|q| q.id()) + .collect(); + write_u32_list(&items, out, len) +} + +/// Returns the number of vacant physical qubits, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn layout_num_vacant_physical(ptr: *const CLayout) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.num_vacant_physical() } +} + +/// Returns the number of vacant physical qubit IDs, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn layout_vacant_physical_qubits_len(ptr: *const CLayout) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.vacant_physical_qubits().count() } +} + +/// Copies the vacant physical qubit IDs in ascending order into `out` +/// (two-step pattern; pair with `layout_vacant_physical_qubits_len`). +/// Returns the total count. +#[unsafe(no_mangle)] +pub extern "C" fn layout_vacant_physical_qubits( + ptr: *const CLayout, + out: *mut u32, + len: usize, +) -> usize { + if ptr.is_null() { + return 0; + } + let items: Vec = unsafe { (*ptr).inner.vacant_physical_qubits() } + .map(|q| q.id()) + .collect(); + write_u32_list(&items, out, len) +} + +/// Swaps the logical qubits carried by two physical qubits; either side may +/// be vacant (a vacant swap moves the logical qubit). +/// +/// Returns 0 on success, -1 on NULL, or -2 when either physical qubit does +/// not belong to the layout. +#[unsafe(no_mangle)] +pub extern "C" fn layout_swap_physical(ptr: *mut CLayout, phys_a: u32, phys_b: u32) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + let wrapper = unsafe { &mut *ptr }; + match wrapper + .inner + .swap_physical(PhysicalQubit::new(phys_a), PhysicalQubit::new(phys_b)) + { + Ok(()) => CqlibError::Ok as i32, + Err(e) => layout_error_code(&e), + } +} + +// ===== Section 3.2.1: layout result helpers (distance table, activity, +// analysis) ===== + +/// Copies the all-pairs undirected shortest-path distance table of the +/// physical layout graph into `out` (two-step pattern). +/// +/// The table is row-major over the graph's physical qubits in ascending ID +/// order (`n * n` entries for `n` qubits; pair with +/// `physical_layout_graph_physical_qubits` for the axis order). Unreachable +/// pairs are written as `UINT32_MAX`. A NULL `out` only queries the length. +/// Returns the total number of entries, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn layout_result_distances( + ptr: *const CPhysicalLayoutGraph, + out: *mut u32, + len: usize, +) -> usize { + if ptr.is_null() { + return 0; + } + let table = unsafe { (*ptr).inner.distances() }; + let qubits = table.qubits(); + let n = qubits.len(); + let total = n * n; + if !out.is_null() { + for i in 0..total.min(len) { + let (row, col) = (i / n, i % n); + let value = table.distance(qubits[row], qubits[col]).unwrap_or(u32::MAX); + unsafe { *out.add(i) = value }; + } + } + total +} + +/// Copies the per-logical-qubit activity weights (sum of incident +/// interaction weights) into parallel arrays sorted by logical ID +/// (two-step pattern): `out_qubits[i]` receives the logical qubit ID and +/// `out_activity[i]` its activity. Either array may be NULL only when both +/// are NULL (length query). Returns the total number of entries, or 0 for +/// NULL. +#[unsafe(no_mangle)] +pub extern "C" fn layout_result_logical_activity( + ptr: *const CCircuitLayoutAnalysis, + out_qubits: *mut u32, + out_activity: *mut f64, + len: usize, +) -> usize { + if ptr.is_null() { + return 0; + } + let activity = unsafe { (*ptr).inner.interactions.logical_activity() }; + let total = activity.len(); + if !out_qubits.is_null() && !out_activity.is_null() { + for (i, (logical, value)) in activity.iter().take(total.min(len)).enumerate() { + unsafe { + *out_qubits.add(i) = logical.id(); + *out_activity.add(i) = *value; + } + } + } + total +} + +/// Extracts the reusable circuit layout analysis captured by the prepared +/// SABRE circuit as an owned `CCircuitLayoutAnalysis*` (the same handle type +/// used by `analyze_circuit_for_layout`; free with +/// `circuit_layout_analysis_free`). Returns NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn layout_result_analysis( + ptr: *const CPreparedSabreCircuit, +) -> *mut CCircuitLayoutAnalysis { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let analysis = unsafe { (*ptr).inner.analysis() }.clone(); + Box::into_raw(Box::new(CCircuitLayoutAnalysis { inner: analysis })) +} diff --git a/crates/binding-c/src/device/mod.rs b/crates/binding-c/src/device/mod.rs new file mode 100644 index 00000000..5dc7a879 --- /dev/null +++ b/crates/binding-c/src/device/mod.rs @@ -0,0 +1,1397 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! C ABI for the device module (Device, Layout, ExecutionResult, NoiseModel). + +#![allow(clippy::not_unsafe_ptr_arg_deref)] + +pub mod layout; +pub mod noise; +pub mod qubit; +pub mod result; +pub mod topology; + +use crate::circuit::CValueOperation; +use crate::error::CqlibError; +use cqlib_core::circuit::gate::{Instruction, StandardGate}; +use cqlib_core::device::{ + Device, DeviceError, EdgeProp, ExecutionResult, InstructionProp, Layout, NoiseModel, + PhysicalQubit, QubitProp, +}; +use std::ffi::{CStr, CString}; +use std::os::raw::c_char; + +/// Opaque handle around [`Device`] for C ABI. +pub struct CDevice { + pub inner: Device, +} + +/// Opaque handle around [`cqlib_core::device::Topology`] for C ABI. +pub struct CTopology { + pub inner: cqlib_core::device::Topology, +} + +/// Opaque handle around [`Layout`] for C ABI. +pub struct CLayout { + pub inner: Layout, +} + +/// Opaque handle around [`ExecutionResult`] for C ABI. +pub struct CExecutionResult { + pub inner: ExecutionResult, +} + +/// Opaque handle around [`NoiseModel`] for C ABI. +pub struct CNoiseModel { + pub inner: NoiseModel, +} + +/// Key/value list of measurement outcomes (bitstring -> weight). +/// +/// Used by `execution_result_counts` and `execution_result_probabilities`; +/// `values` holds counts for the former and probabilities for the latter. +pub struct CCountsList { + pub keys: Vec, + pub values: Vec, +} + +/// Resolves a standard-gate name (e.g. "H", "CX", "RZZ") to a `StandardGate`. +pub(crate) fn standard_gate_from_name(name: &str) -> Option { + use StandardGate as G; + Some(match name { + "I" => G::I, + "H" => G::H, + "RX" => G::RX, + "RXX" => G::RXX, + "RXY" => G::RXY, + "RY" => G::RY, + "RYY" => G::RYY, + "RZ" => G::RZ, + "RZX" => G::RZX, + "RZZ" => G::RZZ, + "S" => G::S, + "SDG" => G::SDG, + "SWAP" => G::SWAP, + "T" => G::T, + "TDG" => G::TDG, + "U" => G::U, + "X" => G::X, + "XY" => G::XY, + "X2P" => G::X2P, + "X2M" => G::X2M, + "XY2P" => G::XY2P, + "XY2M" => G::XY2M, + "Y" => G::Y, + "Y2P" => G::Y2P, + "Y2M" => G::Y2M, + "Z" => G::Z, + "Phase" => G::Phase, + "GPhase" => G::GPhase, + "CX" => G::CX, + "CCX" => G::CCX, + "CY" => G::CY, + "CZ" => G::CZ, + "CRX" => G::CRX, + "CRY" => G::CRY, + "CRZ" => G::CRZ, + "fSim" | "FSIM" => G::FSIM, + _ => return None, + }) +} + +// ===== Section 3.1: device attribute queries and updates ===== + +/// Copies a u32 list into `out` (two-step pattern). Returns the total number +/// of available entries; a NULL `out` only queries the length. +pub(crate) fn write_u32_list(items: &[u32], out: *mut u32, len: usize) -> usize { + let total = items.len(); + if !out.is_null() { + for (i, item) in items.iter().take(total.min(len)).enumerate() { + unsafe { *out.add(i) = *item }; + } + } + total +} + +/// Copies `(a, b)` u32 pairs into `out` (two-step pattern). `out` must have +/// room for `2 * len` entries. Returns the total number of available pairs. +pub(crate) fn write_u32_pairs(pairs: &[(u32, u32)], out: *mut u32, len: usize) -> usize { + let total = pairs.len(); + if !out.is_null() { + for (i, (a, b)) in pairs.iter().take(total.min(len)).enumerate() { + unsafe { + *out.add(i * 2) = *a; + *out.add(i * 2 + 1) = *b; + } + } + } + total +} + +/// Maps a `DeviceError` raised while writing qubit/edge properties to a C +/// error code: physical-resource mismatches map to -2, calibration payload +/// problems map to -8. +fn device_error_code(error: &DeviceError) -> i32 { + match error { + DeviceError::QubitNotInDevice(_) + | DeviceError::QubitNotInTopology(_) + | DeviceError::InvalidOnlineQubit(_) + | DeviceError::EdgeNotInTopology(_, _) + | DeviceError::InvalidTopology(_) => CqlibError::QubitOutOfBounds as i32, + _ => CqlibError::InvalidParam as i32, + } +} + +/// Writes an optional f64 to `*out` (NaN when unset) and returns 0, or -8 +/// when the value is not available. +fn optional_f64(value: Option, out: *mut f64) -> i32 { + match value { + Some(v) => { + if !out.is_null() { + unsafe { *out = v }; + } + CqlibError::Ok as i32 + } + None => { + if !out.is_null() { + unsafe { *out = f64::NAN }; + } + CqlibError::InvalidParam as i32 + } + } +} + +/// Applies `mutate` to a copy of the recorded properties for `qubit` and +/// re-records them on the device. +/// +/// Returns 0 on success, -8 when the qubit has no recorded properties or the +/// mutation is rejected, or the mapped device error when re-recording fails. +unsafe fn update_qubit_properties( + ptr: *mut CDevice, + qubit: u32, + mutate: impl FnOnce(&mut QubitProp) -> Result<(), i32>, +) -> i32 { + let mut props = match unsafe { (*ptr).inner.qubit_properties(PhysicalQubit::new(qubit)) } { + Some(existing) => existing.clone(), + None => return CqlibError::InvalidParam as i32, + }; + if let Err(code) = mutate(&mut props) { + return code; + } + match unsafe { &mut *ptr } + .inner + .add_qubit_properties(PhysicalQubit::new(qubit), props) + { + Ok(()) => CqlibError::Ok as i32, + Err(e) => device_error_code(&e), + } +} + +/// Applies `edit` to the native instruction recorded at `index` for `qubit` +/// and revalidates the whole list (the core type has no in-place list +/// mutation, so the list is rebuilt and revalidated entry by entry). +/// +/// Returns 0 on success, -8 when the qubit has no recorded properties or +/// `index` is out of bounds, and the mapped device error when the edited +/// entry fails validation. +unsafe fn update_qubit_native_instruction( + ptr: *mut CDevice, + qubit: u32, + index: usize, + edit: impl FnOnce(&mut InstructionProp), +) -> i32 { + unsafe { + update_qubit_properties(ptr, qubit, |props| { + if index >= props.native_instructions().len() { + return Err(CqlibError::InvalidParam as i32); + } + let mut list: Vec = props.native_instructions().to_vec(); + edit(&mut list[index]); + let mut rebuilt = QubitProp::new(props.readout_error()); + if let Some(v) = props.t1() { + rebuilt.set_t1(v); + } + if let Some(v) = props.t2() { + rebuilt.set_t2(v); + } + if let Some(v) = props.prob_meas0_prep1() { + rebuilt.set_prob_meas0_prep1(v); + } + if let Some(v) = props.prob_meas1_prep0() { + rebuilt.set_prob_meas1_prep0(v); + } + if let Some(v) = props.frequency() { + rebuilt.set_frequency(v); + } + for item in list { + if let Err(e) = rebuilt.set_native_instruction(item) { + return Err(device_error_code(&e)); + } + } + *props = rebuilt; + Ok(()) + }) + } +} + +/// C snapshot of one native instruction capability (`InstructionProp`). +/// +/// `name` is a heap-allocated gate name the caller frees with +/// `cqlib_string_free`; `length` is NaN when the duration is unset. +#[repr(C)] +pub struct CNativeInstruction { + /// Heap-allocated standard-gate name; free with `cqlib_string_free`. + pub name: *mut c_char, + /// Error rate in [0, 1]. + pub error_rate: f64, + /// Duration in nanoseconds; NaN when unset. + pub length: f64, +} + +/// C snapshot of the per-qubit properties recorded for one physical qubit. +/// +/// Optional coherence fields are NaN when unset. Native instructions are not +/// inlined; query them with `device_qubit_prop_native_instruction` using +/// `num_native_instructions` as the index bound. +#[repr(C)] +pub struct CQubitProp { + /// Readout error rate in [0, 1]. + pub readout_error: f64, + /// T1 relaxation time in microseconds; NaN when unset. + pub t1: f64, + /// T2 dephasing time in microseconds; NaN when unset. + pub t2: f64, + /// P(measure 0 | prepared 1); NaN when unset. + pub prob_meas0_prep1: f64, + /// P(measure 1 | prepared 0); NaN when unset. + pub prob_meas1_prep0: f64, + /// Frequency in GHz; NaN when unset. + pub frequency: f64, + /// Number of native instructions recorded for the qubit. + pub num_native_instructions: usize, +} + +/// C snapshot of the per-edge properties recorded for one directed coupling. +/// +/// Native instructions are not inlined; query them with +/// `device_edge_prop_native_instruction` using `num_native_instructions` as +/// the index bound. +#[repr(C)] +pub struct CEdgeProp { + /// Number of native instructions recorded for the directed edge. + pub num_native_instructions: usize, +} + +/// C input for `device_add_qubit_properties`. +/// +/// Optional f64 fields use NaN to leave the value unset. Native instructions +/// are supplied as parallel arrays of length `num_native_instructions`: +/// gate names, error rates, and durations (NaN = unset). `native_lengths` +/// may be NULL to leave all durations unset. +#[repr(C)] +pub struct CQubitPropInput { + /// Readout error rate in [0, 1]. + pub readout_error: f64, + /// T1 relaxation time in microseconds; NaN = unset. + pub t1: f64, + /// T2 dephasing time in microseconds; NaN = unset. + pub t2: f64, + /// P(measure 0 | prepared 1); NaN = unset. + pub prob_meas0_prep1: f64, + /// P(measure 1 | prepared 0); NaN = unset. + pub prob_meas1_prep0: f64, + /// Frequency in GHz; NaN = unset. + pub frequency: f64, + /// Array of standard-gate names, or NULL when none. + pub native_gate_names: *const *const c_char, + /// Array of error rates, or NULL when none. + pub native_error_rates: *const f64, + /// Array of durations in nanoseconds (NaN = unset), or NULL. + pub native_lengths: *const f64, + /// Number of native instructions. + pub num_native_instructions: usize, +} + +/// C input for `device_add_edge_properties`. +/// +/// Native instructions are supplied as parallel arrays of length +/// `num_native_instructions` (two-qubit standard gates only). Fields mirror +/// [`CQubitPropInput`]. +#[repr(C)] +pub struct CEdgePropInput { + /// Array of standard-gate names, or NULL when none. + pub native_gate_names: *const *const c_char, + /// Array of error rates, or NULL when none. + pub native_error_rates: *const f64, + /// Array of durations in nanoseconds (NaN = unset), or NULL. + pub native_lengths: *const f64, + /// Number of native instructions. + pub num_native_instructions: usize, +} + +/// Builds the instruction list shared by `CQubitPropInput`/`CEdgePropInput`. +/// +/// Returns the gate-name error code (-4) for unknown names or UTF-8 issues, +/// and -1 when required arrays are NULL. +unsafe fn native_instructions_from_c( + names: *const *const c_char, + error_rates: *const f64, + lengths: *const f64, + count: usize, +) -> Result, i32> { + if count == 0 { + return Ok(Vec::new()); + } + if names.is_null() || error_rates.is_null() { + return Err(CqlibError::NullPtr as i32); + } + let mut props = Vec::with_capacity(count); + for i in 0..count { + let raw = unsafe { *names.add(i) }; + if raw.is_null() { + return Err(CqlibError::NullPtr as i32); + } + let name = unsafe { CStr::from_ptr(raw) } + .to_str() + .map_err(|_| CqlibError::ParseError as i32)?; + let gate = standard_gate_from_name(name).ok_or(CqlibError::ParseError as i32)?; + let error_rate = unsafe { *error_rates.add(i) }; + let mut prop = InstructionProp::new(Instruction::Standard(gate), error_rate); + if !lengths.is_null() { + let length = unsafe { *lengths.add(i) }; + if !length.is_nan() { + prop.set_length(length); + } + } + props.push(prop); + } + Ok(props) +} + +/// Returns the T1 relaxation time (μs) for `qubit`, falling back to the +/// device default. Writes the value to `*out` and returns 0, or -8 when no +/// T1 data is available for the qubit. +#[unsafe(no_mangle)] +pub extern "C" fn device_get_t1(ptr: *const CDevice, qubit: u32, out: *mut f64) -> i32 { + if ptr.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + optional_f64( + unsafe { (*ptr).inner.get_t1(PhysicalQubit::new(qubit)) }, + out, + ) +} + +/// Returns the T2 dephasing time (μs) for `qubit`, falling back to the +/// device default. Returns 0 with the value in `*out`, or -8 when unset. +#[unsafe(no_mangle)] +pub extern "C" fn device_get_t2(ptr: *const CDevice, qubit: u32, out: *mut f64) -> i32 { + if ptr.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + optional_f64( + unsafe { (*ptr).inner.get_t2(PhysicalQubit::new(qubit)) }, + out, + ) +} + +/// Returns the readout error rate for `qubit`, falling back to the device +/// default. Returns 0 with the value in `*out`, or -8 when unset. +#[unsafe(no_mangle)] +pub extern "C" fn device_get_readout_error(ptr: *const CDevice, qubit: u32, out: *mut f64) -> i32 { + if ptr.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + optional_f64( + unsafe { (*ptr).inner.get_readout_error(PhysicalQubit::new(qubit)) }, + out, + ) +} + +/// Returns the error rate of the single-qubit standard gate `gate_name` on +/// `qubit`. Returns 0 with the value in `*out`, -4 for an unknown gate name, +/// or -8 when the qubit does not support the gate. +#[unsafe(no_mangle)] +pub extern "C" fn device_single_qubit_error( + ptr: *const CDevice, + gate_name: *const c_char, + qubit: u32, + out: *mut f64, +) -> i32 { + if ptr.is_null() || gate_name.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + let name = match unsafe { CStr::from_ptr(gate_name) }.to_str() { + Ok(s) => s, + Err(_) => return CqlibError::ParseError as i32, + }; + let gate = match standard_gate_from_name(name) { + Some(g) => g, + None => return CqlibError::ParseError as i32, + }; + optional_f64( + unsafe { + (*ptr) + .inner + .single_qubit_error(PhysicalQubit::new(qubit), &Instruction::Standard(gate)) + }, + out, + ) +} + +/// Returns the error rate of the two-qubit standard gate `gate_name` on the +/// directed coupling `control -> target`. Returns 0 with the value in +/// `*out`, -4 for an unknown gate name, or -8 when the coupling does not +/// support the gate. +#[unsafe(no_mangle)] +pub extern "C" fn device_two_qubit_error( + ptr: *const CDevice, + gate_name: *const c_char, + control: u32, + target: u32, + out: *mut f64, +) -> i32 { + if ptr.is_null() || gate_name.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + let name = match unsafe { CStr::from_ptr(gate_name) }.to_str() { + Ok(s) => s, + Err(_) => return CqlibError::ParseError as i32, + }; + let gate = match standard_gate_from_name(name) { + Some(g) => g, + None => return CqlibError::ParseError as i32, + }; + optional_f64( + unsafe { + (*ptr).inner.two_qubit_error( + PhysicalQubit::new(control), + PhysicalQubit::new(target), + &Instruction::Standard(gate), + ) + }, + out, + ) +} + +/// Returns the direction-specific coupling error for `control -> target`, +/// i.e. the best calibrated native two-qubit error on the edge or the device +/// default. Returns 0 with the value in `*out`, or -8 when the directed +/// coupling does not exist. +#[unsafe(no_mangle)] +pub extern "C" fn device_edge_error( + ptr: *const CDevice, + control: u32, + target: u32, + out: *mut f64, +) -> i32 { + if ptr.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + optional_f64( + unsafe { + (*ptr) + .inner + .edge_error(PhysicalQubit::new(control), PhysicalQubit::new(target)) + }, + out, + ) +} + +/// Returns the device-wide default T1 time (μs). Returns 0 with the value +/// in `*out`, or -8 when no default is set. +#[unsafe(no_mangle)] +pub extern "C" fn device_default_t1(ptr: *const CDevice, out: *mut f64) -> i32 { + if ptr.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + optional_f64(unsafe { (*ptr).inner.default_t1() }, out) +} + +/// Returns the device-wide default T2 time (μs). Returns 0 with the value +/// in `*out`, or -8 when no default is set. +#[unsafe(no_mangle)] +pub extern "C" fn device_default_t2(ptr: *const CDevice, out: *mut f64) -> i32 { + if ptr.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + optional_f64(unsafe { (*ptr).inner.default_t2() }, out) +} + +/// Returns the device-wide default readout error rate. Returns 0 with the +/// value in `*out`, or -8 when no default is set. +#[unsafe(no_mangle)] +pub extern "C" fn device_default_readout_error(ptr: *const CDevice, out: *mut f64) -> i32 { + if ptr.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + optional_f64(unsafe { (*ptr).inner.default_readout_error() }, out) +} + +/// Returns the device-wide default single-qubit gate error rate. Returns 0 +/// with the value in `*out`, or -8 when no default is set. +#[unsafe(no_mangle)] +pub extern "C" fn device_default_single_qubit_error(ptr: *const CDevice, out: *mut f64) -> i32 { + if ptr.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + optional_f64(unsafe { (*ptr).inner.default_single_qubit_error() }, out) +} + +/// Returns the device-wide default two-qubit gate error rate. Returns 0 with +/// the value in `*out`, or -8 when no default is set. +#[unsafe(no_mangle)] +pub extern "C" fn device_default_two_qubit_error(ptr: *const CDevice, out: *mut f64) -> i32 { + if ptr.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + optional_f64(unsafe { (*ptr).inner.default_two_qubit_error() }, out) +} + +/// Sets the device-wide default T1 time (μs). +#[unsafe(no_mangle)] +pub extern "C" fn device_set_default_t1(ptr: *mut CDevice, t1: f64) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + unsafe { (*ptr).inner.set_default_t1(t1) }; + CqlibError::Ok as i32 +} + +/// Sets the device-wide default T2 time (μs). +#[unsafe(no_mangle)] +pub extern "C" fn device_set_default_t2(ptr: *mut CDevice, t2: f64) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + unsafe { (*ptr).inner.set_default_t2(t2) }; + CqlibError::Ok as i32 +} + +/// Sets the device-wide default readout error rate. +#[unsafe(no_mangle)] +pub extern "C" fn device_set_default_readout_error(ptr: *mut CDevice, error: f64) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + unsafe { (*ptr).inner.set_default_readout_error(error) }; + CqlibError::Ok as i32 +} + +/// Sets the device-wide default single-qubit gate error rate. +#[unsafe(no_mangle)] +pub extern "C" fn device_set_default_single_qubit_error(ptr: *mut CDevice, error: f64) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + unsafe { (*ptr).inner.set_default_single_qubit_error(error) }; + CqlibError::Ok as i32 +} + +/// Sets the device-wide default two-qubit gate error rate. +#[unsafe(no_mangle)] +pub extern "C" fn device_set_default_two_qubit_error(ptr: *mut CDevice, error: f64) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + unsafe { (*ptr).inner.set_default_two_qubit_error(error) }; + CqlibError::Ok as i32 +} + +// ===== Section 3.1.1: per-qubit stored-value getters and setters ===== + +/// Returns the qubit frequency (GHz) recorded for `qubit` (no default +/// fallback). Returns 0 with the value in `*out`, or -8 when the qubit has +/// no recorded properties or no frequency is set. +#[unsafe(no_mangle)] +pub extern "C" fn device_get_frequency(ptr: *const CDevice, qubit: u32, out: *mut f64) -> i32 { + if ptr.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + let Some(props) = (unsafe { (*ptr).inner.qubit_properties(PhysicalQubit::new(qubit)) }) else { + return CqlibError::InvalidParam as i32; + }; + optional_f64(props.frequency(), out) +} + +/// Returns P(measure 0 | prepared 1) recorded for `qubit` (no default +/// fallback). Returns 0 with the value in `*out`, or -8 when the qubit has +/// no recorded properties or the probability is unset. +#[unsafe(no_mangle)] +pub extern "C" fn device_get_prob_meas0_prep1( + ptr: *const CDevice, + qubit: u32, + out: *mut f64, +) -> i32 { + if ptr.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + let Some(props) = (unsafe { (*ptr).inner.qubit_properties(PhysicalQubit::new(qubit)) }) else { + return CqlibError::InvalidParam as i32; + }; + optional_f64(props.prob_meas0_prep1(), out) +} + +/// Returns P(measure 1 | prepared 0) recorded for `qubit` (no default +/// fallback). Returns 0 with the value in `*out`, or -8 when the qubit has +/// no recorded properties or the probability is unset. +#[unsafe(no_mangle)] +pub extern "C" fn device_get_prob_meas1_prep0( + ptr: *const CDevice, + qubit: u32, + out: *mut f64, +) -> i32 { + if ptr.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + let Some(props) = (unsafe { (*ptr).inner.qubit_properties(PhysicalQubit::new(qubit)) }) else { + return CqlibError::InvalidParam as i32; + }; + optional_f64(props.prob_meas1_prep0(), out) +} + +/// Returns the error rate of the native instruction at `index` recorded for +/// `qubit`. Returns 0 with the value in `*out`, or -8 when the qubit has no +/// recorded properties or `index` is out of bounds. +#[unsafe(no_mangle)] +pub extern "C" fn device_get_error_rate( + ptr: *const CDevice, + qubit: u32, + index: usize, + out: *mut f64, +) -> i32 { + if ptr.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + let Some(props) = (unsafe { (*ptr).inner.qubit_properties(PhysicalQubit::new(qubit)) }) else { + return CqlibError::InvalidParam as i32; + }; + let Some(prop) = props.native_instructions().get(index) else { + return CqlibError::InvalidParam as i32; + }; + unsafe { *out = prop.error_rate() }; + CqlibError::Ok as i32 +} + +/// Returns the duration (ns) of the native instruction at `index` recorded +/// for `qubit`. Returns 0 with the value in `*out`, or -8 when the qubit has +/// no recorded properties, `index` is out of bounds, or the duration is +/// unset. +#[unsafe(no_mangle)] +pub extern "C" fn device_get_length( + ptr: *const CDevice, + qubit: u32, + index: usize, + out: *mut f64, +) -> i32 { + if ptr.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + let Some(props) = (unsafe { (*ptr).inner.qubit_properties(PhysicalQubit::new(qubit)) }) else { + return CqlibError::InvalidParam as i32; + }; + let Some(prop) = props.native_instructions().get(index) else { + return CqlibError::InvalidParam as i32; + }; + optional_f64(prop.length(), out) +} + +/// Sets the T1 relaxation time (μs) recorded for `qubit`. Other recorded +/// properties are preserved. +/// +/// Returns 0 on success, -1 on NULL, or -8 when the qubit has no recorded +/// properties. +#[unsafe(no_mangle)] +pub extern "C" fn device_set_t1(ptr: *mut CDevice, qubit: u32, t1: f64) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + unsafe { + update_qubit_properties(ptr, qubit, |props| { + props.set_t1(t1); + Ok(()) + }) + } +} + +/// Sets the T2 dephasing time (μs) recorded for `qubit`. Other recorded +/// properties are preserved. +/// +/// Returns 0 on success, -1 on NULL, or -8 when the qubit has no recorded +/// properties. +#[unsafe(no_mangle)] +pub extern "C" fn device_set_t2(ptr: *mut CDevice, qubit: u32, t2: f64) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + unsafe { + update_qubit_properties(ptr, qubit, |props| { + props.set_t2(t2); + Ok(()) + }) + } +} + +/// Sets the qubit frequency (GHz) recorded for `qubit`. Other recorded +/// properties are preserved. +/// +/// Returns 0 on success, -1 on NULL, or -8 when the qubit has no recorded +/// properties. +#[unsafe(no_mangle)] +pub extern "C" fn device_set_frequency(ptr: *mut CDevice, qubit: u32, frequency: f64) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + unsafe { + update_qubit_properties(ptr, qubit, |props| { + props.set_frequency(frequency); + Ok(()) + }) + } +} + +/// Sets P(measure 0 | prepared 1) recorded for `qubit`. Other recorded +/// properties are preserved. +/// +/// Returns 0 on success, -1 on NULL, or -8 when the qubit has no recorded +/// properties. +#[unsafe(no_mangle)] +pub extern "C" fn device_set_prob_meas0_prep1(ptr: *mut CDevice, qubit: u32, prob: f64) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + unsafe { + update_qubit_properties(ptr, qubit, |props| { + props.set_prob_meas0_prep1(prob); + Ok(()) + }) + } +} + +/// Sets P(measure 1 | prepared 0) recorded for `qubit`. Other recorded +/// properties are preserved. +/// +/// Returns 0 on success, -1 on NULL, or -8 when the qubit has no recorded +/// properties. +#[unsafe(no_mangle)] +pub extern "C" fn device_set_prob_meas1_prep0(ptr: *mut CDevice, qubit: u32, prob: f64) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + unsafe { + update_qubit_properties(ptr, qubit, |props| { + props.set_prob_meas1_prep0(prob); + Ok(()) + }) + } +} + +/// Sets the error rate of the native instruction at `index` recorded for +/// `qubit`. Other recorded properties are preserved. +/// +/// Returns 0 on success, -1 on NULL, or -8 when the qubit has no recorded +/// properties or `index` is out of bounds. +#[unsafe(no_mangle)] +pub extern "C" fn device_set_error_rate( + ptr: *mut CDevice, + qubit: u32, + index: usize, + error_rate: f64, +) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + unsafe { + update_qubit_native_instruction(ptr, qubit, index, |item| { + item.set_error_rate(error_rate); + }) + } +} + +/// Sets the duration (ns) of the native instruction at `index` recorded for +/// `qubit`. Other recorded properties are preserved. +/// +/// Returns 0 on success, -1 on NULL, or -8 when the qubit has no recorded +/// properties or `index` is out of bounds. +#[unsafe(no_mangle)] +pub extern "C" fn device_set_length( + ptr: *mut CDevice, + qubit: u32, + index: usize, + length: f64, +) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + unsafe { + update_qubit_native_instruction(ptr, qubit, index, |item| { + item.set_length(length); + }) + } +} + +/// Replaces the standard gate carried by the native instruction at `index` +/// recorded for `qubit`. The edited list is revalidated, so changing a +/// single-qubit gate to a two-qubit gate (or vice versa) is rejected. +/// +/// Returns 0 on success, -1 on NULL, -4 for an unknown gate name, or -8 when +/// the qubit has no recorded properties, `index` is out of bounds, or the +/// edited list fails validation. +#[unsafe(no_mangle)] +pub extern "C" fn device_set_instruction( + ptr: *mut CDevice, + qubit: u32, + index: usize, + gate_name: *const c_char, +) -> i32 { + if ptr.is_null() || gate_name.is_null() { + return CqlibError::NullPtr as i32; + } + let name = match unsafe { CStr::from_ptr(gate_name) }.to_str() { + Ok(s) => s, + Err(_) => return CqlibError::ParseError as i32, + }; + let Some(gate) = standard_gate_from_name(name) else { + return CqlibError::ParseError as i32; + }; + unsafe { + update_qubit_native_instruction(ptr, qubit, index, |item| { + item.set_instruction(Instruction::Standard(gate)); + }) + } +} + +/// Appends one native single-qubit instruction (standard gate `gate_name` +/// with `error_rate` and duration `length` in ns; NaN length = unset) to the +/// recorded properties of `qubit`. +/// +/// Returns 0 on success, -1 on NULL, -4 for an unknown gate name, or -8 when +/// the qubit has no recorded properties or the instruction is rejected +/// (wrong arity or invalid calibration values). +#[unsafe(no_mangle)] +pub extern "C" fn device_set_native_instruction( + ptr: *mut CDevice, + qubit: u32, + gate_name: *const c_char, + error_rate: f64, + length: f64, +) -> i32 { + if ptr.is_null() || gate_name.is_null() { + return CqlibError::NullPtr as i32; + } + let name = match unsafe { CStr::from_ptr(gate_name) }.to_str() { + Ok(s) => s, + Err(_) => return CqlibError::ParseError as i32, + }; + let Some(gate) = standard_gate_from_name(name) else { + return CqlibError::ParseError as i32; + }; + let mut prop = InstructionProp::new(Instruction::Standard(gate), error_rate); + if !length.is_nan() { + prop.set_length(length); + } + unsafe { + update_qubit_properties(ptr, qubit, |props| { + props + .set_native_instruction(prop) + .map_err(|e| device_error_code(&e)) + }) + } +} + +/// Returns the system calibration timestamp as milliseconds since the Unix +/// epoch. Returns 0 with the value in `*out`, or -8 when unset. +/// +/// Note: an explicit `set_calibration_time` binding is not yet available +/// because constructing the core timestamp type requires the `time` crate +/// as a direct dependency of `binding-c`. +#[unsafe(no_mangle)] +pub extern "C" fn device_calibration_time(ptr: *const CDevice, out_unix_ms: *mut i64) -> i32 { + if ptr.is_null() || out_unix_ms.is_null() { + return CqlibError::NullPtr as i32; + } + match unsafe { (*ptr).inner.calibration_time() } { + Some(t) => { + let nanos = t.unix_timestamp_nanos(); + unsafe { *out_unix_ms = nanos.div_euclid(1_000_000) as i64 }; + CqlibError::Ok as i32 + } + None => CqlibError::InvalidParam as i32, + } +} + +/// Returns the number of invalid (offline/faulty) qubits, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn device_invalid_qubits_len(ptr: *const CDevice) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.invalid_qubits().count() } +} + +/// Copies the invalid (offline/faulty) qubit IDs into `out` (two-step +/// pattern; pair with `device_invalid_qubits_len`). Returns the total count. +#[unsafe(no_mangle)] +pub extern "C" fn device_invalid_qubits(ptr: *const CDevice, out: *mut u32, len: usize) -> usize { + if ptr.is_null() { + return 0; + } + let items: Vec = unsafe { (*ptr).inner.invalid_qubits() } + .map(|q| q.id()) + .collect(); + write_u32_list(&items, out, len) +} + +/// Replaces the set of invalid (offline/faulty) qubits. +/// +/// Returns 0 on success, -1 on NULL, or -2 when any qubit is not registered +/// with the device (the existing set is then preserved). +#[unsafe(no_mangle)] +pub extern "C" fn device_set_invalid_qubits( + ptr: *mut CDevice, + qubits: *const u32, + len: usize, +) -> i32 { + if ptr.is_null() || (len > 0 && qubits.is_null()) { + return CqlibError::NullPtr as i32; + } + let raw = if len == 0 { + &[] + } else { + unsafe { std::slice::from_raw_parts(qubits, len) } + }; + let set: std::collections::HashSet = + raw.iter().map(|&q| PhysicalQubit::new(q)).collect(); + let wrapper = unsafe { &mut *ptr }; + match wrapper.inner.set_invalid_qubits(set) { + Ok(()) => CqlibError::Ok as i32, + Err(_) => CqlibError::QubitOutOfBounds as i32, + } +} + +/// Returns 1 when `qubit` is registered with the device and not marked +/// invalid, 0 otherwise, or -1 on NULL. +#[unsafe(no_mangle)] +pub extern "C" fn device_is_usable_qubit(ptr: *const CDevice, qubit: u32) -> i32 { + if ptr.is_null() { + return -1; + } + unsafe { (*ptr).inner.is_usable_qubit(PhysicalQubit::new(qubit)) as i32 } +} + +/// Returns the number of usable (registered and not invalid) qubits, or 0 +/// for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn device_num_usable_qubits(ptr: *const CDevice) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.num_usable_qubits() } +} + +/// Returns the number of usable qubit IDs available from +/// `device_usable_qubits`, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn device_usable_qubits_len(ptr: *const CDevice) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.usable_qubits().count() } +} + +/// Copies the usable qubit IDs into `out` (two-step pattern; pair with +/// `device_usable_qubits_len`). Returns the total count. +#[unsafe(no_mangle)] +pub extern "C" fn device_usable_qubits(ptr: *const CDevice, out: *mut u32, len: usize) -> usize { + if ptr.is_null() { + return 0; + } + let items: Vec = unsafe { (*ptr).inner.usable_qubits() } + .map(|q| q.id()) + .collect(); + write_u32_list(&items, out, len) +} + +/// Returns the number of physical qubits registered with the device +/// (including invalid ones), or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn device_qubits_len(ptr: *const CDevice) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.qubits().count() } +} + +/// Copies all registered physical qubit IDs (including invalid ones) into +/// `out` (two-step pattern; pair with `device_qubits_len`). Returns the +/// total count. +#[unsafe(no_mangle)] +pub extern "C" fn device_qubits(ptr: *const CDevice, out: *mut u32, len: usize) -> usize { + if ptr.is_null() { + return 0; + } + let items: Vec = unsafe { (*ptr).inner.qubits() }.map(|q| q.id()).collect(); + write_u32_list(&items, out, len) +} + +/// Writes the per-qubit property snapshot for `qubit` to `*out`. +/// +/// Returns 0 on success, or -8 when no properties have been recorded for +/// the qubit (optional fields in the snapshot are NaN when unset; native +/// instructions must be queried separately). +#[unsafe(no_mangle)] +pub extern "C" fn device_qubit_properties( + ptr: *const CDevice, + qubit: u32, + out: *mut CQubitProp, +) -> i32 { + if ptr.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + let Some(props) = (unsafe { (*ptr).inner.qubit_properties(PhysicalQubit::new(qubit)) }) else { + return CqlibError::InvalidParam as i32; + }; + unsafe { + *out = CQubitProp { + readout_error: props.readout_error(), + t1: props.t1().unwrap_or(f64::NAN), + t2: props.t2().unwrap_or(f64::NAN), + prob_meas0_prep1: props.prob_meas0_prep1().unwrap_or(f64::NAN), + prob_meas1_prep0: props.prob_meas1_prep0().unwrap_or(f64::NAN), + frequency: props.frequency().unwrap_or(f64::NAN), + num_native_instructions: props.native_instructions().len(), + }; + } + CqlibError::Ok as i32 +} + +/// Writes the native instruction at `index` for `qubit` to `*out`. +/// +/// `name` is heap-allocated and must be freed with `cqlib_string_free`; +/// `length` is NaN when unset. Returns 0 on success, or -8 when the qubit +/// has no recorded properties or `index` is out of bounds. +#[unsafe(no_mangle)] +pub extern "C" fn device_qubit_prop_native_instruction( + ptr: *const CDevice, + qubit: u32, + index: usize, + out: *mut CNativeInstruction, +) -> i32 { + if ptr.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + let Some(props) = (unsafe { (*ptr).inner.qubit_properties(PhysicalQubit::new(qubit)) }) else { + return CqlibError::InvalidParam as i32; + }; + let Some(prop) = props.native_instructions().get(index) else { + return CqlibError::InvalidParam as i32; + }; + let name = match CString::new(prop.instruction().name()) { + Ok(cs) => cs.into_raw(), + Err(_) => return CqlibError::InvalidParam as i32, + }; + unsafe { + *out = CNativeInstruction { + name, + error_rate: prop.error_rate(), + length: prop.length().unwrap_or(f64::NAN), + }; + } + CqlibError::Ok as i32 +} + +/// Writes the per-edge property snapshot for the directed coupling +/// `control -> target` to `*out`. +/// +/// Returns 0 on success, or -8 when no properties have been recorded for +/// the edge. +#[unsafe(no_mangle)] +pub extern "C" fn device_edge_properties( + ptr: *const CDevice, + control: u32, + target: u32, + out: *mut CEdgeProp, +) -> i32 { + if ptr.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + let Some(props) = (unsafe { + (*ptr) + .inner + .edge_properties(PhysicalQubit::new(control), PhysicalQubit::new(target)) + }) else { + return CqlibError::InvalidParam as i32; + }; + unsafe { + *out = CEdgeProp { + num_native_instructions: props.native_instructions().len(), + }; + } + CqlibError::Ok as i32 +} + +/// Writes the native instruction at `index` for the directed coupling +/// `control -> target` to `*out` (two-qubit standard gates). +/// +/// `name` is heap-allocated and must be freed with `cqlib_string_free`; +/// `length` is NaN when unset. Returns 0 on success, or -8 when the edge +/// has no recorded properties or `index` is out of bounds. +#[unsafe(no_mangle)] +pub extern "C" fn device_edge_prop_native_instruction( + ptr: *const CDevice, + control: u32, + target: u32, + index: usize, + out: *mut CNativeInstruction, +) -> i32 { + if ptr.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + let Some(props) = (unsafe { + (*ptr) + .inner + .edge_properties(PhysicalQubit::new(control), PhysicalQubit::new(target)) + }) else { + return CqlibError::InvalidParam as i32; + }; + let Some(prop) = props.native_instructions().get(index) else { + return CqlibError::InvalidParam as i32; + }; + let name = match CString::new(prop.instruction().name()) { + Ok(cs) => cs.into_raw(), + Err(_) => return CqlibError::InvalidParam as i32, + }; + unsafe { + *out = CNativeInstruction { + name, + error_rate: prop.error_rate(), + length: prop.length().unwrap_or(f64::NAN), + }; + } + CqlibError::Ok as i32 +} + +/// Records (or replaces) the properties of `qubit`. +/// +/// Returns 0 on success, -1 on NULL, -2 when the qubit is not registered +/// with the device or absent from its topology, -4 for an unknown native +/// gate name, or -8 for invalid native-instruction calibration values. +#[unsafe(no_mangle)] +pub extern "C" fn device_add_qubit_properties( + ptr: *mut CDevice, + qubit: u32, + input: *const CQubitPropInput, +) -> i32 { + if ptr.is_null() || input.is_null() { + return CqlibError::NullPtr as i32; + } + let raw = unsafe { &*input }; + let instructions = match unsafe { + native_instructions_from_c( + raw.native_gate_names, + raw.native_error_rates, + raw.native_lengths, + raw.num_native_instructions, + ) + } { + Ok(props) => props, + Err(code) => return code, + }; + let mut props = QubitProp::new(raw.readout_error); + if !raw.t1.is_nan() { + props.set_t1(raw.t1); + } + if !raw.t2.is_nan() { + props.set_t2(raw.t2); + } + if !raw.prob_meas0_prep1.is_nan() { + props.set_prob_meas0_prep1(raw.prob_meas0_prep1); + } + if !raw.prob_meas1_prep0.is_nan() { + props.set_prob_meas1_prep0(raw.prob_meas1_prep0); + } + if !raw.frequency.is_nan() { + props.set_frequency(raw.frequency); + } + for instruction in instructions { + if let Err(e) = props.set_native_instruction(instruction) { + return device_error_code(&e); + } + } + let wrapper = unsafe { &mut *ptr }; + match wrapper + .inner + .add_qubit_properties(PhysicalQubit::new(qubit), props) + { + Ok(()) => CqlibError::Ok as i32, + Err(e) => device_error_code(&e), + } +} + +/// Records (or replaces) the properties of the directed coupling +/// `control -> target`. +/// +/// Returns 0 on success, -1 on NULL, -2 when the directed coupling is not +/// in the device topology, -4 for an unknown native gate name, or -8 when a +/// native instruction is not a two-qubit standard gate or has invalid +/// calibration values. +#[unsafe(no_mangle)] +pub extern "C" fn device_add_edge_properties( + ptr: *mut CDevice, + control: u32, + target: u32, + input: *const CEdgePropInput, +) -> i32 { + if ptr.is_null() || input.is_null() { + return CqlibError::NullPtr as i32; + } + let raw = unsafe { &*input }; + let instructions = match unsafe { + native_instructions_from_c( + raw.native_gate_names, + raw.native_error_rates, + raw.native_lengths, + raw.num_native_instructions, + ) + } { + Ok(props) => props, + Err(code) => return code, + }; + let mut props = EdgeProp::new(); + for instruction in instructions { + if let Err(e) = props.set_native_instruction(instruction) { + return device_error_code(&e); + } + } + let wrapper = unsafe { &mut *ptr }; + match wrapper.inner.add_edge_properties( + PhysicalQubit::new(control), + PhysicalQubit::new(target), + props, + ) { + Ok(()) => CqlibError::Ok as i32, + Err(e) => device_error_code(&e), + } +} + +/// Returns whether the standard gate `gate_name` can execute natively on +/// the exact ordered physical `qargs` (`num_qargs` u32 IDs; an empty qargs +/// list is valid for `GPhase`). +/// +/// Returns 1 when supported, 0 when not, -1 on NULL, or -4 for an unknown +/// gate name. Calibration values do not affect this capability query. +#[unsafe(no_mangle)] +pub extern "C" fn device_supports_native_instruction( + ptr: *const CDevice, + gate_name: *const c_char, + qargs: *const u32, + num_qargs: usize, +) -> i32 { + if ptr.is_null() || gate_name.is_null() || (num_qargs > 0 && qargs.is_null()) { + return CqlibError::NullPtr as i32; + } + let name = match unsafe { CStr::from_ptr(gate_name) }.to_str() { + Ok(s) => s, + Err(_) => return CqlibError::ParseError as i32, + }; + let Some(gate) = standard_gate_from_name(name) else { + return CqlibError::ParseError as i32; + }; + let qargs: Vec = if num_qargs == 0 { + Vec::new() + } else { + unsafe { std::slice::from_raw_parts(qargs, num_qargs) } + .iter() + .map(|&q| PhysicalQubit::new(q)) + .collect() + }; + unsafe { + (*ptr) + .inner + .supports_native_instruction(&Instruction::Standard(gate), &qargs) as i32 + } +} + +/// Validates one resolved operation snapshot (from `circuit_index`) against +/// the device in the physical-qubit ID space. +/// +/// The core type `Operation` has no dedicated C representation, so this +/// entry point (like the Python binding) validates the value-level +/// `CValueOperation` form via `Device::validate_value_operation`. +/// +/// Returns 0 when valid, -3 when the device rejects the operation, or -1 +/// on NULL. +#[unsafe(no_mangle)] +pub extern "C" fn device_validate_operation( + ptr: *const CDevice, + operation: *const CValueOperation, +) -> i32 { + device_validate_value_operation(ptr, operation) +} + +/// Validates one resolved value-level operation snapshot (from +/// `circuit_index`) against the device in the physical-qubit ID space. +/// Control-flow bodies are checked recursively. +/// +/// Returns 0 when valid, -3 when the device rejects the operation, or -1 +/// on NULL. +#[unsafe(no_mangle)] +pub extern "C" fn device_validate_value_operation( + ptr: *const CDevice, + operation: *const CValueOperation, +) -> i32 { + if ptr.is_null() || operation.is_null() { + return CqlibError::NullPtr as i32; + } + let device = unsafe { &(*ptr).inner }; + let operation = unsafe { &(*operation).inner }; + match device.validate_value_operation(operation) { + Ok(()) => CqlibError::Ok as i32, + Err(_) => CqlibError::CircuitError as i32, + } +} + +/// Creates a device whose physical qubits (given by ID array `qubits`) are +/// connected as a directed line in the supplied order. Returns NULL on +/// error. +#[unsafe(no_mangle)] +pub extern "C" fn device_line_from_qubits( + name: *const c_char, + qubits: *const u32, + num_qubits: usize, +) -> *mut CDevice { + if name.is_null() || (num_qubits > 0 && qubits.is_null()) { + return std::ptr::null_mut(); + } + let name = match unsafe { CStr::from_ptr(name) }.to_str() { + Ok(s) => s, + Err(_) => return std::ptr::null_mut(), + }; + let physical: Vec = if num_qubits == 0 { + Vec::new() + } else { + unsafe { std::slice::from_raw_parts(qubits, num_qubits) } + .iter() + .map(|&q| PhysicalQubit::new(q)) + .collect() + }; + match Device::line_from_qubits(name, physical) { + Ok(device) => Box::into_raw(Box::new(CDevice { inner: device })), + Err(_) => std::ptr::null_mut(), + } +} + +pub use layout::*; +pub use noise::*; +pub use qubit::*; +pub use result::*; +pub use topology::*; diff --git a/crates/binding-c/src/device/noise.rs b/crates/binding-c/src/device/noise.rs new file mode 100644 index 00000000..54eb7a7f --- /dev/null +++ b/crates/binding-c/src/device/noise.rs @@ -0,0 +1,637 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! C ABI for `NoiseModel`. + +use crate::device::{CNoiseModel, standard_gate_from_name}; +use crate::error::CqlibError; +use cqlib_core::circuit::Qubit; +use cqlib_core::device::OperationKey; +use cqlib_core::device::noise::{ReadoutError, SingleQubitNoise, TwoQubitNoise}; +use cqlib_core::qis::pauli::Pauli; +use num_complex::Complex64; +use std::ffi::CStr; +use std::os::raw::c_char; + +/// Single-qubit noise channel tags for `noise_model_add_single_qubit`. +/// +/// | Value | Channel | Parameter meaning | +/// |-------|------------------|--------------------------| +/// | 0 | BitFlip | flip probability p | +/// | 1 | PhaseFlip | flip probability p | +/// | 2 | Depolarizing | depolarizing parameter p | +/// | 3 | AmplitudeDamping | damping parameter gamma | +/// | 4 | PhaseDamping | scattering probability | +pub const NOISE_BIT_FLIP: u8 = 0; +pub const NOISE_PHASE_FLIP: u8 = 1; +pub const NOISE_DEPOLARIZING: u8 = 2; +pub const NOISE_AMPLITUDE_DAMPING: u8 = 3; +pub const NOISE_PHASE_DAMPING: u8 = 4; + +/// Two-qubit noise channel tags for `noise_model_add_two_qubit`. +/// +/// | Value | Channel | Parameter meaning | +/// |-------|--------------|--------------------------| +/// | 0 | Depolarizing | depolarizing parameter p | +pub const NOISE_TWO_DEPOLARIZING: u8 = 0; + +fn single_qubit_noise(tag: u8, p: f64) -> Option { + Some(match tag { + NOISE_BIT_FLIP => SingleQubitNoise::BitFlip(p), + NOISE_PHASE_FLIP => SingleQubitNoise::PhaseFlip(p), + NOISE_DEPOLARIZING => SingleQubitNoise::Depolarizing(p), + NOISE_AMPLITUDE_DAMPING => SingleQubitNoise::AmplitudeDamping(p), + NOISE_PHASE_DAMPING => SingleQubitNoise::PhaseDamping(p), + _ => return None, + }) +} + +/// Creates a new empty noise model. +#[unsafe(no_mangle)] +pub extern "C" fn noise_model_new() -> *mut CNoiseModel { + Box::into_raw(Box::new(CNoiseModel { + inner: cqlib_core::device::NoiseModel::new(), + })) +} + +/// Frees a noise model. NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn noise_model_free(ptr: *mut CNoiseModel) { + if !ptr.is_null() { + unsafe { + let _ = Box::from_raw(ptr); + } + } +} + +/// Adds a single-qubit noise channel to `gate_name` acting on `qubit`. +/// +/// `noise_type` is one of the `NOISE_*` single-qubit tags; `p` is the +/// channel parameter. Returns 0 on success, -4 for unknown gate/noise tag, +/// or -8 for invalid probabilities. +#[unsafe(no_mangle)] +pub extern "C" fn noise_model_add_single_qubit( + ptr: *mut CNoiseModel, + gate_name: *const c_char, + qubit: u32, + noise_type: u8, + p: f64, +) -> i32 { + if ptr.is_null() || gate_name.is_null() { + return CqlibError::NullPtr as i32; + } + let gate_name = match unsafe { CStr::from_ptr(gate_name) }.to_str() { + Ok(s) => s, + Err(_) => return CqlibError::ParseError as i32, + }; + let gate = match standard_gate_from_name(gate_name) { + Some(g) => g, + None => return CqlibError::ParseError as i32, + }; + let noise = match single_qubit_noise(noise_type, p) { + Some(n) => n, + None => return CqlibError::InvalidParam as i32, + }; + let wrapper = unsafe { &mut *ptr }; + match wrapper + .inner + .add_single_qubit_error(gate, Qubit::new(qubit), noise) + { + Ok(()) => 0, + Err(_) => CqlibError::InvalidParam as i32, + } +} + +/// Adds a two-qubit depolarizing channel to `gate_name` acting on +/// `(q0, q1)`. Returns 0 on success, -4 for unknown gate, or -8 when the +/// qubits collide or the probability is invalid. +#[unsafe(no_mangle)] +pub extern "C" fn noise_model_add_two_qubit( + ptr: *mut CNoiseModel, + gate_name: *const c_char, + q0: u32, + q1: u32, + noise_type: u8, + p: f64, +) -> i32 { + if ptr.is_null() || gate_name.is_null() { + return CqlibError::NullPtr as i32; + } + if noise_type != NOISE_TWO_DEPOLARIZING { + return CqlibError::InvalidParam as i32; + } + let gate_name = match unsafe { CStr::from_ptr(gate_name) }.to_str() { + Ok(s) => s, + Err(_) => return CqlibError::ParseError as i32, + }; + let gate = match standard_gate_from_name(gate_name) { + Some(g) => g, + None => return CqlibError::ParseError as i32, + }; + let wrapper = unsafe { &mut *ptr }; + match wrapper.inner.add_two_qubit_error( + gate, + Qubit::new(q0), + Qubit::new(q1), + TwoQubitNoise::Depolarizing(p), + ) { + Ok(()) => 0, + Err(_) => CqlibError::InvalidParam as i32, + } +} + +/// Adds an asymmetric readout error for `qubit`. +/// +/// `p_0_given_1` is P(measure 0 | true 1); `p_1_given_0` is P(1 | 0). +/// Returns 0 on success or -8 for invalid probabilities. +#[unsafe(no_mangle)] +pub extern "C" fn noise_model_add_readout( + ptr: *mut CNoiseModel, + qubit: u32, + p_0_given_1: f64, + p_1_given_0: f64, +) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + let error = ReadoutError { + p_0_given_1, + p_1_given_0, + }; + let wrapper = unsafe { &mut *ptr }; + match wrapper.inner.add_readout_error(Qubit::new(qubit), error) { + Ok(()) => 0, + Err(_) => CqlibError::InvalidParam as i32, + } +} + +// ===== Section 3.4: Pauli channels and noise queries ===== + +/// Single-qubit Pauli channel tag for `noise_model_add_single_qubit_pauli` +/// and `CSingleQubitNoise.tag`; the channel parameters are the probabilities +/// `px`/`py`/`pz` (sum <= 1). +pub const NOISE_PAULI: u8 = 5; + +/// Two-qubit noise channel tags for `CTwoQubitNoise.tag`. +/// +/// | Value | Channel | Parameter meaning | +/// |-------|------------------|--------------------------------------| +/// | 0 | Depolarizing | depolarizing parameter p | +/// | 1 | Independent | per-qubit channels in q0/q1_noise | +/// | 2 | CorrelatedPauli | probability p + `NOISE_PAULI_OP_*` | +pub const NOISE_TWO_INDEPENDENT: u8 = 1; +pub const NOISE_TWO_CORRELATED_PAULI: u8 = 2; + +/// Pauli operator tags used by `CTwoQubitNoise.op_q0`/`op_q1` for the +/// correlated-Pauli channel. +pub const NOISE_PAULI_OP_I: u8 = 0; +pub const NOISE_PAULI_OP_X: u8 = 1; +pub const NOISE_PAULI_OP_Y: u8 = 2; +pub const NOISE_PAULI_OP_Z: u8 = 3; + +/// C snapshot of a single-qubit noise channel. +/// +/// `tag` selects the channel (see the single-qubit `NOISE_*` constants); +/// `p` carries the parameter for tags 0-4 and `px`/`py`/`pz` carry the +/// Pauli probabilities for tag 5. Fields that do not apply to the tagged +/// channel are zero. +#[repr(C)] +pub struct CSingleQubitNoise { + /// One of the single-qubit `NOISE_*` tags (0-5). + pub tag: u8, + /// Channel probability for tags 0-4. + pub p: f64, + /// Pauli-X probability for tag 5 (`NOISE_PAULI`). + pub px: f64, + /// Pauli-Y probability for tag 5 (`NOISE_PAULI`). + pub py: f64, + /// Pauli-Z probability for tag 5 (`NOISE_PAULI`). + pub pz: f64, +} + +/// C snapshot of a two-qubit noise channel. +/// +/// `tag` selects the channel (see the `NOISE_TWO_*` constants); fields that +/// do not apply to the tagged channel are zero. +#[repr(C)] +pub struct CTwoQubitNoise { + /// One of the `NOISE_TWO_*` tags. + pub tag: u8, + /// Channel probability for the depolarizing and correlated-Pauli tags. + pub p: f64, + /// Channel applied to q0 for the independent tag. + pub q0_noise: CSingleQubitNoise, + /// Channel applied to q1 for the independent tag. + pub q1_noise: CSingleQubitNoise, + /// Pauli operator applied to q0 (`NOISE_PAULI_OP_*`), correlated tag. + pub op_q0: u8, + /// Pauli operator applied to q1 (`NOISE_PAULI_OP_*`), correlated tag. + pub op_q1: u8, +} + +/// Converts a core single-qubit channel into the `CSingleQubitNoise` mirror. +fn single_qubit_noise_to_c(noise: &SingleQubitNoise) -> CSingleQubitNoise { + let mut out = CSingleQubitNoise { + tag: 0, + p: 0.0, + px: 0.0, + py: 0.0, + pz: 0.0, + }; + match *noise { + SingleQubitNoise::BitFlip(p) => { + out.tag = NOISE_BIT_FLIP; + out.p = p; + } + SingleQubitNoise::PhaseFlip(p) => { + out.tag = NOISE_PHASE_FLIP; + out.p = p; + } + SingleQubitNoise::Depolarizing(p) => { + out.tag = NOISE_DEPOLARIZING; + out.p = p; + } + SingleQubitNoise::AmplitudeDamping(p) => { + out.tag = NOISE_AMPLITUDE_DAMPING; + out.p = p; + } + SingleQubitNoise::PhaseDamping(p) => { + out.tag = NOISE_PHASE_DAMPING; + out.p = p; + } + SingleQubitNoise::Pauli { px, py, pz } => { + out.tag = NOISE_PAULI; + out.px = px; + out.py = py; + out.pz = pz; + } + } + out +} + +/// Maps a Pauli operator to its `NOISE_PAULI_OP_*` tag. +fn pauli_op_tag(pauli: &Pauli) -> u8 { + match pauli { + Pauli::I => NOISE_PAULI_OP_I, + Pauli::X => NOISE_PAULI_OP_X, + Pauli::Y => NOISE_PAULI_OP_Y, + Pauli::Z => NOISE_PAULI_OP_Z, + } +} + +/// Converts a core two-qubit channel into the `CTwoQubitNoise` mirror. +fn two_qubit_noise_to_c(noise: &TwoQubitNoise) -> CTwoQubitNoise { + let mut out = CTwoQubitNoise { + tag: 0, + p: 0.0, + q0_noise: CSingleQubitNoise { + tag: 0, + p: 0.0, + px: 0.0, + py: 0.0, + pz: 0.0, + }, + q1_noise: CSingleQubitNoise { + tag: 0, + p: 0.0, + px: 0.0, + py: 0.0, + pz: 0.0, + }, + op_q0: 0, + op_q1: 0, + }; + match noise { + TwoQubitNoise::Depolarizing(p) => { + out.tag = NOISE_TWO_DEPOLARIZING; + out.p = *p; + } + TwoQubitNoise::Independent { q0_noise, q1_noise } => { + out.tag = NOISE_TWO_INDEPENDENT; + out.q0_noise = single_qubit_noise_to_c(q0_noise); + out.q1_noise = single_qubit_noise_to_c(q1_noise); + } + TwoQubitNoise::CorrelatedPauli { op_q0, op_q1, p } => { + out.tag = NOISE_TWO_CORRELATED_PAULI; + out.p = *p; + out.op_q0 = pauli_op_tag(op_q0); + out.op_q1 = pauli_op_tag(op_q1); + } + } + out +} + +/// Adds a general single-qubit Pauli channel to `gate_name` acting on +/// `qubit`, with X/Y/Z probabilities `px`, `py`, `pz` (each >= 0, sum <= 1). +/// +/// Returns 0 on success, -1 on NULL, -4 for an unknown gate name, or -8 for +/// invalid probabilities. +#[unsafe(no_mangle)] +pub extern "C" fn noise_model_add_single_qubit_pauli( + ptr: *mut CNoiseModel, + gate_name: *const c_char, + qubit: u32, + px: f64, + py: f64, + pz: f64, +) -> i32 { + if ptr.is_null() || gate_name.is_null() { + return CqlibError::NullPtr as i32; + } + let gate_name = match unsafe { CStr::from_ptr(gate_name) }.to_str() { + Ok(s) => s, + Err(_) => return CqlibError::ParseError as i32, + }; + let gate = match standard_gate_from_name(gate_name) { + Some(g) => g, + None => return CqlibError::ParseError as i32, + }; + let noise = SingleQubitNoise::Pauli { px, py, pz }; + let wrapper = unsafe { &mut *ptr }; + match wrapper + .inner + .add_single_qubit_error(gate, Qubit::new(qubit), noise) + { + Ok(()) => 0, + Err(_) => CqlibError::InvalidParam as i32, + } +} + +/// Returns the number of single-qubit noise channels recorded for the +/// standard gate `gate_name` on `qubit`, or 0 for NULL, an unknown gate +/// name, or absent entries. +#[unsafe(no_mangle)] +pub extern "C" fn noise_model_get_single_qubit_errors_len( + ptr: *const CNoiseModel, + gate_name: *const c_char, + qubit: u32, +) -> usize { + if ptr.is_null() || gate_name.is_null() { + return 0; + } + let name = match unsafe { CStr::from_ptr(gate_name) }.to_str() { + Ok(s) => s, + Err(_) => return 0, + }; + let Some(gate) = standard_gate_from_name(name) else { + return 0; + }; + let key = OperationKey::new_single(gate, Qubit::new(qubit)); + unsafe { + (*ptr) + .inner + .get_single_qubit_errors(&key) + .map_or(0, |v| v.len()) + } +} + +/// Copies the single-qubit noise channels recorded for the standard gate +/// `gate_name` on `qubit` into `out` (two-step pattern; pair with +/// `noise_model_get_single_qubit_errors_len`). +/// +/// Each entry is a `CSingleQubitNoise` snapshot whose valid fields depend on +/// its `tag`. Returns the total number of recorded channels (0 for NULL, an +/// unknown gate name, or absent entries). +#[unsafe(no_mangle)] +pub extern "C" fn noise_model_get_single_qubit_errors( + ptr: *const CNoiseModel, + gate_name: *const c_char, + qubit: u32, + out: *mut CSingleQubitNoise, + len: usize, +) -> usize { + if ptr.is_null() || gate_name.is_null() { + return 0; + } + let name = match unsafe { CStr::from_ptr(gate_name) }.to_str() { + Ok(s) => s, + Err(_) => return 0, + }; + let Some(gate) = standard_gate_from_name(name) else { + return 0; + }; + let key = OperationKey::new_single(gate, Qubit::new(qubit)); + let Some(errors) = (unsafe { (*ptr).inner.get_single_qubit_errors(&key) }) else { + return 0; + }; + let total = errors.len(); + if !out.is_null() { + for (i, noise) in errors.iter().take(total.min(len)).enumerate() { + unsafe { *out.add(i) = single_qubit_noise_to_c(noise) }; + } + } + total +} + +/// Returns the number of two-qubit noise channels recorded for the standard +/// gate `gate_name` on the ordered pair `(q0, q1)`, or 0 for NULL, an +/// unknown gate name, colliding qubits, or absent entries. +#[unsafe(no_mangle)] +pub extern "C" fn noise_model_get_two_qubit_errors_len( + ptr: *const CNoiseModel, + gate_name: *const c_char, + q0: u32, + q1: u32, +) -> usize { + if ptr.is_null() || gate_name.is_null() { + return 0; + } + let name = match unsafe { CStr::from_ptr(gate_name) }.to_str() { + Ok(s) => s, + Err(_) => return 0, + }; + let Some(gate) = standard_gate_from_name(name) else { + return 0; + }; + let Ok(key) = OperationKey::new_double(gate, Qubit::new(q0), Qubit::new(q1)) else { + return 0; + }; + unsafe { + (*ptr) + .inner + .get_two_qubit_errors(&key) + .map_or(0, |v| v.len()) + } +} + +/// Copies the two-qubit noise channels recorded for the standard gate +/// `gate_name` on the ordered pair `(q0, q1)` into `out` (two-step pattern; +/// pair with `noise_model_get_two_qubit_errors_len`). +/// +/// Each entry is a `CTwoQubitNoise` snapshot whose valid fields depend on +/// its `tag`. Returns the total number of recorded channels (0 for NULL, an +/// unknown gate name, colliding qubits, or absent entries). +#[unsafe(no_mangle)] +pub extern "C" fn noise_model_get_two_qubit_errors( + ptr: *const CNoiseModel, + gate_name: *const c_char, + q0: u32, + q1: u32, + out: *mut CTwoQubitNoise, + len: usize, +) -> usize { + if ptr.is_null() || gate_name.is_null() { + return 0; + } + let name = match unsafe { CStr::from_ptr(gate_name) }.to_str() { + Ok(s) => s, + Err(_) => return 0, + }; + let Some(gate) = standard_gate_from_name(name) else { + return 0; + }; + let Ok(key) = OperationKey::new_double(gate, Qubit::new(q0), Qubit::new(q1)) else { + return 0; + }; + let Some(errors) = (unsafe { (*ptr).inner.get_two_qubit_errors(&key) }) else { + return 0; + }; + let total = errors.len(); + if !out.is_null() { + for (i, noise) in errors.iter().take(total.min(len)).enumerate() { + unsafe { *out.add(i) = two_qubit_noise_to_c(noise) }; + } + } + total +} + +/// Writes the readout error recorded for `qubit` to the out parameters. +/// +/// `*out_p_0_given_1` receives P(measure 0 | true 1) and `*out_p_1_given_0` +/// receives P(measure 1 | true 0). Returns 0 on success, -1 on NULL, or -8 +/// when no readout error is recorded for the qubit. +#[unsafe(no_mangle)] +pub extern "C" fn noise_model_get_readout_error( + ptr: *const CNoiseModel, + qubit: u32, + out_p_0_given_1: *mut f64, + out_p_1_given_0: *mut f64, +) -> i32 { + if ptr.is_null() || out_p_0_given_1.is_null() || out_p_1_given_0.is_null() { + return CqlibError::NullPtr as i32; + } + match unsafe { (*ptr).inner.get_readout_error(&Qubit::new(qubit)) } { + Some(error) => { + unsafe { + *out_p_0_given_1 = error.p_0_given_1; + *out_p_1_given_0 = error.p_1_given_0; + } + CqlibError::Ok as i32 + } + None => CqlibError::InvalidParam as i32, + } +} + +// ===== Section 3.5: channel validity and Kraus output ===== + +/// Resolves the single-qubit channel recorded for the standard gate +/// `gate_name` on `qubit` at `index`. +/// +/// Returns the channel, or the error code -1 (NULL), -4 (invalid UTF-8 or +/// unknown gate name), or -8 (no channels recorded for the key or `index` +/// out of bounds). +fn resolve_single_channel( + ptr: *const CNoiseModel, + gate_name: *const c_char, + qubit: u32, + index: usize, +) -> Result { + if ptr.is_null() || gate_name.is_null() { + return Err(CqlibError::NullPtr as i32); + } + let name = unsafe { CStr::from_ptr(gate_name) } + .to_str() + .map_err(|_| CqlibError::ParseError as i32)?; + let gate = standard_gate_from_name(name).ok_or(CqlibError::ParseError as i32)?; + let key = OperationKey::new_single(gate, Qubit::new(qubit)); + let errors = unsafe { (*ptr).inner.get_single_qubit_errors(&key) }; + errors + .ok_or(CqlibError::InvalidParam as i32)? + .get(index) + .copied() + .ok_or(CqlibError::InvalidParam as i32) +} + +/// Reports whether the single-qubit channel recorded for the standard gate +/// `gate_name` on `qubit` at `index` carries valid noise parameters. +/// +/// Returns 1 when valid, 0 when not, -1 on NULL, -4 for an unknown gate +/// name, or -8 when no channel exists at that key/index. +#[unsafe(no_mangle)] +pub extern "C" fn noise_model_is_valid( + ptr: *const CNoiseModel, + gate_name: *const c_char, + qubit: u32, + index: usize, +) -> i32 { + match resolve_single_channel(ptr, gate_name, qubit, index) { + Ok(noise) => i32::from(noise.is_valid()), + Err(code) => code, + } +} + +/// Returns the total number of `Complex64` elements across all Kraus +/// operators of the single-qubit channel recorded for the standard gate +/// `gate_name` on `qubit` at `index` (each operator contributes `dim * dim` +/// elements), or 0 for NULL, an unknown gate name, or an absent key/index. +#[unsafe(no_mangle)] +pub extern "C" fn noise_model_to_kraus_len( + ptr: *const CNoiseModel, + gate_name: *const c_char, + qubit: u32, + index: usize, +) -> usize { + match resolve_single_channel(ptr, gate_name, qubit, index) { + Ok(noise) => noise.to_kraus().iter().map(|op| op.len()).sum(), + Err(_) => 0, + } +} + +/// Copies all Kraus operators of the single-qubit channel recorded for the +/// standard gate `gate_name` on `qubit` at `index` into `out` (two-step +/// pattern; pair with `noise_model_to_kraus_len`). +/// +/// Operators are written one after another, each in row-major order as +/// `dim * dim` `Complex64` re/im pairs. Returns the total element count, or +/// 0 for NULL, an unknown gate name, or an absent key/index. +#[unsafe(no_mangle)] +pub extern "C" fn noise_model_to_kraus( + ptr: *const CNoiseModel, + gate_name: *const c_char, + qubit: u32, + index: usize, + out: *mut Complex64, + len: usize, +) -> usize { + let noise = match resolve_single_channel(ptr, gate_name, qubit, index) { + Ok(noise) => noise, + Err(_) => return 0, + }; + let ops = noise.to_kraus(); + let total: usize = ops.iter().map(|op| op.len()).sum(); + if !out.is_null() { + let mut written = 0usize; + for op in &ops { + let (rows, cols) = op.dim(); + for row in 0..rows { + for col in 0..cols { + if written >= len { + return total; + } + unsafe { *out.add(written) = op[[row, col]] }; + written += 1; + } + } + } + } + total +} diff --git a/crates/binding-c/src/device/qubit.rs b/crates/binding-c/src/device/qubit.rs new file mode 100644 index 00000000..9bab0256 --- /dev/null +++ b/crates/binding-c/src/device/qubit.rs @@ -0,0 +1,171 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating that +// they have been altered from the originals. + +//! C ABI for the strongly typed device-facing qubit identifiers. +//! +//! Circuit operations use [`CQubit`] as logical wire identifiers. +//! Device-facing code distinguishes those logical identifiers from physical +//! hardware positions: [`CLogicalQubit`] marks circuit wires and +//! [`CPhysicalQubit`] marks hardware positions. The two types share the same +//! numeric representation but never convert into each other directly; both +//! convert to and from [`CQubit`]. + +use crate::circuit::qubit::CQubit; +use cqlib_core::device::{LogicalQubit, PhysicalQubit}; +use std::ffi::CString; +use std::os::raw::c_char; + +/// Logical qubit identifier marking a circuit wire; a value handle passed by +/// value. Compare and hash via the numeric ID. +#[repr(C)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct CLogicalQubit { + /// Numeric identifier of the logical qubit. + pub id: u32, +} + +/// Physical qubit identifier marking a hardware position; a value handle +/// passed by value. Compare and hash via the numeric ID. +#[repr(C)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct CPhysicalQubit { + /// Numeric identifier of the physical qubit. + pub id: u32, +} + +impl From for CLogicalQubit { + fn from(qubit: LogicalQubit) -> Self { + Self { id: qubit.id() } + } +} + +impl From for LogicalQubit { + fn from(qubit: CLogicalQubit) -> Self { + LogicalQubit::new(qubit.id) + } +} + +impl From for CPhysicalQubit { + fn from(qubit: PhysicalQubit) -> Self { + Self { id: qubit.id() } + } +} + +impl From for PhysicalQubit { + fn from(qubit: CPhysicalQubit) -> Self { + PhysicalQubit::new(qubit.id) + } +} + +/// Creates a logical qubit identifier from its numeric ID. +#[unsafe(no_mangle)] +pub extern "C" fn logical_qubit_new(id: u32) -> CLogicalQubit { + CLogicalQubit { id } +} + +/// Wraps a circuit qubit as a logical qubit identifier. +#[unsafe(no_mangle)] +pub extern "C" fn logical_qubit_from_qubit(qubit: CQubit) -> CLogicalQubit { + CLogicalQubit { + id: LogicalQubit::from_qubit(qubit.into()).id(), + } +} + +/// Returns the underlying circuit qubit identifier. +#[unsafe(no_mangle)] +pub extern "C" fn logical_qubit_qubit(qubit: CLogicalQubit) -> CQubit { + CQubit::from(LogicalQubit::from(qubit).qubit()) +} + +/// Returns the numeric identifier of the logical qubit. +#[unsafe(no_mangle)] +pub extern "C" fn logical_qubit_id(qubit: CLogicalQubit) -> u32 { + LogicalQubit::from(qubit).id() +} + +/// Returns 1 when both logical qubits carry the same numeric identifier, +/// else 0. +#[unsafe(no_mangle)] +pub extern "C" fn logical_qubit_equal(a: CLogicalQubit, b: CLogicalQubit) -> i32 { + LogicalQubit::from(a).eq(&LogicalQubit::from(b)) as i32 +} + +/// Compares two logical qubits by numeric identifier: -1 when `a < b`, 0 +/// when equal, 1 when `a > b`. +#[unsafe(no_mangle)] +pub extern "C" fn logical_qubit_compare(a: CLogicalQubit, b: CLogicalQubit) -> i32 { + match LogicalQubit::from(a).cmp(&LogicalQubit::from(b)) { + std::cmp::Ordering::Less => -1, + std::cmp::Ordering::Equal => 0, + std::cmp::Ordering::Greater => 1, + } +} + +/// Formats the logical qubit identifier as `L` (e.g. `L3`); free with +/// `cqlib_string_free`. Returns NULL on allocation failure. +#[unsafe(no_mangle)] +pub extern "C" fn logical_qubit_to_string(qubit: CLogicalQubit) -> *mut c_char { + CString::new(format!("{}", LogicalQubit::from(qubit))) + .map_or(std::ptr::null_mut(), |cs| cs.into_raw()) +} + +/// Creates a physical qubit identifier from its numeric ID. +#[unsafe(no_mangle)] +pub extern "C" fn physical_qubit_new(id: u32) -> CPhysicalQubit { + CPhysicalQubit { id } +} + +/// Wraps a circuit qubit as a physical hardware ID. +#[unsafe(no_mangle)] +pub extern "C" fn physical_qubit_from_qubit(qubit: CQubit) -> CPhysicalQubit { + CPhysicalQubit { + id: PhysicalQubit::from_qubit(qubit.into()).id(), + } +} + +/// Returns the underlying circuit qubit identifier. +#[unsafe(no_mangle)] +pub extern "C" fn physical_qubit_qubit(qubit: CPhysicalQubit) -> CQubit { + CQubit::from(PhysicalQubit::from(qubit).qubit()) +} + +/// Returns the numeric identifier of the physical qubit. +#[unsafe(no_mangle)] +pub extern "C" fn physical_qubit_id(qubit: CPhysicalQubit) -> u32 { + PhysicalQubit::from(qubit).id() +} + +/// Returns 1 when both physical qubits carry the same numeric identifier, +/// else 0. +#[unsafe(no_mangle)] +pub extern "C" fn physical_qubit_equal(a: CPhysicalQubit, b: CPhysicalQubit) -> i32 { + PhysicalQubit::from(a).eq(&PhysicalQubit::from(b)) as i32 +} + +/// Compares two physical qubits by numeric identifier: -1 when `a < b`, 0 +/// when equal, 1 when `a > b`. +#[unsafe(no_mangle)] +pub extern "C" fn physical_qubit_compare(a: CPhysicalQubit, b: CPhysicalQubit) -> i32 { + match PhysicalQubit::from(a).cmp(&PhysicalQubit::from(b)) { + std::cmp::Ordering::Less => -1, + std::cmp::Ordering::Equal => 0, + std::cmp::Ordering::Greater => 1, + } +} + +/// Formats the physical qubit identifier as `P` (e.g. `P11`); free with +/// `cqlib_string_free`. Returns NULL on allocation failure. +#[unsafe(no_mangle)] +pub extern "C" fn physical_qubit_to_string(qubit: CPhysicalQubit) -> *mut c_char { + CString::new(format!("{}", PhysicalQubit::from(qubit))) + .map_or(std::ptr::null_mut(), |cs| cs.into_raw()) +} diff --git a/crates/binding-c/src/device/result.rs b/crates/binding-c/src/device/result.rs new file mode 100644 index 00000000..ef56a9e7 --- /dev/null +++ b/crates/binding-c/src/device/result.rs @@ -0,0 +1,601 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! C ABI for `ExecutionResult` (measurement counts and probabilities). + +use crate::device::{CCountsList, CExecutionResult, write_u32_list}; +use crate::error::CqlibError; +use cqlib_core::circuit::Qubit; +use cqlib_core::device::{ExecutionResult, Outcome, Status}; +use std::collections::HashMap; +use std::ffi::{CStr, CString}; +use std::os::raw::c_char; + +fn bitstring_key(outcome: &Outcome, num_qubits: usize) -> String { + outcome.to_bitstring(num_qubits) +} + +/// Creates a completed execution result from measurement counts. +/// +/// `bitstrings` and `counts` are parallel arrays of length `num_entries`; +/// each bitstring must have exactly `num_qubits` characters. Returns NULL +/// on error. +#[unsafe(no_mangle)] +pub extern "C" fn execution_result_from_counts( + task_id: *const c_char, + num_qubits: usize, + shots: usize, + bitstrings: *const *const c_char, + counts: *const u64, + num_entries: usize, +) -> *mut CExecutionResult { + if task_id.is_null() || (num_entries > 0 && (bitstrings.is_null() || counts.is_null())) { + return std::ptr::null_mut(); + } + let task_id = match unsafe { CStr::from_ptr(task_id) }.to_str() { + Ok(s) => s.to_string(), + Err(_) => return std::ptr::null_mut(), + }; + let mut map: HashMap = HashMap::new(); + for i in 0..num_entries { + let raw = unsafe { *bitstrings.add(i) }; + if raw.is_null() { + return std::ptr::null_mut(); + } + let bits = match unsafe { CStr::from_ptr(raw) }.to_str() { + Ok(s) => s, + Err(_) => return std::ptr::null_mut(), + }; + let outcome = match Outcome::from_bitstring(bits) { + Ok(o) => o, + Err(_) => return std::ptr::null_mut(), + }; + let count = unsafe { *counts.add(i) } as usize; + *map.entry(outcome).or_insert(0) += count; + } + let qubits: Vec = (0..num_qubits as u32).map(Qubit::new).collect(); + let result = ExecutionResult::from_counts(task_id, qubits, shots, num_qubits, None, map); + Box::into_raw(Box::new(CExecutionResult { inner: result })) +} + +/// Frees an execution result. NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn execution_result_free(ptr: *mut CExecutionResult) { + if !ptr.is_null() { + unsafe { + let _ = Box::from_raw(ptr); + } + } +} + +/// Returns the number of shots, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn execution_result_shots(ptr: *const CExecutionResult) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.shots() } +} + +/// Returns the number of qubits, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn execution_result_num_qubits(ptr: *const CExecutionResult) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.num_qubits() } +} + +/// Returns the measurement counts as an owned `CCountsList*` +/// (bitstring keys, integer weights as f64). Caller must free with +/// `counts_list_free`. Returns NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn execution_result_counts(ptr: *const CExecutionResult) -> *mut CCountsList { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let result = unsafe { &*ptr }; + let n = result.inner.num_qubits(); + let mut keys = Vec::new(); + let mut values = Vec::new(); + for (outcome, count) in result.inner.counts() { + keys.push(bitstring_key(outcome, n)); + values.push(*count as f64); + } + Box::into_raw(Box::new(CCountsList { keys, values })) +} + +/// Computes (if needed) and returns the probability distribution as an +/// owned `CCountsList*` (bitstring keys, f64 probabilities). +/// Caller must free with `counts_list_free`. Returns NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn execution_result_probabilities(ptr: *mut CExecutionResult) -> *mut CCountsList { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let result = unsafe { &mut *ptr }; + result.inner.calc_probabilities(); + let n = result.inner.num_qubits(); + let mut keys = Vec::new(); + let mut values = Vec::new(); + if let Some(probs) = result.inner.probabilities() { + for (outcome, probability) in probs { + keys.push(bitstring_key(outcome, n)); + values.push(*probability); + } + } + Box::into_raw(Box::new(CCountsList { keys, values })) +} + +/// Returns the number of entries in the list, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn counts_list_len(ptr: *const CCountsList) -> usize { + if ptr.is_null() { + return 0; + } + let list = unsafe { &*ptr }; + list.keys.len() +} + +/// Returns the bitstring key at `index` as a heap-allocated C string. +/// Caller must free with `cqlib_string_free`. Returns NULL on out-of-bounds. +#[unsafe(no_mangle)] +pub extern "C" fn counts_list_get_key(ptr: *const CCountsList, index: usize) -> *mut c_char { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let list = unsafe { &*ptr }; + match list.keys.get(index) { + Some(key) => match CString::new(key.as_str()) { + Ok(cs) => cs.into_raw(), + Err(_) => std::ptr::null_mut(), + }, + None => std::ptr::null_mut(), + } +} + +/// Returns the weight (count or probability) at `index`, or NaN on error. +#[unsafe(no_mangle)] +pub extern "C" fn counts_list_get_value(ptr: *const CCountsList, index: usize) -> f64 { + if ptr.is_null() { + return f64::NAN; + } + let list = unsafe { &*ptr }; + list.values.get(index).copied().unwrap_or(f64::NAN) +} + +/// Frees a counts list. NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn counts_list_free(ptr: *mut CCountsList) { + if !ptr.is_null() { + unsafe { + let _ = Box::from_raw(ptr); + } + } +} + +// ===== Section 3.5: lifecycle, status, and metadata ===== + +/// `execution_result_status` tag: task submitted and queued. +pub const EXECUTION_STATUS_QUEUED: u8 = 0; +/// `execution_result_status` tag: task currently running. +pub const EXECUTION_STATUS_RUNNING: u8 = 1; +/// `execution_result_status` tag: task completed successfully. +pub const EXECUTION_STATUS_COMPLETED: u8 = 2; +/// `execution_result_status` tag: task failed (query the error code and +/// message for details). +pub const EXECUTION_STATUS_FAILED: u8 = 3; +/// `execution_result_status` tag: task cancelled by the user. +pub const EXECUTION_STATUS_CANCELLED: u8 = 4; + +/// Converts a Unix-timestamp nanosecond count to whole milliseconds. +fn unix_nanos_to_ms(nanos: i128) -> i64 { + nanos.div_euclid(1_000_000) as i64 +} + +/// Creates an execution result in Queued status. +/// +/// `qubits` points to `num_qubits` measured-qubit IDs (entry `i` corresponds +/// to bit `i` of the outcome keys); `backend` may be NULL. The creation +/// timestamp is the current time. Returns NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn execution_result_new( + task_id: *const c_char, + qubits: *const u32, + num_qubits: usize, + shots: usize, + backend: *const c_char, +) -> *mut CExecutionResult { + if task_id.is_null() || (num_qubits > 0 && qubits.is_null()) { + return std::ptr::null_mut(); + } + let task_id = match unsafe { CStr::from_ptr(task_id) }.to_str() { + Ok(s) => s.to_string(), + Err(_) => return std::ptr::null_mut(), + }; + let backend = if backend.is_null() { + None + } else { + match unsafe { CStr::from_ptr(backend) }.to_str() { + Ok(s) => Some(s.to_string()), + Err(_) => return std::ptr::null_mut(), + } + }; + let qubits: Vec = if num_qubits == 0 { + Vec::new() + } else { + unsafe { std::slice::from_raw_parts(qubits, num_qubits) } + .iter() + .map(|&q| Qubit::new(q)) + .collect() + }; + let result = ExecutionResult::new(task_id, qubits, shots, num_qubits, backend, None); + Box::into_raw(Box::new(CExecutionResult { inner: result })) +} + +/// Marks the job as running and records the current time as the start +/// timestamp. Returns 0 on success or -1 on NULL. +#[unsafe(no_mangle)] +pub extern "C" fn execution_result_start(ptr: *mut CExecutionResult) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + let wrapper = unsafe { &mut *ptr }; + wrapper.inner.start(None); + CqlibError::Ok as i32 +} + +/// Marks the job as completed and replaces the measurement counts. +/// +/// `bitstrings` and `counts` are parallel arrays of length `num_entries`; +/// repeated bitstrings accumulate. The finish timestamp is the current time. +/// Returns 0 on success, -1 on NULL, or -4 for an invalid bitstring. +#[unsafe(no_mangle)] +pub extern "C" fn execution_result_finish( + ptr: *mut CExecutionResult, + bitstrings: *const *const c_char, + counts: *const u64, + num_entries: usize, +) -> i32 { + if ptr.is_null() || (num_entries > 0 && (bitstrings.is_null() || counts.is_null())) { + return CqlibError::NullPtr as i32; + } + let mut map: HashMap = HashMap::new(); + for i in 0..num_entries { + let raw = unsafe { *bitstrings.add(i) }; + if raw.is_null() { + return CqlibError::NullPtr as i32; + } + let bits = match unsafe { CStr::from_ptr(raw) }.to_str() { + Ok(s) => s, + Err(_) => return CqlibError::ParseError as i32, + }; + let outcome = match Outcome::from_bitstring(bits) { + Ok(o) => o, + Err(_) => return CqlibError::ParseError as i32, + }; + let count = unsafe { *counts.add(i) } as usize; + *map.entry(outcome).or_insert(0) += count; + } + let wrapper = unsafe { &mut *ptr }; + wrapper.inner.finish(map, None); + CqlibError::Ok as i32 +} + +/// Marks the job as failed with an error message and code. +/// +/// Returns 0 on success, -1 on NULL, or -4 when the message is not valid +/// UTF-8. +#[unsafe(no_mangle)] +pub extern "C" fn execution_result_fail( + ptr: *mut CExecutionResult, + msg: *const c_char, + code: i32, +) -> i32 { + if ptr.is_null() || msg.is_null() { + return CqlibError::NullPtr as i32; + } + let msg = match unsafe { CStr::from_ptr(msg) }.to_str() { + Ok(s) => s.to_string(), + Err(_) => return CqlibError::ParseError as i32, + }; + let wrapper = unsafe { &mut *ptr }; + wrapper.inner.fail(msg, code); + CqlibError::Ok as i32 +} + +/// Marks the job as cancelled. Returns 0 on success or -1 on NULL. +#[unsafe(no_mangle)] +pub extern "C" fn execution_result_cancel(ptr: *mut CExecutionResult) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + let wrapper = unsafe { &mut *ptr }; + wrapper.inner.cancel(); + CqlibError::Ok as i32 +} + +/// Writes the execution status tag to `*out_tag` (one of the +/// `EXECUTION_STATUS_*` constants). +/// +/// When the status is failed, `*out_error_code` receives the stored error +/// code (0 otherwise); `out_error_code` may be NULL when not needed. +/// Returns 0 on success or -1 on NULL. +#[unsafe(no_mangle)] +pub extern "C" fn execution_result_status( + ptr: *const CExecutionResult, + out_tag: *mut u8, + out_error_code: *mut i32, +) -> i32 { + if ptr.is_null() || out_tag.is_null() { + return CqlibError::NullPtr as i32; + } + let status = unsafe { (*ptr).inner.status() }; + let (tag, code) = match status { + Status::Queued => (EXECUTION_STATUS_QUEUED, 0), + Status::Running => (EXECUTION_STATUS_RUNNING, 0), + Status::Completed => (EXECUTION_STATUS_COMPLETED, 0), + Status::Failed { error_code, .. } => (EXECUTION_STATUS_FAILED, *error_code), + Status::Cancelled => (EXECUTION_STATUS_CANCELLED, 0), + }; + unsafe { + *out_tag = tag; + if !out_error_code.is_null() { + *out_error_code = code; + } + } + CqlibError::Ok as i32 +} + +/// Returns the error message stored by the failed status as a +/// heap-allocated C string. Caller must free with `cqlib_string_free`. +/// Returns NULL on NULL input or when the status is not failed. +#[unsafe(no_mangle)] +pub extern "C" fn execution_result_error_message(ptr: *const CExecutionResult) -> *mut c_char { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let status = unsafe { (*ptr).inner.status() }; + let Status::Failed { error_msg, .. } = status else { + return std::ptr::null_mut(); + }; + match CString::new(error_msg.as_str()) { + Ok(cs) => cs.into_raw(), + Err(_) => std::ptr::null_mut(), + } +} + +/// Returns the task ID as a heap-allocated C string. Caller must free with +/// `cqlib_string_free`. Returns NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn execution_result_task_id(ptr: *const CExecutionResult) -> *mut c_char { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let task_id = unsafe { (*ptr).inner.task_id() }.to_string(); + match CString::new(task_id) { + Ok(cs) => cs.into_raw(), + Err(_) => std::ptr::null_mut(), + } +} + +/// Returns the backend name as a heap-allocated C string. Caller must free +/// with `cqlib_string_free`. Returns NULL on error or when no backend is set. +#[unsafe(no_mangle)] +pub extern "C" fn execution_result_backend(ptr: *const CExecutionResult) -> *mut c_char { + if ptr.is_null() { + return std::ptr::null_mut(); + } + match unsafe { (*ptr).inner.backend() } { + Some(backend) => match CString::new(backend.as_str()) { + Ok(cs) => cs.into_raw(), + Err(_) => std::ptr::null_mut(), + }, + None => std::ptr::null_mut(), + } +} + +/// Returns the task creation timestamp in milliseconds since the Unix epoch. +/// Returns 0 with the value in `*out_ms`, or -1 on NULL. +#[unsafe(no_mangle)] +pub extern "C" fn execution_result_created_at( + ptr: *const CExecutionResult, + out_ms: *mut i64, +) -> i32 { + if ptr.is_null() || out_ms.is_null() { + return CqlibError::NullPtr as i32; + } + let t = unsafe { (*ptr).inner.created_at() }; + unsafe { *out_ms = unix_nanos_to_ms(t.unix_timestamp_nanos()) }; + CqlibError::Ok as i32 +} + +/// Returns the execution start timestamp in milliseconds since the Unix +/// epoch. Returns 0 with the value in `*out_ms`, or -8 when the job has not +/// started. +#[unsafe(no_mangle)] +pub extern "C" fn execution_result_started_at( + ptr: *const CExecutionResult, + out_ms: *mut i64, +) -> i32 { + if ptr.is_null() || out_ms.is_null() { + return CqlibError::NullPtr as i32; + } + match unsafe { (*ptr).inner.started_at() } { + Some(t) => { + unsafe { *out_ms = unix_nanos_to_ms(t.unix_timestamp_nanos()) }; + CqlibError::Ok as i32 + } + None => CqlibError::InvalidParam as i32, + } +} + +/// Returns the execution finish timestamp in milliseconds since the Unix +/// epoch. Returns 0 with the value in `*out_ms`, or -8 when the job has not +/// finished. +#[unsafe(no_mangle)] +pub extern "C" fn execution_result_finished_at( + ptr: *const CExecutionResult, + out_ms: *mut i64, +) -> i32 { + if ptr.is_null() || out_ms.is_null() { + return CqlibError::NullPtr as i32; + } + match unsafe { (*ptr).inner.finished_at() } { + Some(t) => { + unsafe { *out_ms = unix_nanos_to_ms(t.unix_timestamp_nanos()) }; + CqlibError::Ok as i32 + } + None => CqlibError::InvalidParam as i32, + } +} + +/// Returns the number of measured qubits, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn execution_result_qubits_len(ptr: *const CExecutionResult) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.qubits().len() } +} + +/// Copies the measured qubit IDs into `out` (two-step pattern; pair with +/// `execution_result_qubits_len`). Entry `i` corresponds to bit `i` of the +/// outcome keys. Returns the total count. +#[unsafe(no_mangle)] +pub extern "C" fn execution_result_qubits( + ptr: *const CExecutionResult, + out: *mut u32, + len: usize, +) -> usize { + if ptr.is_null() { + return 0; + } + let items: Vec = unsafe { (*ptr).inner.qubits() } + .iter() + .map(|q| q.id()) + .collect(); + write_u32_list(&items, out, len) +} + +// ===== Section 3.6: status flags, bitstrings, and two-step probabilities ===== + +/// Returns 1 when the execution completed successfully, 0 otherwise, or -1 +/// on NULL. +#[unsafe(no_mangle)] +pub extern "C" fn execution_result_is_success(ptr: *const CExecutionResult) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + i32::from(unsafe { (*ptr).inner.status().is_success() }) +} + +/// Returns 1 when the execution reached a terminal state (completed, failed, +/// or cancelled), 0 otherwise, or -1 on NULL. +#[unsafe(no_mangle)] +pub extern "C" fn execution_result_is_terminal(ptr: *const CExecutionResult) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + i32::from(unsafe { (*ptr).inner.status().is_terminal() }) +} + +/// Formats the raw outcome bitmask as a big-endian bitstring over the +/// result's measured-qubit width (bit `i` of `outcome` is measured qubit +/// `i`; high bits beyond the width are truncated). +/// +/// Returns a heap-allocated C string freed with `cqlib_string_free`, or NULL +/// on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn execution_result_to_bitstring( + ptr: *const CExecutionResult, + outcome: u64, +) -> *mut c_char { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let n = unsafe { (*ptr).inner.num_qubits() }; + let indices = (0..u64::BITS as usize).filter(|&bit| outcome & (1u64 << bit) != 0); + let bitstring = Outcome::from_indices(n, indices).to_bitstring(n); + match CString::new(bitstring) { + Ok(cs) => cs.into_raw(), + Err(_) => std::ptr::null_mut(), + } +} + +/// Computes the probability distribution and returns the number of entries, +/// or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn execution_result_calc_probabilities_len(ptr: *mut CExecutionResult) -> usize { + if ptr.is_null() { + return 0; + } + let wrapper = unsafe { &mut *ptr }; + wrapper.inner.calc_probabilities(); + wrapper + .inner + .probabilities() + .as_ref() + .map_or(0, |probs| probs.len()) +} + +/// Computes the probability distribution and copies the probabilities into +/// `out` (two-step pattern; pair with +/// `execution_result_calc_probabilities_len`). +/// +/// Entries are sorted ascending by outcome value, where bit `i` of the +/// outcome value is measured qubit `i` (matching +/// `execution_result_to_bitstring`). Returns the total number of entries, or +/// 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn execution_result_calc_probabilities( + ptr: *mut CExecutionResult, + out: *mut f64, + len: usize, +) -> usize { + if ptr.is_null() { + return 0; + } + let wrapper = unsafe { &mut *ptr }; + wrapper.inner.calc_probabilities(); + let Some(probs) = wrapper.inner.probabilities() else { + return 0; + }; + // Sort keys ascending in big-endian chunk order (bit i = qubit i). Pad + // all keys to a common chunk width so mismatched storage widths still + // compare numerically. + let mut entries: Vec<(Vec, f64)> = probs + .iter() + .map(|(outcome, probability)| { + ( + outcome.0.iter().copied().collect::>(), + *probability, + ) + }) + .collect(); + let width = entries + .iter() + .map(|(chunks, _)| chunks.len()) + .max() + .unwrap_or(0); + for (chunks, _) in &mut entries { + chunks.resize(width, 0); + chunks.reverse(); + } + entries.sort_by(|a, b| a.0.cmp(&b.0)); + if !out.is_null() { + for (i, (_, probability)) in entries.iter().take(entries.len().min(len)).enumerate() { + unsafe { *out.add(i) = *probability }; + } + } + entries.len() +} diff --git a/crates/binding-c/src/device/topology.rs b/crates/binding-c/src/device/topology.rs new file mode 100644 index 00000000..340e08ef --- /dev/null +++ b/crates/binding-c/src/device/topology.rs @@ -0,0 +1,697 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! C ABI for device construction, topology, and circuit validation. + +use crate::circuit::CCircuit; +use crate::device::{CDevice, CTopology, standard_gate_from_name, write_u32_list, write_u32_pairs}; +use crate::error::CqlibError; +use cqlib_core::circuit::gate::Instruction; +use cqlib_core::device::error::TopologyError; +use cqlib_core::device::{Device, PhysicalQubit, Topology}; +use std::ffi::{CStr, CString}; +use std::os::raw::c_char; + +/// Creates a device named `name` with `num_qubits` isolated physical qubits +/// and no couplings. Use `device_from_edges` for custom topologies. +/// Returns NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn device_new(name: *const c_char, num_qubits: u32) -> *mut CDevice { + if name.is_null() { + return std::ptr::null_mut(); + } + let name = match unsafe { CStr::from_ptr(name) }.to_str() { + Ok(s) => s, + Err(_) => return std::ptr::null_mut(), + }; + let qubits: Vec<_> = (0..num_qubits) + .map(cqlib_core::device::PhysicalQubit::new) + .collect(); + let topology = match cqlib_core::device::Topology::new(qubits.clone(), Vec::new()) { + Ok(t) => t, + Err(_) => return std::ptr::null_mut(), + }; + match Device::new(name, qubits.into_iter().collect(), topology) { + Ok(device) => Box::into_raw(Box::new(CDevice { inner: device })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Frees a device. NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn device_free(ptr: *mut CDevice) { + if !ptr.is_null() { + unsafe { + let _ = Box::from_raw(ptr); + } + } +} + +fn boxed_device(result: Result) -> *mut CDevice { + match result { + Ok(device) => Box::into_raw(Box::new(CDevice { inner: device })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Creates a device with a directed line topology `0 -> 1 -> ... -> n-1`. +#[unsafe(no_mangle)] +pub extern "C" fn device_line(name: *const c_char, num_qubits: u32) -> *mut CDevice { + if name.is_null() { + return std::ptr::null_mut(); + } + let name = match unsafe { CStr::from_ptr(name) }.to_str() { + Ok(s) => s, + Err(_) => return std::ptr::null_mut(), + }; + boxed_device(Device::line(name, num_qubits)) +} + +/// Creates a device with a bidirectional line topology. +#[unsafe(no_mangle)] +pub extern "C" fn device_bidirectional_line(name: *const c_char, num_qubits: u32) -> *mut CDevice { + if name.is_null() { + return std::ptr::null_mut(); + } + let name = match unsafe { CStr::from_ptr(name) }.to_str() { + Ok(s) => s, + Err(_) => return std::ptr::null_mut(), + }; + boxed_device(Device::bidirectional_line(name, num_qubits)) +} + +/// Creates a device with a bidirectional ring topology. +#[unsafe(no_mangle)] +pub extern "C" fn device_ring(name: *const c_char, num_qubits: u32) -> *mut CDevice { + if name.is_null() { + return std::ptr::null_mut(); + } + let name = match unsafe { CStr::from_ptr(name) }.to_str() { + Ok(s) => s, + Err(_) => return std::ptr::null_mut(), + }; + boxed_device(Device::ring(name, num_qubits)) +} + +/// Creates a device with a bidirectional grid topology (row-major qubit IDs). +#[unsafe(no_mangle)] +pub extern "C" fn device_grid(name: *const c_char, rows: u32, cols: u32) -> *mut CDevice { + if name.is_null() { + return std::ptr::null_mut(); + } + let name = match unsafe { CStr::from_ptr(name) }.to_str() { + Ok(s) => s, + Err(_) => return std::ptr::null_mut(), + }; + boxed_device(Device::grid(name, rows, cols)) +} + +/// Creates a device with a bidirectional star topology around `center`. +#[unsafe(no_mangle)] +pub extern "C" fn device_star(name: *const c_char, num_qubits: u32, center: u32) -> *mut CDevice { + if name.is_null() { + return std::ptr::null_mut(); + } + let name = match unsafe { CStr::from_ptr(name) }.to_str() { + Ok(s) => s, + Err(_) => return std::ptr::null_mut(), + }; + boxed_device(Device::star(name, num_qubits, center)) +} + +/// Creates a device from explicit directed edges. +/// +/// `edges` points to `2 * num_edges` u32 values laid out as consecutive +/// `(control, target)` pairs. Returns NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn device_from_edges( + name: *const c_char, + num_qubits: u32, + edges: *const u32, + num_edges: usize, +) -> *mut CDevice { + if name.is_null() || (num_edges > 0 && edges.is_null()) { + return std::ptr::null_mut(); + } + let name = match unsafe { CStr::from_ptr(name) }.to_str() { + Ok(s) => s, + Err(_) => return std::ptr::null_mut(), + }; + let raw = if num_edges == 0 { + &[] + } else { + unsafe { std::slice::from_raw_parts(edges, num_edges * 2) } + }; + let pairs: Vec<(u32, u32)> = raw.chunks_exact(2).map(|c| (c[0], c[1])).collect(); + boxed_device(Device::from_edges(name, num_qubits, &pairs)) +} + +/// Replaces the device's native gate set with the gates named in +/// `gate_names` (comma separated, e.g. "H,CX,RZ"). Returns 0 on success, +/// -4 if any name is unknown, or a negative error code. +#[unsafe(no_mangle)] +pub extern "C" fn device_with_native_gates(ptr: *mut CDevice, gate_names: *const c_char) -> i32 { + if ptr.is_null() || gate_names.is_null() { + return CqlibError::NullPtr as i32; + } + let names = match unsafe { CStr::from_ptr(gate_names) }.to_str() { + Ok(s) => s, + Err(_) => return CqlibError::ParseError as i32, + }; + let mut gates = Vec::new(); + for part in names.split(',') { + let part = part.trim(); + if part.is_empty() { + continue; + } + match standard_gate_from_name(part) { + Some(gate) => gates.push(Instruction::Standard(gate)), + None => return CqlibError::ParseError as i32, + } + } + if gates.is_empty() { + return CqlibError::InvalidParam as i32; + } + let wrapper = unsafe { &mut *ptr }; + // `with_native_gates` consumes self; work on a clone so the original + // stays intact when the builder rejects the gate set. + match wrapper.inner.clone().with_native_gates(gates) { + Ok(new_device) => { + wrapper.inner = new_device; + 0 + } + Err(_) => CqlibError::CircuitError as i32, + } +} + +/// Validates that `circuit` is compatible with the device. +/// Returns 0 when valid, -3 when validation fails, or a negative error code. +#[unsafe(no_mangle)] +pub extern "C" fn device_validate_circuit(ptr: *const CDevice, circuit: *const CCircuit) -> i32 { + if ptr.is_null() || circuit.is_null() { + return CqlibError::NullPtr as i32; + } + let device = unsafe { &(*ptr).inner }; + let circuit = unsafe { &(*circuit).inner }; + match device.validate_circuit(circuit) { + Ok(()) => 0, + Err(_) => CqlibError::CircuitError as i32, + } +} + +/// Returns the number of usable physical qubits, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn device_num_qubits(ptr: *const CDevice) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.num_usable_qubits() } +} + +/// Returns the device name as a heap-allocated C string. +/// Caller must free with `cqlib_string_free`. Returns NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn device_name(ptr: *const CDevice) -> *mut c_char { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let name = unsafe { (*ptr).inner.name() }.to_string(); + match CString::new(name) { + Ok(cs) => cs.into_raw(), + Err(_) => std::ptr::null_mut(), + } +} + +/// Returns the native gate names as a comma-separated heap-allocated C +/// string (e.g. "H,CX"). Caller must free with `cqlib_string_free`. +/// Returns NULL on error or when the gate set is empty. +#[unsafe(no_mangle)] +pub extern "C" fn device_native_gates(ptr: *const CDevice) -> *mut c_char { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let names: Vec = unsafe { (*ptr).inner.native_gates() } + .iter() + .map(|gate| gate.name().to_string()) + .collect(); + if names.is_empty() { + return std::ptr::null_mut(); + } + match CString::new(names.join(",")) { + Ok(cs) => cs.into_raw(), + Err(_) => std::ptr::null_mut(), + } +} + +/// Returns a clone of the device topology as an owned `CTopology*`. +/// Caller must free with `topology_free`. Returns NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn device_topology(ptr: *const CDevice) -> *mut CTopology { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let topology = unsafe { (*ptr).inner.topology() }.clone(); + Box::into_raw(Box::new(CTopology { inner: topology })) +} + +/// Frees a topology returned by `device_topology`. NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn topology_free(ptr: *mut CTopology) { + if !ptr.is_null() { + unsafe { + let _ = Box::from_raw(ptr); + } + } +} + +/// Returns the number of qubits in the topology, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn topology_num_qubits(ptr: *const CTopology) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.num_qubits() } +} + +/// Returns the number of coupling edges in the topology, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn topology_num_couplings(ptr: *const CTopology) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.num_couplings() } +} + +// ===== Section 3.3: topology graph queries and updates ===== + +/// Maps a `TopologyError` to a C error code: missing qubits or couplings map +/// to -2, conflicting or duplicated updates map to -8. +fn topology_error_code(error: &TopologyError) -> i32 { + match error { + TopologyError::QubitNotFound(_) | TopologyError::CouplingNotFound { .. } => { + CqlibError::QubitOutOfBounds as i32 + } + _ => CqlibError::InvalidParam as i32, + } +} + +/// Reads a u32 ID array into physical qubits; `len == 0` yields an empty vec. +/// Returns -1 when the array is NULL despite a positive length. +fn physical_qubits_from_c(qubits: *const u32, len: usize) -> Result, i32> { + if len == 0 { + return Ok(Vec::new()); + } + if qubits.is_null() { + return Err(CqlibError::NullPtr as i32); + } + Ok(unsafe { std::slice::from_raw_parts(qubits, len) } + .iter() + .map(|&q| PhysicalQubit::new(q)) + .collect()) +} + +/// Creates a topology whose physical qubits (given by ID array `qubits`) are +/// connected as a directed line in the supplied order. Returns NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn topology_line(qubits: *const u32, num_qubits: usize) -> *mut CTopology { + let physical = match physical_qubits_from_c(qubits, num_qubits) { + Ok(q) => q, + Err(_) => return std::ptr::null_mut(), + }; + match Topology::line(physical) { + Ok(topology) => Box::into_raw(Box::new(CTopology { inner: topology })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Adds qubits (given by ID array) to the topology. +/// +/// Returns 0 on success, -1 on NULL, or -8 when a qubit already exists in +/// the topology or is listed twice in the request. +#[unsafe(no_mangle)] +pub extern "C" fn topology_add_qubits(ptr: *mut CTopology, qubits: *const u32, len: usize) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + let items = match physical_qubits_from_c(qubits, len) { + Ok(q) => q, + Err(code) => return code, + }; + let wrapper = unsafe { &mut *ptr }; + match wrapper.inner.add_qubits(items) { + Ok(()) => CqlibError::Ok as i32, + Err(e) => topology_error_code(&e), + } +} + +/// Removes qubits (given by ID array) and every coupling touching them. +/// +/// Returns 0 on success, -1 on NULL, -2 when a qubit is not in the topology, +/// or -8 when a qubit is listed twice. +#[unsafe(no_mangle)] +pub extern "C" fn topology_remove_qubits( + ptr: *mut CTopology, + qubits: *const u32, + len: usize, +) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + let items = match physical_qubits_from_c(qubits, len) { + Ok(q) => q, + Err(code) => return code, + }; + let wrapper = unsafe { &mut *ptr }; + match wrapper.inner.remove_qubits(items) { + Ok(()) => CqlibError::Ok as i32, + Err(e) => topology_error_code(&e), + } +} + +/// Adds directed coupling edges to the topology. +/// +/// `edges` points to `2 * num_edges` u32 values laid out as consecutive +/// `(control, target)` pairs. `names` optionally points to `num_edges` C +/// strings (NULL entries or a NULL array leave the coupling unnamed). +/// Returns 0 on success, -1 on NULL, -4 for invalid UTF-8 in a name, -2 when +/// an endpoint qubit is not in the topology, or -8 for duplicate or self +/// couplings. +#[unsafe(no_mangle)] +pub extern "C" fn topology_add_couplings( + ptr: *mut CTopology, + edges: *const u32, + num_edges: usize, + names: *const *const c_char, +) -> i32 { + if ptr.is_null() || (num_edges > 0 && edges.is_null()) { + return CqlibError::NullPtr as i32; + } + let raw = if num_edges == 0 { + &[] + } else { + unsafe { std::slice::from_raw_parts(edges, num_edges * 2) } + }; + let mut couplings = Vec::with_capacity(num_edges); + for (i, chunk) in raw.chunks_exact(2).enumerate() { + let name = if names.is_null() { + String::new() + } else { + let raw_name = unsafe { *names.add(i) }; + if raw_name.is_null() { + String::new() + } else { + match unsafe { CStr::from_ptr(raw_name) }.to_str() { + Ok(s) => s.to_string(), + Err(_) => return CqlibError::ParseError as i32, + } + } + }; + couplings.push(( + PhysicalQubit::new(chunk[0]), + PhysicalQubit::new(chunk[1]), + name, + )); + } + let wrapper = unsafe { &mut *ptr }; + match wrapper.inner.add_couplings(couplings) { + Ok(()) => CqlibError::Ok as i32, + Err(e) => topology_error_code(&e), + } +} + +/// Removes directed coupling edges from the topology. +/// +/// `edges` points to `2 * num_edges` u32 values laid out as consecutive +/// `(control, target)` pairs. Returns 0 on success, -1 on NULL, -2 when a +/// qubit or coupling is not in the topology, or -8 when a coupling is listed +/// twice. +#[unsafe(no_mangle)] +pub extern "C" fn topology_remove_couplings( + ptr: *mut CTopology, + edges: *const u32, + num_edges: usize, +) -> i32 { + if ptr.is_null() || (num_edges > 0 && edges.is_null()) { + return CqlibError::NullPtr as i32; + } + let raw = if num_edges == 0 { + &[] + } else { + unsafe { std::slice::from_raw_parts(edges, num_edges * 2) } + }; + let pairs: Vec<(PhysicalQubit, PhysicalQubit)> = raw + .chunks_exact(2) + .map(|c| (PhysicalQubit::new(c[0]), PhysicalQubit::new(c[1]))) + .collect(); + let wrapper = unsafe { &mut *ptr }; + match wrapper.inner.remove_couplings(pairs) { + Ok(()) => CqlibError::Ok as i32, + Err(e) => topology_error_code(&e), + } +} + +/// Returns 1 when `qubit` is part of the topology, 0 otherwise, or -1 on NULL. +#[unsafe(no_mangle)] +pub extern "C" fn topology_contains_qubit(ptr: *const CTopology, qubit: u32) -> i32 { + if ptr.is_null() { + return -1; + } + unsafe { (*ptr).inner.contains_qubit(&PhysicalQubit::new(qubit)) as i32 } +} + +/// Returns the name of the directed coupling `control -> target` as a +/// heap-allocated C string; an existing but unnamed coupling yields an empty +/// string. Caller must free with `cqlib_string_free`. Returns NULL on NULL +/// input or when the coupling does not exist. +#[unsafe(no_mangle)] +pub extern "C" fn topology_get_coupling_name( + ptr: *const CTopology, + control: u32, + target: u32, +) -> *mut c_char { + if ptr.is_null() { + return std::ptr::null_mut(); + } + match unsafe { + (*ptr) + .inner + .get_coupling_name(PhysicalQubit::new(control), PhysicalQubit::new(target)) + } { + Some(name) => match CString::new(name) { + Ok(cs) => cs.into_raw(), + Err(_) => std::ptr::null_mut(), + }, + None => std::ptr::null_mut(), + } +} + +/// Returns the number of qubit IDs available from `topology_qubits`, or 0 +/// for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn topology_qubits_len(ptr: *const CTopology) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.qubits().count() } +} + +/// Copies the qubit IDs into `out` (two-step pattern; pair with +/// `topology_qubits_len`). Returns the total count. +#[unsafe(no_mangle)] +pub extern "C" fn topology_qubits(ptr: *const CTopology, out: *mut u32, len: usize) -> usize { + if ptr.is_null() { + return 0; + } + let items: Vec = unsafe { (*ptr).inner.qubits() }.map(|q| q.id()).collect(); + write_u32_list(&items, out, len) +} + +/// Returns the number of undirected neighbors of `qubit` (successors and +/// predecessors, deduplicated), or 0 for NULL or an unknown qubit. +#[unsafe(no_mangle)] +pub extern "C" fn topology_neighbors_undirected_len(ptr: *const CTopology, qubit: u32) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { + (*ptr) + .inner + .neighbors_undirected(PhysicalQubit::new(qubit)) + .count() + } +} + +/// Copies the undirected neighbor IDs of `qubit` (ascending) into `out` +/// (two-step pattern; pair with `topology_neighbors_undirected_len`). +/// Returns the total count. +#[unsafe(no_mangle)] +pub extern "C" fn topology_neighbors_undirected( + ptr: *const CTopology, + qubit: u32, + out: *mut u32, + len: usize, +) -> usize { + if ptr.is_null() { + return 0; + } + let items: Vec = unsafe { (*ptr).inner.neighbors_undirected(PhysicalQubit::new(qubit)) } + .map(|q| q.id()) + .collect(); + write_u32_list(&items, out, len) +} + +/// Returns the number of qubits with a directed coupling into `qubit`, or 0 +/// for NULL or an unknown qubit. +#[unsafe(no_mangle)] +pub extern "C" fn topology_predecessors_len(ptr: *const CTopology, qubit: u32) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.predecessors(PhysicalQubit::new(qubit)).count() } +} + +/// Copies the predecessor IDs of `qubit` (qubits with a directed coupling +/// into it, ascending) into `out` (two-step pattern; pair with +/// `topology_predecessors_len`). Returns the total count. +#[unsafe(no_mangle)] +pub extern "C" fn topology_predecessors( + ptr: *const CTopology, + qubit: u32, + out: *mut u32, + len: usize, +) -> usize { + if ptr.is_null() { + return 0; + } + let mut items: Vec = unsafe { (*ptr).inner.predecessors(PhysicalQubit::new(qubit)) } + .map(|q| q.id()) + .collect(); + items.sort_unstable(); + write_u32_list(&items, out, len) +} + +/// Returns the number of qubits reachable through outgoing couplings from +/// `qubit`, or 0 for NULL or an unknown qubit. +#[unsafe(no_mangle)] +pub extern "C" fn topology_successors_len(ptr: *const CTopology, qubit: u32) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.successors(PhysicalQubit::new(qubit)).count() } +} + +/// Copies the successor IDs of `qubit` (qubits reachable through outgoing +/// couplings, ascending) into `out` (two-step pattern; pair with +/// `topology_successors_len`). Returns the total count. +#[unsafe(no_mangle)] +pub extern "C" fn topology_successors( + ptr: *const CTopology, + qubit: u32, + out: *mut u32, + len: usize, +) -> usize { + if ptr.is_null() { + return 0; + } + let mut items: Vec = unsafe { (*ptr).inner.successors(PhysicalQubit::new(qubit)) } + .map(|q| q.id()) + .collect(); + items.sort_unstable(); + write_u32_list(&items, out, len) +} + +/// Returns the number of incoming couplings to `qubit` (0 for NULL or an +/// unknown qubit). +#[unsafe(no_mangle)] +pub extern "C" fn topology_in_degree(ptr: *const CTopology, qubit: u32) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.in_degree(&PhysicalQubit::new(qubit)) } +} + +/// Returns the number of outgoing couplings from `qubit` (0 for NULL or an +/// unknown qubit). +#[unsafe(no_mangle)] +pub extern "C" fn topology_out_degree(ptr: *const CTopology, qubit: u32) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.out_degree(&PhysicalQubit::new(qubit)) } +} + +/// Returns 1 when the directed coupling `control -> target` exists, 0 +/// otherwise, or -1 on NULL. +#[unsafe(no_mangle)] +pub extern "C" fn topology_supports_directed_coupling( + ptr: *const CTopology, + control: u32, + target: u32, +) -> i32 { + if ptr.is_null() { + return -1; + } + unsafe { + (*ptr) + .inner + .supports_directed_coupling(PhysicalQubit::new(control), PhysicalQubit::new(target)) + as i32 + } +} + +/// Returns 1 when a coupling between the two qubits exists in either +/// direction, 0 otherwise, or -1 on NULL. +#[unsafe(no_mangle)] +pub extern "C" fn topology_supports_coupling_either_direction( + ptr: *const CTopology, + a: u32, + b: u32, +) -> i32 { + if ptr.is_null() { + return -1; + } + unsafe { + (*ptr) + .inner + .supports_coupling_either_direction(PhysicalQubit::new(a), PhysicalQubit::new(b)) + as i32 + } +} + +/// Returns the number of unique undirected coupling pairs (bidirectional +/// couplings collapse to one pair), or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn topology_undirected_edges_len(ptr: *const CTopology) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.undirected_edges().count() } +} + +/// Copies the unique undirected coupling pairs into `out` as consecutive +/// `(low, high)` u32 pairs sorted ascending (two-step pattern; pair with +/// `topology_undirected_edges_len`). `out` must have room for `2 * len` +/// entries. Returns the total number of pairs. +#[unsafe(no_mangle)] +pub extern "C" fn topology_undirected_edges( + ptr: *const CTopology, + out: *mut u32, + len: usize, +) -> usize { + if ptr.is_null() { + return 0; + } + let pairs: Vec<(u32, u32)> = unsafe { (*ptr).inner.undirected_edges() } + .map(|(a, b)| (a.id(), b.id())) + .collect(); + write_u32_pairs(&pairs, out, len) +} diff --git a/crates/binding-c/src/error.rs b/crates/binding-c/src/error.rs new file mode 100644 index 00000000..b89c74a0 --- /dev/null +++ b/crates/binding-c/src/error.rs @@ -0,0 +1,43 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! Error codes and shared utilities for C bindings. + +#![allow(dead_code)] + +use std::ffi::CString; +use std::os::raw::c_char; + +/// Error codes returned by C binding functions. +#[repr(i32)] +pub enum CqlibError { + Ok = 0, + NullPtr = -1, + QubitOutOfBounds = -2, + CircuitError = -3, + ParseError = -4, + IoError = -5, + CompilerError = -6, + SimulationError = -7, + InvalidParam = -8, +} + +/// Free a string returned by the C bindings (e.g. `qasm2_dumps`). +#[unsafe(no_mangle)] +pub extern "C" fn cqlib_string_free(ptr: *mut c_char) { + if ptr.is_null() { + return; + } + unsafe { + drop(CString::from_raw(ptr)); + } +} diff --git a/crates/binding-c/src/error_mitigation/mod.rs b/crates/binding-c/src/error_mitigation/mod.rs new file mode 100644 index 00000000..a37d7fd9 --- /dev/null +++ b/crates/binding-c/src/error_mitigation/mod.rs @@ -0,0 +1,113 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! C ABI for the error mitigation module. + +#![allow(clippy::not_unsafe_ptr_arg_deref)] + +pub mod unified; +pub mod virtual_distillation; +pub mod zne; + +use crate::circuit::CCircuit; +use crate::error::CqlibError; +use crate::qis::CHamiltonian; +use cqlib_core::circuit::Circuit; +use cqlib_core::error_mitigation::{ErrorMitigation, ErrorMitigationError}; +use cqlib_core::qis::Hamiltonian; + +/// Opaque handle around [`ErrorMitigation`] for C ABI. +pub struct CErrorMitigation { + pub inner: ErrorMitigation, +} + +/// Opaque handle around [`cqlib_core::error_mitigation::ZNEMitigation`] for C ABI +/// (defined in `zne`). +pub use zne::CZneMitigation; + +/// Opaque handle around [`cqlib_core::error_mitigation::VirtualDistillation`] for +/// C ABI. +pub struct CVirtualDistillation { + pub inner: cqlib_core::error_mitigation::VirtualDistillation, +} + +/// Owned list of folded circuits returned by `zne_mitigation_fold_circuit`. +pub struct CCircuitList { + pub items: Vec, +} + +/// C estimator callback invoked by `error_mitigation_run`. +/// +/// The callback receives the folded circuit, the observable, and the shot +/// count (0 means "not specified"). It must write the estimated expectation +/// value (and optionally its variance, NaN if unknown) through the out +/// pointers. All pointers are valid only for the duration of the call; +/// the Hamiltonian handle must not be freed by the callback. +pub type CEstimatorFn = extern "C" fn( + circuit: *const CCircuit, + hamiltonian: *const CHamiltonian, + shots: usize, + expectation: *mut f64, + variance: *mut f64, +); + +/// Wraps a C callback into the Rust `Estimator` closure. +/// +/// The C side receives borrowed handles that are only valid during the +/// callback; the Hamiltonian pointer may be null. +pub(crate) fn wrap_estimator( + callback: CEstimatorFn, +) -> impl Fn(&Circuit, Option<&Hamiltonian>, Option) -> (f64, f64) { + move |circuit: &Circuit, hamiltonian: Option<&Hamiltonian>, shots: Option| { + let circuit_wrapper = CCircuit { + inner: circuit.clone(), + }; + let hamiltonian_wrapper = hamiltonian.map(|h| CHamiltonian { inner: h.clone() }); + let mut expectation = f64::NAN; + let mut variance = f64::NAN; + callback( + &circuit_wrapper, + hamiltonian_wrapper + .as_ref() + .map(|w| w as *const CHamiltonian) + .unwrap_or(std::ptr::null()), + shots.unwrap_or(0), + &mut expectation, + &mut variance, + ); + (expectation, variance) + } +} + +/// Maps an `ErrorMitigationError` to a C error code. +/// +/// Invalid parameter shapes (copies, fold level, polynomial degree, result +/// length, sign) map to `InvalidParam` (-8); circuit-level failures map to +/// `CircuitError` (-3); everything else follows the unified-facade precedent +/// and maps to `SimulationError` (-7). +pub(crate) fn em_error_code(error: &ErrorMitigationError) -> i32 { + match error { + ErrorMitigationError::Circuit(_) => CqlibError::CircuitError as i32, + ErrorMitigationError::InvalidCopies(_) + | ErrorMitigationError::InvalidFoldLevel(_) + | ErrorMitigationError::InvalidPolynomialDegree { .. } + | ErrorMitigationError::HamiltonianQubitCountMismatch { .. } + | ErrorMitigationError::EmptyNoisyResults + | ErrorMitigationError::NoisyResultsLengthMismatch { .. } + | ErrorMitigationError::NonPositiveNoisyResults => CqlibError::InvalidParam as i32, + _ => CqlibError::SimulationError as i32, + } +} + +pub use unified::*; +pub use virtual_distillation::*; +pub use zne::*; diff --git a/crates/binding-c/src/error_mitigation/unified.rs b/crates/binding-c/src/error_mitigation/unified.rs new file mode 100644 index 00000000..49dd5296 --- /dev/null +++ b/crates/binding-c/src/error_mitigation/unified.rs @@ -0,0 +1,180 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! C ABI for the unified `ErrorMitigation` facade. + +use crate::circuit::CCircuit; +use crate::error::CqlibError; +use crate::error_mitigation::{CErrorMitigation, CEstimatorFn}; +use crate::qis::CHamiltonian; +use cqlib_core::error_mitigation::{ + ErrorMitigation, MitigationMethod, ProcessArgs, RunArgs, VirtualDistillationConfig, ZneConfig, +}; + +/// Mitigation method tags for `error_mitigation_new`. +/// +/// | Value | Method | Extra parameter | +/// |-------|---------------------|----------------------------| +/// | 0 | ZNE | `fold_levels` array | +/// | 1 | VirtualDistillation | `copies` | +pub const MITIGATION_ZNE: u8 = 0; +pub const MITIGATION_VIRTUAL_DISTILLATION: u8 = 1; + +/// Extrapolation method tags for `error_mitigation_get_mitigated`. +/// +/// | Value | Method | +/// |-------|---------------------| +/// | 0 | Zne + Polynomial | +/// | 1 | Zne + Exponential | +/// | 2 | VirtualDistillation | +pub const PROCESS_ZNE_POLYNOMIAL: u8 = 0; +pub const PROCESS_ZNE_EXPONENTIAL: u8 = 1; +pub const PROCESS_VIRTUAL_DISTILLATION: u8 = 2; + +/// Creates a mitigation pipeline over `circuit`. +/// +/// - `method == MITIGATION_ZNE`: reads `num_levels` fold levels from +/// `fold_levels` (e.g. `[0, 1, 2]`). +/// - `method == MITIGATION_VIRTUAL_DISTILLATION`: uses `copies`. +/// +/// Returns NULL on error or when the parameters do not match the method. +#[unsafe(no_mangle)] +pub extern "C" fn error_mitigation_new( + circuit: *const CCircuit, + method: u8, + fold_levels: *const i32, + num_levels: usize, + copies: usize, +) -> *mut CErrorMitigation { + if circuit.is_null() { + return std::ptr::null_mut(); + } + let circuit = unsafe { &(*circuit).inner }.clone(); + let mitigation_method = match method { + MITIGATION_ZNE => { + if num_levels == 0 || fold_levels.is_null() { + return std::ptr::null_mut(); + } + let levels = unsafe { std::slice::from_raw_parts(fold_levels, num_levels) }.to_vec(); + MitigationMethod::Zne(ZneConfig { + fold_levels: levels, + }) + } + MITIGATION_VIRTUAL_DISTILLATION => { + if copies == 0 { + return std::ptr::null_mut(); + } + MitigationMethod::VirtualDistillation(VirtualDistillationConfig { copies }) + } + _ => return std::ptr::null_mut(), + }; + match ErrorMitigation::new(circuit, mitigation_method) { + Ok(inner) => Box::into_raw(Box::new(CErrorMitigation { inner })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Frees a mitigation pipeline. NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn error_mitigation_free(ptr: *mut CErrorMitigation) { + if !ptr.is_null() { + unsafe { + let _ = Box::from_raw(ptr); + } + } +} + +/// Executes the mitigation sampling stage. +/// +/// `method` must match the method configured in `error_mitigation_new` +/// (`MITIGATION_ZNE` or `MITIGATION_VIRTUAL_DISTILLATION`). +/// +/// - ZNE method: uses `shots_a` as the shot count (0 = unspecified); +/// `shots_b` is ignored. +/// - VirtualDistillation method: uses `shots_a` for the numerator circuit +/// and `shots_b` for the denominator circuit (both must be > 0). +/// +/// `estimator` must not be NULL; it is invoked once per required circuit +/// execution. Returns 0 on success or a negative error code. +#[unsafe(no_mangle)] +pub extern "C" fn error_mitigation_run( + ptr: *mut CErrorMitigation, + method: u8, + hamiltonian: *const CHamiltonian, + shots_a: usize, + shots_b: usize, + estimator: CEstimatorFn, +) -> i32 { + if ptr.is_null() || hamiltonian.is_null() { + return CqlibError::NullPtr as i32; + } + let run_args = match method { + MITIGATION_ZNE => RunArgs::Zne { + gate_set: None, + shots: if shots_a > 0 { Some(shots_a) } else { None }, + }, + MITIGATION_VIRTUAL_DISTILLATION => RunArgs::VirtualDistillation { + shots_numerator: shots_a, + shots_denominator: shots_b, + }, + _ => return CqlibError::InvalidParam as i32, + }; + let wrapper = unsafe { &mut *ptr }; + let hamiltonian = unsafe { &(*hamiltonian).inner }; + let estimator = crate::error_mitigation::wrap_estimator(estimator); + match wrapper.inner.run(hamiltonian, run_args, &estimator) { + Ok(()) => 0, + Err(_) => CqlibError::SimulationError as i32, + } +} + +/// Produces the final mitigated estimate after `error_mitigation_run`. +/// +/// `process_method` is one of the `PROCESS_*` tags. `degree` selects the +/// polynomial degree for ZNE (0 = automatic). Writes the mitigated +/// expectation to `out_expectation` and the variance to `out_variance` +/// (NaN when unavailable). Returns 0 on success or a negative error code. +#[unsafe(no_mangle)] +pub extern "C" fn error_mitigation_get_mitigated( + ptr: *mut CErrorMitigation, + process_method: u8, + degree: usize, + out_expectation: *mut f64, + out_variance: *mut f64, +) -> i32 { + if ptr.is_null() || out_expectation.is_null() || out_variance.is_null() { + return CqlibError::NullPtr as i32; + } + let wrapper = unsafe { &mut *ptr }; + let process_args = match process_method { + PROCESS_ZNE_POLYNOMIAL => ProcessArgs::Zne { + method: cqlib_core::error_mitigation::ExtrapolateMethod::Polynomial, + degree: if degree > 0 { Some(degree) } else { None }, + }, + PROCESS_ZNE_EXPONENTIAL => ProcessArgs::Zne { + method: cqlib_core::error_mitigation::ExtrapolateMethod::Exponential, + degree: if degree > 0 { Some(degree) } else { None }, + }, + PROCESS_VIRTUAL_DISTILLATION => ProcessArgs::VirtualDistillation, + _ => return CqlibError::InvalidParam as i32, + }; + match wrapper.inner.get_mitigated(process_args) { + Ok(result) => { + unsafe { + *out_expectation = result.expectation; + *out_variance = result.variance.unwrap_or(f64::NAN); + } + 0 + } + Err(_) => CqlibError::SimulationError as i32, + } +} diff --git a/crates/binding-c/src/error_mitigation/virtual_distillation.rs b/crates/binding-c/src/error_mitigation/virtual_distillation.rs new file mode 100644 index 00000000..78b42084 --- /dev/null +++ b/crates/binding-c/src/error_mitigation/virtual_distillation.rs @@ -0,0 +1,212 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! C ABI for `VirtualDistillation`. + +use crate::circuit::CCircuit; +use crate::error::CqlibError; +use crate::error_mitigation::{CEstimatorFn, CVirtualDistillation, em_error_code}; +use crate::qis::CHamiltonian; +use cqlib_core::error_mitigation::VirtualDistillation; + +/// Creates a virtual distillation helper over `circuit` with `copies` +/// replicated registers. Returns NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn virtual_distillation_new( + circuit: *const CCircuit, + copies: usize, +) -> *mut CVirtualDistillation { + if circuit.is_null() || copies == 0 { + return std::ptr::null_mut(); + } + let circuit = unsafe { &(*circuit).inner }.clone(); + match VirtualDistillation::new(circuit, copies) { + Ok(inner) => Box::into_raw(Box::new(CVirtualDistillation { inner })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Frees a virtual distillation helper. NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn virtual_distillation_free(ptr: *mut CVirtualDistillation) { + if !ptr.is_null() { + unsafe { + let _ = Box::from_raw(ptr); + } + } +} + +/// Builds the copy-swap circuit used by the virtual distillation protocol. +/// +/// Returns an owned `CCircuit*` (free with `circuit_free`), or NULL +/// on error. +#[unsafe(no_mangle)] +pub extern "C" fn virtual_distillation_build_circuit( + ptr: *const CVirtualDistillation, +) -> *mut CCircuit { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let vd = unsafe { &(*ptr).inner }; + match vd.build_copy_swap_circuit() { + Ok(circuit) => Box::into_raw(Box::new(CCircuit { inner: circuit })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Builds the copy-swap circuit from the base circuit (the C mirror of +/// `VirtualDistillation::build_copy_swap_circuit`). +/// +/// The circuit contains one preparation per configured copy plus pairwise +/// SWAPs against the first copy. Returns an owned `CCircuit*` (free with +/// `circuit_free`), or NULL on NULL input or a build failure. +#[unsafe(no_mangle)] +pub extern "C" fn virtual_distillation_build_copy_swap_circuit( + ptr: *const CVirtualDistillation, +) -> *mut CCircuit { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let vd = unsafe { &(*ptr).inner }; + match vd.build_copy_swap_circuit() { + Ok(circuit) => Box::into_raw(Box::new(CCircuit { inner: circuit })), + Err(_) => std::ptr::null_mut(), + } +} + +// ===== Fine-grained VirtualDistillation API (checklist section 5) ===== + +/// Returns the configured number of copies, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn virtual_distillation_copies(ptr: *const CVirtualDistillation) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.copies() } +} + +/// Updates the configured number of copies (at least 2 required). +/// +/// Returns 0 on success, -1 for NULL, or -8 for invalid copies. +#[unsafe(no_mangle)] +pub extern "C" fn virtual_distillation_set_copies( + ptr: *mut CVirtualDistillation, + copies: usize, +) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + let vd = unsafe { &mut *ptr }; + match vd.inner.set_copies(copies) { + Ok(()) => CqlibError::Ok as i32, + Err(err) => em_error_code(&err), + } +} + +/// Runs the numerator circuit through `estimator` and writes the estimated +/// mean and variance to the out pointers. +/// +/// The estimator receives the copy-swap circuit, a Hamiltonian expanded to +/// the full copy-swap width, and `shots`. Returns 0 on success or a negative +/// error code. +#[unsafe(no_mangle)] +pub extern "C" fn virtual_distillation_run_numerator_circuit( + ptr: *const CVirtualDistillation, + hamiltonian: *const CHamiltonian, + shots: usize, + estimator: CEstimatorFn, + out_mean: *mut f64, + out_variance: *mut f64, +) -> i32 { + if ptr.is_null() || hamiltonian.is_null() || out_mean.is_null() || out_variance.is_null() { + return CqlibError::NullPtr as i32; + } + let vd = unsafe { &(*ptr).inner }; + let hamiltonian = unsafe { &(*hamiltonian).inner }; + let estimator = crate::error_mitigation::wrap_estimator(estimator); + match vd.run_numerator_circuit(hamiltonian, shots, &estimator) { + Ok((mean, variance)) => { + unsafe { + *out_mean = mean; + *out_variance = variance; + } + CqlibError::Ok as i32 + } + Err(err) => em_error_code(&err), + } +} + +/// Runs the denominator circuit through `estimator` and writes the estimated +/// mean and variance to the out pointers. +/// +/// The estimator receives the copy-swap circuit, no Hamiltonian (NULL), and +/// `shots`. Returns 0 on success or a negative error code. +#[unsafe(no_mangle)] +pub extern "C" fn virtual_distillation_run_denominator_circuit( + ptr: *const CVirtualDistillation, + shots: usize, + estimator: CEstimatorFn, + out_mean: *mut f64, + out_variance: *mut f64, +) -> i32 { + if ptr.is_null() || out_mean.is_null() || out_variance.is_null() { + return CqlibError::NullPtr as i32; + } + let vd = unsafe { &(*ptr).inner }; + let estimator = crate::error_mitigation::wrap_estimator(estimator); + match vd.run_denominator_circuit(shots, &estimator) { + Ok((mean, variance)) => { + unsafe { + *out_mean = mean; + *out_variance = variance; + } + CqlibError::Ok as i32 + } + Err(_) => CqlibError::CircuitError as i32, + } +} + +/// Runs the full virtual distillation protocol and writes the mitigated mean +/// and variance to the out pointers (`mu_vd = mu_numerator / mu_denominator`). +/// +/// `shots_numerator` and `shots_denominator` are forwarded to the estimator; +/// sampling randomness is owned by the estimator (the core API takes no +/// seed). Returns 0 on success or a negative error code (-8 when the +/// Hamiltonian width does not match the circuit, -7 when the denominator +/// mean is zero). +#[unsafe(no_mangle)] +pub extern "C" fn virtual_distillation_run_vd( + ptr: *const CVirtualDistillation, + hamiltonian: *const CHamiltonian, + shots_numerator: usize, + shots_denominator: usize, + estimator: CEstimatorFn, + out_mean: *mut f64, + out_variance: *mut f64, +) -> i32 { + if ptr.is_null() || hamiltonian.is_null() || out_mean.is_null() || out_variance.is_null() { + return CqlibError::NullPtr as i32; + } + let vd = unsafe { &(*ptr).inner }; + let hamiltonian = unsafe { &(*hamiltonian).inner }; + let estimator = crate::error_mitigation::wrap_estimator(estimator); + match vd.run_vd(hamiltonian, shots_numerator, shots_denominator, &estimator) { + Ok((mean, variance)) => { + unsafe { + *out_mean = mean; + *out_variance = variance; + } + CqlibError::Ok as i32 + } + Err(err) => em_error_code(&err), + } +} diff --git a/crates/binding-c/src/error_mitigation/zne.rs b/crates/binding-c/src/error_mitigation/zne.rs new file mode 100644 index 00000000..99d5269c --- /dev/null +++ b/crates/binding-c/src/error_mitigation/zne.rs @@ -0,0 +1,487 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! C ABI for `ZNEMitigation` (zero-noise extrapolation helper). + +use crate::circuit::CCircuit; +use crate::device::standard_gate_from_name; +use crate::error::CqlibError; +use crate::error_mitigation::{CCircuitList, CEstimatorFn, em_error_code}; +use crate::qis::CHamiltonian; +use cqlib_core::circuit::gate::Instruction; +use cqlib_core::error_mitigation::{ExtrapolateMethod, ZNEMitigation}; +use std::ffi::CStr; +use std::os::raw::c_char; + +/// Opaque handle around [`ZNEMitigation`] for C ABI. +pub struct CZneMitigation { + pub inner: ZNEMitigation, +} + +/// Creates a ZNE helper for `circuit` with the given fold levels +/// (noise factors are derived as `2 * level + 1`). Returns NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn zne_mitigation_new( + circuit: *const CCircuit, + fold_levels: *const i32, + num_levels: usize, +) -> *mut CZneMitigation { + if circuit.is_null() || num_levels == 0 || fold_levels.is_null() { + return std::ptr::null_mut(); + } + let circuit = unsafe { &(*circuit).inner }.clone(); + let levels = unsafe { std::slice::from_raw_parts(fold_levels, num_levels) }.to_vec(); + Box::into_raw(Box::new(CZneMitigation { + inner: ZNEMitigation::new(circuit, levels), + })) +} + +/// Frees a ZNE helper. NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn zne_mitigation_free(ptr: *mut CZneMitigation) { + if !ptr.is_null() { + unsafe { + let _ = Box::from_raw(ptr); + } + } +} + +/// Folds the circuit at every configured level. +/// +/// `gate_names` is an optional comma-separated list of gate names to fold +/// selectively (e.g. "H,CX"); NULL folds the whole circuit globally. +/// Returns an owned `CCircuitList*` (one folded circuit per level), +/// or NULL on error. Free with `circuit_list_free`. +#[unsafe(no_mangle)] +pub extern "C" fn zne_mitigation_fold_circuit( + ptr: *const CZneMitigation, + gate_names: *const c_char, +) -> *mut CCircuitList { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let gate_set = if gate_names.is_null() { + None + } else { + let names = match unsafe { CStr::from_ptr(gate_names) }.to_str() { + Ok(s) => s, + Err(_) => return std::ptr::null_mut(), + }; + let mut gates = Vec::new(); + for part in names.split(',') { + let part = part.trim(); + if part.is_empty() { + continue; + } + match standard_gate_from_name(part) { + Some(gate) => gates.push(Instruction::Standard(gate)), + None => return std::ptr::null_mut(), + } + } + Some(gates) + }; + let zne = unsafe { &(*ptr).inner }; + match zne.fold_circuits(gate_set.as_deref()) { + Ok(circuits) => Box::into_raw(Box::new(CCircuitList { items: circuits })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Returns the number of circuits in the list, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_list_len(ptr: *const CCircuitList) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).items.len() } +} + +/// Returns the circuit at `index` as an owned `CCircuit*`. +/// Caller must free with `circuit_free`. Returns NULL on out-of-bounds. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_list_get(ptr: *const CCircuitList, index: usize) -> *mut CCircuit { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let list = unsafe { &*ptr }; + match list.items.get(index) { + Some(circuit) => Box::into_raw(Box::new(CCircuit { + inner: circuit.clone(), + })), + None => std::ptr::null_mut(), + } +} + +/// Frees a circuit list. NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_list_free(ptr: *mut CCircuitList) { + if !ptr.is_null() { + unsafe { + let _ = Box::from_raw(ptr); + } + } +} + +/// Returns the noise factor for fold level `index` (`2 * level + 1`), +/// or 0 on error. +#[unsafe(no_mangle)] +pub extern "C" fn zne_mitigation_noise_factor(ptr: *const CZneMitigation, index: usize) -> i32 { + if ptr.is_null() { + return 0; + } + let zne = unsafe { &(*ptr).inner }; + zne.noise_factors().get(index).copied().unwrap_or(0) +} + +// ===== Fine-grained ZNE API (checklist section 5) ===== + +/// Extrapolation method tags for `zne_extrapolate`. +/// +/// | Value | Method | +/// |-------|-------------| +/// | 0 | Polynomial | +/// | 1 | Exponential | +pub const ZNE_EXTRAPOLATE_POLYNOMIAL: u8 = 0; +pub const ZNE_EXTRAPOLATE_EXPONENTIAL: u8 = 1; + +/// Parses an optional comma-separated gate-name list into a selective-folding +/// gate set. `Ok(None)` means global folding; `Err(())` means an unknown gate +/// name or invalid UTF-8. +fn parse_gate_set(gate_names: *const c_char) -> Result>, ()> { + if gate_names.is_null() { + return Ok(None); + } + let names = unsafe { CStr::from_ptr(gate_names) } + .to_str() + .map_err(|_| ())?; + let mut gates = Vec::new(); + for part in names.split(',') { + let part = part.trim(); + if part.is_empty() { + continue; + } + match standard_gate_from_name(part) { + Some(gate) => gates.push(Instruction::Standard(gate)), + None => return Err(()), + } + } + Ok(Some(gates)) +} + +/// Returns the original (unfolded) circuit as an owned `CCircuit*`. +/// Caller must free with `circuit_free`. Returns NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn zne_circuit(ptr: *const CZneMitigation) -> *mut CCircuit { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let zne = unsafe { &(*ptr).inner }; + Box::into_raw(Box::new(CCircuit { + inner: zne.circuit().clone(), + })) +} + +/// Returns the number of configured fold levels, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn zne_fold_levels_len(ptr: *const CZneMitigation) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.fold_levels().len() } +} + +/// Copies the configured fold levels into `buffer` (two-step pattern; pair +/// with `zne_fold_levels_len`). `len` must equal `zne_fold_levels_len(ptr)`. +/// Returns 0 on success or a negative error code. +#[unsafe(no_mangle)] +pub extern "C" fn zne_fold_levels(ptr: *const CZneMitigation, buffer: *mut i32, len: usize) -> i32 { + if ptr.is_null() || buffer.is_null() { + return CqlibError::NullPtr as i32; + } + let levels = unsafe { &(*ptr).inner }.fold_levels(); + if len != levels.len() { + return CqlibError::InvalidParam as i32; + } + unsafe { + std::ptr::copy_nonoverlapping(levels.as_ptr(), buffer, len); + } + CqlibError::Ok as i32 +} + +/// Returns the number of noise factors, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn zne_noise_factors_len(ptr: *const CZneMitigation) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.noise_factors().len() } +} + +/// Copies the noise factors (`2 * level + 1` per fold level) into `buffer` +/// (two-step pattern; pair with `zne_noise_factors_len`). `len` must equal +/// `zne_noise_factors_len(ptr)`. Returns 0 on success or a negative error code. +#[unsafe(no_mangle)] +pub extern "C" fn zne_noise_factors( + ptr: *const CZneMitigation, + buffer: *mut i32, + len: usize, +) -> i32 { + if ptr.is_null() || buffer.is_null() { + return CqlibError::NullPtr as i32; + } + let factors = unsafe { &(*ptr).inner }.noise_factors(); + if len != factors.len() { + return CqlibError::InvalidParam as i32; + } + unsafe { + std::ptr::copy_nonoverlapping(factors.as_ptr(), buffer, len); + } + CqlibError::Ok as i32 +} + +/// Returns the number of folded circuits produced for the configured fold +/// levels (one per level), or 0 for NULL or an invalid `gate_names` string. +#[unsafe(no_mangle)] +pub extern "C" fn zne_fold_circuits_len( + ptr: *const CZneMitigation, + gate_names: *const c_char, +) -> usize { + if ptr.is_null() || parse_gate_set(gate_names).is_err() { + return 0; + } + unsafe { (*ptr).inner.fold_levels().len() } +} + +/// Folds the circuit at every configured level (two-step pattern; pair with +/// `zne_fold_circuits_len`). +/// +/// `gate_names` is an optional comma-separated list of gate names to fold +/// selectively (e.g. "H,CX"); NULL folds the whole circuit globally. `buffer` +/// receives `len` owned `CCircuit*` handles, one per fold level; the caller +/// must free every entry with `circuit_free`. Returns 0 on success or a +/// negative error code (-4 for unknown gate names, -8 for a length +/// mismatch, -3 for folding failures). +#[unsafe(no_mangle)] +pub extern "C" fn zne_fold_circuits( + ptr: *const CZneMitigation, + gate_names: *const c_char, + buffer: *mut *mut CCircuit, + len: usize, +) -> i32 { + if ptr.is_null() || buffer.is_null() { + return CqlibError::NullPtr as i32; + } + let gate_set = match parse_gate_set(gate_names) { + Ok(gate_set) => gate_set, + Err(()) => return CqlibError::ParseError as i32, + }; + let zne = unsafe { &(*ptr).inner }; + if len != zne.fold_levels().len() { + return CqlibError::InvalidParam as i32; + } + match zne.fold_circuits(gate_set.as_deref()) { + Ok(circuits) => { + for (slot, circuit) in unsafe { std::slice::from_raw_parts_mut(buffer, len) } + .iter_mut() + .zip(circuits) + { + *slot = Box::into_raw(Box::new(CCircuit { inner: circuit })); + } + CqlibError::Ok as i32 + } + Err(_) => CqlibError::CircuitError as i32, + } +} + +/// Extrapolates the zero-noise expectation from `noisy_results` (one value +/// per noise factor, in fold-level order). +/// +/// `method` is `ZNE_EXTRAPOLATE_POLYNOMIAL` (uses `degree`; it must be +/// smaller than the number of data points) or `ZNE_EXTRAPOLATE_EXPONENTIAL` +/// (requires all values to be positive). Writes the extrapolated value to +/// `out_value`. Returns 0 on success or a negative error code. +#[unsafe(no_mangle)] +pub extern "C" fn zne_extrapolate( + ptr: *const CZneMitigation, + noisy_results: *const f64, + len: usize, + method: u8, + degree: usize, + out_value: *mut f64, +) -> i32 { + if ptr.is_null() || noisy_results.is_null() || out_value.is_null() { + return CqlibError::NullPtr as i32; + } + let method = match method { + ZNE_EXTRAPOLATE_POLYNOMIAL => ExtrapolateMethod::Polynomial, + ZNE_EXTRAPOLATE_EXPONENTIAL => ExtrapolateMethod::Exponential, + _ => return CqlibError::InvalidParam as i32, + }; + let results = unsafe { std::slice::from_raw_parts(noisy_results, len) }; + let zne = unsafe { &(*ptr).inner }; + match zne.extrapolate(results, method, degree) { + Ok(value) => { + unsafe { *out_value = value }; + CqlibError::Ok as i32 + } + Err(err) => em_error_code(&err), + } +} + +/// Extrapolates the zero-noise expectation with a polynomial fit of the +/// given `degree` (must be smaller than the number of noise factors). +/// Writes the extrapolated value to `out_value`. Returns 0 on success or a +/// negative error code. +#[unsafe(no_mangle)] +pub extern "C" fn zne_poly_extrapolate( + ptr: *const CZneMitigation, + noisy_results: *const f64, + len: usize, + degree: usize, + out_value: *mut f64, +) -> i32 { + if ptr.is_null() || noisy_results.is_null() || out_value.is_null() { + return CqlibError::NullPtr as i32; + } + let results = unsafe { std::slice::from_raw_parts(noisy_results, len) }; + let zne = unsafe { &(*ptr).inner }; + match zne.poly_extrapolate(results, degree) { + Ok(value) => { + unsafe { *out_value = value }; + CqlibError::Ok as i32 + } + Err(err) => em_error_code(&err), + } +} + +/// Extrapolates the zero-noise expectation with an exponential-decay fit +/// `y(x) = A * exp(-x / tau)` (all values must be positive). Writes `A`, the +/// extrapolated value at `x = 0`, to `out_value`. Returns 0 on success or a +/// negative error code. +#[unsafe(no_mangle)] +pub extern "C" fn zne_exp_extrapolate( + ptr: *const CZneMitigation, + noisy_results: *const f64, + len: usize, + out_value: *mut f64, +) -> i32 { + if ptr.is_null() || noisy_results.is_null() || out_value.is_null() { + return CqlibError::NullPtr as i32; + } + let results = unsafe { std::slice::from_raw_parts(noisy_results, len) }; + let zne = unsafe { &(*ptr).inner }; + match zne.exp_extrapolate(results) { + Ok(value) => { + unsafe { *out_value = value }; + CqlibError::Ok as i32 + } + Err(err) => em_error_code(&err), + } +} + +/// Returns the number of expectation values produced by the error-mitigation +/// sequence (one per fold level), or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn zne_run_em_sequence_len(ptr: *const CZneMitigation) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.noise_factors().len() } +} + +/// Returns the number of expectation values produced by the error-mitigation +/// sequence with explicit shots (one per fold level), or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn zne_run_em_sequence_with_shots_len(ptr: *const CZneMitigation) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.noise_factors().len() } +} + +/// Runs the error-mitigation sequence: folds the circuit at every configured +/// level and estimates one expectation value per folded circuit through the +/// `estimator` callback (two-step pattern; pair with +/// `zne_run_em_sequence_len`). +/// +/// `gate_names` optionally selects gates for folding (NULL folds globally). +/// The estimator receives the folded circuit, the Hamiltonian, and shot +/// count 0 ("not specified"). `buffer` receives `len` f64 values. Returns 0 +/// on success or a negative error code. +#[unsafe(no_mangle)] +pub extern "C" fn zne_run_em_sequence( + ptr: *const CZneMitigation, + gate_names: *const c_char, + hamiltonian: *const CHamiltonian, + estimator: CEstimatorFn, + buffer: *mut f64, + len: usize, +) -> i32 { + zne_run_em_sequence_impl(ptr, gate_names, hamiltonian, None, estimator, buffer, len) +} + +/// Same as [`zne_run_em_sequence`], but forwards `shots` to the estimator +/// instead of "not specified". +#[unsafe(no_mangle)] +pub extern "C" fn zne_run_em_sequence_with_shots( + ptr: *const CZneMitigation, + gate_names: *const c_char, + hamiltonian: *const CHamiltonian, + shots: usize, + estimator: CEstimatorFn, + buffer: *mut f64, + len: usize, +) -> i32 { + zne_run_em_sequence_impl( + ptr, + gate_names, + hamiltonian, + Some(shots), + estimator, + buffer, + len, + ) +} + +/// Shared implementation for the `zne_run_em_sequence*` entry points. +fn zne_run_em_sequence_impl( + ptr: *const CZneMitigation, + gate_names: *const c_char, + hamiltonian: *const CHamiltonian, + shots: Option, + estimator: CEstimatorFn, + buffer: *mut f64, + len: usize, +) -> i32 { + if ptr.is_null() || hamiltonian.is_null() || buffer.is_null() { + return CqlibError::NullPtr as i32; + } + let gate_set = match parse_gate_set(gate_names) { + Ok(gate_set) => gate_set, + Err(()) => return CqlibError::ParseError as i32, + }; + let zne = unsafe { &(*ptr).inner }; + if len != zne.noise_factors().len() { + return CqlibError::InvalidParam as i32; + } + let hamiltonian = unsafe { &(*hamiltonian).inner }; + let estimator = crate::error_mitigation::wrap_estimator(estimator); + match zne.run_em_sequence_with_shots(gate_set.as_deref(), hamiltonian, shots, &estimator) { + Ok(sequence) => { + unsafe { + std::ptr::copy_nonoverlapping(sequence.as_ptr(), buffer, len); + } + CqlibError::Ok as i32 + } + Err(err) => em_error_code(&err), + } +} diff --git a/crates/binding-c/src/ir/mod.rs b/crates/binding-c/src/ir/mod.rs new file mode 100644 index 00000000..b7bfe5c9 --- /dev/null +++ b/crates/binding-c/src/ir/mod.rs @@ -0,0 +1,21 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! C ABI for IR module (QASM2/QASM3/QCIS load and dump). + +pub mod qasm2; +pub mod qasm3; +pub mod qcis; + +pub use qasm2::{qasm2_dump, qasm2_dumps, qasm2_load, qasm2_loads}; +pub use qasm3::{qasm3_dump, qasm3_dumps, qasm3_load, qasm3_loads}; +pub use qcis::{qcis_dump, qcis_dumps, qcis_load, qcis_loads}; diff --git a/crates/binding-c/src/ir/qasm2.rs b/crates/binding-c/src/ir/qasm2.rs new file mode 100644 index 00000000..0a2650c0 --- /dev/null +++ b/crates/binding-c/src/ir/qasm2.rs @@ -0,0 +1,92 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! C ABI for OpenQASM 2.0 load and dump. + +use crate::circuit::CCircuit; +use crate::error::CqlibError; +use cqlib_core::ir; +use std::ffi::{CStr, CString}; +use std::os::raw::c_char; + +/// Parse an OpenQASM 2.0 string into a circuit. +/// Returns a heap-allocated `CCircuit*`, or NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn qasm2_loads(source: *const c_char) -> *mut CCircuit { + if source.is_null() { + return std::ptr::null_mut(); + } + let source = unsafe { CStr::from_ptr(source) }; + let source = match source.to_str() { + Ok(s) => s, + Err(_) => return std::ptr::null_mut(), + }; + match ir::qasm2_loads(source) { + Ok(circuit) => Box::into_raw(Box::new(CCircuit { inner: circuit })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Load an OpenQASM 2.0 file into a circuit. +/// Returns a heap-allocated `CCircuit*`, or NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn qasm2_load(path: *const c_char) -> *mut CCircuit { + if path.is_null() { + return std::ptr::null_mut(); + } + let path = unsafe { CStr::from_ptr(path) }; + let path = match path.to_str() { + Ok(s) => s, + Err(_) => return std::ptr::null_mut(), + }; + match ir::qasm2_load(path) { + Ok(circuit) => Box::into_raw(Box::new(CCircuit { inner: circuit })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Dump a circuit to an OpenQASM 2.0 string. +/// Returns a heap-allocated C string (caller must free with `cqlib_string_free`), +/// or NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn qasm2_dumps(circuit: *const CCircuit) -> *mut c_char { + if circuit.is_null() { + return std::ptr::null_mut(); + } + let circuit = unsafe { &(*circuit).inner }; + match ir::qasm2_dumps(circuit) { + Ok(s) => match CString::new(s) { + Ok(cs) => cs.into_raw(), + Err(_) => std::ptr::null_mut(), + }, + Err(_) => std::ptr::null_mut(), + } +} + +/// Dump a circuit to an OpenQASM 2.0 file. +/// Returns 0 on success, or a negative error code on failure. +#[unsafe(no_mangle)] +pub extern "C" fn qasm2_dump(circuit: *const CCircuit, path: *const c_char) -> i32 { + if circuit.is_null() || path.is_null() { + return CqlibError::NullPtr as i32; + } + let circuit = unsafe { &(*circuit).inner }; + let path = unsafe { CStr::from_ptr(path) }; + let path = match path.to_str() { + Ok(s) => s, + Err(_) => return CqlibError::InvalidParam as i32, + }; + match ir::qasm2_dump(circuit, path) { + Ok(()) => CqlibError::Ok as i32, + Err(_) => CqlibError::IoError as i32, + } +} diff --git a/crates/binding-c/src/ir/qasm3.rs b/crates/binding-c/src/ir/qasm3.rs new file mode 100644 index 00000000..b20e824e --- /dev/null +++ b/crates/binding-c/src/ir/qasm3.rs @@ -0,0 +1,92 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! C ABI for OpenQASM 3.0 load and dump. + +use crate::circuit::CCircuit; +use crate::error::CqlibError; +use cqlib_core::ir; +use std::ffi::{CStr, CString}; +use std::os::raw::c_char; + +/// Parse an OpenQASM 3.0 string into a circuit. +/// Returns a heap-allocated `CCircuit*`, or NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn qasm3_loads(source: *const c_char) -> *mut CCircuit { + if source.is_null() { + return std::ptr::null_mut(); + } + let source = unsafe { CStr::from_ptr(source) }; + let source = match source.to_str() { + Ok(s) => s, + Err(_) => return std::ptr::null_mut(), + }; + match ir::qasm3_loads(source) { + Ok(circuit) => Box::into_raw(Box::new(CCircuit { inner: circuit })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Load an OpenQASM 3.0 file into a circuit. +/// Returns a heap-allocated `CCircuit*`, or NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn qasm3_load(path: *const c_char) -> *mut CCircuit { + if path.is_null() { + return std::ptr::null_mut(); + } + let path = unsafe { CStr::from_ptr(path) }; + let path = match path.to_str() { + Ok(s) => s, + Err(_) => return std::ptr::null_mut(), + }; + match ir::qasm3_load(path) { + Ok(circuit) => Box::into_raw(Box::new(CCircuit { inner: circuit })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Dump a circuit to an OpenQASM 3.0 string. +/// Returns a heap-allocated C string (caller must free with `cqlib_string_free`), +/// or NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn qasm3_dumps(circuit: *const CCircuit) -> *mut c_char { + if circuit.is_null() { + return std::ptr::null_mut(); + } + let circuit = unsafe { &(*circuit).inner }; + match ir::qasm3_dumps(circuit) { + Ok(s) => match CString::new(s) { + Ok(cs) => cs.into_raw(), + Err(_) => std::ptr::null_mut(), + }, + Err(_) => std::ptr::null_mut(), + } +} + +/// Dump a circuit to an OpenQASM 3.0 file. +/// Returns 0 on success, or a negative error code on failure. +#[unsafe(no_mangle)] +pub extern "C" fn qasm3_dump(circuit: *const CCircuit, path: *const c_char) -> i32 { + if circuit.is_null() || path.is_null() { + return CqlibError::NullPtr as i32; + } + let circuit = unsafe { &(*circuit).inner }; + let path = unsafe { CStr::from_ptr(path) }; + let path = match path.to_str() { + Ok(s) => s, + Err(_) => return CqlibError::InvalidParam as i32, + }; + match ir::qasm3_dump(circuit, path) { + Ok(()) => CqlibError::Ok as i32, + Err(_) => CqlibError::IoError as i32, + } +} diff --git a/crates/binding-c/src/ir/qcis.rs b/crates/binding-c/src/ir/qcis.rs new file mode 100644 index 00000000..d396addc --- /dev/null +++ b/crates/binding-c/src/ir/qcis.rs @@ -0,0 +1,92 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! C ABI for QCIS load and dump. + +use crate::circuit::CCircuit; +use crate::error::CqlibError; +use cqlib_core::ir; +use std::ffi::{CStr, CString}; +use std::os::raw::c_char; + +/// Parse a QCIS string into a circuit. +/// Returns a heap-allocated `CCircuit*`, or NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn qcis_loads(source: *const c_char) -> *mut CCircuit { + if source.is_null() { + return std::ptr::null_mut(); + } + let source = unsafe { CStr::from_ptr(source) }; + let source = match source.to_str() { + Ok(s) => s, + Err(_) => return std::ptr::null_mut(), + }; + match ir::qcis_loads(source) { + Ok(circuit) => Box::into_raw(Box::new(CCircuit { inner: circuit })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Load a QCIS file into a circuit. +/// Returns a heap-allocated `CCircuit*`, or NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn qcis_load(path: *const c_char) -> *mut CCircuit { + if path.is_null() { + return std::ptr::null_mut(); + } + let path = unsafe { CStr::from_ptr(path) }; + let path = match path.to_str() { + Ok(s) => s, + Err(_) => return std::ptr::null_mut(), + }; + match ir::qcis_load(path) { + Ok(circuit) => Box::into_raw(Box::new(CCircuit { inner: circuit })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Dump a circuit to a QCIS string. +/// Returns a heap-allocated C string (caller must free with `cqlib_string_free`), +/// or NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn qcis_dumps(circuit: *const CCircuit) -> *mut c_char { + if circuit.is_null() { + return std::ptr::null_mut(); + } + let circuit = unsafe { &(*circuit).inner }; + match ir::qcis_dumps(circuit) { + Ok(s) => match CString::new(s) { + Ok(cs) => cs.into_raw(), + Err(_) => std::ptr::null_mut(), + }, + Err(_) => std::ptr::null_mut(), + } +} + +/// Dump a circuit to a QCIS file. +/// Returns 0 on success, or a negative error code on failure. +#[unsafe(no_mangle)] +pub extern "C" fn qcis_dump(circuit: *const CCircuit, path: *const c_char) -> i32 { + if circuit.is_null() || path.is_null() { + return CqlibError::NullPtr as i32; + } + let circuit = unsafe { &(*circuit).inner }; + let path = unsafe { CStr::from_ptr(path) }; + let path = match path.to_str() { + Ok(s) => s, + Err(_) => return CqlibError::InvalidParam as i32, + }; + match ir::qcis_dump(circuit, path) { + Ok(()) => CqlibError::Ok as i32, + Err(_) => CqlibError::IoError as i32, + } +} diff --git a/crates/binding-c/src/lib.rs b/crates/binding-c/src/lib.rs index 7ca4bbeb..869d26fa 100644 --- a/crates/binding-c/src/lib.rs +++ b/crates/binding-c/src/lib.rs @@ -10,9 +10,17 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. -//! Minimal C binding for Cqlib quantum circuits. +//! C binding for Cqlib quantum computing library. -// Allow clippy warnings for FFI functions that dereference raw pointers #![allow(clippy::not_unsafe_ptr_arg_deref)] pub mod circuit; +pub mod compile; +pub mod device; +pub mod error; +pub mod error_mitigation; +pub mod ir; +pub mod qis; +pub mod visualization; + +pub use error::cqlib_string_free; diff --git a/crates/binding-c/src/qis/density_matrix.rs b/crates/binding-c/src/qis/density_matrix.rs new file mode 100644 index 00000000..e8119644 --- /dev/null +++ b/crates/binding-c/src/qis/density_matrix.rs @@ -0,0 +1,864 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! C ABI for `DensityMatrix` simulator. + +use crate::circuit::CCircuit; +use crate::error::CqlibError; +use crate::qis::{CDensityMatrix, COutcomeList, check_qubit, qis_err_code}; +use cqlib_core::qis::{DensityMatrix, Observable}; +use num_complex::Complex64; +use std::ffi::CString; +use std::os::raw::c_char; + +#[inline] +fn map_err(err: cqlib_core::qis::QisError) -> i32 { + qis_err_code(&err) +} + +fn apply_single( + ptr: *mut CDensityMatrix, + qubit: u32, + f: impl FnOnce(&mut DensityMatrix, usize) -> Result<(), cqlib_core::qis::QisError>, +) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + let wrapper = unsafe { &mut *ptr }; + let q = match check_qubit(wrapper.inner.num_qubits, qubit) { + Ok(q) => q, + Err(code) => return code, + }; + f(&mut wrapper.inner, q) + .map_err(map_err) + .map_or_else(|e| e, |_| 0) +} + +fn apply_two( + ptr: *mut CDensityMatrix, + q0: u32, + q1: u32, + f: impl FnOnce(&mut DensityMatrix, usize, usize) -> Result<(), cqlib_core::qis::QisError>, +) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + let wrapper = unsafe { &mut *ptr }; + let a = match check_qubit(wrapper.inner.num_qubits, q0) { + Ok(q) => q, + Err(code) => return code, + }; + let b = match check_qubit(wrapper.inner.num_qubits, q1) { + Ok(q) => q, + Err(code) => return code, + }; + f(&mut wrapper.inner, a, b) + .map_err(map_err) + .map_or_else(|e| e, |_| 0) +} + +fn apply_three( + ptr: *mut CDensityMatrix, + q0: u32, + q1: u32, + q2: u32, + f: impl FnOnce(&mut DensityMatrix, usize, usize, usize) -> Result<(), cqlib_core::qis::QisError>, +) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + let wrapper = unsafe { &mut *ptr }; + let a = match check_qubit(wrapper.inner.num_qubits, q0) { + Ok(q) => q, + Err(code) => return code, + }; + let b = match check_qubit(wrapper.inner.num_qubits, q1) { + Ok(q) => q, + Err(code) => return code, + }; + let c = match check_qubit(wrapper.inner.num_qubits, q2) { + Ok(q) => q, + Err(code) => return code, + }; + f(&mut wrapper.inner, a, b, c) + .map_err(map_err) + .map_or_else(|e| e, |_| 0) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_new(num_qubits: usize) -> *mut CDensityMatrix { + Box::into_raw(Box::new(CDensityMatrix { + inner: DensityMatrix::new(num_qubits), + })) +} + +/// Create a density matrix filled entirely with zeros. This is not a valid +/// physical state (trace = 0); it is useful as an accumulator during +/// operations like Kraus channel application. Returns NULL when the matrix +/// size overflows the addressable range. +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_zeros(num_qubits: usize) -> *mut CDensityMatrix { + let Some(bits) = num_qubits.checked_mul(2) else { + return std::ptr::null_mut(); + }; + if bits >= usize::BITS as usize { + return std::ptr::null_mut(); + } + Box::into_raw(Box::new(CDensityMatrix { + inner: DensityMatrix::zeros(num_qubits), + })) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_free(ptr: *mut CDensityMatrix) { + if !ptr.is_null() { + unsafe { + let _ = Box::from_raw(ptr); + } + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_from_circuit(circuit: *const CCircuit) -> *mut CDensityMatrix { + if circuit.is_null() { + return std::ptr::null_mut(); + } + let circuit = unsafe { &(*circuit).inner }; + match DensityMatrix::from_circuit(circuit) { + Ok(dm) => Box::into_raw(Box::new(CDensityMatrix { inner: dm })), + Err(_) => std::ptr::null_mut(), + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_apply_circuit( + ptr: *mut CDensityMatrix, + circuit: *const CCircuit, +) -> i32 { + if ptr.is_null() || circuit.is_null() { + return CqlibError::NullPtr as i32; + } + let wrapper = unsafe { &mut *ptr }; + let circuit = unsafe { &(*circuit).inner }; + wrapper + .inner + .apply_circuit(circuit) + .map_err(map_err) + .map_or_else(|e| e, |_| 0) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_apply_h(ptr: *mut CDensityMatrix, qubit: u32) -> i32 { + apply_single(ptr, qubit, DensityMatrix::apply_h) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_apply_x(ptr: *mut CDensityMatrix, qubit: u32) -> i32 { + apply_single(ptr, qubit, DensityMatrix::apply_x) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_apply_y(ptr: *mut CDensityMatrix, qubit: u32) -> i32 { + apply_single(ptr, qubit, DensityMatrix::apply_y) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_apply_z(ptr: *mut CDensityMatrix, qubit: u32) -> i32 { + apply_single(ptr, qubit, DensityMatrix::apply_z) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_apply_s(ptr: *mut CDensityMatrix, qubit: u32) -> i32 { + apply_single(ptr, qubit, DensityMatrix::apply_s) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_apply_sdg(ptr: *mut CDensityMatrix, qubit: u32) -> i32 { + apply_single(ptr, qubit, DensityMatrix::apply_sdg) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_apply_t(ptr: *mut CDensityMatrix, qubit: u32) -> i32 { + apply_single(ptr, qubit, DensityMatrix::apply_t) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_apply_tdg(ptr: *mut CDensityMatrix, qubit: u32) -> i32 { + apply_single(ptr, qubit, DensityMatrix::apply_tdg) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_apply_x2p(ptr: *mut CDensityMatrix, qubit: u32) -> i32 { + apply_single(ptr, qubit, DensityMatrix::apply_x2p) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_apply_x2m(ptr: *mut CDensityMatrix, qubit: u32) -> i32 { + apply_single(ptr, qubit, DensityMatrix::apply_x2m) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_apply_y2p(ptr: *mut CDensityMatrix, qubit: u32) -> i32 { + apply_single(ptr, qubit, DensityMatrix::apply_y2p) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_apply_y2m(ptr: *mut CDensityMatrix, qubit: u32) -> i32 { + apply_single(ptr, qubit, DensityMatrix::apply_y2m) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_apply_rx(ptr: *mut CDensityMatrix, qubit: u32, theta: f64) -> i32 { + if !theta.is_finite() { + return CqlibError::InvalidParam as i32; + } + apply_single(ptr, qubit, |dm, q| dm.apply_rx(q, theta)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_apply_ry(ptr: *mut CDensityMatrix, qubit: u32, theta: f64) -> i32 { + if !theta.is_finite() { + return CqlibError::InvalidParam as i32; + } + apply_single(ptr, qubit, |dm, q| dm.apply_ry(q, theta)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_apply_rz(ptr: *mut CDensityMatrix, qubit: u32, theta: f64) -> i32 { + if !theta.is_finite() { + return CqlibError::InvalidParam as i32; + } + apply_single(ptr, qubit, |dm, q| dm.apply_rz(q, theta)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_apply_phase( + ptr: *mut CDensityMatrix, + qubit: u32, + theta: f64, +) -> i32 { + if !theta.is_finite() { + return CqlibError::InvalidParam as i32; + } + apply_single(ptr, qubit, |dm, q| dm.apply_phase(q, theta)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_apply_u( + ptr: *mut CDensityMatrix, + qubit: u32, + theta: f64, + phi: f64, + lambda: f64, +) -> i32 { + if !theta.is_finite() || !phi.is_finite() || !lambda.is_finite() { + return CqlibError::InvalidParam as i32; + } + apply_single(ptr, qubit, |dm, q| dm.apply_u(q, theta, phi, lambda)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_apply_xy(ptr: *mut CDensityMatrix, qubit: u32, theta: f64) -> i32 { + if !theta.is_finite() { + return CqlibError::InvalidParam as i32; + } + apply_single(ptr, qubit, |dm, q| dm.apply_xy(q, theta)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_apply_xy2p( + ptr: *mut CDensityMatrix, + qubit: u32, + theta: f64, +) -> i32 { + if !theta.is_finite() { + return CqlibError::InvalidParam as i32; + } + apply_single(ptr, qubit, |dm, q| dm.apply_xy2p(q, theta)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_apply_xy2m( + ptr: *mut CDensityMatrix, + qubit: u32, + theta: f64, +) -> i32 { + if !theta.is_finite() { + return CqlibError::InvalidParam as i32; + } + apply_single(ptr, qubit, |dm, q| dm.apply_xy2m(q, theta)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_apply_rxy( + ptr: *mut CDensityMatrix, + qubit: u32, + theta: f64, + phi: f64, +) -> i32 { + if !theta.is_finite() || !phi.is_finite() { + return CqlibError::InvalidParam as i32; + } + apply_single(ptr, qubit, |dm, q| dm.apply_rxy(q, theta, phi)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_apply_gphase(ptr: *mut CDensityMatrix, phi: f64) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + if !phi.is_finite() { + return CqlibError::InvalidParam as i32; + } + let wrapper = unsafe { &mut *ptr }; + // Global phase has no effect on a density matrix; mirror the Rust API. + wrapper.inner.apply_gphase(phi); + 0 +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_apply_cx( + ptr: *mut CDensityMatrix, + control: u32, + target: u32, +) -> i32 { + apply_two(ptr, control, target, DensityMatrix::apply_cx) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_apply_cy( + ptr: *mut CDensityMatrix, + control: u32, + target: u32, +) -> i32 { + apply_two(ptr, control, target, DensityMatrix::apply_cy) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_apply_cz(ptr: *mut CDensityMatrix, q0: u32, q1: u32) -> i32 { + apply_two(ptr, q0, q1, DensityMatrix::apply_cz) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_apply_swap(ptr: *mut CDensityMatrix, q0: u32, q1: u32) -> i32 { + apply_two(ptr, q0, q1, DensityMatrix::apply_swap) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_apply_ccx( + ptr: *mut CDensityMatrix, + c0: u32, + c1: u32, + target: u32, +) -> i32 { + apply_three(ptr, c0, c1, target, DensityMatrix::apply_ccx) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_apply_rxx( + ptr: *mut CDensityMatrix, + q0: u32, + q1: u32, + theta: f64, +) -> i32 { + if !theta.is_finite() { + return CqlibError::InvalidParam as i32; + } + apply_two(ptr, q0, q1, |dm, a, b| dm.apply_rxx(a, b, theta)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_apply_ryy( + ptr: *mut CDensityMatrix, + q0: u32, + q1: u32, + theta: f64, +) -> i32 { + if !theta.is_finite() { + return CqlibError::InvalidParam as i32; + } + apply_two(ptr, q0, q1, |dm, a, b| dm.apply_ryy(a, b, theta)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_apply_rzz( + ptr: *mut CDensityMatrix, + q0: u32, + q1: u32, + theta: f64, +) -> i32 { + if !theta.is_finite() { + return CqlibError::InvalidParam as i32; + } + apply_two(ptr, q0, q1, |dm, a, b| dm.apply_rzz(a, b, theta)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_apply_rzx( + ptr: *mut CDensityMatrix, + q0: u32, + q1: u32, + theta: f64, +) -> i32 { + if !theta.is_finite() { + return CqlibError::InvalidParam as i32; + } + apply_two(ptr, q0, q1, |dm, a, b| dm.apply_rzx(a, b, theta)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_apply_crx( + ptr: *mut CDensityMatrix, + control: u32, + target: u32, + theta: f64, +) -> i32 { + if !theta.is_finite() { + return CqlibError::InvalidParam as i32; + } + apply_two(ptr, control, target, |dm, a, b| dm.apply_crx(a, b, theta)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_apply_cry( + ptr: *mut CDensityMatrix, + control: u32, + target: u32, + theta: f64, +) -> i32 { + if !theta.is_finite() { + return CqlibError::InvalidParam as i32; + } + apply_two(ptr, control, target, |dm, a, b| dm.apply_cry(a, b, theta)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_apply_crz( + ptr: *mut CDensityMatrix, + control: u32, + target: u32, + theta: f64, +) -> i32 { + if !theta.is_finite() { + return CqlibError::InvalidParam as i32; + } + apply_two(ptr, control, target, |dm, a, b| dm.apply_crz(a, b, theta)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_apply_fsim( + ptr: *mut CDensityMatrix, + q0: u32, + q1: u32, + theta: f64, + phi: f64, +) -> i32 { + if !theta.is_finite() || !phi.is_finite() { + return CqlibError::InvalidParam as i32; + } + apply_two(ptr, q0, q1, |dm, a, b| dm.apply_fsim(a, b, theta, phi)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_probabilities_len(ptr: *const CDensityMatrix) -> usize { + if ptr.is_null() { + return 0; + } + let wrapper = unsafe { &*ptr }; + 1usize << wrapper.inner.num_qubits +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_probabilities( + ptr: *const CDensityMatrix, + buffer: *mut f64, + len: usize, +) -> i32 { + if ptr.is_null() || buffer.is_null() { + return CqlibError::NullPtr as i32; + } + let wrapper = unsafe { &*ptr }; + let probs = wrapper.inner.probabilities(); + if probs.len() != len { + return CqlibError::InvalidParam as i32; + } + unsafe { + std::ptr::copy_nonoverlapping(probs.as_ptr(), buffer, len); + } + 0 +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_expectation( + ptr: *const CDensityMatrix, + observable: *const crate::qis::CHamiltonian, + out: *mut f64, +) -> i32 { + if ptr.is_null() || observable.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + let wrapper = unsafe { &*ptr }; + let h = unsafe { &(*observable).inner }; + match wrapper.inner.expectation(h as &dyn Observable) { + Ok(value) => { + unsafe { *out = value }; + 0 + } + Err(err) => qis_err_code(&err), + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_measure(ptr: *mut CDensityMatrix, qubit: u32) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + let wrapper = unsafe { &mut *ptr }; + let q = match check_qubit(wrapper.inner.num_qubits, qubit) { + Ok(q) => q, + Err(code) => return code, + }; + match wrapper.inner.measure(q) { + Ok(true) => 1, + Ok(false) => 0, + Err(err) => qis_err_code(&err), + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_measure_all(ptr: *mut CDensityMatrix) -> *mut c_char { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let wrapper = unsafe { &mut *ptr }; + let outcome = wrapper.inner.measure_all(); + let bitstring = outcome.to_bitstring(wrapper.inner.num_qubits); + match CString::new(bitstring) { + Ok(cs) => cs.into_raw(), + Err(_) => std::ptr::null_mut(), + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_sample_shots( + ptr: *const CDensityMatrix, + shots: usize, +) -> *mut COutcomeList { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let wrapper = unsafe { &*ptr }; + let n = wrapper.inner.num_qubits; + let outcomes = wrapper.inner.sample_shots(shots); + let items = outcomes.into_iter().map(|o| o.to_bitstring(n)).collect(); + Box::into_raw(Box::new(COutcomeList { items })) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_reset(ptr: *mut CDensityMatrix, qubit: u32) -> i32 { + apply_single(ptr, qubit, DensityMatrix::reset) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_num_qubits(ptr: *const CDensityMatrix) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.num_qubits } +} + +/// Apply a Kraus operator channel to the targeted qubits. +/// +/// `ops` is a flattened array of `num_ops * op_dim * op_dim` Complex64 values +/// (row-major). Each operator has dimension `op_dim = 2^num_target_qubits`. +/// `qubits` is the array of target qubit indices of length `num_target_qubits`. +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_apply_kraus( + ptr: *mut CDensityMatrix, + ops: *const Complex64, + num_ops: usize, + op_dim: usize, + qubits: *const u32, + num_target_qubits: usize, +) -> i32 { + if ptr.is_null() || ops.is_null() || qubits.is_null() { + return CqlibError::NullPtr as i32; + } + if num_ops == 0 || op_dim == 0 || num_target_qubits == 0 { + return CqlibError::InvalidParam as i32; + } + let wrapper = unsafe { &mut *ptr }; + let qs = unsafe { std::slice::from_raw_parts(qubits, num_target_qubits) }; + let targets: Result, i32> = qs + .iter() + .map(|&q| check_qubit(wrapper.inner.num_qubits, q)) + .collect(); + let targets = match targets { + Ok(t) => t, + Err(code) => return code, + }; + let expected_len = op_dim * op_dim; + let ops_slice = unsafe { std::slice::from_raw_parts(ops, num_ops * expected_len) }; + let mut operators: Vec> = Vec::with_capacity(num_ops); + for chunk in ops_slice.chunks_exact(expected_len) { + operators.push(chunk.to_vec()); + } + wrapper + .inner + .apply_kraus(&operators, &targets) + .map_err(map_err) + .map_or_else(|e| e, |_| 0) +} + +/// Compute the partial trace, keeping `keep_qubits`. Returns a new +/// `CDensityMatrix*` or NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_partial_trace( + ptr: *const CDensityMatrix, + keep_qubits: *const u32, + num_keep: usize, +) -> *mut CDensityMatrix { + if ptr.is_null() || (num_keep > 0 && keep_qubits.is_null()) { + return std::ptr::null_mut(); + } + let wrapper = unsafe { &*ptr }; + let qs = if num_keep == 0 { + Vec::new() + } else { + unsafe { std::slice::from_raw_parts(keep_qubits, num_keep) } + .iter() + .map(|&q| q as usize) + .collect() + }; + match wrapper.inner.partial_trace(&qs) { + Ok(dm) => Box::into_raw(Box::new(CDensityMatrix { inner: dm })), + Err(_) => std::ptr::null_mut(), + } +} + +// ===== Simulator supplement (checklist 2.5) ===== + +use std::ffi::CStr; + +use crate::device::standard_gate_from_name; + +/// Resolves a NUL-terminated standard-gate name (e.g. "H", "CX", "RZZ"). +/// Returns -1 for NULL input and -8 for an unknown name or invalid UTF-8. +fn parse_standard_gate(name: *const c_char) -> Result { + if name.is_null() { + return Err(CqlibError::NullPtr as i32); + } + let name = unsafe { CStr::from_ptr(name) }; + let name = match name.to_str() { + Ok(s) => s, + Err(_) => return Err(CqlibError::InvalidParam as i32), + }; + standard_gate_from_name(name).ok_or(CqlibError::InvalidParam as i32) +} + +/// Collects and bounds-checks a qubit-index array for this simulator. +fn resolve_qubits(num_qubits: usize, qubits: *const u32, len: usize) -> Result, i32> { + let slice = unsafe { std::slice::from_raw_parts(qubits, len) }; + slice.iter().map(|&q| check_qubit(num_qubits, q)).collect() +} + +/// Builds the gate-parameter slice, validating that every value is finite. +/// A NULL pointer is allowed when `num_params` is zero. +fn resolve_params(params: *const f64, num_params: usize) -> Result, i32> { + if num_params == 0 { + return Ok(Vec::new()); + } + if params.is_null() { + return Err(CqlibError::NullPtr as i32); + } + let slice = unsafe { std::slice::from_raw_parts(params, num_params) }; + if slice.iter().any(|v| !v.is_finite()) { + return Err(CqlibError::InvalidParam as i32); + } + Ok(slice.to_vec()) +} + +/// Return the number of matrix elements (4^N), or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_data_len(ptr: *const CDensityMatrix) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.data().len() } +} + +/// Copy the flattened row-major matrix elements into `buffer` (4^N Complex64 +/// values). Returns 0 on success, -1 on NULL pointers, -8 when `len` does not +/// equal `density_matrix_data_len`. +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_data( + ptr: *const CDensityMatrix, + buffer: *mut Complex64, + len: usize, +) -> i32 { + if ptr.is_null() || buffer.is_null() { + return CqlibError::NullPtr as i32; + } + let data = unsafe { (*ptr).inner.data() }; + if data.len() != len { + return CqlibError::InvalidParam as i32; + } + unsafe { + std::ptr::copy_nonoverlapping(data.as_ptr(), buffer, len); + } + 0 +} + +/// Create a density matrix from a normalized pure-state amplitude array of +/// 2^N Complex64 values, computing the outer product rho = |psi> *mut CDensityMatrix { + if initial_state.is_null() { + return std::ptr::null_mut(); + } + let amplitudes = unsafe { std::slice::from_raw_parts(initial_state, len) }.to_vec(); + match DensityMatrix::from_state(num_qubits, amplitudes) { + Ok(dm) => Box::into_raw(Box::new(CDensityMatrix { inner: dm })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Compute the trace of the density matrix. Writes the real and imaginary +/// parts to `out_re` / `out_im` and returns 0 on success. +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_trace( + ptr: *const CDensityMatrix, + out_re: *mut f64, + out_im: *mut f64, +) -> i32 { + if ptr.is_null() || out_re.is_null() || out_im.is_null() { + return CqlibError::NullPtr as i32; + } + let trace = unsafe { (*ptr).inner.trace() }; + unsafe { + *out_re = trace.re; + *out_im = trace.im; + } + 0 +} + +/// Check whether the density matrix is Hermitian within tolerance `tol`. +/// Writes the result to `out` and returns 0 on success. +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_is_hermitian( + ptr: *const CDensityMatrix, + tol: f64, + out: *mut bool, +) -> i32 { + if ptr.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + if !tol.is_finite() { + return CqlibError::InvalidParam as i32; + } + let hermitian = unsafe { (*ptr).inner.is_hermitian(tol) }; + unsafe { *out = hermitian }; + 0 +} + +/// Check whether the density matrix is positive semidefinite within +/// tolerance `tol` (all eigenvalues satisfy lambda >= -tol). Writes 1/0 to +/// `out` and returns 0 on success. Returns -1 for a NULL handle or output +/// pointer and -8 for a non-finite `tol`. +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_is_positive_semidefinite( + ptr: *const CDensityMatrix, + tol: f64, + out: *mut bool, +) -> i32 { + if ptr.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + if !tol.is_finite() { + return CqlibError::InvalidParam as i32; + } + let positive = unsafe { (*ptr).inner.is_positive_semidefinite_approx(tol) }; + unsafe { *out = positive }; + 0 +} + +/// Validate the physicality constraints of the density matrix within +/// tolerance `tol`: Hermiticity, positive semidefiniteness, and unit +/// trace. The matrix is only read. Returns 0 when the state is physical, +/// or a negative error code (`-1` NULL handle or non-finite `tol`; `-7` +/// not Hermitian or not positive semidefinite; `-8` trace not equal to 1). +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_validate_physical(ptr: *const CDensityMatrix, tol: f64) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + if !tol.is_finite() { + return CqlibError::InvalidParam as i32; + } + let wrapper = unsafe { &*ptr }; + match wrapper.inner.validate_physical(tol) { + Ok(()) => 0, + Err(err) => map_err(err), + } +} + +/// Create the maximally mixed state I / 2^N of `num_qubits` qubits. +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_maximally_mixed(num_qubits: usize) -> *mut CDensityMatrix { + Box::into_raw(Box::new(CDensityMatrix { + inner: DensityMatrix::maximally_mixed(num_qubits), + })) +} + +/// Apply a standard gate identified by name (e.g. "H", "CX", "RZZ", "FSIM"). +/// +/// `qubits` holds `num_target_qubits` target indices and `params` holds +/// `num_params` gate parameters (NULL is allowed when `num_params` is 0). +/// Returns 0 on success, -1 for NULL pointers, -2 for an out-of-bounds qubit, +/// -8 for an unknown gate name or invalid argument counts. +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_apply_standard_gate( + ptr: *mut CDensityMatrix, + gate_name: *const c_char, + qubits: *const u32, + num_target_qubits: usize, + params: *const f64, + num_params: usize, +) -> i32 { + if ptr.is_null() || qubits.is_null() { + return CqlibError::NullPtr as i32; + } + let gate = match parse_standard_gate(gate_name) { + Ok(gate) => gate, + Err(code) => return code, + }; + let wrapper = unsafe { &mut *ptr }; + let targets = match resolve_qubits(wrapper.inner.num_qubits, qubits, num_target_qubits) { + Ok(targets) => targets, + Err(code) => return code, + }; + let params = match resolve_params(params, num_params) { + Ok(params) => params, + Err(code) => return code, + }; + wrapper + .inner + .apply_standard_gate(gate, &targets, ¶ms) + .map_err(map_err) + .map_or_else(|e| e, |_| 0) +} diff --git a/crates/binding-c/src/qis/density_matrix_noise.rs b/crates/binding-c/src/qis/density_matrix_noise.rs new file mode 100644 index 00000000..b521a736 --- /dev/null +++ b/crates/binding-c/src/qis/density_matrix_noise.rs @@ -0,0 +1,807 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! C ABI for the `DensityMatrixNoise` simulator. +//! +//! Mirrors the `Statevector`/`DensityMatrix` binding patterns. A noise model +//! is attached at construction time via a `CNoiseModel*` handle (NULL for an +//! ideal simulation); gate noise is applied automatically after every gate. + +use crate::circuit::CCircuit; +use crate::device::{CNoiseModel, standard_gate_from_name}; +use crate::error::CqlibError; +use crate::qis::{CDensityMatrixNoise, CHamiltonian, COutcomeList, check_qubit, qis_err_code}; +use cqlib_core::qis::{DensityMatrixNoise, Observable}; +use ndarray::Array2; +use num_complex::Complex64; +use std::ffi::{CStr, CString}; +use std::os::raw::c_char; + +#[inline] +fn map_err(err: cqlib_core::qis::QisError) -> i32 { + qis_err_code(&err) +} + +#[inline] +fn num_qubits_of(wrapper: &CDensityMatrixNoise) -> usize { + wrapper.inner.state.num_qubits +} + +/// Clone the optional noise model referenced by a C handle. +fn clone_noise_model(noise_model: *const CNoiseModel) -> Option { + if noise_model.is_null() { + None + } else { + Some(unsafe { (*noise_model).inner.clone() }) + } +} + +/// Apply a single-qubit method that takes only a qubit index. +fn apply_single( + ptr: *mut CDensityMatrixNoise, + qubit: u32, + f: impl FnOnce(&mut DensityMatrixNoise, usize) -> Result<(), cqlib_core::qis::QisError>, +) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + let wrapper = unsafe { &mut *ptr }; + let q = match check_qubit(num_qubits_of(wrapper), qubit) { + Ok(q) => q, + Err(code) => return code, + }; + f(&mut wrapper.inner, q) + .map_err(map_err) + .map_or_else(|e| e, |_| 0) +} + +/// Apply a two-qubit method. +fn apply_two( + ptr: *mut CDensityMatrixNoise, + q0: u32, + q1: u32, + f: impl FnOnce(&mut DensityMatrixNoise, usize, usize) -> Result<(), cqlib_core::qis::QisError>, +) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + let wrapper = unsafe { &mut *ptr }; + let a = match check_qubit(num_qubits_of(wrapper), q0) { + Ok(q) => q, + Err(code) => return code, + }; + let b = match check_qubit(num_qubits_of(wrapper), q1) { + Ok(q) => q, + Err(code) => return code, + }; + f(&mut wrapper.inner, a, b) + .map_err(map_err) + .map_or_else(|e| e, |_| 0) +} + +/// Apply a three-qubit method. +fn apply_three( + ptr: *mut CDensityMatrixNoise, + q0: u32, + q1: u32, + q2: u32, + f: impl FnOnce( + &mut DensityMatrixNoise, + usize, + usize, + usize, + ) -> Result<(), cqlib_core::qis::QisError>, +) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + let wrapper = unsafe { &mut *ptr }; + let a = match check_qubit(num_qubits_of(wrapper), q0) { + Ok(q) => q, + Err(code) => return code, + }; + let b = match check_qubit(num_qubits_of(wrapper), q1) { + Ok(q) => q, + Err(code) => return code, + }; + let c = match check_qubit(num_qubits_of(wrapper), q2) { + Ok(q) => q, + Err(code) => return code, + }; + f(&mut wrapper.inner, a, b, c) + .map_err(map_err) + .map_or_else(|e| e, |_| 0) +} + +/// Create a new noisy simulator with `num_qubits` initialised to |0...0>. +/// Pass NULL `noise_model` for an ideal (noise-free) simulation. +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_noise_new( + num_qubits: usize, + noise_model: *const CNoiseModel, +) -> *mut CDensityMatrixNoise { + let noise = clone_noise_model(noise_model); + Box::into_raw(Box::new(CDensityMatrixNoise { + inner: DensityMatrixNoise::new(num_qubits, noise), + })) +} + +/// Free a noisy simulator. NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_noise_free(ptr: *mut CDensityMatrixNoise) { + if !ptr.is_null() { + unsafe { + let _ = Box::from_raw(ptr); + } + } +} + +/// Build a noisy simulator by simulating `circuit`. Returns NULL on error. +/// Pass NULL `noise_model` for an ideal (noise-free) simulation. +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_noise_from_circuit( + circuit: *const CCircuit, + noise_model: *const CNoiseModel, +) -> *mut CDensityMatrixNoise { + if circuit.is_null() { + return std::ptr::null_mut(); + } + let circuit = unsafe { &(*circuit).inner }; + let noise = clone_noise_model(noise_model); + match DensityMatrixNoise::from_circuit(circuit, noise) { + Ok(sim) => Box::into_raw(Box::new(CDensityMatrixNoise { inner: sim })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Return the number of qubits, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_noise_num_qubits(ptr: *const CDensityMatrixNoise) -> usize { + if ptr.is_null() { + return 0; + } + num_qubits_of(unsafe { &*ptr }) +} + +/// Apply `circuit` to the simulator in place, injecting gate noise after +/// each operation when a noise model is attached. Returns 0 on success. +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_noise_apply_circuit( + ptr: *mut CDensityMatrixNoise, + circuit: *const CCircuit, +) -> i32 { + if ptr.is_null() || circuit.is_null() { + return CqlibError::NullPtr as i32; + } + let wrapper = unsafe { &mut *ptr }; + let circuit = unsafe { &(*circuit).inner }; + wrapper + .inner + .apply_circuit(circuit) + .map_err(map_err) + .map_or_else(|e| e, |_| 0) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_noise_apply_h(ptr: *mut CDensityMatrixNoise, qubit: u32) -> i32 { + apply_single(ptr, qubit, DensityMatrixNoise::apply_h) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_noise_apply_x(ptr: *mut CDensityMatrixNoise, qubit: u32) -> i32 { + apply_single(ptr, qubit, DensityMatrixNoise::apply_x) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_noise_apply_y(ptr: *mut CDensityMatrixNoise, qubit: u32) -> i32 { + apply_single(ptr, qubit, DensityMatrixNoise::apply_y) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_noise_apply_z(ptr: *mut CDensityMatrixNoise, qubit: u32) -> i32 { + apply_single(ptr, qubit, DensityMatrixNoise::apply_z) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_noise_apply_s(ptr: *mut CDensityMatrixNoise, qubit: u32) -> i32 { + apply_single(ptr, qubit, DensityMatrixNoise::apply_s) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_noise_apply_sdg(ptr: *mut CDensityMatrixNoise, qubit: u32) -> i32 { + apply_single(ptr, qubit, DensityMatrixNoise::apply_sdg) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_noise_apply_t(ptr: *mut CDensityMatrixNoise, qubit: u32) -> i32 { + apply_single(ptr, qubit, DensityMatrixNoise::apply_t) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_noise_apply_tdg(ptr: *mut CDensityMatrixNoise, qubit: u32) -> i32 { + apply_single(ptr, qubit, DensityMatrixNoise::apply_tdg) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_noise_apply_x2p(ptr: *mut CDensityMatrixNoise, qubit: u32) -> i32 { + apply_single(ptr, qubit, DensityMatrixNoise::apply_x2p) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_noise_apply_x2m(ptr: *mut CDensityMatrixNoise, qubit: u32) -> i32 { + apply_single(ptr, qubit, DensityMatrixNoise::apply_x2m) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_noise_apply_y2p(ptr: *mut CDensityMatrixNoise, qubit: u32) -> i32 { + apply_single(ptr, qubit, DensityMatrixNoise::apply_y2p) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_noise_apply_y2m(ptr: *mut CDensityMatrixNoise, qubit: u32) -> i32 { + apply_single(ptr, qubit, DensityMatrixNoise::apply_y2m) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_noise_apply_rx( + ptr: *mut CDensityMatrixNoise, + qubit: u32, + theta: f64, +) -> i32 { + if !theta.is_finite() { + return CqlibError::InvalidParam as i32; + } + apply_single(ptr, qubit, |sim, q| sim.apply_rx(q, theta)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_noise_apply_ry( + ptr: *mut CDensityMatrixNoise, + qubit: u32, + theta: f64, +) -> i32 { + if !theta.is_finite() { + return CqlibError::InvalidParam as i32; + } + apply_single(ptr, qubit, |sim, q| sim.apply_ry(q, theta)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_noise_apply_rz( + ptr: *mut CDensityMatrixNoise, + qubit: u32, + theta: f64, +) -> i32 { + if !theta.is_finite() { + return CqlibError::InvalidParam as i32; + } + apply_single(ptr, qubit, |sim, q| sim.apply_rz(q, theta)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_noise_apply_phase( + ptr: *mut CDensityMatrixNoise, + qubit: u32, + theta: f64, +) -> i32 { + if !theta.is_finite() { + return CqlibError::InvalidParam as i32; + } + apply_single(ptr, qubit, |sim, q| sim.apply_phase(q, theta)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_noise_apply_u( + ptr: *mut CDensityMatrixNoise, + qubit: u32, + theta: f64, + phi: f64, + lambda: f64, +) -> i32 { + if !theta.is_finite() || !phi.is_finite() || !lambda.is_finite() { + return CqlibError::InvalidParam as i32; + } + apply_single(ptr, qubit, |sim, q| sim.apply_u(q, theta, phi, lambda)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_noise_apply_xy( + ptr: *mut CDensityMatrixNoise, + qubit: u32, + theta: f64, +) -> i32 { + if !theta.is_finite() { + return CqlibError::InvalidParam as i32; + } + apply_single(ptr, qubit, |sim, q| sim.apply_xy(q, theta)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_noise_apply_xy2p( + ptr: *mut CDensityMatrixNoise, + qubit: u32, + theta: f64, +) -> i32 { + if !theta.is_finite() { + return CqlibError::InvalidParam as i32; + } + apply_single(ptr, qubit, |sim, q| sim.apply_xy2p(q, theta)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_noise_apply_xy2m( + ptr: *mut CDensityMatrixNoise, + qubit: u32, + theta: f64, +) -> i32 { + if !theta.is_finite() { + return CqlibError::InvalidParam as i32; + } + apply_single(ptr, qubit, |sim, q| sim.apply_xy2m(q, theta)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_noise_apply_rxy( + ptr: *mut CDensityMatrixNoise, + qubit: u32, + theta: f64, + phi: f64, +) -> i32 { + if !theta.is_finite() || !phi.is_finite() { + return CqlibError::InvalidParam as i32; + } + apply_single(ptr, qubit, |sim, q| sim.apply_rxy(q, theta, phi)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_noise_apply_gphase( + ptr: *mut CDensityMatrixNoise, + phi: f64, +) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + if !phi.is_finite() { + return CqlibError::InvalidParam as i32; + } + let wrapper = unsafe { &mut *ptr }; + wrapper + .inner + .apply_gphase(phi) + .map_err(map_err) + .map_or_else(|e| e, |_| 0) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_noise_apply_cx( + ptr: *mut CDensityMatrixNoise, + control: u32, + target: u32, +) -> i32 { + apply_two(ptr, control, target, DensityMatrixNoise::apply_cx) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_noise_apply_cy( + ptr: *mut CDensityMatrixNoise, + control: u32, + target: u32, +) -> i32 { + apply_two(ptr, control, target, DensityMatrixNoise::apply_cy) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_noise_apply_cz( + ptr: *mut CDensityMatrixNoise, + q0: u32, + q1: u32, +) -> i32 { + apply_two(ptr, q0, q1, DensityMatrixNoise::apply_cz) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_noise_apply_swap( + ptr: *mut CDensityMatrixNoise, + q0: u32, + q1: u32, +) -> i32 { + apply_two(ptr, q0, q1, DensityMatrixNoise::apply_swap) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_noise_apply_ccx( + ptr: *mut CDensityMatrixNoise, + c0: u32, + c1: u32, + target: u32, +) -> i32 { + apply_three(ptr, c0, c1, target, DensityMatrixNoise::apply_ccx) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_noise_apply_rxx( + ptr: *mut CDensityMatrixNoise, + q0: u32, + q1: u32, + theta: f64, +) -> i32 { + if !theta.is_finite() { + return CqlibError::InvalidParam as i32; + } + apply_two(ptr, q0, q1, |sim, a, b| sim.apply_rxx(a, b, theta)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_noise_apply_ryy( + ptr: *mut CDensityMatrixNoise, + q0: u32, + q1: u32, + theta: f64, +) -> i32 { + if !theta.is_finite() { + return CqlibError::InvalidParam as i32; + } + apply_two(ptr, q0, q1, |sim, a, b| sim.apply_ryy(a, b, theta)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_noise_apply_rzz( + ptr: *mut CDensityMatrixNoise, + q0: u32, + q1: u32, + theta: f64, +) -> i32 { + if !theta.is_finite() { + return CqlibError::InvalidParam as i32; + } + apply_two(ptr, q0, q1, |sim, a, b| sim.apply_rzz(a, b, theta)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_noise_apply_rzx( + ptr: *mut CDensityMatrixNoise, + q0: u32, + q1: u32, + theta: f64, +) -> i32 { + if !theta.is_finite() { + return CqlibError::InvalidParam as i32; + } + apply_two(ptr, q0, q1, |sim, a, b| sim.apply_rzx(a, b, theta)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_noise_apply_crx( + ptr: *mut CDensityMatrixNoise, + control: u32, + target: u32, + theta: f64, +) -> i32 { + if !theta.is_finite() { + return CqlibError::InvalidParam as i32; + } + apply_two(ptr, control, target, |sim, a, b| sim.apply_crx(a, b, theta)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_noise_apply_cry( + ptr: *mut CDensityMatrixNoise, + control: u32, + target: u32, + theta: f64, +) -> i32 { + if !theta.is_finite() { + return CqlibError::InvalidParam as i32; + } + apply_two(ptr, control, target, |sim, a, b| sim.apply_cry(a, b, theta)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_noise_apply_crz( + ptr: *mut CDensityMatrixNoise, + control: u32, + target: u32, + theta: f64, +) -> i32 { + if !theta.is_finite() { + return CqlibError::InvalidParam as i32; + } + apply_two(ptr, control, target, |sim, a, b| sim.apply_crz(a, b, theta)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_noise_apply_fsim( + ptr: *mut CDensityMatrixNoise, + q0: u32, + q1: u32, + theta: f64, + phi: f64, +) -> i32 { + if !theta.is_finite() || !phi.is_finite() { + return CqlibError::InvalidParam as i32; + } + apply_two(ptr, q0, q1, |sim, a, b| sim.apply_fsim(a, b, theta, phi)) +} + +/// Apply a standard gate identified by `gate_name` (e.g. "H", "CX", "RZZ", +/// "GPhase", "FSIM"), followed by any matching gate noise from the attached +/// noise model. Unknown gate names return -8 (InvalidParam). +/// +/// `qubits` holds `qubit_len` target qubit indices; `params` holds +/// `param_len` gate parameters. Both counts must match the gate's arity. +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_noise_apply_standard_gate_noise( + ptr: *mut CDensityMatrixNoise, + gate_name: *const c_char, + qubits: *const u32, + qubit_len: usize, + params: *const f64, + param_len: usize, +) -> i32 { + if ptr.is_null() || gate_name.is_null() { + return CqlibError::NullPtr as i32; + } + if qubit_len > 0 && qubits.is_null() { + return CqlibError::NullPtr as i32; + } + if param_len > 0 && params.is_null() { + return CqlibError::NullPtr as i32; + } + let name = match unsafe { CStr::from_ptr(gate_name) }.to_str() { + Ok(s) => s, + Err(_) => return CqlibError::ParseError as i32, + }; + let gate = match standard_gate_from_name(name) { + Some(g) => g, + None => return CqlibError::InvalidParam as i32, + }; + let wrapper = unsafe { &mut *ptr }; + let qs: Result, i32> = if qubit_len == 0 { + Ok(Vec::new()) + } else { + unsafe { std::slice::from_raw_parts(qubits, qubit_len) } + .iter() + .map(|&q| check_qubit(num_qubits_of(wrapper), q)) + .collect() + }; + let qs = match qs { + Ok(qs) => qs, + Err(code) => return code, + }; + let ps: Vec = if param_len == 0 { + Vec::new() + } else { + unsafe { std::slice::from_raw_parts(params, param_len) }.to_vec() + }; + if ps.iter().any(|p| !p.is_finite()) { + return CqlibError::InvalidParam as i32; + } + wrapper + .inner + .apply_standard_gate_noise(gate, &qs, &ps) + .map_err(map_err) + .map_or_else(|e| e, |_| 0) +} + +/// Apply an arbitrary unitary gate given as a `matrix_dim x matrix_dim` +/// row-major array of `Complex64` values. `matrix_dim` must equal +/// `2^qubit_len`; no gate noise is applied for generic unitaries. +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_noise_apply_unitary_gate( + ptr: *mut CDensityMatrixNoise, + qubits: *const u32, + qubit_len: usize, + matrix: *const Complex64, + matrix_dim: usize, +) -> i32 { + if ptr.is_null() || matrix.is_null() || qubits.is_null() { + return CqlibError::NullPtr as i32; + } + if qubit_len == 0 || matrix_dim == 0 { + return CqlibError::InvalidParam as i32; + } + let wrapper = unsafe { &mut *ptr }; + let qs: Result, i32> = unsafe { std::slice::from_raw_parts(qubits, qubit_len) } + .iter() + .map(|&q| check_qubit(num_qubits_of(wrapper), q)) + .collect(); + let qs = match qs { + Ok(qs) => qs, + Err(code) => return code, + }; + let expected_len = matrix_dim * matrix_dim; + let flat = unsafe { std::slice::from_raw_parts(matrix, expected_len) }; + let mat = match Array2::from_shape_vec((matrix_dim, matrix_dim), flat.to_vec()) { + Ok(m) => m, + Err(_) => return CqlibError::InvalidParam as i32, + }; + wrapper + .inner + .apply_unitary_gate(&qs, &mat) + .map_err(map_err) + .map_or_else(|e| e, |_| 0) +} + +/// Return the number of basis-state probabilities (2^N), or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_noise_probabilities_len(ptr: *const CDensityMatrixNoise) -> usize { + if ptr.is_null() { + return 0; + } + 1usize << num_qubits_of(unsafe { &*ptr }) +} + +/// Copy the ideal probability distribution (diagonal of the density matrix, +/// without readout error) into `buffer`. Returns 0 on success. +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_noise_probabilities( + ptr: *const CDensityMatrixNoise, + buffer: *mut f64, + len: usize, +) -> i32 { + if ptr.is_null() || buffer.is_null() { + return CqlibError::NullPtr as i32; + } + let wrapper = unsafe { &*ptr }; + let probs = wrapper.inner.probabilities(); + if probs.len() != len { + return CqlibError::InvalidParam as i32; + } + unsafe { + std::ptr::copy_nonoverlapping(probs.as_ptr(), buffer, len); + } + 0 +} + +/// Return the number of basis-state probabilities (2^N), or 0 for NULL. +/// +/// The buffer size is independent of the measured-qubit selection passed to +/// `density_matrix_noise_probabilities_with_readout`. +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_noise_probabilities_with_readout_len( + ptr: *const CDensityMatrixNoise, +) -> usize { + if ptr.is_null() { + return 0; + } + 1usize << num_qubits_of(unsafe { &*ptr }) +} + +/// Copy the probability distribution including readout errors configured in +/// the noise model for `qubits` into `buffer`. Returns 0 on success. +/// +/// `qubits` may be NULL when `qubit_len` is 0, in which case no readout +/// error is applied and the result matches `density_matrix_noise_probabilities`. +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_noise_probabilities_with_readout( + ptr: *const CDensityMatrixNoise, + buffer: *mut f64, + len: usize, + qubits: *const u32, + qubit_len: usize, +) -> i32 { + if ptr.is_null() || buffer.is_null() { + return CqlibError::NullPtr as i32; + } + if qubit_len > 0 && qubits.is_null() { + return CqlibError::NullPtr as i32; + } + let wrapper = unsafe { &*ptr }; + let qs: Result, i32> = if qubit_len == 0 { + Ok(Vec::new()) + } else { + unsafe { std::slice::from_raw_parts(qubits, qubit_len) } + .iter() + .map(|&q| check_qubit(num_qubits_of(wrapper), q)) + .collect() + }; + let qs = match qs { + Ok(qs) => qs, + Err(code) => return code, + }; + match wrapper.inner.probabilities_with_readout(&qs) { + Ok(probs) => { + if probs.len() != len { + return CqlibError::InvalidParam as i32; + } + unsafe { + std::ptr::copy_nonoverlapping(probs.as_ptr(), buffer, len); + } + 0 + } + Err(err) => qis_err_code(&err), + } +} + +/// Compute the expectation value ` = Tr(H*rho)` of an observable. +/// Writes the value to `out` and returns 0 on success. Readout noise is +/// not included (it does not affect the quantum state). +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_noise_expectation( + ptr: *const CDensityMatrixNoise, + observable: *const CHamiltonian, + out: *mut f64, +) -> i32 { + if ptr.is_null() || observable.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + let wrapper = unsafe { &*ptr }; + let h = unsafe { &(*observable).inner }; + match wrapper.inner.expectation(h as &dyn Observable) { + Ok(value) => { + unsafe { *out = value }; + 0 + } + Err(err) => qis_err_code(&err), + } +} + +/// Measure `qubit` in the Z basis, collapsing the state. +/// Returns 0 for outcome |0>, 1 for outcome |1>, or a negative error code. +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_noise_measure(ptr: *mut CDensityMatrixNoise, qubit: u32) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + let wrapper = unsafe { &mut *ptr }; + let q = match check_qubit(num_qubits_of(wrapper), qubit) { + Ok(q) => q, + Err(code) => return code, + }; + match wrapper.inner.measure(q) { + Ok(true) => 1, + Ok(false) => 0, + Err(err) => qis_err_code(&err), + } +} + +/// Measure all qubits and return the outcome as a heap-allocated big-endian +/// bitstring (MSB = qubit N-1, LSB = qubit 0). Caller must free with +/// `cqlib_string_free`. Returns NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_noise_measure_all(ptr: *mut CDensityMatrixNoise) -> *mut c_char { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let wrapper = unsafe { &mut *ptr }; + let n = num_qubits_of(wrapper); + let outcome = wrapper.inner.measure_all(); + let bitstring = outcome.to_bitstring(n); + match CString::new(bitstring) { + Ok(cs) => cs.into_raw(), + Err(_) => std::ptr::null_mut(), + } +} + +/// Reset `qubit` to |0>. Returns 0 on success. +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_noise_reset(ptr: *mut CDensityMatrixNoise, qubit: u32) -> i32 { + apply_single(ptr, qubit, DensityMatrixNoise::reset) +} + +/// Sample `shots` independent measurement outcomes in parallel. +/// Returns a heap-allocated `COutcomeList*` (caller must free with +/// `outcome_list_free`), or NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_noise_sample_shots( + ptr: *const CDensityMatrixNoise, + shots: usize, +) -> *mut COutcomeList { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let wrapper = unsafe { &*ptr }; + let n = num_qubits_of(wrapper); + let outcomes = wrapper.inner.sample_shots(shots); + let items = outcomes.into_iter().map(|o| o.to_bitstring(n)).collect(); + Box::into_raw(Box::new(COutcomeList { items })) +} diff --git a/crates/binding-c/src/qis/entropy.rs b/crates/binding-c/src/qis/entropy.rs new file mode 100644 index 00000000..419cded8 --- /dev/null +++ b/crates/binding-c/src/qis/entropy.rs @@ -0,0 +1,195 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! C ABI for entropy and entanglement measures (`qis/entropy.rs`). + +use crate::error::CqlibError; +use crate::qis::{CDensityMatrix, CStatevector, qis_err_code}; +use cqlib_core::qis::entropy; + +/// Convert a C qubit-index array (`ptr`, `len`) into a `Vec`. +/// An empty array is valid; a NULL pointer with a positive length is not. +#[inline] +fn qubit_indices(ptr: *const u32, len: usize) -> Result, i32> { + if len == 0 { + return Ok(Vec::new()); + } + if ptr.is_null() { + return Err(CqlibError::NullPtr as i32); + } + Ok(unsafe { std::slice::from_raw_parts(ptr, len) } + .iter() + .map(|&q| q as usize) + .collect()) +} + +/// Compute the linear entropy `S_L = 1 - Tr(rho^2)` of a density matrix. +/// Writes the value to `out` and returns 0 on success. +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_linear_entropy(dm: *const CDensityMatrix, out: *mut f64) -> i32 { + if dm.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + let wrapper = unsafe { &*dm }; + match entropy::linear_entropy(&wrapper.inner) { + Ok(value) => { + unsafe { *out = value }; + 0 + } + Err(err) => qis_err_code(&err), + } +} + +/// Compute the Renyi entropy of order `alpha` (base-2 logarithm). +/// `alpha` must be positive; values close to 1 fall back to the Von +/// Neumann entropy. Writes the value to `out` and returns 0 on success. +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_renyi_entropy( + dm: *const CDensityMatrix, + alpha: f64, + out: *mut f64, +) -> i32 { + if dm.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + if !alpha.is_finite() { + return CqlibError::InvalidParam as i32; + } + let wrapper = unsafe { &*dm }; + match entropy::renyi_entropy(&wrapper.inner, alpha) { + Ok(value) => { + unsafe { *out = value }; + 0 + } + Err(err) => qis_err_code(&err), + } +} + +/// Compute the bipartite entanglement entropy of a pure state with +/// respect to subsystem A (`subsys_a` is an array of `len` qubit indices). +/// Writes the value (in bits) to `out` and returns 0 on success. +#[unsafe(no_mangle)] +pub extern "C" fn statevector_entanglement_entropy( + sv: *const CStatevector, + subsys_a: *const u32, + len: usize, + out: *mut f64, +) -> i32 { + if sv.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + let indices = match qubit_indices(subsys_a, len) { + Ok(indices) => indices, + Err(code) => return code, + }; + let wrapper = unsafe { &*sv }; + match entropy::entanglement_entropy_pure(&wrapper.inner, &indices) { + Ok(value) => { + unsafe { *out = value }; + 0 + } + Err(err) => qis_err_code(&err), + } +} + +/// Compute the bipartite entanglement entropy of a pure state with +/// respect to subsystem A (`subsys_a` is an array of `len` qubit indices). +/// Writes the value (in bits) to `out` and returns 0 on success. +#[unsafe(no_mangle)] +pub extern "C" fn statevector_entanglement_entropy_pure( + sv: *const CStatevector, + subsys_a: *const u32, + len: usize, + out: *mut f64, +) -> i32 { + if sv.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + let indices = match qubit_indices(subsys_a, len) { + Ok(indices) => indices, + Err(code) => return code, + }; + let wrapper = unsafe { &*sv }; + match entropy::entanglement_entropy_pure(&wrapper.inner, &indices) { + Ok(value) => { + unsafe { *out = value }; + 0 + } + Err(err) => qis_err_code(&err), + } +} + +/// Compute the negativity entanglement measure of a bipartite density +/// matrix, transposing subsystem A (`subsys_a` is an array of `len` qubit +/// indices). Writes the value to `out` and returns 0 on success. +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_negativity( + dm: *const CDensityMatrix, + subsys_a: *const u32, + len: usize, + out: *mut f64, +) -> i32 { + if dm.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + let indices = match qubit_indices(subsys_a, len) { + Ok(indices) => indices, + Err(code) => return code, + }; + let wrapper = unsafe { &*dm }; + match entropy::negativity(&wrapper.inner, &indices) { + Ok(value) => { + unsafe { *out = value }; + 0 + } + Err(err) => qis_err_code(&err), + } +} + +/// Compute the concurrence of a 2-qubit density matrix (0 for separable +/// states, 1 for maximally entangled states). Writes the value to `out` +/// and returns 0 on success; non-2-qubit inputs return an error code. +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_concurrence(dm: *const CDensityMatrix, out: *mut f64) -> i32 { + if dm.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + let wrapper = unsafe { &*dm }; + match entropy::concurrence(&wrapper.inner) { + Ok(value) => { + unsafe { *out = value }; + 0 + } + Err(err) => qis_err_code(&err), + } +} + +/// Compute the entanglement of formation (in bits) of a 2-qubit density +/// matrix, derived from the concurrence. Writes the value to `out` and +/// returns 0 on success; non-2-qubit inputs return an error code. +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_entanglement_of_formation( + dm: *const CDensityMatrix, + out: *mut f64, +) -> i32 { + if dm.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + let wrapper = unsafe { &*dm }; + match entropy::entanglement_of_formation(&wrapper.inner) { + Ok(value) => { + unsafe { *out = value }; + 0 + } + Err(err) => qis_err_code(&err), + } +} diff --git a/crates/binding-c/src/qis/hamiltonian.rs b/crates/binding-c/src/qis/hamiltonian.rs new file mode 100644 index 00000000..25a18be3 --- /dev/null +++ b/crates/binding-c/src/qis/hamiltonian.rs @@ -0,0 +1,354 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! C ABI for `Hamiltonian` observables. + +use crate::circuit::CCircuit; +use crate::error::CqlibError; +use crate::qis::{CHamiltonian, CPauliString, CStatevector, qis_err_code}; +use cqlib_core::qis::evolution::TrotterMode; +use cqlib_core::qis::{Hamiltonian, Observable}; +use num_complex::Complex64; +use std::collections::HashMap; +use std::ffi::CStr; +use std::os::raw::c_char; + +/// C-side snapshot of one measurement basis with its observed outcome +/// probabilities, used by `hamiltonian_expectation_probs`. +#[repr(C)] +pub struct CProbMeasurement { + /// Measurement basis as a Pauli string (borrowed, not consumed). + pub basis: *const CPauliString, + /// Array of `len` computational-basis bitstrings, one `'0'`/`'1'` char per + /// qubit, big-endian (qubit 0 is the last character). + pub states: *const *const c_char, + /// Array of `len` observed probabilities, paired with `states` by index. + pub probs: *const f64, + /// Number of entries in `states` and `probs`. + pub len: usize, +} + +/// Trotter-Suzuki decomposition mode tag: first-order Lie-Trotter. +pub const TROTTER_MODE_FIRST_ORDER: u32 = 0; +/// Trotter-Suzuki decomposition mode tag: second-order Strang splitting. +pub const TROTTER_MODE_SECOND_ORDER: u32 = 1; + +/// Create a new empty Hamiltonian acting on `num_qubits` qubits. +#[unsafe(no_mangle)] +pub extern "C" fn hamiltonian_new(num_qubits: usize) -> *mut CHamiltonian { + Box::into_raw(Box::new(CHamiltonian { + inner: Hamiltonian::new(num_qubits), + })) +} + +/// Free a Hamiltonian. NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn hamiltonian_free(ptr: *mut CHamiltonian) { + if !ptr.is_null() { + unsafe { + let _ = Box::from_raw(ptr); + } + } +} + +/// Construct a Hamiltonian from a single Pauli string (coefficient = 1.0). +/// Takes ownership of the `CPauliString` (frees it). +#[unsafe(no_mangle)] +pub extern "C" fn hamiltonian_from_pauli(pauli: *mut CPauliString) -> *mut CHamiltonian { + if pauli.is_null() { + return std::ptr::null_mut(); + } + let pauli = unsafe { Box::from_raw(pauli) }; + let ham = Hamiltonian::from_pauli(pauli.inner); + Box::into_raw(Box::new(CHamiltonian { inner: ham })) +} + +/// Add a Pauli term with a complex coefficient. +/// The Pauli string is cloned; the handle is not consumed. +/// `coeff_re` and `coeff_im` form the real and imaginary parts. +/// Returns 0 on success, or a negative error code on qubit-count mismatch. +#[unsafe(no_mangle)] +pub extern "C" fn hamiltonian_add_term( + ptr: *mut CHamiltonian, + pauli: *const CPauliString, + coeff_re: f64, + coeff_im: f64, +) -> i32 { + if ptr.is_null() || pauli.is_null() { + return CqlibError::NullPtr as i32; + } + let wrapper = unsafe { &mut *ptr }; + let p = unsafe { &(*pauli).inner }; + let coeff = Complex64::new(coeff_re, coeff_im); + match wrapper.inner.add_term(p.clone(), coeff) { + Ok(()) => 0, + Err(err) => qis_err_code(&err), + } +} + +/// Return the number of qubits, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn hamiltonian_num_qubits(ptr: *const CHamiltonian) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.num_qubits } +} + +/// Return the number of Pauli terms, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn hamiltonian_num_terms(ptr: *const CHamiltonian) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.terms.len() } +} + +/// Simplify the Hamiltonian in place: combine terms with the same Pauli +/// string and remove near-zero coefficients. Returns 0 on success. +#[unsafe(no_mangle)] +pub extern "C" fn hamiltonian_simplify(ptr: *mut CHamiltonian) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + let wrapper = unsafe { &mut *ptr }; + wrapper.inner.simplify(); + 0 +} + +/// Scale all coefficients by a complex factor in place. +/// `factor_re` and `factor_im` form the real and imaginary parts. +/// Returns 0 on success. +#[unsafe(no_mangle)] +pub extern "C" fn hamiltonian_scale(ptr: *mut CHamiltonian, factor_re: f64, factor_im: f64) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + if !factor_re.is_finite() || !factor_im.is_finite() { + return CqlibError::InvalidParam as i32; + } + let wrapper = unsafe { &mut *ptr }; + wrapper.inner.scale(Complex64::new(factor_re, factor_im)); + 0 +} + +/// Return the side length of the dense matrix (`2^N`), or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn hamiltonian_to_matrix_len(ptr: *const CHamiltonian) -> usize { + if ptr.is_null() { + return 0; + } + 1usize << unsafe { (*ptr).inner.num_qubits } +} + +/// Copy the dense `2^N x 2^N` matrix into `buffer` (row-major, +/// `Complex64` values). `len` must equal +/// `hamiltonian_to_matrix_len(ptr) * hamiltonian_to_matrix_len(ptr)`. +/// Returns 0 on success, or a negative error code. +#[unsafe(no_mangle)] +pub extern "C" fn hamiltonian_to_matrix( + ptr: *const CHamiltonian, + buffer: *mut Complex64, + len: usize, +) -> i32 { + if ptr.is_null() || buffer.is_null() { + return CqlibError::NullPtr as i32; + } + let wrapper = unsafe { &*ptr }; + let matrix = match wrapper.inner.to_matrix() { + Ok(matrix) => matrix, + Err(err) => return qis_err_code(&err), + }; + if matrix.len() != len { + return CqlibError::InvalidParam as i32; + } + let flat: Vec = matrix.iter().cloned().collect(); + unsafe { + std::ptr::copy_nonoverlapping(flat.as_ptr(), buffer, len); + } + 0 +} + +/// Return 1 if all Pauli terms of the Hamiltonian mutually commute, +/// 0 otherwise, or -1 for NULL. When all terms commute, the time +/// evolution decomposes exactly into per-term Pauli rotations. +#[unsafe(no_mangle)] +pub extern "C" fn hamiltonian_all_terms_commute(ptr: *const CHamiltonian) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + let wrapper = unsafe { &*ptr }; + if wrapper.inner.all_terms_commute() { + 1 + } else { + 0 + } +} + +/// Map a C Trotter-mode tag to the Rust enum. The `Randomized` variant +/// is not exposed through the C ABI. +#[inline] +fn trotter_mode_from_tag(mode: u32) -> Option { + match mode { + TROTTER_MODE_FIRST_ORDER => Some(TrotterMode::FirstOrder), + TROTTER_MODE_SECOND_ORDER => Some(TrotterMode::SecondOrder), + _ => None, + } +} + +/// Convert the Hamiltonian to a Trotterized time-evolution circuit +/// approximating `U(t) = exp(-i H t)`. +/// +/// `mode` must be `TROTTER_MODE_FIRST_ORDER` (0) or +/// `TROTTER_MODE_SECOND_ORDER` (1). Returns a new `CCircuit*` (free with +/// `circuit_free`), or NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn hamiltonian_to_trotter_circuit( + ptr: *const CHamiltonian, + time: f64, + steps: usize, + mode: u32, +) -> *mut CCircuit { + if ptr.is_null() { + return std::ptr::null_mut(); + } + if !time.is_finite() { + return std::ptr::null_mut(); + } + let mode = match trotter_mode_from_tag(mode) { + Some(mode) => mode, + None => return std::ptr::null_mut(), + }; + let wrapper = unsafe { &*ptr }; + match wrapper.inner.to_trotter_circuit(time, steps, mode) { + Ok(circuit) => Box::into_raw(Box::new(CCircuit { inner: circuit })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Convert the Hamiltonian to a time-evolution circuit, using the exact +/// single-pass decomposition when all terms commute and falling back to +/// the Trotter approximation (`mode`, `steps`) otherwise. +/// +/// `mode` must be `TROTTER_MODE_FIRST_ORDER` (0) or +/// `TROTTER_MODE_SECOND_ORDER` (1). Returns a new `CCircuit*` (free with +/// `circuit_free`), or NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn hamiltonian_to_evolution_circuit( + ptr: *const CHamiltonian, + time: f64, + steps: usize, + mode: u32, +) -> *mut CCircuit { + if ptr.is_null() { + return std::ptr::null_mut(); + } + if !time.is_finite() { + return std::ptr::null_mut(); + } + let mode = match trotter_mode_from_tag(mode) { + Some(mode) => mode, + None => return std::ptr::null_mut(), + }; + let wrapper = unsafe { &*ptr }; + match wrapper.inner.to_evolution_circuit(time, steps, mode) { + Ok(circuit) => Box::into_raw(Box::new(CCircuit { inner: circuit })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Compute the expectation value of the Hamiltonian from measurement outcome +/// probabilities. +/// +/// `measurements` lists one entry per measured basis; each term of the +/// Hamiltonian is evaluated in the first compatible measured basis. The +/// result is written to `out`. +/// +/// Returns 0 on success, -1 on NULL inputs (including NULL `measurements` +/// for a non-zero count, NULL basis, or NULL state/prob arrays for a +/// non-zero entry length), -4 for invalid UTF-8 state strings, or another +/// negative error code for core failures (e.g. -7 when a term has no +/// compatible measurement basis). +#[unsafe(no_mangle)] +pub extern "C" fn hamiltonian_expectation_probs( + ptr: *const CHamiltonian, + measurements: *const CProbMeasurement, + measurements_len: usize, + out: *mut f64, +) -> i32 { + if ptr.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + if measurements_len > 0 && measurements.is_null() { + return CqlibError::NullPtr as i32; + } + let mut entries: Vec<(cqlib_core::qis::PauliString, HashMap)> = + Vec::with_capacity(measurements_len); + for i in 0..measurements_len { + let entry = unsafe { &*measurements.add(i) }; + if entry.basis.is_null() { + return CqlibError::NullPtr as i32; + } + if entry.len > 0 && (entry.states.is_null() || entry.probs.is_null()) { + return CqlibError::NullPtr as i32; + } + let mut probs = HashMap::with_capacity(entry.len); + for j in 0..entry.len { + let state_ptr = unsafe { *entry.states.add(j) }; + if state_ptr.is_null() { + return CqlibError::NullPtr as i32; + } + let state = match unsafe { CStr::from_ptr(state_ptr) }.to_str() { + Ok(state) => state, + Err(_) => return CqlibError::ParseError as i32, + }; + probs.insert(state.to_string(), unsafe { *entry.probs.add(j) }); + } + entries.push((unsafe { (*entry.basis).inner.clone() }, probs)); + } + let wrapper = unsafe { &*ptr }; + match wrapper.inner.expectation_probs(&entries) { + Ok(value) => { + unsafe { + *out = value; + } + CqlibError::Ok as i32 + } + Err(err) => qis_err_code(&err), + } +} + +/// Compute the variance of the Hamiltonian for a statevector and write it to +/// `out`. Returns 0 on success, -1 on NULL pointers, -8 on qubit-count +/// mismatch, non-Hermitian terms, or other invalid parameters, or -7 for +/// other simulation errors. +#[unsafe(no_mangle)] +pub extern "C" fn hamiltonian_variance_statevector( + ptr: *const CHamiltonian, + sv: *const CStatevector, + out: *mut f64, +) -> i32 { + if ptr.is_null() || sv.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + let wrapper = unsafe { &*ptr }; + let sv = unsafe { &(*sv).inner }; + match wrapper.inner.variance_statevector(sv) { + Ok(variance) => { + unsafe { + *out = variance; + } + CqlibError::Ok as i32 + } + Err(err) => qis_err_code(&err), + } +} diff --git a/crates/binding-c/src/qis/measurement.rs b/crates/binding-c/src/qis/measurement.rs new file mode 100644 index 00000000..52e53b1a --- /dev/null +++ b/crates/binding-c/src/qis/measurement.rs @@ -0,0 +1,294 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! C ABI for `Measurement` handles and their `ClassicalValue` credentials. +//! +//! A [`Measurement`] pairs the immutable circuit value produced by a +//! measurement with the measured qubits and their result bit order. The +//! qubit-order utilities (`project`, `project_basis`, `check_qubits`) are +//! self-contained and work on standalone handles, while the value side is +//! meant to flow back into the classical expression layer. + +use crate::circuit::{ + CClassicalExpr, CQLIB_CLASSICAL_TYPE_BIT, CQLIB_CLASSICAL_TYPE_BIT_VEC, + CQLIB_CLASSICAL_TYPE_BOOL, CQLIB_CLASSICAL_TYPE_UINT, +}; +use crate::error::CqlibError; +use crate::qis::{CClassicalValue, CMeasurement, qis_err_code}; +use cqlib_core::circuit::{CircuitId, ClassicalType, ClassicalValue, Measurement, Qubit}; +use std::ffi::{CStr, CString}; +use std::os::raw::c_char; + +/// Converts a C (tag, width) pair into a core `ClassicalType`. +fn classical_type_from_tag(tag: u32, width: u32) -> Option { + match tag { + CQLIB_CLASSICAL_TYPE_BIT => Some(ClassicalType::Bit), + CQLIB_CLASSICAL_TYPE_BOOL => Some(ClassicalType::Bool), + CQLIB_CLASSICAL_TYPE_UINT => ClassicalType::uint(width), + CQLIB_CLASSICAL_TYPE_BIT_VEC => ClassicalType::bit_vec(width), + _ => None, + } +} + +/// Converts a core `ClassicalType` into a C (tag, width) pair. +fn classical_type_parts(ty: ClassicalType) -> (u32, u32) { + match ty { + ClassicalType::Bit => (CQLIB_CLASSICAL_TYPE_BIT, 1), + ClassicalType::Bool => (CQLIB_CLASSICAL_TYPE_BOOL, 1), + ClassicalType::UInt(width) => (CQLIB_CLASSICAL_TYPE_UINT, width.get()), + ClassicalType::BitVec(width) => (CQLIB_CLASSICAL_TYPE_BIT_VEC, width.get()), + } +} + +/// Create a standalone measurement descriptor from raw components. +/// +/// The wrapped `ClassicalValue` is built with a fresh circuit identity, so it +/// is **not registered in any circuit**: expressions produced from it are +/// rejected by `circuit_validate_classical_expr`. Use the `circuit_measure_*` +/// builders for circuit-integrated measurements; this constructor targets the +/// self-contained qubit-order utilities (`measurement_project`, +/// `measurement_project_basis`, `measurement_check_qubits`). +/// +/// `ty_tag` / `ty_width` describe the result type (one of +/// `CQLIB_CLASSICAL_TYPE_*`; `Bit` and `BitVec` are the direct +/// measurement-target types). `qubits` holds `len` qubit indices where +/// `qubits[0]` becomes result bit 0 (the least-significant bit). Returns NULL +/// on NULL `qubits`, an empty list, or an invalid type. +#[unsafe(no_mangle)] +pub extern "C" fn measurement_new( + value_index: u32, + ty_tag: u32, + ty_width: u32, + qubits: *const u32, + len: usize, +) -> *mut CMeasurement { + if qubits.is_null() || len == 0 { + return std::ptr::null_mut(); + } + let Some(ty) = classical_type_from_tag(ty_tag, ty_width) else { + return std::ptr::null_mut(); + }; + let value = ClassicalValue::new(CircuitId::new(), value_index, ty); + let qubits: Vec = unsafe { std::slice::from_raw_parts(qubits, len) } + .iter() + .map(|&id| Qubit::new(id)) + .collect(); + Box::into_raw(Box::new(CMeasurement { + inner: Measurement::new(value, qubits.into()), + })) +} + +/// Free a measurement handle. Passing NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn measurement_free(ptr: *mut CMeasurement) { + if !ptr.is_null() { + unsafe { + let _ = Box::from_raw(ptr); + } + } +} + +/// Return the immutable classical value produced by this measurement as a new +/// `CClassicalValue*` handle (free with `classical_value_free`). +/// Returns NULL on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn measurement_value(ptr: *const CMeasurement) -> *mut CClassicalValue { + if ptr.is_null() { + return std::ptr::null_mut(); + } + Box::into_raw(Box::new(CClassicalValue { + inner: unsafe { (*ptr).inner.value() }, + })) +} + +/// Create an expression that reads this measurement's immutable value. +/// Returns a new `CClassicalExpr*` handle (free with `classical_expr_free`), +/// or NULL on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn measurement_expr(ptr: *const CMeasurement) -> *mut CClassicalExpr { + if ptr.is_null() { + return std::ptr::null_mut(); + } + Box::into_raw(Box::new(CClassicalExpr { + inner: unsafe { (*ptr).inner.expr() }, + })) +} + +/// Return the number of measured qubits, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn measurement_qubits_len(ptr: *const CMeasurement) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.qubits().len() } +} + +/// Copy the measured qubit indices (in result bit order) into `buffer`. +/// Returns 0 on success, -1 on NULL pointers, -8 when `len` does not equal +/// `measurement_qubits_len`. +#[unsafe(no_mangle)] +pub extern "C" fn measurement_qubits( + ptr: *const CMeasurement, + buffer: *mut u32, + len: usize, +) -> i32 { + if ptr.is_null() || buffer.is_null() { + return CqlibError::NullPtr as i32; + } + let qubits = unsafe { (*ptr).inner.qubits() }; + if qubits.len() != len { + return CqlibError::InvalidParam as i32; + } + for (i, qubit) in qubits.iter().enumerate() { + unsafe { *buffer.add(i) = qubit.index() as u32 }; + } + 0 +} + +/// Return the number of measured bits, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn measurement_width(ptr: *const CMeasurement) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.width() } +} + +/// Write the measurement result type (tag + width) to the out-parameters. +/// Returns 0 on success, -1 on NULL pointers. +#[unsafe(no_mangle)] +pub extern "C" fn measurement_ty(ptr: *const CMeasurement, tag: *mut u32, width: *mut u32) -> i32 { + if ptr.is_null() || tag.is_null() || width.is_null() { + return CqlibError::NullPtr as i32; + } + let (ty_tag, ty_width) = classical_type_parts(unsafe { (*ptr).inner.ty() }); + unsafe { + *tag = ty_tag; + *width = ty_width; + } + 0 +} + +/// Check that all measured qubits are valid for a state of `num_qubits` +/// qubits. Returns 0 on success, -1 on NULL input, -2 when a qubit index is +/// out of bounds. +#[unsafe(no_mangle)] +pub extern "C" fn measurement_check_qubits(ptr: *const CMeasurement, num_qubits: usize) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + let wrapper = unsafe { &*ptr }; + wrapper + .inner + .check_qubits(num_qubits) + .map_or_else(|err| qis_err_code(&err), |_| 0) +} + +/// Project a full-register outcome onto this measurement's qubit order. +/// +/// `bitstring` is a big-endian '0'/'1' string over the full register (MSB = +/// last qubit, matching `statevector_measure_all` output). If `qubits[i]` is +/// one in the outcome, result bit `i` is set. Returns the projected outcome +/// of `measurement_width` bits as a heap-allocated big-endian bitstring +/// (caller frees with `cqlib_string_free`), or NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn measurement_project( + ptr: *const CMeasurement, + bitstring: *const c_char, +) -> *mut c_char { + if ptr.is_null() || bitstring.is_null() { + return std::ptr::null_mut(); + } + let wrapper = unsafe { &*ptr }; + let source = unsafe { CStr::from_ptr(bitstring) }; + let Ok(source) = source.to_str() else { + return std::ptr::null_mut(); + }; + let Ok(full) = cqlib_core::device::Outcome::from_bitstring(source) else { + return std::ptr::null_mut(); + }; + let projected = wrapper.inner.project(&full); + match CString::new(projected.to_bitstring(wrapper.inner.width())) { + Ok(cs) => cs.into_raw(), + Err(_) => std::ptr::null_mut(), + } +} + +/// Project a computational-basis index onto this measurement's qubit order. +/// +/// If bit `qubits[i]` is one in `basis`, result bit `i` is set. Returns the +/// projected outcome of `measurement_width` bits as a heap-allocated +/// big-endian bitstring (caller frees with `cqlib_string_free`), or NULL on +/// error. +#[unsafe(no_mangle)] +pub extern "C" fn measurement_project_basis(ptr: *const CMeasurement, basis: usize) -> *mut c_char { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let wrapper = unsafe { &*ptr }; + let projected = wrapper.inner.project_basis(basis); + match CString::new(projected.to_bitstring(wrapper.inner.width())) { + Ok(cs) => cs.into_raw(), + Err(_) => std::ptr::null_mut(), + } +} + +/// Free a classical value handle. Passing NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn classical_value_free(ptr: *mut CClassicalValue) { + if !ptr.is_null() { + unsafe { + let _ = Box::from_raw(ptr); + } + } +} + +/// Return the circuit-local value table index, or `UINT32_MAX` for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn classical_value_index(ptr: *const CClassicalValue) -> u32 { + if ptr.is_null() { + return u32::MAX; + } + unsafe { (*ptr).inner.index() } +} + +/// Write the value type (tag + width) to the out-parameters. +/// Returns 0 on success, -1 on NULL pointers. +#[unsafe(no_mangle)] +pub extern "C" fn classical_value_ty( + ptr: *const CClassicalValue, + tag: *mut u32, + width: *mut u32, +) -> i32 { + if ptr.is_null() || tag.is_null() || width.is_null() { + return CqlibError::NullPtr as i32; + } + let (ty_tag, ty_width) = classical_type_parts(unsafe { (*ptr).inner.ty() }); + unsafe { + *tag = ty_tag; + *width = ty_width; + } + 0 +} + +/// Create an expression that reads this immutable runtime value. +/// Returns a new `CClassicalExpr*` handle (free with `classical_expr_free`), +/// or NULL on NULL input. +#[unsafe(no_mangle)] +pub extern "C" fn classical_value_expr(ptr: *const CClassicalValue) -> *mut CClassicalExpr { + if ptr.is_null() { + return std::ptr::null_mut(); + } + Box::into_raw(Box::new(CClassicalExpr { + inner: unsafe { (*ptr).inner.expr() }, + })) +} diff --git a/crates/binding-c/src/qis/metrics.rs b/crates/binding-c/src/qis/metrics.rs new file mode 100644 index 00000000..6858f764 --- /dev/null +++ b/crates/binding-c/src/qis/metrics.rs @@ -0,0 +1,245 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! C ABI for quantum information metrics (`qis/metrics.rs`). + +use crate::error::CqlibError; +use crate::qis::{CDensityMatrix, CStatevector, qis_err_code}; +use cqlib_core::qis::metrics; + +/// Convert a C qubit-index array (`ptr`, `len`) into a `Vec`. +/// An empty array is valid; a NULL pointer with a positive length is not. +#[inline] +fn qubit_indices(ptr: *const u32, len: usize) -> Result, i32> { + if len == 0 { + return Ok(Vec::new()); + } + if ptr.is_null() { + return Err(CqlibError::NullPtr as i32); + } + Ok(unsafe { std::slice::from_raw_parts(ptr, len) } + .iter() + .map(|&q| q as usize) + .collect()) +} + +/// Compute the purity `Tr(rho^2)`-style norm of a statevector (1.0 for a +/// normalized pure state). Writes the value to `out`, returns 0 on success. +#[unsafe(no_mangle)] +pub extern "C" fn statevector_purity(sv: *const CStatevector, out: *mut f64) -> i32 { + if sv.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + let wrapper = unsafe { &*sv }; + match metrics::purity_pure(&wrapper.inner) { + Ok(value) => { + unsafe { *out = value }; + 0 + } + Err(err) => qis_err_code(&err), + } +} + +/// Compute the purity `Tr(rho^2)` of a density matrix. Writes the value +/// to `out` and returns 0 on success. +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_purity(dm: *const CDensityMatrix, out: *mut f64) -> i32 { + if dm.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + let wrapper = unsafe { &*dm }; + match metrics::purity_mixed(&wrapper.inner) { + Ok(value) => { + unsafe { *out = value }; + 0 + } + Err(err) => qis_err_code(&err), + } +} + +/// Compute the state fidelity `||^2` between two statevectors. +/// Writes the value to `out` and returns 0 on success. +#[unsafe(no_mangle)] +pub extern "C" fn statevector_fidelity( + sv1: *const CStatevector, + sv2: *const CStatevector, + out: *mut f64, +) -> i32 { + if sv1.is_null() || sv2.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + let a = unsafe { &(*sv1).inner }; + let b = unsafe { &(*sv2).inner }; + match metrics::state_fidelity_pure(a, b) { + Ok(value) => { + unsafe { *out = value }; + 0 + } + Err(err) => qis_err_code(&err), + } +} + +/// Compute the trace distance `sqrt(1 - ||^2)` between two pure +/// states. Writes the value to `out` and returns 0 on success. +#[unsafe(no_mangle)] +pub extern "C" fn statevector_trace_distance( + sv1: *const CStatevector, + sv2: *const CStatevector, + out: *mut f64, +) -> i32 { + if sv1.is_null() || sv2.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + let a = unsafe { &(*sv1).inner }; + let b = unsafe { &(*sv2).inner }; + match metrics::trace_distance_pure(a, b) { + Ok(value) => { + unsafe { *out = value }; + 0 + } + Err(err) => qis_err_code(&err), + } +} + +/// Compute the fidelity `` between a pure statevector and a +/// density matrix. Writes the value to `out` and returns 0 on success. +#[unsafe(no_mangle)] +pub extern "C" fn statevector_fidelity_pure_mixed( + sv: *const CStatevector, + dm: *const CDensityMatrix, + out: *mut f64, +) -> i32 { + if sv.is_null() || dm.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + let a = unsafe { &(*sv).inner }; + let b = unsafe { &(*dm).inner }; + match metrics::state_fidelity_pure_mixed(a, b) { + Ok(value) => { + unsafe { *out = value }; + 0 + } + Err(err) => qis_err_code(&err), + } +} + +/// Compute the Von Neumann entropy `-Tr(rho log2 rho)` of a density +/// matrix (in bits). Writes the value to `out` and returns 0 on success. +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_entropy(dm: *const CDensityMatrix, out: *mut f64) -> i32 { + if dm.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + let wrapper = unsafe { &*dm }; + match metrics::entropy(&wrapper.inner) { + Ok(value) => { + unsafe { *out = value }; + 0 + } + Err(err) => qis_err_code(&err), + } +} + +/// Compute the trace distance `1/2 Tr|rho - sigma|` between two density +/// matrices. Writes the value to `out` and returns 0 on success. +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_trace_distance( + dm1: *const CDensityMatrix, + dm2: *const CDensityMatrix, + out: *mut f64, +) -> i32 { + if dm1.is_null() || dm2.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + let a = unsafe { &(*dm1).inner }; + let b = unsafe { &(*dm2).inner }; + match metrics::trace_distance_mixed(a, b) { + Ok(value) => { + unsafe { *out = value }; + 0 + } + Err(err) => qis_err_code(&err), + } +} + +/// Compute the state fidelity `(Tr sqrt(sqrt(rho) sigma sqrt(rho)))^2` +/// between two density matrices. Writes the value to `out` and returns +/// 0 on success. +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_fidelity( + dm1: *const CDensityMatrix, + dm2: *const CDensityMatrix, + out: *mut f64, +) -> i32 { + if dm1.is_null() || dm2.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + let a = unsafe { &(*dm1).inner }; + let b = unsafe { &(*dm2).inner }; + match metrics::state_fidelity_mixed(a, b) { + Ok(value) => { + unsafe { *out = value }; + 0 + } + Err(err) => qis_err_code(&err), + } +} + +/// Perform the partial transpose on `qubits` (array of `len` indices). +/// Returns a new `CDensityMatrix*` (free with `density_matrix_free`), +/// or NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_partial_transpose( + dm: *const CDensityMatrix, + qubits: *const u32, + len: usize, +) -> *mut CDensityMatrix { + if dm.is_null() { + return std::ptr::null_mut(); + } + let indices = match qubit_indices(qubits, len) { + Ok(indices) => indices, + Err(_) => return std::ptr::null_mut(), + }; + let wrapper = unsafe { &*dm }; + match metrics::partial_transpose(&wrapper.inner, &indices) { + Ok(result) => Box::into_raw(Box::new(CDensityMatrix { inner: result })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Compute the logarithmic negativity `log2 ||rho^{T_A}||_1` with respect +/// to subsystem A (`sys_a` is an array of `len` qubit indices). Writes the +/// value to `out` and returns 0 on success. +#[unsafe(no_mangle)] +pub extern "C" fn density_matrix_logarithmic_negativity( + dm: *const CDensityMatrix, + sys_a: *const u32, + len: usize, + out: *mut f64, +) -> i32 { + if dm.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + let indices = match qubit_indices(sys_a, len) { + Ok(indices) => indices, + Err(code) => return code, + }; + let wrapper = unsafe { &*dm }; + match metrics::logarithmic_negativity(&wrapper.inner, &indices) { + Ok(value) => { + unsafe { *out = value }; + 0 + } + Err(err) => qis_err_code(&err), + } +} diff --git a/crates/binding-c/src/qis/mod.rs b/crates/binding-c/src/qis/mod.rs new file mode 100644 index 00000000..8628c2fc --- /dev/null +++ b/crates/binding-c/src/qis/mod.rs @@ -0,0 +1,111 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! C ABI for QIS module (Statevector, DensityMatrix, StabilizerState, etc.). + +#![allow(clippy::not_unsafe_ptr_arg_deref)] + +pub mod density_matrix; +pub mod density_matrix_noise; +pub mod entropy; +pub mod hamiltonian; +pub mod measurement; +pub mod metrics; +pub mod pauli; +pub mod stabilizer; +pub mod statevector; + +use cqlib_core::circuit::{ClassicalValue, Measurement}; +use cqlib_core::qis::{ + DensityMatrix, DensityMatrixNoise, Hamiltonian, PauliString, StabilizerState, Statevector, +}; + +/// Opaque handle around [`Statevector`] for C ABI. +pub struct CStatevector { + pub inner: Statevector, +} + +/// Opaque handle around [`DensityMatrix`] for C ABI. +pub struct CDensityMatrix { + pub inner: DensityMatrix, +} + +/// Opaque handle around [`StabilizerState`] for C ABI. +pub struct CStabilizerState { + pub inner: StabilizerState, +} + +/// Opaque handle around [`Hamiltonian`] for C ABI. +pub struct CHamiltonian { + pub inner: Hamiltonian, +} + +/// Opaque handle around [`PauliString`] for C ABI. +pub struct CPauliString { + pub inner: PauliString, +} + +/// Opaque handle around [`DensityMatrixNoise`] for C ABI. +pub struct CDensityMatrixNoise { + pub inner: DensityMatrixNoise, +} + +/// Opaque handle around [`Measurement`] for C ABI. +pub struct CMeasurement { + pub inner: Measurement, +} + +/// Opaque handle around [`ClassicalValue`] for C ABI. +pub struct CClassicalValue { + pub inner: ClassicalValue, +} + +/// Owned list of measurement outcome bitstrings returned by `*_sample_shots`. +/// +/// Each entry is a big-endian binary string (MSB left, qubit N-1 first, +/// qubit 0 last) suitable for direct use from C. +pub struct COutcomeList { + pub items: Vec, +} + +/// Validates a qubit index against a simulator width, mapping to `QisError`. +#[inline] +pub(crate) fn check_qubit(num_qubits: usize, qubit: u32) -> Result { + if qubit as usize >= num_qubits { + return Err(-2); + } + Ok(qubit as usize) +} + +/// Maps a `QisError` to a negative C error code. +pub(crate) fn qis_err_code(err: &cqlib_core::qis::QisError) -> i32 { + use cqlib_core::qis::QisError; + match err { + QisError::IndexOutOfBounds { .. } => -2, + QisError::InvalidParameterValue(_) | QisError::InvalidStateDimension(_) => -8, + QisError::NotNormalized => -8, + QisError::QubitMismatch { .. } => -8, + QisError::CircuitError(_) => -3, + QisError::UnsupportedOperation(_) => -7, + _ => -7, + } +} + +pub use density_matrix::*; +pub use density_matrix_noise::*; +pub use entropy::*; +pub use hamiltonian::*; +pub use measurement::*; +pub use metrics::*; +pub use pauli::*; +pub use stabilizer::*; +pub use statevector::*; diff --git a/crates/binding-c/src/qis/pauli.rs b/crates/binding-c/src/qis/pauli.rs new file mode 100644 index 00000000..7a88e9a7 --- /dev/null +++ b/crates/binding-c/src/qis/pauli.rs @@ -0,0 +1,398 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! C ABI for `PauliString` and single-qubit Pauli operators. + +use crate::error::CqlibError; +use crate::qis::{CPauliString, qis_err_code}; +use cqlib_core::qis::{Pauli, PauliString}; +use std::collections::HashMap; +use std::ffi::{CStr, CString}; +use std::os::raw::c_char; +use std::str::FromStr; + +/// C-side Pauli operator enum. Mirrors `Pauli` but with a stable `#[repr(u8)]` +/// layout suitable for direct use from C. +/// +/// | Value | Operator | +/// |-------|----------| +/// | 0 | I | +/// | 1 | X | +/// | 2 | Y | +/// | 3 | Z | +pub const PAULI_I: u8 = 0; +pub const PAULI_X: u8 = 1; +pub const PAULI_Y: u8 = 2; +pub const PAULI_Z: u8 = 3; + +fn from_c_pauli(value: u8) -> Option { + match value { + PAULI_I => Some(Pauli::I), + PAULI_X => Some(Pauli::X), + PAULI_Y => Some(Pauli::Y), + PAULI_Z => Some(Pauli::Z), + _ => None, + } +} + +fn to_c_pauli(pauli: Pauli) -> u8 { + match pauli { + Pauli::I => PAULI_I, + Pauli::X => PAULI_X, + Pauli::Y => PAULI_Y, + Pauli::Z => PAULI_Z, + } +} + +/// Multiply two single-qubit Pauli operators as `left * right` and return +/// the result together with the Pauli-group phase factor (e.g. X * Z = -iY). +/// +/// On success, writes the resulting Pauli tag (one of `PAULI_I/X/Y/Z`) to +/// `out_pauli` and the complex phase (one of 1, i, -1, -i) to `out_phase`, +/// then returns 0. Returns -1 when an output pointer is NULL and -8 when an +/// input tag is not one of `PAULI_I/X/Y/Z`. +#[unsafe(no_mangle)] +pub extern "C" fn pauli_mul_with_phase( + left: u8, + right: u8, + out_pauli: *mut u8, + out_phase: *mut num_complex::Complex64, +) -> i32 { + if out_pauli.is_null() || out_phase.is_null() { + return CqlibError::NullPtr as i32; + } + let left = match from_c_pauli(left) { + Some(p) => p, + None => return CqlibError::InvalidParam as i32, + }; + let right = match from_c_pauli(right) { + Some(p) => p, + None => return CqlibError::InvalidParam as i32, + }; + let (result, phase) = left.mul_with_phase(right); + unsafe { + *out_pauli = to_c_pauli(result); + *out_phase = phase.to_complex(); + } + 0 +} + +/// Create a new identity Pauli string on `num_qubits` qubits. +/// Returns NULL for 0 qubits. +#[unsafe(no_mangle)] +pub extern "C" fn pauli_string_new(num_qubits: usize) -> *mut CPauliString { + if num_qubits == 0 { + return std::ptr::null_mut(); + } + Box::into_raw(Box::new(CPauliString { + inner: PauliString::new(num_qubits), + })) +} + +/// Free a Pauli string. NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn pauli_string_free(ptr: *mut CPauliString) { + if !ptr.is_null() { + unsafe { + let _ = Box::from_raw(ptr); + } + } +} + +/// Parse a Pauli string from a C string (e.g. "XYZ", "ZZI"). +/// Returns NULL on parse failure. +#[unsafe(no_mangle)] +pub extern "C" fn pauli_string_parse(source: *const c_char) -> *mut CPauliString { + if source.is_null() { + return std::ptr::null_mut(); + } + let source = unsafe { CStr::from_ptr(source) }; + let source = match source.to_str() { + Ok(s) => s, + Err(_) => return std::ptr::null_mut(), + }; + match PauliString::from_str(source) { + Ok(ps) => Box::into_raw(Box::new(CPauliString { inner: ps })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Set the Pauli operator at qubit index `idx`. `pauli` must be one of +/// `PAULI_I`, `PAULI_X`, `PAULI_Y`, `PAULI_Z`. +/// Returns 0 on success, or a negative error code on failure. +#[unsafe(no_mangle)] +pub extern "C" fn pauli_string_set_pauli(ptr: *mut CPauliString, idx: usize, pauli: u8) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + let wrapper = unsafe { &mut *ptr }; + let p = match from_c_pauli(pauli) { + Some(p) => p, + None => return CqlibError::InvalidParam as i32, + }; + if idx >= wrapper.inner.num_qubits { + return CqlibError::QubitOutOfBounds as i32; + } + wrapper.inner.set_pauli(idx, p); + 0 +} + +/// Get the Pauli operator at qubit index `idx`, returned as one of +/// `PAULI_I/X/Y/Z`. Returns 255 (0xFF) on error. +#[unsafe(no_mangle)] +pub extern "C" fn pauli_string_get_pauli(ptr: *const CPauliString, idx: usize) -> u8 { + if ptr.is_null() { + return 0xFF; + } + let wrapper = unsafe { &*ptr }; + if idx >= wrapper.inner.num_qubits { + return 0xFF; + } + to_c_pauli(wrapper.inner.get_pauli(idx)) +} + +/// Return the number of qubits the Pauli string acts on, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn pauli_string_num_qubits(ptr: *const CPauliString) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.num_qubits } +} + +/// Format the Pauli string as a C string (e.g. "+XYZ"). +/// Caller must free with `cqlib_string_free`. Returns NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn pauli_string_to_string(ptr: *const CPauliString) -> *mut c_char { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let wrapper = unsafe { &*ptr }; + let s = wrapper.inner.to_string(); + match CString::new(s) { + Ok(cs) => cs.into_raw(), + Err(_) => std::ptr::null_mut(), + } +} + +/// Flatten the dense matrix representation into `buffer`. +/// +/// The matrix has dimensions `2^N × 2^N` and is stored in row-major order +/// as `Complex64` values. Use `pauli_string_matrix_len` first to obtain +/// the side length (`2^N`). Returns 0 on success. +#[unsafe(no_mangle)] +pub extern "C" fn pauli_string_matrix_len(ptr: *const CPauliString) -> usize { + if ptr.is_null() { + return 0; + } + let wrapper = unsafe { &*ptr }; + 1usize << wrapper.inner.num_qubits +} + +/// Copy the dense matrix into `buffer` (row-major, `Complex64` values). +/// `len` must equal `pauli_string_matrix_len(ptr) * pauli_string_matrix_len(ptr)`. +#[unsafe(no_mangle)] +pub extern "C" fn pauli_string_matrix( + ptr: *const CPauliString, + buffer: *mut num_complex::Complex64, + len: usize, +) -> i32 { + if ptr.is_null() || buffer.is_null() { + return CqlibError::NullPtr as i32; + } + let wrapper = unsafe { &*ptr }; + let matrix = wrapper.inner.to_matrix(); + let dim = 1usize << wrapper.inner.num_qubits; + if matrix.len() != len || len != dim * dim { + return CqlibError::InvalidParam as i32; + } + let flat: Vec = matrix.iter().cloned().collect(); + unsafe { + std::ptr::copy_nonoverlapping(flat.as_ptr(), buffer, len); + } + 0 +} + +/// Return 1 if this Pauli string commutes with `other`, 0 otherwise. +/// Returns -1 for NULL handles and -8 when the qubit counts differ. +#[unsafe(no_mangle)] +pub extern "C" fn pauli_string_commutes_with( + ptr: *const CPauliString, + other: *const CPauliString, +) -> i32 { + if ptr.is_null() || other.is_null() { + return CqlibError::NullPtr as i32; + } + let a = unsafe { &(*ptr).inner }; + let b = unsafe { &(*other).inner }; + if a.num_qubits != b.num_qubits { + return CqlibError::InvalidParam as i32; + } + if a.commutes_with(b) { 1 } else { 0 } +} + +/// Return the X-component bitmask: bit i is set when qubit i carries an +/// X or Y operator. Returns 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn pauli_string_x_mask(ptr: *const CPauliString) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.x_mask() } +} + +/// Return the Z-component bitmask: bit i is set when qubit i carries a +/// Z or Y operator. Returns 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn pauli_string_z_mask(ptr: *const CPauliString) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.z_mask() } +} + +/// Compute the phase factor `i^n` contributed by the n Y operators of +/// the string. Writes the complex value to `out` and returns 0 on +/// success. +#[unsafe(no_mangle)] +pub extern "C" fn pauli_string_y_phase( + ptr: *const CPauliString, + out: *mut num_complex::Complex64, +) -> i32 { + if ptr.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + let wrapper = unsafe { &*ptr }; + unsafe { *out = wrapper.inner.y_phase() }; + 0 +} + +/// Return the number of non-identity qubits (support size), or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn pauli_string_support_len(ptr: *const CPauliString) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.support().len() } +} + +/// Copy the support (indices of non-identity operators, ascending) into +/// `buffer` as `uint32_t` values. `len` must equal +/// `pauli_string_support_len(ptr)`. Returns 0 on success, or a negative +/// error code. +#[unsafe(no_mangle)] +pub extern "C" fn pauli_string_support( + ptr: *const CPauliString, + buffer: *mut u32, + len: usize, +) -> i32 { + if ptr.is_null() || buffer.is_null() { + return CqlibError::NullPtr as i32; + } + let wrapper = unsafe { &*ptr }; + let support = wrapper.inner.support(); + if support.len() != len { + return CqlibError::InvalidParam as i32; + } + for (i, &q) in support.iter().enumerate() { + unsafe { + *buffer.add(i) = q as u32; + } + } + 0 +} + +/// Get the Pauli operator at qubit index `idx` without panicking. +/// Writes one of `PAULI_I/X/Y/Z` to `out` and returns 0 on success, or +/// a negative error code (`-2` out of bounds). +#[unsafe(no_mangle)] +pub extern "C" fn pauli_string_try_get_pauli( + ptr: *const CPauliString, + idx: usize, + out: *mut u8, +) -> i32 { + if ptr.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + let wrapper = unsafe { &*ptr }; + match wrapper.inner.try_get_pauli(idx) { + Ok(pauli) => { + unsafe { *out = to_c_pauli(pauli) }; + 0 + } + Err(err) => qis_err_code(&err), + } +} + +/// Set the Pauli operator at qubit index `idx` without panicking. +/// `pauli` must be one of `PAULI_I/X/Y/Z`. Returns 0 on success, or a +/// negative error code (`-2` out of bounds, `-8` invalid tag). +#[unsafe(no_mangle)] +pub extern "C" fn pauli_string_try_set_pauli(ptr: *mut CPauliString, idx: usize, pauli: u8) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + let wrapper = unsafe { &mut *ptr }; + let p = match from_c_pauli(pauli) { + Some(p) => p, + None => return CqlibError::InvalidParam as i32, + }; + match wrapper.inner.try_set_pauli(idx, p) { + Ok(()) => 0, + Err(err) => qis_err_code(&err), + } +} + +/// Compute the expectation value `

` given a probability distribution +/// over computational basis states. `bitstrings[i]` pairs with `probs[i]`; +/// each bitstring uses the little-endian convention (the rightmost +/// character is qubit 0) and must be exactly `num_qubits` characters of +/// '0'/'1'. Strings containing X or Y operators yield 0.0. Writes the +/// value to `out` and returns 0 on success, or a negative error code +/// (`-1` NULL handle, buffer or output; `-4` non-UTF-8 bitstring; `-8` +/// NULL bitstring entry; `-7` for core rejections such as a wrong-length +/// or invalid-character bitstring). +#[unsafe(no_mangle)] +pub extern "C" fn pauli_string_expectation( + ptr: *const CPauliString, + bitstrings: *const *const c_char, + probs: *const f64, + len: usize, + out: *mut f64, +) -> i32 { + if ptr.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + if len > 0 && (bitstrings.is_null() || probs.is_null()) { + return CqlibError::NullPtr as i32; + } + let wrapper = unsafe { &*ptr }; + let mut distribution: HashMap = HashMap::with_capacity(len); + for i in 0..len { + let raw = unsafe { *bitstrings.add(i) }; + if raw.is_null() { + return CqlibError::InvalidParam as i32; + } + let state = match unsafe { CStr::from_ptr(raw) }.to_str() { + Ok(state) => state.to_string(), + Err(_) => return CqlibError::ParseError as i32, + }; + distribution.insert(state, unsafe { *probs.add(i) }); + } + match wrapper.inner.expectation(&distribution) { + Ok(value) => { + unsafe { *out = value }; + 0 + } + Err(err) => qis_err_code(&err), + } +} diff --git a/crates/binding-c/src/qis/stabilizer.rs b/crates/binding-c/src/qis/stabilizer.rs new file mode 100644 index 00000000..9908dc2e --- /dev/null +++ b/crates/binding-c/src/qis/stabilizer.rs @@ -0,0 +1,523 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! C ABI for `StabilizerState` (Clifford-circuit simulator). + +use crate::circuit::CCircuit; +use crate::error::CqlibError; +use crate::qis::{COutcomeList, CPauliString, CStabilizerState, check_qubit, qis_err_code}; +use cqlib_core::qis::StabilizerState; +use std::ffi::CString; +use std::os::raw::c_char; + +#[inline] +fn map_err(err: cqlib_core::qis::QisError) -> i32 { + qis_err_code(&err) +} + +fn apply_single( + ptr: *mut CStabilizerState, + qubit: u32, + f: impl FnOnce(&mut StabilizerState, usize) -> Result<(), cqlib_core::qis::QisError>, +) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + let wrapper = unsafe { &mut *ptr }; + let n = wrapper.inner.num_qubits(); + let q = match check_qubit(n, qubit) { + Ok(q) => q, + Err(code) => return code, + }; + f(&mut wrapper.inner, q) + .map_err(map_err) + .map_or_else(|e| e, |_| 0) +} + +fn apply_two( + ptr: *mut CStabilizerState, + q0: u32, + q1: u32, + f: impl FnOnce(&mut StabilizerState, usize, usize) -> Result<(), cqlib_core::qis::QisError>, +) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + let wrapper = unsafe { &mut *ptr }; + let n = wrapper.inner.num_qubits(); + let a = match check_qubit(n, q0) { + Ok(q) => q, + Err(code) => return code, + }; + let b = match check_qubit(n, q1) { + Ok(q) => q, + Err(code) => return code, + }; + f(&mut wrapper.inner, a, b) + .map_err(map_err) + .map_or_else(|e| e, |_| 0) +} + +#[unsafe(no_mangle)] +pub extern "C" fn stabilizer_new(num_qubits: usize) -> *mut CStabilizerState { + if num_qubits == 0 { + return std::ptr::null_mut(); + } + Box::into_raw(Box::new(CStabilizerState { + inner: StabilizerState::new(num_qubits), + })) +} + +#[unsafe(no_mangle)] +pub extern "C" fn stabilizer_free(ptr: *mut CStabilizerState) { + if !ptr.is_null() { + unsafe { + let _ = Box::from_raw(ptr); + } + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn stabilizer_from_circuit(circuit: *const CCircuit) -> *mut CStabilizerState { + if circuit.is_null() { + return std::ptr::null_mut(); + } + let circuit = unsafe { &(*circuit).inner }; + match StabilizerState::from_circuit(circuit) { + Ok(ss) => Box::into_raw(Box::new(CStabilizerState { inner: ss })), + Err(_) => std::ptr::null_mut(), + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn stabilizer_apply_circuit( + ptr: *mut CStabilizerState, + circuit: *const CCircuit, +) -> i32 { + if ptr.is_null() || circuit.is_null() { + return CqlibError::NullPtr as i32; + } + let wrapper = unsafe { &mut *ptr }; + let circuit = unsafe { &(*circuit).inner }; + wrapper + .inner + .apply_circuit(circuit) + .map_err(map_err) + .map_or_else(|e| e, |_| 0) +} + +#[unsafe(no_mangle)] +pub extern "C" fn stabilizer_apply_h(ptr: *mut CStabilizerState, qubit: u32) -> i32 { + apply_single(ptr, qubit, StabilizerState::apply_h) +} + +#[unsafe(no_mangle)] +pub extern "C" fn stabilizer_apply_x(ptr: *mut CStabilizerState, qubit: u32) -> i32 { + apply_single(ptr, qubit, StabilizerState::apply_x) +} + +#[unsafe(no_mangle)] +pub extern "C" fn stabilizer_apply_y(ptr: *mut CStabilizerState, qubit: u32) -> i32 { + apply_single(ptr, qubit, StabilizerState::apply_y) +} + +#[unsafe(no_mangle)] +pub extern "C" fn stabilizer_apply_z(ptr: *mut CStabilizerState, qubit: u32) -> i32 { + apply_single(ptr, qubit, StabilizerState::apply_z) +} + +#[unsafe(no_mangle)] +pub extern "C" fn stabilizer_apply_s(ptr: *mut CStabilizerState, qubit: u32) -> i32 { + apply_single(ptr, qubit, StabilizerState::apply_s) +} + +#[unsafe(no_mangle)] +pub extern "C" fn stabilizer_apply_sdg(ptr: *mut CStabilizerState, qubit: u32) -> i32 { + apply_single(ptr, qubit, StabilizerState::apply_sdg) +} + +#[unsafe(no_mangle)] +pub extern "C" fn stabilizer_apply_x2p(ptr: *mut CStabilizerState, qubit: u32) -> i32 { + apply_single(ptr, qubit, StabilizerState::apply_x2p) +} + +#[unsafe(no_mangle)] +pub extern "C" fn stabilizer_apply_x2m(ptr: *mut CStabilizerState, qubit: u32) -> i32 { + apply_single(ptr, qubit, StabilizerState::apply_x2m) +} + +#[unsafe(no_mangle)] +pub extern "C" fn stabilizer_apply_y2p(ptr: *mut CStabilizerState, qubit: u32) -> i32 { + apply_single(ptr, qubit, StabilizerState::apply_y2p) +} + +#[unsafe(no_mangle)] +pub extern "C" fn stabilizer_apply_y2m(ptr: *mut CStabilizerState, qubit: u32) -> i32 { + apply_single(ptr, qubit, StabilizerState::apply_y2m) +} + +#[unsafe(no_mangle)] +pub extern "C" fn stabilizer_apply_cx( + ptr: *mut CStabilizerState, + control: u32, + target: u32, +) -> i32 { + apply_two(ptr, control, target, StabilizerState::apply_cx) +} + +#[unsafe(no_mangle)] +pub extern "C" fn stabilizer_apply_cy( + ptr: *mut CStabilizerState, + control: u32, + target: u32, +) -> i32 { + apply_two(ptr, control, target, StabilizerState::apply_cy) +} + +#[unsafe(no_mangle)] +pub extern "C" fn stabilizer_apply_cz(ptr: *mut CStabilizerState, q0: u32, q1: u32) -> i32 { + apply_two(ptr, q0, q1, StabilizerState::apply_cz) +} + +#[unsafe(no_mangle)] +pub extern "C" fn stabilizer_apply_swap(ptr: *mut CStabilizerState, q0: u32, q1: u32) -> i32 { + apply_two(ptr, q0, q1, StabilizerState::apply_swap) +} + +#[unsafe(no_mangle)] +pub extern "C" fn stabilizer_measure(ptr: *mut CStabilizerState, qubit: u32) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + let wrapper = unsafe { &mut *ptr }; + let n = wrapper.inner.num_qubits(); + let q = match check_qubit(n, qubit) { + Ok(q) => q, + Err(code) => return code, + }; + match wrapper.inner.measure(q) { + Ok(true) => 1, + Ok(false) => 0, + Err(err) => qis_err_code(&err), + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn stabilizer_measure_all(ptr: *mut CStabilizerState) -> *mut c_char { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let wrapper = unsafe { &mut *ptr }; + let n = wrapper.inner.num_qubits(); + let outcome = wrapper.inner.measure_all(); + let bitstring = outcome.to_bitstring(n); + match CString::new(bitstring) { + Ok(cs) => cs.into_raw(), + Err(_) => std::ptr::null_mut(), + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn stabilizer_reset(ptr: *mut CStabilizerState, qubit: u32) -> i32 { + apply_single(ptr, qubit, StabilizerState::reset) +} + +/// Return the number of basis-state probabilities (2^N), or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn stabilizer_probabilities_len(ptr: *const CStabilizerState) -> usize { + if ptr.is_null() { + return 0; + } + let wrapper = unsafe { &*ptr }; + 1usize << wrapper.inner.num_qubits() +} + +/// Copy the probability distribution into `buffer`. +/// Returns 0 on success, or a negative error code. +#[unsafe(no_mangle)] +pub extern "C" fn stabilizer_probabilities( + ptr: *const CStabilizerState, + buffer: *mut f64, + len: usize, +) -> i32 { + if ptr.is_null() || buffer.is_null() { + return CqlibError::NullPtr as i32; + } + let wrapper = unsafe { &*ptr }; + let probs = match wrapper.inner.probabilities() { + Ok(p) => p, + Err(err) => return qis_err_code(&err), + }; + if probs.len() != len { + return CqlibError::InvalidParam as i32; + } + unsafe { + std::ptr::copy_nonoverlapping(probs.as_ptr(), buffer, len); + } + 0 +} + +#[unsafe(no_mangle)] +pub extern "C" fn stabilizer_sample_shots( + ptr: *const CStabilizerState, + shots: usize, +) -> *mut COutcomeList { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let wrapper = unsafe { &*ptr }; + let n = wrapper.inner.num_qubits(); + let outcomes = wrapper.inner.sample_shots(shots); + let items = outcomes.into_iter().map(|o| o.to_bitstring(n)).collect(); + Box::into_raw(Box::new(COutcomeList { items })) +} + +#[unsafe(no_mangle)] +pub extern "C" fn stabilizer_num_qubits(ptr: *const CStabilizerState) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.num_qubits() } +} + +/// Compute the Pauli expectation value

for the given Pauli string. +/// Writes the value (-1, 0, or +1) to `out` and returns 0 on success. +#[unsafe(no_mangle)] +pub extern "C" fn stabilizer_pauli_expectation( + ptr: *const CStabilizerState, + pauli: *const CPauliString, + out: *mut i32, +) -> i32 { + if ptr.is_null() || pauli.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + let wrapper = unsafe { &*ptr }; + let p = unsafe { &(*pauli).inner }; + match wrapper.inner.pauli_expectation(p) { + Ok(value) => { + unsafe { *out = value }; + 0 + } + Err(err) => qis_err_code(&err), + } +} + +/// Return the number of stabilizer generators (always N), or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn stabilizer_get_stabilizers_len(ptr: *const CStabilizerState) -> usize { + if ptr.is_null() { + return 0; + } + let wrapper = unsafe { &*ptr }; + wrapper.inner.num_qubits() +} + +/// Return the stabilizer generator at `index` as a heap-allocated bitstring. +/// Caller must free with `cqlib_string_free`. Returns NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn stabilizer_get_stabilizer( + ptr: *const CStabilizerState, + index: usize, +) -> *mut c_char { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let wrapper = unsafe { &*ptr }; + let stabilizers = wrapper.inner.get_stabilizers(); + match stabilizers.get(index) { + Some(pauli) => { + // Display uses the same convention as a bitstring: +XYZ format + // where Y counts as a stabilizer label. + let label = pauli.to_string(); + match CString::new(label) { + Ok(cs) => cs.into_raw(), + Err(_) => std::ptr::null_mut(), + } + } + None => std::ptr::null_mut(), + } +} + +// ===== Simulator supplement (checklist 2.5) ===== + +use std::ffi::CStr; + +use crate::device::standard_gate_from_name; + +/// Resolves a NUL-terminated standard-gate name (e.g. "H", "CX", "SWAP"). +/// Returns -1 for NULL input and -8 for an unknown name or invalid UTF-8. +fn parse_standard_gate(name: *const c_char) -> Result { + if name.is_null() { + return Err(CqlibError::NullPtr as i32); + } + let name = unsafe { CStr::from_ptr(name) }; + let name = match name.to_str() { + Ok(s) => s, + Err(_) => return Err(CqlibError::InvalidParam as i32), + }; + standard_gate_from_name(name).ok_or(CqlibError::InvalidParam as i32) +} + +/// Collects and bounds-checks a qubit-index array for this simulator. +fn resolve_qubits(num_qubits: usize, qubits: *const u32, len: usize) -> Result, i32> { + let slice = unsafe { std::slice::from_raw_parts(qubits, len) }; + slice.iter().map(|&q| check_qubit(num_qubits, q)).collect() +} + +/// Builds the gate-parameter slice, validating that every value is finite. +/// A NULL pointer is allowed when `num_params` is zero. +fn resolve_params(params: *const f64, num_params: usize) -> Result, i32> { + if num_params == 0 { + return Ok(Vec::new()); + } + if params.is_null() { + return Err(CqlibError::NullPtr as i32); + } + let slice = unsafe { std::slice::from_raw_parts(params, num_params) }; + if slice.iter().any(|v| !v.is_finite()) { + return Err(CqlibError::InvalidParam as i32); + } + Ok(slice.to_vec()) +} + +/// Apply a Clifford standard gate identified by name (e.g. "H", "CX", "SWAP"). +/// Non-Clifford gates are rejected by the simulator. +/// +/// `qubits` holds `num_target_qubits` target indices and `params` holds +/// `num_params` gate parameters (NULL is allowed when `num_params` is 0). +/// Returns 0 on success, -1 for NULL pointers, -2 for an out-of-bounds qubit, +/// -8 for an unknown gate name or invalid argument counts, -7 for a +/// non-Clifford gate. +#[unsafe(no_mangle)] +pub extern "C" fn stabilizer_apply_standard_gate( + ptr: *mut CStabilizerState, + gate_name: *const c_char, + qubits: *const u32, + num_target_qubits: usize, + params: *const f64, + num_params: usize, +) -> i32 { + if ptr.is_null() || qubits.is_null() { + return CqlibError::NullPtr as i32; + } + let gate = match parse_standard_gate(gate_name) { + Ok(gate) => gate, + Err(code) => return code, + }; + let wrapper = unsafe { &mut *ptr }; + let targets = match resolve_qubits(wrapper.inner.num_qubits(), qubits, num_target_qubits) { + Ok(targets) => targets, + Err(code) => return code, + }; + let params = match resolve_params(params, num_params) { + Ok(params) => params, + Err(code) => return code, + }; + wrapper + .inner + .apply_standard_gate(gate, &targets, ¶ms) + .map_err(map_err) + .map_or_else(|e| e, |_| 0) +} + +/// Return the number of destabilizer generators (always N), or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn stabilizer_get_destabilizers_len(ptr: *const CStabilizerState) -> usize { + if ptr.is_null() { + return 0; + } + let wrapper = unsafe { &*ptr }; + wrapper.inner.num_qubits() +} + +/// Return the destabilizer generator at `index` as a heap-allocated Pauli +/// label (same "+XYZ" format as `stabilizer_get_stabilizer`). Caller must free +/// with `cqlib_string_free`. Returns NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn stabilizer_get_destabilizer( + ptr: *const CStabilizerState, + index: usize, +) -> *mut c_char { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let wrapper = unsafe { &*ptr }; + let destabilizers = wrapper.inner.get_destabilizers(); + match destabilizers.get(index) { + Some(pauli) => match CString::new(pauli.to_string()) { + Ok(cs) => cs.into_raw(), + Err(_) => std::ptr::null_mut(), + }, + None => std::ptr::null_mut(), + } +} + +/// Return the probability of measuring a specific computational-basis +/// outcome. `bits` is an array of `len` booleans where `bits[0]` is qubit 0; +/// `len` must equal the number of qubits. Writes the probability to `out` and +/// returns 0 on success, -1 on NULL pointers, -8 on a length mismatch. +#[unsafe(no_mangle)] +pub extern "C" fn stabilizer_probability_of( + ptr: *const CStabilizerState, + bits: *const bool, + len: usize, + out: *mut f64, +) -> i32 { + if ptr.is_null() || bits.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + let wrapper = unsafe { &*ptr }; + let bits = unsafe { std::slice::from_raw_parts(bits, len) }; + match wrapper.inner.probability_of(bits) { + Ok(probability) => { + unsafe { *out = probability }; + 0 + } + Err(err) => qis_err_code(&err), + } +} + +/// Execute a Clifford circuit from |0...0> and return the final state. +/// +/// Unlike `stabilizer_from_circuit` / `stabilizer_apply_circuit`, which ignore +/// terminal measurement declarations, this entry point executes `MeasureBit` +/// and `MeasureBits` operations, so the returned state is already collapsed. +/// Runtime classical results are not yet exposed through the C ABI. +/// Returns NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn stabilizer_run_circuit(circuit: *const CCircuit) -> *mut CStabilizerState { + if circuit.is_null() { + return std::ptr::null_mut(); + } + let circuit = unsafe { &(*circuit).inner }; + match StabilizerState::run_circuit(circuit) { + Ok(result) => Box::into_raw(Box::new(CStabilizerState { + inner: result.state, + })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Export the stabilizer generators in Stim-compatible text format (one +/// "+XYZ" Pauli label per line). Caller must free with `cqlib_string_free`. +/// Returns NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn stabilizer_to_stim_format(ptr: *const CStabilizerState) -> *mut c_char { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let wrapper = unsafe { &*ptr }; + match CString::new(wrapper.inner.to_stim_format()) { + Ok(cs) => cs.into_raw(), + Err(_) => std::ptr::null_mut(), + } +} diff --git a/crates/binding-c/src/qis/statevector.rs b/crates/binding-c/src/qis/statevector.rs new file mode 100644 index 00000000..cdb492ca --- /dev/null +++ b/crates/binding-c/src/qis/statevector.rs @@ -0,0 +1,820 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! C ABI for `Statevector` simulator. + +use crate::circuit::CCircuit; +use crate::error::CqlibError; +use crate::qis::{COutcomeList, CStatevector, check_qubit, qis_err_code}; +use cqlib_core::qis::{Observable, Statevector}; +use std::ffi::CString; +use std::os::raw::c_char; + +#[inline] +fn map_err(err: cqlib_core::qis::QisError) -> i32 { + qis_err_code(&err) +} + +/// Apply a single-qubit method that takes only a qubit index. +fn apply_single( + ptr: *mut CStatevector, + qubit: u32, + f: impl FnOnce(&mut Statevector, usize) -> Result<(), cqlib_core::qis::QisError>, +) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + let wrapper = unsafe { &mut *ptr }; + let q = match check_qubit(wrapper.inner.num_qubits, qubit) { + Ok(q) => q, + Err(code) => return code, + }; + f(&mut wrapper.inner, q) + .map_err(map_err) + .map_or_else(|e| e, |_| 0) +} + +/// Apply a two-qubit method. +fn apply_two( + ptr: *mut CStatevector, + q0: u32, + q1: u32, + f: impl FnOnce(&mut Statevector, usize, usize) -> Result<(), cqlib_core::qis::QisError>, +) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + let wrapper = unsafe { &mut *ptr }; + let a = match check_qubit(wrapper.inner.num_qubits, q0) { + Ok(q) => q, + Err(code) => return code, + }; + let b = match check_qubit(wrapper.inner.num_qubits, q1) { + Ok(q) => q, + Err(code) => return code, + }; + f(&mut wrapper.inner, a, b) + .map_err(map_err) + .map_or_else(|e| e, |_| 0) +} + +/// Apply a three-qubit method. +fn apply_three( + ptr: *mut CStatevector, + q0: u32, + q1: u32, + q2: u32, + f: impl FnOnce(&mut Statevector, usize, usize, usize) -> Result<(), cqlib_core::qis::QisError>, +) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + let wrapper = unsafe { &mut *ptr }; + let a = match check_qubit(wrapper.inner.num_qubits, q0) { + Ok(q) => q, + Err(code) => return code, + }; + let b = match check_qubit(wrapper.inner.num_qubits, q1) { + Ok(q) => q, + Err(code) => return code, + }; + let c = match check_qubit(wrapper.inner.num_qubits, q2) { + Ok(q) => q, + Err(code) => return code, + }; + f(&mut wrapper.inner, a, b, c) + .map_err(map_err) + .map_or_else(|e| e, |_| 0) +} + +/// Create a new statevector with `num_qubits` initialised to |0...0>. +#[unsafe(no_mangle)] +pub extern "C" fn statevector_new(num_qubits: usize) -> *mut CStatevector { + Box::into_raw(Box::new(CStatevector { + inner: Statevector::new(num_qubits), + })) +} + +/// Free a statevector. NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn statevector_free(ptr: *mut CStatevector) { + if !ptr.is_null() { + unsafe { + let _ = Box::from_raw(ptr); + } + } +} + +/// Build a statevector by simulating `circuit`. Returns NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn statevector_from_circuit(circuit: *const CCircuit) -> *mut CStatevector { + if circuit.is_null() { + return std::ptr::null_mut(); + } + let circuit = unsafe { &(*circuit).inner }; + match Statevector::from_circuit(circuit) { + Ok(sv) => Box::into_raw(Box::new(CStatevector { inner: sv })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Apply `circuit` to the statevector in place. Returns 0 on success. +#[unsafe(no_mangle)] +pub extern "C" fn statevector_apply_circuit( + ptr: *mut CStatevector, + circuit: *const CCircuit, +) -> i32 { + if ptr.is_null() || circuit.is_null() { + return CqlibError::NullPtr as i32; + } + let wrapper = unsafe { &mut *ptr }; + let circuit = unsafe { &(*circuit).inner }; + wrapper + .inner + .apply_circuit(circuit) + .map_err(map_err) + .map_or_else(|e| e, |_| 0) +} + +#[unsafe(no_mangle)] +pub extern "C" fn statevector_apply_h(ptr: *mut CStatevector, qubit: u32) -> i32 { + apply_single(ptr, qubit, Statevector::apply_h) +} + +#[unsafe(no_mangle)] +pub extern "C" fn statevector_apply_x(ptr: *mut CStatevector, qubit: u32) -> i32 { + apply_single(ptr, qubit, Statevector::apply_x) +} + +#[unsafe(no_mangle)] +pub extern "C" fn statevector_apply_y(ptr: *mut CStatevector, qubit: u32) -> i32 { + apply_single(ptr, qubit, Statevector::apply_y) +} + +#[unsafe(no_mangle)] +pub extern "C" fn statevector_apply_z(ptr: *mut CStatevector, qubit: u32) -> i32 { + apply_single(ptr, qubit, Statevector::apply_z) +} + +#[unsafe(no_mangle)] +pub extern "C" fn statevector_apply_s(ptr: *mut CStatevector, qubit: u32) -> i32 { + apply_single(ptr, qubit, Statevector::apply_s) +} + +#[unsafe(no_mangle)] +pub extern "C" fn statevector_apply_sdg(ptr: *mut CStatevector, qubit: u32) -> i32 { + apply_single(ptr, qubit, Statevector::apply_sdg) +} + +#[unsafe(no_mangle)] +pub extern "C" fn statevector_apply_t(ptr: *mut CStatevector, qubit: u32) -> i32 { + apply_single(ptr, qubit, Statevector::apply_t) +} + +#[unsafe(no_mangle)] +pub extern "C" fn statevector_apply_tdg(ptr: *mut CStatevector, qubit: u32) -> i32 { + apply_single(ptr, qubit, Statevector::apply_tdg) +} + +#[unsafe(no_mangle)] +pub extern "C" fn statevector_apply_x2p(ptr: *mut CStatevector, qubit: u32) -> i32 { + apply_single(ptr, qubit, Statevector::apply_x2p) +} + +#[unsafe(no_mangle)] +pub extern "C" fn statevector_apply_x2m(ptr: *mut CStatevector, qubit: u32) -> i32 { + apply_single(ptr, qubit, Statevector::apply_x2m) +} + +#[unsafe(no_mangle)] +pub extern "C" fn statevector_apply_y2p(ptr: *mut CStatevector, qubit: u32) -> i32 { + apply_single(ptr, qubit, Statevector::apply_y2p) +} + +#[unsafe(no_mangle)] +pub extern "C" fn statevector_apply_y2m(ptr: *mut CStatevector, qubit: u32) -> i32 { + apply_single(ptr, qubit, Statevector::apply_y2m) +} + +#[unsafe(no_mangle)] +pub extern "C" fn statevector_apply_rx(ptr: *mut CStatevector, qubit: u32, theta: f64) -> i32 { + if !theta.is_finite() { + return CqlibError::InvalidParam as i32; + } + apply_single(ptr, qubit, |sv, q| sv.apply_rx(q, theta)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn statevector_apply_ry(ptr: *mut CStatevector, qubit: u32, theta: f64) -> i32 { + if !theta.is_finite() { + return CqlibError::InvalidParam as i32; + } + apply_single(ptr, qubit, |sv, q| sv.apply_ry(q, theta)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn statevector_apply_rz(ptr: *mut CStatevector, qubit: u32, theta: f64) -> i32 { + if !theta.is_finite() { + return CqlibError::InvalidParam as i32; + } + apply_single(ptr, qubit, |sv, q| sv.apply_rz(q, theta)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn statevector_apply_phase(ptr: *mut CStatevector, qubit: u32, theta: f64) -> i32 { + if !theta.is_finite() { + return CqlibError::InvalidParam as i32; + } + apply_single(ptr, qubit, |sv, q| sv.apply_phase(q, theta)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn statevector_apply_u( + ptr: *mut CStatevector, + qubit: u32, + theta: f64, + phi: f64, + lambda: f64, +) -> i32 { + if !theta.is_finite() || !phi.is_finite() || !lambda.is_finite() { + return CqlibError::InvalidParam as i32; + } + apply_single(ptr, qubit, |sv, q| sv.apply_u(q, theta, phi, lambda)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn statevector_apply_xy(ptr: *mut CStatevector, qubit: u32, theta: f64) -> i32 { + if !theta.is_finite() { + return CqlibError::InvalidParam as i32; + } + apply_single(ptr, qubit, |sv, q| sv.apply_xy(q, theta)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn statevector_apply_xy2p(ptr: *mut CStatevector, qubit: u32, theta: f64) -> i32 { + if !theta.is_finite() { + return CqlibError::InvalidParam as i32; + } + apply_single(ptr, qubit, |sv, q| sv.apply_xy2p(q, theta)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn statevector_apply_xy2m(ptr: *mut CStatevector, qubit: u32, theta: f64) -> i32 { + if !theta.is_finite() { + return CqlibError::InvalidParam as i32; + } + apply_single(ptr, qubit, |sv, q| sv.apply_xy2m(q, theta)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn statevector_apply_rxy( + ptr: *mut CStatevector, + qubit: u32, + theta: f64, + phi: f64, +) -> i32 { + if !theta.is_finite() || !phi.is_finite() { + return CqlibError::InvalidParam as i32; + } + apply_single(ptr, qubit, |sv, q| sv.apply_rxy(q, theta, phi)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn statevector_apply_gphase(ptr: *mut CStatevector, phi: f64) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + if !phi.is_finite() { + return CqlibError::InvalidParam as i32; + } + let wrapper = unsafe { &mut *ptr }; + wrapper + .inner + .apply_gphase(phi) + .map_err(map_err) + .map_or_else(|e| e, |_| 0) +} + +#[unsafe(no_mangle)] +pub extern "C" fn statevector_apply_cx(ptr: *mut CStatevector, control: u32, target: u32) -> i32 { + apply_two(ptr, control, target, Statevector::apply_cx) +} + +#[unsafe(no_mangle)] +pub extern "C" fn statevector_apply_cy(ptr: *mut CStatevector, control: u32, target: u32) -> i32 { + apply_two(ptr, control, target, Statevector::apply_cy) +} + +#[unsafe(no_mangle)] +pub extern "C" fn statevector_apply_cz(ptr: *mut CStatevector, q0: u32, q1: u32) -> i32 { + apply_two(ptr, q0, q1, Statevector::apply_cz) +} + +#[unsafe(no_mangle)] +pub extern "C" fn statevector_apply_swap(ptr: *mut CStatevector, q0: u32, q1: u32) -> i32 { + apply_two(ptr, q0, q1, Statevector::apply_swap) +} + +#[unsafe(no_mangle)] +pub extern "C" fn statevector_apply_ccx( + ptr: *mut CStatevector, + c0: u32, + c1: u32, + target: u32, +) -> i32 { + apply_three(ptr, c0, c1, target, Statevector::apply_ccx) +} + +#[unsafe(no_mangle)] +pub extern "C" fn statevector_apply_rxx( + ptr: *mut CStatevector, + q0: u32, + q1: u32, + theta: f64, +) -> i32 { + if !theta.is_finite() { + return CqlibError::InvalidParam as i32; + } + apply_two(ptr, q0, q1, |sv, a, b| sv.apply_rxx(a, b, theta)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn statevector_apply_ryy( + ptr: *mut CStatevector, + q0: u32, + q1: u32, + theta: f64, +) -> i32 { + if !theta.is_finite() { + return CqlibError::InvalidParam as i32; + } + apply_two(ptr, q0, q1, |sv, a, b| sv.apply_ryy(a, b, theta)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn statevector_apply_rzz( + ptr: *mut CStatevector, + q0: u32, + q1: u32, + theta: f64, +) -> i32 { + if !theta.is_finite() { + return CqlibError::InvalidParam as i32; + } + apply_two(ptr, q0, q1, |sv, a, b| sv.apply_rzz(a, b, theta)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn statevector_apply_rzx( + ptr: *mut CStatevector, + q0: u32, + q1: u32, + theta: f64, +) -> i32 { + if !theta.is_finite() { + return CqlibError::InvalidParam as i32; + } + apply_two(ptr, q0, q1, |sv, a, b| sv.apply_rzx(a, b, theta)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn statevector_apply_crx( + ptr: *mut CStatevector, + control: u32, + target: u32, + theta: f64, +) -> i32 { + if !theta.is_finite() { + return CqlibError::InvalidParam as i32; + } + apply_two(ptr, control, target, |sv, a, b| sv.apply_crx(a, b, theta)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn statevector_apply_cry( + ptr: *mut CStatevector, + control: u32, + target: u32, + theta: f64, +) -> i32 { + if !theta.is_finite() { + return CqlibError::InvalidParam as i32; + } + apply_two(ptr, control, target, |sv, a, b| sv.apply_cry(a, b, theta)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn statevector_apply_crz( + ptr: *mut CStatevector, + control: u32, + target: u32, + theta: f64, +) -> i32 { + if !theta.is_finite() { + return CqlibError::InvalidParam as i32; + } + apply_two(ptr, control, target, |sv, a, b| sv.apply_crz(a, b, theta)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn statevector_apply_fsim( + ptr: *mut CStatevector, + q0: u32, + q1: u32, + theta: f64, + phi: f64, +) -> i32 { + if !theta.is_finite() || !phi.is_finite() { + return CqlibError::InvalidParam as i32; + } + apply_two(ptr, q0, q1, |sv, a, b| sv.apply_fsim(a, b, theta, phi)) +} + +/// Return the number of basis-state amplitudes (2^N), or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn statevector_probabilities_len(ptr: *const CStatevector) -> usize { + if ptr.is_null() { + return 0; + } + let wrapper = unsafe { &*ptr }; + 1usize << wrapper.inner.num_qubits +} + +/// Copy the probability distribution into `buffer`. +/// Returns 0 on success, or a negative error code. +#[unsafe(no_mangle)] +pub extern "C" fn statevector_probabilities( + ptr: *const CStatevector, + buffer: *mut f64, + len: usize, +) -> i32 { + if ptr.is_null() || buffer.is_null() { + return CqlibError::NullPtr as i32; + } + let wrapper = unsafe { &*ptr }; + let probs = wrapper.inner.probabilities(); + if probs.len() != len { + return CqlibError::InvalidParam as i32; + } + unsafe { + std::ptr::copy_nonoverlapping(probs.as_ptr(), buffer, len); + } + 0 +} + +/// Compute the expectation value `` of an observable. +/// Writes the value to `out` and returns 0 on success. +#[unsafe(no_mangle)] +pub extern "C" fn statevector_expectation( + ptr: *const CStatevector, + observable: *const crate::qis::CHamiltonian, + out: *mut f64, +) -> i32 { + if ptr.is_null() || observable.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + let wrapper = unsafe { &*ptr }; + let h = unsafe { &(*observable).inner }; + let result = wrapper.inner.expectation(h as &dyn Observable); + match result { + Ok(value) => { + unsafe { *out = value }; + 0 + } + Err(err) => qis_err_code(&err), + } +} + +/// Measure `qubit` in the Z basis, collapsing the state. +/// Returns 0 for outcome |0>, 1 for outcome |1>, or a negative error code. +#[unsafe(no_mangle)] +pub extern "C" fn statevector_measure(ptr: *mut CStatevector, qubit: u32) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + let wrapper = unsafe { &mut *ptr }; + let q = match check_qubit(wrapper.inner.num_qubits, qubit) { + Ok(q) => q, + Err(code) => return code, + }; + match wrapper.inner.measure(q) { + Ok(true) => 1, + Ok(false) => 0, + Err(err) => qis_err_code(&err), + } +} + +/// Measure all qubits and return the outcome as a heap-allocated big-endian +/// bitstring (MSB = qubit N-1, LSB = qubit 0). Caller must free with +/// `cqlib_string_free`. Returns NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn statevector_measure_all(ptr: *mut CStatevector) -> *mut c_char { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let wrapper = unsafe { &mut *ptr }; + let outcome = wrapper.inner.measure_all(); + let bitstring = outcome.to_bitstring(wrapper.inner.num_qubits); + match CString::new(bitstring) { + Ok(cs) => cs.into_raw(), + Err(_) => std::ptr::null_mut(), + } +} + +/// Sample `shots` independent measurement outcomes. +/// Returns a heap-allocated `COutcomeList*` (caller must free with +/// `outcome_list_free`), or NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn statevector_sample_shots( + ptr: *const CStatevector, + shots: usize, +) -> *mut COutcomeList { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let wrapper = unsafe { &*ptr }; + let n = wrapper.inner.num_qubits; + let outcomes = wrapper.inner.sample_shots(shots); + let items = outcomes.into_iter().map(|o| o.to_bitstring(n)).collect(); + Box::into_raw(Box::new(COutcomeList { items })) +} + +/// Reset `qubit` to |0>. Returns 0 on success. +#[unsafe(no_mangle)] +pub extern "C" fn statevector_reset(ptr: *mut CStatevector, qubit: u32) -> i32 { + apply_single(ptr, qubit, Statevector::reset) +} + +/// Return the number of qubits, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn statevector_num_qubits(ptr: *const CStatevector) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.num_qubits } +} + +/// Free an `COutcomeList` allocated by `*_sample_shots`. NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn outcome_list_free(ptr: *mut COutcomeList) { + if !ptr.is_null() { + unsafe { + let _ = Box::from_raw(ptr); + } + } +} + +/// Return the number of outcomes in the list, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn outcome_list_len(ptr: *const COutcomeList) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).items.len() } +} + +/// Return the bitstring at `index` as a heap-allocated C string. +/// Caller must free with `cqlib_string_free`. Returns NULL on out-of-bounds. +#[unsafe(no_mangle)] +pub extern "C" fn outcome_list_get(ptr: *const COutcomeList, index: usize) -> *mut c_char { + if ptr.is_null() { + return std::ptr::null_mut(); + } + let list = unsafe { &*ptr }; + match list.items.get(index) { + Some(s) => match CString::new(s.as_str()) { + Ok(cs) => cs.into_raw(), + Err(_) => std::ptr::null_mut(), + }, + None => std::ptr::null_mut(), + } +} + +// ===== Simulator supplement (checklist 2.5) ===== + +use num_complex::Complex64; +use std::ffi::CStr; + +use crate::device::standard_gate_from_name; +use crate::qis::CPauliString; + +/// Resolves a NUL-terminated standard-gate name (e.g. "H", "CX", "RZZ"). +/// Returns -1 for NULL input and -8 for an unknown name or invalid UTF-8. +fn parse_standard_gate(name: *const c_char) -> Result { + if name.is_null() { + return Err(CqlibError::NullPtr as i32); + } + let name = unsafe { CStr::from_ptr(name) }; + let name = match name.to_str() { + Ok(s) => s, + Err(_) => return Err(CqlibError::InvalidParam as i32), + }; + standard_gate_from_name(name).ok_or(CqlibError::InvalidParam as i32) +} + +/// Collects and bounds-checks a qubit-index array for this simulator. +fn resolve_qubits(num_qubits: usize, qubits: *const u32, len: usize) -> Result, i32> { + let slice = unsafe { std::slice::from_raw_parts(qubits, len) }; + slice.iter().map(|&q| check_qubit(num_qubits, q)).collect() +} + +/// Builds the gate-parameter slice, validating that every value is finite. +/// A NULL pointer is allowed when `num_params` is zero. +fn resolve_params(params: *const f64, num_params: usize) -> Result, i32> { + if num_params == 0 { + return Ok(Vec::new()); + } + if params.is_null() { + return Err(CqlibError::NullPtr as i32); + } + let slice = unsafe { std::slice::from_raw_parts(params, num_params) }; + if slice.iter().any(|v| !v.is_finite()) { + return Err(CqlibError::InvalidParam as i32); + } + Ok(slice.to_vec()) +} + +/// Return the number of complex amplitudes (2^N), or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn statevector_data_len(ptr: *const CStatevector) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.data().len() } +} + +/// Copy the complex amplitudes into `buffer` (2^N Complex64 values). +/// Returns 0 on success, -1 on NULL pointers, -8 when `len` does not equal +/// `statevector_data_len`. +#[unsafe(no_mangle)] +pub extern "C" fn statevector_data( + ptr: *const CStatevector, + buffer: *mut Complex64, + len: usize, +) -> i32 { + if ptr.is_null() || buffer.is_null() { + return CqlibError::NullPtr as i32; + } + let data = unsafe { (*ptr).inner.data() }; + if data.len() != len { + return CqlibError::InvalidParam as i32; + } + unsafe { + std::ptr::copy_nonoverlapping(data.as_ptr(), buffer, len); + } + 0 +} + +/// Overwrite the complex amplitudes by copying `len` Complex64 values from +/// `buffer` into the statevector. This is the writable two-step counterpart +/// of `statevector_data` and mirrors `Statevector::data_mut`. +/// +/// `len` must equal `statevector_data_len`. Overwriting the amplitudes can +/// break normalization; the caller is responsible for preserving a valid +/// statevector. Returns 0 on success, -1 on NULL pointers, -8 when `len` +/// does not equal `statevector_data_len`. +#[unsafe(no_mangle)] +pub extern "C" fn statevector_data_mut( + ptr: *mut CStatevector, + buffer: *const Complex64, + len: usize, +) -> i32 { + if ptr.is_null() || buffer.is_null() { + return CqlibError::NullPtr as i32; + } + let wrapper = unsafe { &mut *ptr }; + let data = wrapper.inner.data_mut(); + if data.len() != len { + return CqlibError::InvalidParam as i32; + } + unsafe { + std::ptr::copy_nonoverlapping(buffer, data.as_mut_ptr(), len); + } + 0 +} + +/// Create a statevector from a normalized amplitude array of 2^N Complex64 +/// values. Returns NULL on error (NULL input, wrong dimension, or a state +/// that is not normalized). +#[unsafe(no_mangle)] +pub extern "C" fn statevector_from_state( + num_qubits: usize, + initial_state: *const Complex64, + len: usize, +) -> *mut CStatevector { + if initial_state.is_null() { + return std::ptr::null_mut(); + } + let amplitudes = unsafe { std::slice::from_raw_parts(initial_state, len) }.to_vec(); + match Statevector::from_state(num_qubits, amplitudes) { + Ok(sv) => Box::into_raw(Box::new(CStatevector { inner: sv })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Apply a standard gate identified by name (e.g. "H", "CX", "RZZ", "FSIM"). +/// +/// `qubits` holds `num_target_qubits` target indices and `params` holds +/// `num_params` gate parameters (NULL is allowed when `num_params` is 0). +/// Returns 0 on success, -1 for NULL pointers, -2 for an out-of-bounds qubit, +/// -8 for an unknown gate name or invalid argument counts. +#[unsafe(no_mangle)] +pub extern "C" fn statevector_apply_standard_gate( + ptr: *mut CStatevector, + gate_name: *const c_char, + qubits: *const u32, + num_target_qubits: usize, + params: *const f64, + num_params: usize, +) -> i32 { + if ptr.is_null() || qubits.is_null() { + return CqlibError::NullPtr as i32; + } + let gate = match parse_standard_gate(gate_name) { + Ok(gate) => gate, + Err(code) => return code, + }; + let wrapper = unsafe { &mut *ptr }; + let targets = match resolve_qubits(wrapper.inner.num_qubits, qubits, num_target_qubits) { + Ok(targets) => targets, + Err(code) => return code, + }; + let params = match resolve_params(params, num_params) { + Ok(params) => params, + Err(code) => return code, + }; + wrapper + .inner + .apply_standard_gate(gate, &targets, ¶ms) + .map_err(map_err) + .map_or_else(|e| e, |_| 0) +} + +/// Apply a native Pauli-string rotation `exp(-i * theta / 2 * P)` in place. +/// The Pauli string must span the full register and be Hermitian. +/// Returns 0 on success or a negative error code. +#[unsafe(no_mangle)] +pub extern "C" fn statevector_apply_pauli_rotation( + ptr: *mut CStatevector, + pauli: *const CPauliString, + theta: f64, +) -> i32 { + if ptr.is_null() || pauli.is_null() { + return CqlibError::NullPtr as i32; + } + if !theta.is_finite() { + return CqlibError::InvalidParam as i32; + } + let wrapper = unsafe { &mut *ptr }; + let pauli = unsafe { &(*pauli).inner }; + wrapper + .inner + .apply_pauli_rotation(pauli, theta) + .map_err(map_err) + .map_or_else(|e| e, |_| 0) +} + +/// Apply an arbitrary unitary matrix to `qubits` in place. +/// +/// `matrix` is a flattened row-major `dim x dim` Complex64 array where +/// `dim = 2^num_target_qubits`. Qubit indices must not repeat. Returns 0 on +/// success or a negative error code. +#[unsafe(no_mangle)] +pub extern "C" fn statevector_apply_unitary_gate( + ptr: *mut CStatevector, + qubits: *const u32, + num_target_qubits: usize, + matrix: *const Complex64, + dim: usize, +) -> i32 { + if ptr.is_null() || qubits.is_null() || matrix.is_null() { + return CqlibError::NullPtr as i32; + } + if num_target_qubits == 0 || dim == 0 { + return CqlibError::InvalidParam as i32; + } + let wrapper = unsafe { &mut *ptr }; + let targets = match resolve_qubits(wrapper.inner.num_qubits, qubits, num_target_qubits) { + Ok(targets) => targets, + Err(code) => return code, + }; + let flat = unsafe { std::slice::from_raw_parts(matrix, dim * dim) }; + let matrix = match ndarray::Array2::from_shape_vec((dim, dim), flat.to_vec()) { + Ok(matrix) => matrix, + Err(_) => return CqlibError::InvalidParam as i32, + }; + wrapper + .inner + .apply_unitary_gate(&targets, &matrix) + .map_err(map_err) + .map_or_else(|e| e, |_| 0) +} diff --git a/crates/binding-c/src/visualization/circuit_render.rs b/crates/binding-c/src/visualization/circuit_render.rs new file mode 100644 index 00000000..cdaa1706 --- /dev/null +++ b/crates/binding-c/src/visualization/circuit_render.rs @@ -0,0 +1,168 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! C ABI entry points for circuit rendering and matrix export. + +use crate::circuit::CCircuit; +use crate::error::CqlibError; +use cqlib_core::visualization::{FigureDrawerOptions, TextDrawerOptions}; +use std::ffi::{CStr, CString}; +use std::os::raw::c_char; + +/// C-side text drawing options. Mirrors the subset of `TextDrawerOptions` +/// that makes sense from C; unset fields use library defaults. +#[repr(C)] +pub struct TextDrawerOptionsC { + /// Whether to append parameter text to gate labels (0/1). + pub show_params: u8, + /// Whether to decompose circuit-gates before drawing (0/1). + pub decompose_circuit_gates: u8, + /// Max width per wrapped segment; `<= 0` disables wrapping. + pub line_width: isize, + /// Whether to show `|0>` at the start of each qubit wire (0/1). + pub initial_state: u8, + /// Whether to reverse the displayed qubit order (0/1). + pub reverse_bits: u8, +} + +/// C-side figure drawing options. Mirrors the subset of +/// `FigureDrawerOptions` that makes sense from C. +#[repr(C)] +pub struct FigureDrawerOptionsC { + /// Whether to append parameter text to gate labels (0/1). + pub show_params: u8, + /// Whether to decompose circuit-gates before drawing (0/1). + pub decompose_circuit_gates: u8, + /// Figure width scale per logical column. + pub width_per_column: f64, + /// Figure height scale per qubit. + pub height_per_qubit: f64, + /// Rasterization DPI when exporting PNG (must be > 0). + pub dpi: u32, + /// Maximum columns per row; `<= 0` disables folding. + pub fold: i32, + /// Whether to show `|0>` in qubit labels (0/1). + pub initial_state: u8, + /// Whether to reverse display order of qubits (0/1). + pub reverse_bits: u8, +} + +fn text_options_from_c(options: *const TextDrawerOptionsC) -> TextDrawerOptions { + let defaults = TextDrawerOptions::default(); + let Some(options) = (unsafe { options.as_ref() }) else { + return defaults; + }; + TextDrawerOptions { + cell_width: defaults.cell_width, + show_params: options.show_params != 0, + decompose_circuit_gates: options.decompose_circuit_gates != 0, + line_width: options.line_width, + initial_state: options.initial_state != 0, + reverse_bits: options.reverse_bits != 0, + } +} + +fn figure_options_from_c(options: *const FigureDrawerOptionsC) -> FigureDrawerOptions { + let mut result = FigureDrawerOptions::default(); + let Some(options) = (unsafe { options.as_ref() }) else { + return result; + }; + result.show_params = options.show_params != 0; + result.decompose_circuit_gates = options.decompose_circuit_gates != 0; + if options.width_per_column > 0.0 { + result.width_per_column = options.width_per_column; + } + if options.height_per_qubit > 0.0 { + result.height_per_qubit = options.height_per_qubit; + } + result.dpi = options.dpi; + result.fold = if options.fold > 0 { options.fold } else { -1 }; + result.initial_state = options.initial_state != 0; + result.reverse_bits = options.reverse_bits != 0; + result +} + +/// Draws the circuit as UTF-8 text (box-drawing characters). +/// +/// `options` may be NULL to use library defaults. Returns a heap-allocated +/// C string; caller must free with `cqlib_string_free`. Returns NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_to_text( + circuit: *const CCircuit, + options: *const TextDrawerOptionsC, +) -> *mut c_char { + if circuit.is_null() { + return std::ptr::null_mut(); + } + let circuit = unsafe { &(*circuit).inner }; + let opts = text_options_from_c(options); + match cqlib_core::visualization::circuit_to_text(circuit, &opts) { + Ok(text) => match CString::new(text) { + Ok(cs) => cs.into_raw(), + Err(_) => std::ptr::null_mut(), + }, + Err(_) => std::ptr::null_mut(), + } +} + +/// Draws the circuit as an SVG figure. +/// +/// `options` may be NULL to use library defaults. Returns a heap-allocated +/// C string containing SVG markup; caller must free with +/// `cqlib_string_free`. Returns NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn circuit_to_figure( + circuit: *const CCircuit, + options: *const FigureDrawerOptionsC, +) -> *mut c_char { + if circuit.is_null() { + return std::ptr::null_mut(); + } + let circuit = unsafe { &(*circuit).inner }; + let opts = figure_options_from_c(options); + match cqlib_core::visualization::circuit_to_figure(circuit, &opts) { + Ok(svg) => match CString::new(svg) { + Ok(cs) => cs.into_raw(), + Err(_) => std::ptr::null_mut(), + }, + Err(_) => std::ptr::null_mut(), + } +} + +/// Renders the circuit to an output file (`.svg` or `.png`). +/// +/// Returns 0 on success, -5 on I/O or rendering failure, or -1 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn render_figure_to_file( + circuit: *const CCircuit, + output_path: *const c_char, + options: *const FigureDrawerOptionsC, +) -> i32 { + if circuit.is_null() || output_path.is_null() { + return CqlibError::NullPtr as i32; + } + let path = match unsafe { CStr::from_ptr(output_path) }.to_str() { + Ok(s) => s, + Err(_) => return CqlibError::ParseError as i32, + }; + let circuit = unsafe { &(*circuit).inner }; + let opts = figure_options_from_c(options); + match cqlib_core::visualization::render_figure_to_file(circuit, path, &opts) { + Ok(()) => 0, + Err(_) => CqlibError::IoError as i32, + } +} + +// Note: unitary-matrix export (`circuit_to_matrix(&circuit, qubits_order)`) +// is already covered by `circuit_to_matrix_len` / `circuit_to_matrix` +// in the circuit module (src/circuit/properties.rs), so no duplicate C +// symbols are defined here. diff --git a/crates/binding-c/src/visualization/mod.rs b/crates/binding-c/src/visualization/mod.rs new file mode 100644 index 00000000..7584b8da --- /dev/null +++ b/crates/binding-c/src/visualization/mod.rs @@ -0,0 +1,27 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! C ABI for circuit visualization (text drawing, SVG figure, matrix export). + +#![allow(clippy::not_unsafe_ptr_arg_deref)] + +pub mod circuit_render; +pub mod plot_render; +pub mod result_plot; +pub mod state_plot; +pub mod visual_ir; + +pub use circuit_render::*; +pub use plot_render::*; +pub use result_plot::*; +pub use state_plot::*; +pub use visual_ir::*; diff --git a/crates/binding-c/src/visualization/plot_render.rs b/crates/binding-c/src/visualization/plot_render.rs new file mode 100644 index 00000000..a5154969 --- /dev/null +++ b/crates/binding-c/src/visualization/plot_render.rs @@ -0,0 +1,73 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! C ABI for writing state and result plots to `.svg` / `.png` files. + +use crate::error::CqlibError; +use cqlib_core::visualization::VisualizationError; +use std::ffi::CStr; +use std::os::raw::c_char; + +/// Shared body for the two file-rendering entries: validates inputs, then +/// delegates to a core renderer that maps failures to `IoError`. +fn render_svg_file( + svg: *const c_char, + output_path: *const c_char, + render: impl FnOnce(&str, &str) -> Result<(), VisualizationError>, +) -> i32 { + if svg.is_null() || output_path.is_null() { + return CqlibError::NullPtr as i32; + } + let svg = match unsafe { CStr::from_ptr(svg) }.to_str() { + Ok(svg) => svg, + Err(_) => return CqlibError::ParseError as i32, + }; + let path = match unsafe { CStr::from_ptr(output_path) }.to_str() { + Ok(path) => path, + Err(_) => return CqlibError::ParseError as i32, + }; + match render(svg, path) { + Ok(()) => CqlibError::Ok as i32, + Err(_) => CqlibError::IoError as i32, + } +} + +/// Writes state-plot SVG markup to `output_path` (`.svg` writes vector +/// output, `.png` writes raster output). +/// +/// Returns 0 on success, -1 for NULL input, -4 for invalid UTF-8, or -5 on +/// I/O or rendering failure. +#[unsafe(no_mangle)] +pub extern "C" fn render_state_plot_to_file(svg: *const c_char, output_path: *const c_char) -> i32 { + render_svg_file( + svg, + output_path, + cqlib_core::visualization::render_state_plot_to_file, + ) +} + +/// Writes result-plot SVG markup to `output_path` (`.svg` writes vector +/// output, `.png` writes raster output). +/// +/// Returns 0 on success, -1 for NULL input, -4 for invalid UTF-8, or -5 on +/// I/O or rendering failure. +#[unsafe(no_mangle)] +pub extern "C" fn render_result_plot_to_file( + svg: *const c_char, + output_path: *const c_char, +) -> i32 { + render_svg_file( + svg, + output_path, + cqlib_core::visualization::render_result_plot_to_file, + ) +} diff --git a/crates/binding-c/src/visualization/result_plot.rs b/crates/binding-c/src/visualization/result_plot.rs new file mode 100644 index 00000000..7c3cf31b --- /dev/null +++ b/crates/binding-c/src/visualization/result_plot.rs @@ -0,0 +1,173 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! C ABI for measurement-result plotting (histograms, distributions). +//! +//! Core result entries accept an `ExecutionResult` handle directly, so the C +//! side takes a `CExecutionResult*` (see `src/device/result.rs` for handle +//! construction). Entries return heap-allocated SVG markup the caller frees +//! with `cqlib_string_free`. + +use crate::device::CExecutionResult; +use crate::error::CqlibError; +use cqlib_core::visualization::ResultPlotOptions; +use std::ffi::{CStr, CString}; +use std::os::raw::c_char; + +/// C-side result-plot options. Mirrors the fields of core +/// `ResultPlotOptions`; NULL optional fields use library defaults. +#[repr(C)] +pub struct CResultPlotOptions { + /// Non-zero when `fig_width` / `fig_height` carry a custom figure size. + pub has_figsize: u8, + /// Custom figure width in inch-like units; ignored when `has_figsize == 0`. + pub fig_width: f64, + /// Custom figure height in inch-like units; ignored when `has_figsize == 0`. + pub fig_height: f64, + /// Per-dataset colors; NULL or `color_len == 0` uses the built-in palette. + pub color: *const *const c_char, + /// Number of entries in `color`. + pub color_len: usize, + /// Keep the largest k bars and aggregate the rest into `rest`; + /// negative disables aggregation. + pub number_to_keep: isize, + /// Sort policy: `asc`, `desc`, `value`, `value_desc`, or `hamming`; + /// NULL defaults to `asc`. + pub sort: *const c_char, + /// Target bitstring used by `sort = "hamming"`; NULL means unset. + pub target_string: *const c_char, + /// Optional legend entries; NULL draws no legend. + pub legend: *const *const c_char, + /// Number of entries in `legend`. + pub legend_len: usize, + /// Whether to draw numeric labels above bars (0/1). + pub bar_labels: u8, + /// Optional chart title; NULL draws no title. + pub title: *const c_char, +} + +/// Reads a UTF-8 C string; maps invalid UTF-8 to `ParseError`. +unsafe fn utf8_str<'a>(ptr: *const c_char) -> Result<&'a str, CqlibError> { + unsafe { CStr::from_ptr(ptr) } + .to_str() + .map_err(|_| CqlibError::ParseError) +} + +/// Converts C-side result-plot options; a NULL pointer maps to defaults. +fn result_options_from_c( + options: *const CResultPlotOptions, +) -> Result { + let Some(options) = (unsafe { options.as_ref() }) else { + return Ok(ResultPlotOptions::default()); + }; + let mut result = ResultPlotOptions::default(); + if options.has_figsize != 0 { + result.figsize = Some((options.fig_width, options.fig_height)); + } + if options.color_len > 0 { + if options.color.is_null() { + return Err(CqlibError::NullPtr); + } + let mut colors = Vec::with_capacity(options.color_len); + for idx in 0..options.color_len { + let entry = unsafe { *options.color.add(idx) }; + if entry.is_null() { + return Err(CqlibError::NullPtr); + } + colors.push(unsafe { utf8_str(entry) }?.to_string()); + } + result.color = colors; + } + if options.number_to_keep >= 0 { + result.number_to_keep = Some(options.number_to_keep as usize); + } + if !options.sort.is_null() { + result.sort = unsafe { utf8_str(options.sort) }?.to_string(); + } + if !options.target_string.is_null() { + result.target_string = Some(unsafe { utf8_str(options.target_string) }?.to_string()); + } + if !options.legend.is_null() { + let mut legend = Vec::with_capacity(options.legend_len); + for idx in 0..options.legend_len { + let entry = unsafe { *options.legend.add(idx) }; + if entry.is_null() { + return Err(CqlibError::NullPtr); + } + legend.push(unsafe { utf8_str(entry) }?.to_string()); + } + result.legend = Some(legend); + } + result.bar_labels = options.bar_labels != 0; + if !options.title.is_null() { + result.title = Some(unsafe { utf8_str(options.title) }?.to_string()); + } + Ok(result) +} + +/// Packs an SVG string into a heap-allocated C string; NULL on any error. +fn svg_into_c_string(svg: Result) -> *mut c_char { + match svg { + Ok(svg) => match CString::new(svg) { + Ok(cs) => cs.into_raw(), + Err(_) => std::ptr::null_mut(), + }, + Err(_) => std::ptr::null_mut(), + } +} + +/// Plots an execution result's measured counts as an SVG histogram. +/// +/// `options` may be NULL to use library defaults. Returns a heap-allocated +/// C string containing SVG markup; caller must free with +/// `cqlib_string_free`. Returns NULL on error (including a result with no +/// counts). +#[unsafe(no_mangle)] +pub extern "C" fn plot_histogram( + result: *const CExecutionResult, + options: *const CResultPlotOptions, +) -> *mut c_char { + if result.is_null() { + return std::ptr::null_mut(); + } + let options = match result_options_from_c(options) { + Ok(options) => options, + Err(_) => return std::ptr::null_mut(), + }; + let result = unsafe { &(*result).inner }; + svg_into_c_string(cqlib_core::visualization::plot_histogram(result, &options)) +} + +/// Plots an execution result's measured counts as a normalized SVG +/// probability distribution. +/// +/// `options` may be NULL to use library defaults. Returns a heap-allocated +/// C string containing SVG markup; caller must free with +/// `cqlib_string_free`. Returns NULL on error (including a result with no +/// counts or counts summing to zero). +#[unsafe(no_mangle)] +pub extern "C" fn plot_distribution( + result: *const CExecutionResult, + options: *const CResultPlotOptions, +) -> *mut c_char { + if result.is_null() { + return std::ptr::null_mut(); + } + let options = match result_options_from_c(options) { + Ok(options) => options, + Err(_) => return std::ptr::null_mut(), + }; + let result = unsafe { &(*result).inner }; + svg_into_c_string(cqlib_core::visualization::plot_distribution( + result, &options, + )) +} diff --git a/crates/binding-c/src/visualization/state_plot.rs b/crates/binding-c/src/visualization/state_plot.rs new file mode 100644 index 00000000..a4782c46 --- /dev/null +++ b/crates/binding-c/src/visualization/state_plot.rs @@ -0,0 +1,349 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! C ABI for quantum-state plotting (Bloch vectors, state city, Pauli basis). +//! +//! Core plotting entries are generic over `StateVisualizationSource`; on the +//! C side the two implementors (`Statevector`, `DensityMatrix`) are distinct +//! opaque handles, so each entry is exposed twice: the base name accepts a +//! `CStatevector` and the `_density_matrix` variant accepts a +//! `CDensityMatrix`. All entries return heap-allocated SVG markup the caller +//! frees with `cqlib_string_free`. + +use crate::error::CqlibError; +use crate::qis::{CDensityMatrix, CStatevector}; +use cqlib_core::visualization::StatePlotOptions; +use cqlib_core::visualization::{ + local_bloch_vectors as core_local_bloch_vectors, + state_to_density_matrix as core_state_to_density_matrix, +}; +use num_complex::Complex64; +use std::ffi::{CStr, CString}; +use std::os::raw::c_char; + +/// C-side state-plot options. Mirrors the fields of core `StatePlotOptions`; +/// NULL optional fields use library defaults. +#[repr(C)] +pub struct CStatePlotOptions { + /// Optional chart title; NULL draws no title. + pub title: *const c_char, + /// Optional color array; NULL or `color_len == 0` uses plot-family defaults. + pub color: *const *const c_char, + /// Number of entries in `color`. + pub color_len: usize, + /// Fill opacity for state-city bars. + pub alpha: f64, + /// Whether to reverse displayed computational-basis bit order (0/1). + pub reverse_bits: u8, + /// Non-zero when `fig_width` / `fig_height` carry a custom figure size. + pub has_figsize: u8, + /// Custom figure width in inch-like units; ignored when `has_figsize == 0`. + pub fig_width: f64, + /// Custom figure height in inch-like units; ignored when `has_figsize == 0`. + pub fig_height: f64, +} + +/// Reads a UTF-8 C string; maps invalid UTF-8 to `ParseError`. +unsafe fn utf8_str<'a>(ptr: *const c_char) -> Result<&'a str, CqlibError> { + unsafe { CStr::from_ptr(ptr) } + .to_str() + .map_err(|_| CqlibError::ParseError) +} + +/// Converts C-side state-plot options; a NULL pointer maps to library defaults. +fn state_options_from_c(options: *const CStatePlotOptions) -> Result { + let Some(options) = (unsafe { options.as_ref() }) else { + return Ok(StatePlotOptions::default()); + }; + let mut result = StatePlotOptions::default(); + if !options.title.is_null() { + result.title = Some(unsafe { utf8_str(options.title) }?.to_string()); + } + if options.color_len > 0 { + if options.color.is_null() { + return Err(CqlibError::NullPtr); + } + let mut colors = Vec::with_capacity(options.color_len); + for idx in 0..options.color_len { + let entry = unsafe { *options.color.add(idx) }; + if entry.is_null() { + return Err(CqlibError::NullPtr); + } + colors.push(unsafe { utf8_str(entry) }?.to_string()); + } + result.color = colors; + } + result.alpha = options.alpha; + result.reverse_bits = options.reverse_bits != 0; + if options.has_figsize != 0 { + result.figsize = Some((options.fig_width, options.fig_height)); + } + Ok(result) +} + +/// Packs an SVG string into a heap-allocated C string; NULL on any error. +fn svg_into_c_string(svg: Result) -> *mut c_char { + match svg { + Ok(svg) => match CString::new(svg) { + Ok(cs) => cs.into_raw(), + Err(_) => std::ptr::null_mut(), + }, + Err(_) => std::ptr::null_mut(), + } +} + +/// Plots a single Bloch vector as SVG. +/// +/// `options` may be NULL to use library defaults. Returns a heap-allocated +/// C string containing SVG markup; caller must free with +/// `cqlib_string_free`. Returns NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn plot_bloch_vector( + x: f64, + y: f64, + z: f64, + options: *const CStatePlotOptions, +) -> *mut c_char { + let options = match state_options_from_c(options) { + Ok(options) => options, + Err(_) => return std::ptr::null_mut(), + }; + svg_into_c_string(cqlib_core::visualization::plot_bloch_vector( + [x, y, z], + &options, + )) +} + +/// Plots one reduced Bloch vector per qubit of a statevector as SVG. +/// +/// `options` may be NULL to use library defaults. Returns a heap-allocated +/// C string containing SVG markup; caller must free with +/// `cqlib_string_free`. Returns NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn plot_bloch_multivector( + state: *const CStatevector, + options: *const CStatePlotOptions, +) -> *mut c_char { + if state.is_null() { + return std::ptr::null_mut(); + } + let options = match state_options_from_c(options) { + Ok(options) => options, + Err(_) => return std::ptr::null_mut(), + }; + let state = unsafe { &(*state).inner }; + svg_into_c_string(cqlib_core::visualization::plot_bloch_multivector( + state, &options, + )) +} + +/// `plot_bloch_multivector` for a density-matrix input. +/// +/// `options` may be NULL to use library defaults. Returns a heap-allocated +/// C string containing SVG markup; caller must free with +/// `cqlib_string_free`. Returns NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn plot_bloch_multivector_density_matrix( + state: *const CDensityMatrix, + options: *const CStatePlotOptions, +) -> *mut c_char { + if state.is_null() { + return std::ptr::null_mut(); + } + let options = match state_options_from_c(options) { + Ok(options) => options, + Err(_) => return std::ptr::null_mut(), + }; + let state = unsafe { &(*state).inner }; + svg_into_c_string(cqlib_core::visualization::plot_bloch_multivector( + state, &options, + )) +} + +/// Plots the real and imaginary density-matrix panels of a statevector. +/// +/// `options` may be NULL to use library defaults. Returns a heap-allocated +/// C string containing SVG markup; caller must free with +/// `cqlib_string_free`. Returns NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn plot_state_city( + state: *const CStatevector, + options: *const CStatePlotOptions, +) -> *mut c_char { + if state.is_null() { + return std::ptr::null_mut(); + } + let options = match state_options_from_c(options) { + Ok(options) => options, + Err(_) => return std::ptr::null_mut(), + }; + let state = unsafe { &(*state).inner }; + svg_into_c_string(cqlib_core::visualization::plot_state_city(state, &options)) +} + +/// `plot_state_city` for a density-matrix input. +/// +/// `options` may be NULL to use library defaults. Returns a heap-allocated +/// C string containing SVG markup; caller must free with +/// `cqlib_string_free`. Returns NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn plot_state_city_density_matrix( + state: *const CDensityMatrix, + options: *const CStatePlotOptions, +) -> *mut c_char { + if state.is_null() { + return std::ptr::null_mut(); + } + let options = match state_options_from_c(options) { + Ok(options) => options, + Err(_) => return std::ptr::null_mut(), + }; + let state = unsafe { &(*state).inner }; + svg_into_c_string(cqlib_core::visualization::plot_state_city(state, &options)) +} + +/// Plots Pauli-basis expectation values of a statevector as an SVG bar chart. +/// +/// `options` may be NULL to use library defaults. Returns a heap-allocated +/// C string containing SVG markup; caller must free with +/// `cqlib_string_free`. Returns NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn plot_state_paulivec( + state: *const CStatevector, + options: *const CStatePlotOptions, +) -> *mut c_char { + if state.is_null() { + return std::ptr::null_mut(); + } + let options = match state_options_from_c(options) { + Ok(options) => options, + Err(_) => return std::ptr::null_mut(), + }; + let state = unsafe { &(*state).inner }; + svg_into_c_string(cqlib_core::visualization::plot_state_paulivec( + state, &options, + )) +} + +/// `plot_state_paulivec` for a density-matrix input. +/// +/// `options` may be NULL to use library defaults. Returns a heap-allocated +/// C string containing SVG markup; caller must free with +/// `cqlib_string_free`. Returns NULL on error. +#[unsafe(no_mangle)] +pub extern "C" fn plot_state_paulivec_density_matrix( + state: *const CDensityMatrix, + options: *const CStatePlotOptions, +) -> *mut c_char { + if state.is_null() { + return std::ptr::null_mut(); + } + let options = match state_options_from_c(options) { + Ok(options) => options, + Err(_) => return std::ptr::null_mut(), + }; + let state = unsafe { &(*state).inner }; + svg_into_c_string(cqlib_core::visualization::plot_state_paulivec( + state, &options, + )) +} + +// ===== State inspection helpers (two-step array outputs) ===== + +/// Returns `3 * num_qubits`, the flattened buffer length required by +/// `local_bloch_vectors`, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn local_bloch_vectors_len(state: *const CStatevector) -> usize { + if state.is_null() { + return 0; + } + 3 * unsafe { (*state).inner.num_qubits } +} + +/// Compute one reduced single-qubit Bloch vector per qubit of a statevector. +/// +/// `out` receives `3 * num_qubits` doubles laid out flat as +/// `[x0, y0, z0, x1, y1, z1, ...]`, where entry `q * 3 .. q * 3 + 3` holds the +/// ``, ``, `` expectation values of qubit `q`'s reduced state. +/// `len` must equal `local_bloch_vectors_len(state)`. Returns 0 on success, +/// -1 on NULL pointers, or -8 when `len` does not match or the state payload +/// is invalid. +#[unsafe(no_mangle)] +pub extern "C" fn local_bloch_vectors( + state: *const CStatevector, + out: *mut f64, + len: usize, +) -> i32 { + if state.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + let state = unsafe { &(*state).inner }; + let vectors = match core_local_bloch_vectors(state) { + Ok(vectors) => vectors, + Err(_) => return CqlibError::InvalidParam as i32, + }; + if len != vectors.len() * 3 { + return CqlibError::InvalidParam as i32; + } + for (qubit, vector) in &vectors { + unsafe { + *out.add(3 * qubit) = vector[0]; + *out.add(3 * qubit + 1) = vector[1]; + *out.add(3 * qubit + 2) = vector[2]; + } + } + CqlibError::Ok as i32 +} + +/// Returns `4^num_qubits`, the flattened buffer length required by +/// `state_to_density_matrix`, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn state_to_density_matrix_len(state: *const CStatevector) -> usize { + if state.is_null() { + return 0; + } + let num_qubits = unsafe { (*state).inner.num_qubits }; + if 2 * num_qubits >= usize::BITS as usize { + return 0; + } + 1usize << (2 * num_qubits) +} + +/// Convert a statevector into its density matrix. +/// +/// `out` receives `4^num_qubits` `Complex64` values laid out row-major as an +/// `N x N` matrix with `N = 2^num_qubits`; element `(row, col)` is stored at +/// index `row * N + col`. `len` must equal +/// `state_to_density_matrix_len(state)`. Returns 0 on success, -1 on NULL +/// pointers, or -8 when `len` does not match or the state payload is invalid. +#[unsafe(no_mangle)] +pub extern "C" fn state_to_density_matrix( + state: *const CStatevector, + out: *mut Complex64, + len: usize, +) -> i32 { + if state.is_null() || out.is_null() { + return CqlibError::NullPtr as i32; + } + let state = unsafe { &(*state).inner }; + let (_, rho) = match core_state_to_density_matrix(state) { + Ok(result) => result, + Err(_) => return CqlibError::InvalidParam as i32, + }; + if rho.len() != len { + return CqlibError::InvalidParam as i32; + } + unsafe { + std::ptr::copy_nonoverlapping(rho.as_ptr(), out, len); + } + CqlibError::Ok as i32 +} diff --git a/crates/binding-c/src/visualization/visual_ir.rs b/crates/binding-c/src/visualization/visual_ir.rs new file mode 100644 index 00000000..cc938f65 --- /dev/null +++ b/crates/binding-c/src/visualization/visual_ir.rs @@ -0,0 +1,585 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! C ABI for the visualization IR (build a `VisualCircuit`, inspect it, and +//! re-draw it with text or figure backends). +//! +//! The C side mirrors the two-step Rust pipeline: build the backend-agnostic +//! IR with `build_visual_circuit`, then render it with `draw_text_from_visual` +//! or `draw_figure_from_visual`. Individual operations are exposed through +//! per-field getters (label, lanes, params, style) following the accessor +//! style of `src/circuit/dag.rs`. + +use crate::circuit::CCircuit; +use crate::error::CqlibError; +use crate::visualization::circuit_render::{FigureDrawerOptionsC, TextDrawerOptionsC}; +use cqlib_core::visualization::{ + FigureDrawerOptions, ParameterDisplayMode, ParameterFormatOptions, TextDrawerOptions, + VisualBuildOptions, VisualCircuit, VisualControlFlowKind, VisualOpStyle, VisualOperation, +}; +use std::ffi::CString; +use std::os::raw::c_char; + +// ===== build options ===== + +/// `CParameterFormatOptions::mode`: prefer numeric display when evaluable. +pub const PARAM_MODE_NUMERIC: u32 = 0; +/// `CParameterFormatOptions::mode`: prefer symbolic expression display. +pub const PARAM_MODE_SYMBOLIC: u32 = 1; +/// `CParameterFormatOptions::mode`: symbolic expression with numeric value. +pub const PARAM_MODE_SYMBOLIC_WITH_VALUE: u32 = 2; +/// `CParameterFormatOptions::mode`: prefer `k*pi/n` fraction display. +pub const PARAM_MODE_PI_FRACTION_PREFERRED: u32 = 3; + +/// C-side parameter label formatting options. Mirrors the fields of core +/// `ParameterFormatOptions`. +#[repr(C)] +pub struct CParameterFormatOptions { + /// One of the `PARAM_MODE_*` constants; unknown values fall back to numeric. + pub mode: u32, + /// Decimal precision for fixed-point/scientific formatting. + pub decimal_precision: usize, + /// Values in `(0, scientific_lower_bound)` use scientific notation. + pub scientific_lower_bound: f64, + /// Values `>= scientific_upper_bound` use scientific notation. + pub scientific_upper_bound: f64, + /// Tolerance when matching `value / pi` to rational fractions. + pub pi_tolerance: f64, + /// Maximum denominator used for pi fraction matching. + pub pi_max_denominator: i64, +} + +/// C-side visual IR build options. Mirrors the fields of core +/// `VisualBuildOptions`; a NULL pointer maps to library defaults. +#[repr(C)] +pub struct CVisualBuildOptions { + /// Whether to decompose circuit-gates before layout (0/1). + pub decompose_circuit_gates: u8, + /// Whether to reserve the full lane span for multi-qubit operations (0/1). + pub reserve_full_span_for_multi_qubit: u8, + /// Parameter label formatting options. + pub parameter_format: CParameterFormatOptions, +} + +fn build_options_from_c(options: *const CVisualBuildOptions) -> VisualBuildOptions { + let Some(options) = (unsafe { options.as_ref() }) else { + return VisualBuildOptions::default(); + }; + let format = &options.parameter_format; + let parameter_format = ParameterFormatOptions { + mode: match format.mode { + PARAM_MODE_SYMBOLIC => ParameterDisplayMode::Symbolic, + PARAM_MODE_SYMBOLIC_WITH_VALUE => ParameterDisplayMode::SymbolicWithValue, + PARAM_MODE_PI_FRACTION_PREFERRED => ParameterDisplayMode::PiFractionPreferred, + _ => ParameterDisplayMode::Numeric, + }, + decimal_precision: format.decimal_precision, + scientific_lower_bound: format.scientific_lower_bound, + scientific_upper_bound: format.scientific_upper_bound, + pi_tolerance: format.pi_tolerance, + pi_max_denominator: format.pi_max_denominator, + }; + VisualBuildOptions { + decompose_circuit_gates: options.decompose_circuit_gates != 0, + reserve_full_span_for_multi_qubit: options.reserve_full_span_for_multi_qubit != 0, + parameter_format, + } +} + +// ===== visual circuit handle ===== + +/// Opaque handle around a built [`VisualCircuit`] IR. +pub struct CVisualCircuit { + pub inner: VisualCircuit, +} + +/// Builds the backend-agnostic visualization IR from a circuit. +/// +/// `options` may be NULL to use library defaults. Returns a newly allocated +/// `CVisualCircuit*` (free with `visual_circuit_free`), or NULL on NULL +/// input or build failure. +#[unsafe(no_mangle)] +pub extern "C" fn build_visual_circuit( + circuit: *const CCircuit, + options: *const CVisualBuildOptions, +) -> *mut CVisualCircuit { + if circuit.is_null() { + return std::ptr::null_mut(); + } + let options = build_options_from_c(options); + match cqlib_core::visualization::build_visual_circuit(unsafe { &(*circuit).inner }, &options) { + Ok(inner) => Box::into_raw(Box::new(CVisualCircuit { inner })), + Err(_) => std::ptr::null_mut(), + } +} + +/// Frees a visual-circuit handle. NULL is allowed. +#[unsafe(no_mangle)] +pub extern "C" fn visual_circuit_free(ptr: *mut CVisualCircuit) { + if !ptr.is_null() { + unsafe { + drop(Box::from_raw(ptr)); + } + } +} + +/// Returns the number of qubit lanes, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn visual_circuit_num_qubits(ptr: *const CVisualCircuit) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.num_qubits() } +} + +/// Returns the number of operations, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn visual_circuit_num_operations(ptr: *const CVisualCircuit) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.operations.len() } +} + +/// Returns the number of occupied columns, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn visual_circuit_num_columns(ptr: *const CVisualCircuit) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.num_columns } +} + +/// Returns the number of qubit ids, or 0 for NULL. +#[unsafe(no_mangle)] +pub extern "C" fn visual_circuit_qubits_len(ptr: *const CVisualCircuit) -> usize { + if ptr.is_null() { + return 0; + } + unsafe { (*ptr).inner.qubits.len() } +} + +/// Copies the qubit ids in display order into `buffer` (two-step pattern; +/// pair with `visual_circuit_qubits_len`). Returns 0 on success, -1 for NULL +/// input, -8 when `len` is too small. +#[unsafe(no_mangle)] +pub extern "C" fn visual_circuit_qubits( + ptr: *const CVisualCircuit, + buffer: *mut u32, + len: usize, +) -> i32 { + if ptr.is_null() { + return CqlibError::NullPtr as i32; + } + let qubits = unsafe { &(*ptr).inner.qubits }; + if len < qubits.len() { + return CqlibError::InvalidParam as i32; + } + if qubits.is_empty() { + return CqlibError::Ok as i32; + } + if buffer.is_null() { + return CqlibError::NullPtr as i32; + } + let slice = unsafe { std::slice::from_raw_parts_mut(buffer, qubits.len()) }; + for (slot, qubit) in slice.iter_mut().zip(qubits.iter()) { + *slot = qubit.id(); + } + CqlibError::Ok as i32 +} + +/// Borrows the operation at `index`, or None for NULL / out-of-bounds. +fn operation_at<'a>(ptr: *const CVisualCircuit, index: usize) -> Option<&'a VisualOperation> { + let handle = (unsafe { ptr.as_ref() })?; + handle.inner.operations.get(index) +} + +/// Returns the primary display label of operation `index` as a +/// heap-allocated C string. Caller must free with `cqlib_string_free`. +/// Returns NULL on NULL input or out-of-bounds. +#[unsafe(no_mangle)] +pub extern "C" fn visual_circuit_operation_label( + ptr: *const CVisualCircuit, + index: usize, +) -> *mut c_char { + match operation_at(ptr, index) { + Some(op) => match CString::new(op.label.as_str()) { + Ok(cs) => cs.into_raw(), + Err(_) => std::ptr::null_mut(), + }, + None => std::ptr::null_mut(), + } +} + +/// Returns the time column of operation `index`, or `usize::MAX` on NULL +/// input or out-of-bounds. +#[unsafe(no_mangle)] +pub extern "C" fn visual_circuit_operation_column( + ptr: *const CVisualCircuit, + index: usize, +) -> usize { + operation_at(ptr, index).map_or(usize::MAX, |op| op.column) +} + +/// Returns the number of logical columns reserved by operation `index`, or +/// `usize::MAX` on NULL input or out-of-bounds. +#[unsafe(no_mangle)] +pub extern "C" fn visual_circuit_operation_span_cols( + ptr: *const CVisualCircuit, + index: usize, +) -> usize { + operation_at(ptr, index).map_or(usize::MAX, |op| op.span_cols) +} + +/// Returns whether operation `index` is drawn as a span box: 1 or 0, or -1 +/// on NULL input or out-of-bounds. +#[unsafe(no_mangle)] +pub extern "C" fn visual_circuit_operation_is_span_box( + ptr: *const CVisualCircuit, + index: usize, +) -> i32 { + operation_at(ptr, index).map_or(-1, |op| op.span_box as i32) +} + +/// Returns the number of operand lanes of operation `index`, or 0 on NULL +/// input or out-of-bounds. +#[unsafe(no_mangle)] +pub extern "C" fn visual_circuit_operation_lanes_len( + ptr: *const CVisualCircuit, + index: usize, +) -> usize { + operation_at(ptr, index).map_or(0, |op| op.lanes.len()) +} + +/// Copies the operand lanes of operation `index` into `buffer` (two-step +/// pattern; pair with `visual_circuit_operation_lanes_len`). Returns 0 on +/// success, -1 for NULL input, -8 when `len` is too small. +#[unsafe(no_mangle)] +pub extern "C" fn visual_circuit_operation_lanes( + ptr: *const CVisualCircuit, + index: usize, + buffer: *mut usize, + len: usize, +) -> i32 { + let Some(op) = operation_at(ptr, index) else { + return CqlibError::NullPtr as i32; + }; + let lanes = &op.lanes; + if len < lanes.len() { + return CqlibError::InvalidParam as i32; + } + if lanes.is_empty() { + return CqlibError::Ok as i32; + } + if buffer.is_null() { + return CqlibError::NullPtr as i32; + } + let slice = unsafe { std::slice::from_raw_parts_mut(buffer, lanes.len()) }; + slice.copy_from_slice(lanes); + CqlibError::Ok as i32 +} + +/// Returns the number of lanes reserved by operation `index` in its column, +/// or 0 on NULL input or out-of-bounds. +#[unsafe(no_mangle)] +pub extern "C" fn visual_circuit_operation_covered_lanes_len( + ptr: *const CVisualCircuit, + index: usize, +) -> usize { + operation_at(ptr, index).map_or(0, |op| op.covered_lanes.len()) +} + +/// Copies the lanes reserved by operation `index` into `buffer` (two-step +/// pattern; pair with `visual_circuit_operation_covered_lanes_len`). +/// Returns 0 on success, -1 for NULL input, -8 when `len` is too small. +#[unsafe(no_mangle)] +pub extern "C" fn visual_circuit_operation_covered_lanes( + ptr: *const CVisualCircuit, + index: usize, + buffer: *mut usize, + len: usize, +) -> i32 { + let Some(op) = operation_at(ptr, index) else { + return CqlibError::NullPtr as i32; + }; + let lanes = &op.covered_lanes; + if len < lanes.len() { + return CqlibError::InvalidParam as i32; + } + if lanes.is_empty() { + return CqlibError::Ok as i32; + } + if buffer.is_null() { + return CqlibError::NullPtr as i32; + } + let slice = unsafe { std::slice::from_raw_parts_mut(buffer, lanes.len()) }; + slice.copy_from_slice(lanes); + CqlibError::Ok as i32 +} + +/// Returns the number of formatted parameter labels of operation `index`, +/// or 0 on NULL input or out-of-bounds. +#[unsafe(no_mangle)] +pub extern "C" fn visual_circuit_operation_params_len( + ptr: *const CVisualCircuit, + index: usize, +) -> usize { + operation_at(ptr, index).map_or(0, |op| op.params.len()) +} + +/// Copies the formatted parameter labels of operation `index` into `out` +/// (two-step pattern; pair with +/// `visual_circuit_operation_params_len`). Each written element is a freshly +/// allocated C string the caller frees with `cqlib_string_free`. Returns 0 +/// on success, -1 for NULL input, -8 when `len` is too small. +#[unsafe(no_mangle)] +pub extern "C" fn visual_circuit_operation_params( + ptr: *const CVisualCircuit, + index: usize, + out: *mut *mut c_char, + len: usize, +) -> i32 { + let Some(op) = operation_at(ptr, index) else { + return CqlibError::NullPtr as i32; + }; + let params = &op.params; + if len < params.len() { + return CqlibError::InvalidParam as i32; + } + if params.is_empty() { + return CqlibError::Ok as i32; + } + if out.is_null() { + return CqlibError::NullPtr as i32; + } + let slice = unsafe { std::slice::from_raw_parts_mut(out, params.len()) }; + for (slot, param) in slice.iter_mut().zip(params.iter()) { + match CString::new(param.as_str()) { + Ok(text) => *slot = text.into_raw(), + Err(_) => return CqlibError::CircuitError as i32, + } + } + CqlibError::Ok as i32 +} + +// ===== operation style tags ===== + +/// `visual_circuit_operation_style` tag: generic gate-like box. +pub const VISUAL_OP_STYLE_GATE: u32 = 0; +/// `visual_circuit_operation_style` tag: controlled operation; query the +/// control count with `visual_circuit_operation_num_controls`. +pub const VISUAL_OP_STYLE_CONTROLLED: u32 = 1; +/// `visual_circuit_operation_style` tag: controlled-Z marker. +pub const VISUAL_OP_STYLE_CZ: u32 = 2; +/// `visual_circuit_operation_style` tag: swap marker. +pub const VISUAL_OP_STYLE_SWAP: u32 = 3; +/// `visual_circuit_operation_style` tag: barrier marker. +pub const VISUAL_OP_STYLE_BARRIER: u32 = 4; +/// `visual_circuit_operation_style` tag: measurement marker. +pub const VISUAL_OP_STYLE_MEASURE: u32 = 5; +/// `visual_circuit_operation_style` tag: reset marker. +pub const VISUAL_OP_STYLE_RESET: u32 = 6; +/// `visual_circuit_operation_style` tag: delay marker. +pub const VISUAL_OP_STYLE_DELAY: u32 = 7; +/// `visual_circuit_operation_style` tag: control-flow marker; query the +/// kind with `visual_circuit_operation_control_flow_kind`. +pub const VISUAL_OP_STYLE_CONTROL_FLOW: u32 = 8; + +/// Returns the drawing style of operation `index` as one of the +/// `VISUAL_OP_STYLE_*` constants, or `u32::MAX` on NULL input or +/// out-of-bounds. +#[unsafe(no_mangle)] +pub extern "C" fn visual_circuit_operation_style(ptr: *const CVisualCircuit, index: usize) -> u32 { + let Some(op) = operation_at(ptr, index) else { + return u32::MAX; + }; + match op.style { + VisualOpStyle::Gate => VISUAL_OP_STYLE_GATE, + VisualOpStyle::Controlled { .. } => VISUAL_OP_STYLE_CONTROLLED, + VisualOpStyle::Cz => VISUAL_OP_STYLE_CZ, + VisualOpStyle::Swap => VISUAL_OP_STYLE_SWAP, + VisualOpStyle::Barrier => VISUAL_OP_STYLE_BARRIER, + VisualOpStyle::Measure => VISUAL_OP_STYLE_MEASURE, + VisualOpStyle::Reset => VISUAL_OP_STYLE_RESET, + VisualOpStyle::Delay => VISUAL_OP_STYLE_DELAY, + VisualOpStyle::ControlFlow { .. } => VISUAL_OP_STYLE_CONTROL_FLOW, + } +} + +/// Returns the number of control qubits of operation `index`; only +/// meaningful when the style is `VISUAL_OP_STYLE_CONTROLLED`. Returns 0 on +/// NULL input, out-of-bounds, or non-controlled styles. +#[unsafe(no_mangle)] +pub extern "C" fn visual_circuit_operation_num_controls( + ptr: *const CVisualCircuit, + index: usize, +) -> usize { + operation_at(ptr, index).map_or(0, |op| match op.style { + VisualOpStyle::Controlled { num_controls } => num_controls, + _ => 0, + }) +} + +// ===== control-flow kind tags ===== + +/// `visual_circuit_operation_control_flow_kind` tag: source IfElse block. +pub const VISUAL_CF_KIND_IF_ELSE_BLOCK: u32 = 0; +/// `visual_circuit_operation_control_flow_kind` tag: source while-loop block. +pub const VISUAL_CF_KIND_WHILE_BLOCK: u32 = 1; +/// `visual_circuit_operation_control_flow_kind` tag: source for-loop block. +pub const VISUAL_CF_KIND_FOR_BLOCK: u32 = 2; +/// `visual_circuit_operation_control_flow_kind` tag: source switch block. +pub const VISUAL_CF_KIND_SWITCH_BLOCK: u32 = 3; +/// `visual_circuit_operation_control_flow_kind` tag: structured break. +pub const VISUAL_CF_KIND_BREAK: u32 = 4; +/// `visual_circuit_operation_control_flow_kind` tag: structured continue. +pub const VISUAL_CF_KIND_CONTINUE: u32 = 5; +/// `visual_circuit_operation_control_flow_kind` tag: flattened if start. +pub const VISUAL_CF_KIND_IF_START: u32 = 6; +/// `visual_circuit_operation_control_flow_kind` tag: flattened else start. +pub const VISUAL_CF_KIND_ELSE_START: u32 = 7; +/// `visual_circuit_operation_control_flow_kind` tag: flattened while start. +pub const VISUAL_CF_KIND_WHILE_START: u32 = 8; +/// `visual_circuit_operation_control_flow_kind` tag: flattened for start. +pub const VISUAL_CF_KIND_FOR_START: u32 = 9; +/// `visual_circuit_operation_control_flow_kind` tag: flattened switch start. +pub const VISUAL_CF_KIND_SWITCH_START: u32 = 10; +/// `visual_circuit_operation_control_flow_kind` tag: flattened case start. +pub const VISUAL_CF_KIND_CASE_START: u32 = 11; +/// `visual_circuit_operation_control_flow_kind` tag: flattened default start. +pub const VISUAL_CF_KIND_DEFAULT_START: u32 = 12; +/// `visual_circuit_operation_control_flow_kind` tag: flattened block end. +pub const VISUAL_CF_KIND_END: u32 = 13; + +/// Returns the control-flow kind of operation `index` as one of the +/// `VISUAL_CF_KIND_*` constants; only meaningful when the style is +/// `VISUAL_OP_STYLE_CONTROL_FLOW`. Returns `u32::MAX` on NULL input, +/// out-of-bounds, or non-control-flow styles. +#[unsafe(no_mangle)] +pub extern "C" fn visual_circuit_operation_control_flow_kind( + ptr: *const CVisualCircuit, + index: usize, +) -> u32 { + let Some(op) = operation_at(ptr, index) else { + return u32::MAX; + }; + match &op.style { + VisualOpStyle::ControlFlow { kind } => match kind { + VisualControlFlowKind::IfElseBlock { .. } => VISUAL_CF_KIND_IF_ELSE_BLOCK, + VisualControlFlowKind::WhileBlock { .. } => VISUAL_CF_KIND_WHILE_BLOCK, + VisualControlFlowKind::ForBlock { .. } => VISUAL_CF_KIND_FOR_BLOCK, + VisualControlFlowKind::SwitchBlock { .. } => VISUAL_CF_KIND_SWITCH_BLOCK, + VisualControlFlowKind::Break => VISUAL_CF_KIND_BREAK, + VisualControlFlowKind::Continue => VISUAL_CF_KIND_CONTINUE, + VisualControlFlowKind::IfStart => VISUAL_CF_KIND_IF_START, + VisualControlFlowKind::ElseStart => VISUAL_CF_KIND_ELSE_START, + VisualControlFlowKind::WhileStart => VISUAL_CF_KIND_WHILE_START, + VisualControlFlowKind::ForStart => VISUAL_CF_KIND_FOR_START, + VisualControlFlowKind::SwitchStart => VISUAL_CF_KIND_SWITCH_START, + VisualControlFlowKind::CaseStart => VISUAL_CF_KIND_CASE_START, + VisualControlFlowKind::DefaultStart => VISUAL_CF_KIND_DEFAULT_START, + VisualControlFlowKind::End => VISUAL_CF_KIND_END, + }, + _ => u32::MAX, + } +} + +// ===== drawing from pre-built IR ===== + +/// Converts C-side text-drawer options; NULL maps to library defaults. +fn text_options_from_c(options: *const TextDrawerOptionsC) -> TextDrawerOptions { + let defaults = TextDrawerOptions::default(); + let Some(options) = (unsafe { options.as_ref() }) else { + return defaults; + }; + TextDrawerOptions { + cell_width: defaults.cell_width, + show_params: options.show_params != 0, + decompose_circuit_gates: options.decompose_circuit_gates != 0, + line_width: options.line_width, + initial_state: options.initial_state != 0, + reverse_bits: options.reverse_bits != 0, + } +} + +/// Converts C-side figure-drawer options; NULL maps to library defaults. +fn figure_options_from_c(options: *const FigureDrawerOptionsC) -> FigureDrawerOptions { + let mut result = FigureDrawerOptions::default(); + let Some(options) = (unsafe { options.as_ref() }) else { + return result; + }; + result.show_params = options.show_params != 0; + result.decompose_circuit_gates = options.decompose_circuit_gates != 0; + if options.width_per_column > 0.0 { + result.width_per_column = options.width_per_column; + } + if options.height_per_qubit > 0.0 { + result.height_per_qubit = options.height_per_qubit; + } + result.dpi = options.dpi; + result.fold = if options.fold > 0 { options.fold } else { -1 }; + result.initial_state = options.initial_state != 0; + result.reverse_bits = options.reverse_bits != 0; + result +} + +/// Draws pre-built visualization IR as UTF-8 text (box-drawing characters). +/// +/// `options` may be NULL to use library defaults. Returns a heap-allocated +/// C string; caller must free with `cqlib_string_free`. Returns NULL on +/// error. +#[unsafe(no_mangle)] +pub extern "C" fn draw_text_from_visual( + visual: *const CVisualCircuit, + options: *const TextDrawerOptionsC, +) -> *mut c_char { + if visual.is_null() { + return std::ptr::null_mut(); + } + let options = text_options_from_c(options); + let visual = unsafe { &(*visual).inner }; + match cqlib_core::visualization::draw_text_from_visual(visual, &options) { + Ok(text) => match CString::new(text) { + Ok(cs) => cs.into_raw(), + Err(_) => std::ptr::null_mut(), + }, + Err(_) => std::ptr::null_mut(), + } +} + +/// Draws pre-built visualization IR as an SVG figure. +/// +/// `options` may be NULL to use library defaults. Returns a heap-allocated +/// C string containing SVG markup; caller must free with +/// `cqlib_string_free`. Returns NULL on error. The core signature keeps an +/// output-path parameter for compatibility; it is ignored and therefore +/// not exposed here. +#[unsafe(no_mangle)] +pub extern "C" fn draw_figure_from_visual( + visual: *const CVisualCircuit, + options: *const FigureDrawerOptionsC, +) -> *mut c_char { + if visual.is_null() { + return std::ptr::null_mut(); + } + let options = figure_options_from_c(options); + let visual = unsafe { &(*visual).inner }; + match CString::new(cqlib_core::visualization::draw_figure_from_visual( + visual, &options, None, + )) { + Ok(cs) => cs.into_raw(), + Err(_) => std::ptr::null_mut(), + } +} + +// Note: freeing a label or param string with `cqlib_string_free` matches the +// `CString::into_raw` allocation used above; no separate free entry is needed. diff --git a/crates/binding-c/tests/circuit_advanced_test.rs b/crates/binding-c/tests/circuit_advanced_test.rs new file mode 100644 index 00000000..ff109b55 --- /dev/null +++ b/crates/binding-c/tests/circuit_advanced_test.rs @@ -0,0 +1,817 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! Rust FFI tests for the circuit parameter-metadata and advanced-instruction +//! bindings (`circuit/advanced.rs`, checklist sections 1.4 and 1.5). + +use binding_c::circuit::{ + CCircuit, CCircuitParam, CIRCUIT_PARAM_TAG_FIXED, CIRCUIT_PARAM_TAG_INDEX, CParameter, + CParameterValue, PARAMETER_VALUE_TAG_FIXED, PARAMETER_VALUE_TAG_PARAM, circuit_add_parameter, + circuit_assign_params, circuit_ccx, circuit_circuit_gate, circuit_cx, circuit_delay, + circuit_delay_param, circuit_free, circuit_from_qubits, circuit_gate_free, circuit_gate_name, + circuit_gate_num_params, circuit_gate_num_qubits, circuit_gate_signature_params, + circuit_gate_signature_params_len, circuit_global_phase, circuit_h, circuit_id, circuit_index, + circuit_map_param, circuit_multi_control, circuit_new, circuit_num_operations, + circuit_num_parameters, circuit_num_qubits, circuit_parameter_value, circuit_parameters, + circuit_parameters_len, circuit_phase, circuit_qubits, circuit_remove_operations, + circuit_resolve_parameter, circuit_rx, circuit_rx_param, circuit_set_global_phase, + circuit_set_global_phase_param, circuit_symbols, circuit_symbols_len, circuit_to_gate, + circuit_to_matrix, circuit_to_matrix_len, circuit_unitary, circuit_unitary_with_params, + circuit_used_symbols, circuit_used_symbols_len, circuit_uses_symbol, circuit_validate, + circuit_x, param_evaluate, param_free, param_parse, value_operation_free, + value_operation_instruction_type, value_operation_label, value_operation_name, + value_operation_num_params, value_operation_num_qubits, value_operation_param, + value_operation_qubits, +}; +use binding_c::cqlib_string_free; +use num_complex::Complex64; +use std::ffi::{CStr, CString}; +use std::os::raw::c_char; +use std::ptr; + +fn cstr(s: &str) -> CString { + CString::new(s).unwrap() +} + +fn matrix_of(circuit: *const CCircuit) -> Vec { + let len = circuit_to_matrix_len(circuit, ptr::null(), 0); + assert!(len > 0); + let mut buf = vec![0.0f64; len * 2]; + assert_eq!( + circuit_to_matrix(circuit, ptr::null(), 0, buf.as_mut_ptr(), buf.len()), + len + ); + buf +} + +fn assert_matrices_close(a: &[f64], b: &[f64]) { + assert_eq!(a.len(), b.len()); + for (x, y) in a.iter().zip(b.iter()) { + assert!((x - y).abs() < 1e-12, "matrix mismatch: {x} vs {y}"); + } +} + +#[test] +fn parameter_metadata_roundtrip() { + let theta = cstr("theta"); + let theta_p = param_parse(theta.as_ptr()); + assert!(!theta_p.is_null()); + + let circuit = circuit_new(1); + + // Interning a parameter registers it and its symbols, but does not mark + // it as used by the executable IR. + assert_eq!(circuit_add_parameter(circuit, theta_p), 0); + assert_eq!(circuit_parameters_len(circuit), 1); + assert_eq!(circuit_num_parameters(circuit), 1); + assert_eq!(circuit_symbols_len(circuit), 1); + assert_eq!(circuit_used_symbols_len(circuit), 0); + assert_eq!(circuit_uses_symbol(circuit, theta.as_ptr()), 0); + + // Referencing it through a gate marks the symbol as used. + assert_eq!(circuit_rx_param(circuit, 0, theta_p), 0); + assert_eq!(circuit_uses_symbol(circuit, theta.as_ptr()), 1); + assert_eq!(circuit_used_symbols_len(circuit), 1); + + // Symbol list contents (two-step, caller frees each string). + let mut names: [*mut c_char; 1] = [ptr::null_mut(); 1]; + assert_eq!(circuit_symbols(circuit, names.as_mut_ptr(), 1), 1); + assert_eq!( + unsafe { CStr::from_ptr(names[0]) }.to_str().unwrap(), + "theta" + ); + cqlib_string_free(names[0]); + assert_eq!(circuit_used_symbols(circuit, names.as_mut_ptr(), 1), 1); + assert_eq!( + unsafe { CStr::from_ptr(names[0]) }.to_str().unwrap(), + "theta" + ); + cqlib_string_free(names[0]); + + // Parameter list contents: cloned handles the caller frees. + let mut params: [*mut binding_c::circuit::CParameter; 1] = [ptr::null_mut(); 1]; + assert_eq!(circuit_parameters(circuit, params.as_mut_ptr(), 1), 1); + let bindings = cstr("theta:2"); + assert!((param_evaluate(params[0], bindings.as_ptr()) - 2.0).abs() < 1e-12); + param_free(params[0]); + + // An unused symbol stays in the registry but not in the used set. + let unused = cstr("unused"); + let unused_p = param_parse(unused.as_ptr()); + assert_eq!(circuit_add_parameter(circuit, unused_p), 0); + assert_eq!(circuit_symbols_len(circuit), 2); + assert_eq!(circuit_used_symbols_len(circuit), 1); + assert_eq!(circuit_uses_symbol(circuit, unused.as_ptr()), 0); + + circuit_free(circuit); + param_free(unused_p); + param_free(theta_p); +} + +#[test] +fn map_resolve_and_parameter_value() { + let circuit = circuit_new(1); + let theta = cstr("theta"); + let theta_p = param_parse(theta.as_ptr()); + assert_eq!(circuit_rx_param(circuit, 0, theta_p), 0); + + // Mapping a symbolic expression interns it and yields an index. + let phi = cstr("phi * 2"); + let phi_p = param_parse(phi.as_ptr()); + let mut mapped = CCircuitParam { + tag: 255, + index: 0, + value: 0.0, + }; + assert_eq!(circuit_map_param(circuit, phi_p, &mut mapped), 0); + assert_eq!(mapped.tag, CIRCUIT_PARAM_TAG_INDEX); + assert_eq!(mapped.index, 1); // theta is index 0, phi*2 is index 1 + + // Mapping a constant yields a fixed value. + let half = cstr("0.5"); + let half_p = param_parse(half.as_ptr()); + let mut fixed = CCircuitParam { + tag: 255, + index: 0, + value: 0.0, + }; + assert_eq!(circuit_map_param(circuit, half_p, &mut fixed), 0); + assert_eq!(fixed.tag, CIRCUIT_PARAM_TAG_FIXED); + assert!((fixed.value - 0.5).abs() < 1e-12); + + // Resolve an index back into its expression and evaluate it. + let resolved = circuit_resolve_parameter(circuit, &mapped); + assert!(!resolved.is_null()); + let bindings = cstr("theta:0.25,phi:1.5"); + assert!((param_evaluate(resolved, bindings.as_ptr()) - 3.0).abs() < 1e-12); + param_free(resolved); + + // Parameter values: index -> symbolic handle, fixed -> number. + let mut value = CParameterValue { + tag: 255, + value: 0.0, + param: ptr::null_mut(), + }; + assert_eq!(circuit_parameter_value(circuit, &mapped, &mut value), 0); + assert_eq!(value.tag, PARAMETER_VALUE_TAG_PARAM); + assert!(!value.param.is_null()); + assert!((param_evaluate(value.param, bindings.as_ptr()) - 3.0).abs() < 1e-12); + param_free(value.param); + + assert_eq!(circuit_parameter_value(circuit, &fixed, &mut value), 0); + assert_eq!(value.tag, PARAMETER_VALUE_TAG_FIXED); + assert!((value.value - 0.5).abs() < 1e-12); + + // Error paths: invalid index -> -3, invalid tag -> -8. + let bad_index = CCircuitParam { + tag: CIRCUIT_PARAM_TAG_INDEX, + index: 99, + value: 0.0, + }; + assert_eq!(circuit_parameter_value(circuit, &bad_index, &mut value), -3); + assert!(circuit_resolve_parameter(circuit, &bad_index).is_null()); + let bad_tag = CCircuitParam { + tag: 7, + index: 0, + value: 1.0, + }; + assert_eq!(circuit_parameter_value(circuit, &bad_tag, &mut value), -8); + + circuit_free(circuit); + param_free(half_p); + param_free(phi_p); + param_free(theta_p); +} + +#[test] +fn global_phase_metadata() { + let circuit = circuit_new(1); + assert_eq!(circuit_set_global_phase(circuit, 0.25), 0); + + let mut value = CParameterValue { + tag: 255, + value: 0.0, + param: ptr::null_mut(), + }; + assert_eq!(circuit_global_phase(circuit, &mut value), 0); + assert_eq!(value.tag, PARAMETER_VALUE_TAG_FIXED); + assert!((value.value - 0.25).abs() < 1e-12); + + let alpha = cstr("alpha"); + let alpha_p = param_parse(alpha.as_ptr()); + assert_eq!(circuit_set_global_phase_param(circuit, alpha_p), 0); + assert_eq!(circuit_global_phase(circuit, &mut value), 0); + assert_eq!(value.tag, PARAMETER_VALUE_TAG_PARAM); + assert!(!value.param.is_null()); + let bindings = cstr("alpha:1.5"); + assert!((param_evaluate(value.param, bindings.as_ptr()) - 1.5).abs() < 1e-12); + param_free(value.param); + + assert_eq!(circuit_global_phase(ptr::null(), &mut value), -1); + circuit_free(circuit); + param_free(alpha_p); +} + +#[test] +fn id_gate_alias_and_from_qubits() { + let circuit = circuit_new(2); + assert_eq!(circuit_id(circuit, 0), 0); + assert_eq!(circuit_id(circuit, 1), 0); + assert_eq!(circuit_num_operations(circuit), 2); + assert_eq!(circuit_id(circuit, 5), -2); + assert_eq!(circuit_id(ptr::null_mut(), 0), -1); + assert_eq!(circuit_validate(circuit), 0); + circuit_free(circuit); + + let ids = [0u32, 2, 5]; + let sparse = circuit_from_qubits(ids.as_ptr(), 3); + assert!(!sparse.is_null()); + assert_eq!(circuit_num_qubits(sparse), 3); + let mut out = [0u32; 3]; + assert_eq!(circuit_qubits(sparse, out.as_mut_ptr(), 3), 3); + assert_eq!(out, [0, 2, 5]); + circuit_free(sparse); + + let duplicates = [1u32, 1]; + assert!(circuit_from_qubits(duplicates.as_ptr(), 2).is_null()); + assert!(circuit_from_qubits(ptr::null(), 1).is_null()); + let empty = circuit_from_qubits(ptr::null(), 0); + assert!(!empty.is_null()); + circuit_free(empty); +} + +#[test] +fn bell_matrix_known_values() { + let circuit = circuit_new(2); + assert_eq!(circuit_h(circuit, 0), 0); + assert_eq!(circuit_cx(circuit, 0, 1), 0); + + let matrix = matrix_of(circuit); + // Little-endian basis (qubit 0 = LSB), U[row][col] = . + // Columns are the Bell images of the basis states: |00> -> (|00>+|11>)/sqrt2, + // |01> -> (|00>-|11>)/sqrt2, |10> -> (|01>+|10>)/sqrt2, |11> -> (-|01>+|10>)/sqrt2. + let s = 1.0 / 2.0f64.sqrt(); + let expected = [ + [s, s, 0.0, 0.0], + [0.0, 0.0, s, -s], + [0.0, 0.0, s, s], + [s, -s, 0.0, 0.0], + ]; + for row in 0..4 { + for col in 0..4 { + let re = matrix[(row * 4 + col) * 2]; + let im = matrix[(row * 4 + col) * 2 + 1]; + assert!( + (re - expected[row][col]).abs() < 1e-12, + "({row},{col}) re = {re}" + ); + assert!(im.abs() < 1e-12, "({row},{col}) im = {im}"); + } + } + circuit_free(circuit); +} + +#[test] +fn unitary_matches_standard_gate_matrix() { + // Pauli-X as a custom 1-qubit unitary, row-major Complex64. + let x_matrix = [ + Complex64::new(0.0, 0.0), + Complex64::new(1.0, 0.0), + Complex64::new(1.0, 0.0), + Complex64::new(0.0, 0.0), + ]; + let label = cstr("my_x"); + let targets = [0u32]; + + let circuit = circuit_new(2); + assert_eq!( + circuit_unitary( + circuit, + label.as_ptr(), + 1, + x_matrix.as_ptr(), + targets.as_ptr(), + 1 + ), + 0 + ); + + let reference = circuit_new(2); + assert_eq!(circuit_x(reference, 0), 0); + assert_matrices_close(&matrix_of(circuit), &matrix_of(reference)); + + // Error paths. + let oob = [5u32]; + assert_eq!( + circuit_unitary( + circuit, + label.as_ptr(), + 1, + x_matrix.as_ptr(), + oob.as_ptr(), + 1 + ), + -2 + ); + assert_eq!( + circuit_unitary( + circuit, + label.as_ptr(), + 21, + x_matrix.as_ptr(), + targets.as_ptr(), + 1 + ), + -8 + ); + assert_eq!( + circuit_unitary( + ptr::null_mut(), + label.as_ptr(), + 1, + x_matrix.as_ptr(), + targets.as_ptr(), + 1 + ), + -1 + ); + + circuit_free(reference); + circuit_free(circuit); +} + +#[test] +fn unitary_with_params_evaluates_symbolic_matrix() { + // diag(1, exp(i*theta)) as a symbolic matrix; bind theta = 0.5. + let re = [cstr("1"), cstr("0"), cstr("0"), cstr("cos(theta)")]; + let im = [cstr("0"), cstr("0"), cstr("0"), cstr("sin(theta)")]; + let names = [cstr("theta")]; + let re_ptrs: [*const c_char; 4] = [ + re[0].as_ptr(), + re[1].as_ptr(), + re[2].as_ptr(), + re[3].as_ptr(), + ]; + let im_ptrs: [*const c_char; 4] = [ + im[0].as_ptr(), + im[1].as_ptr(), + im[2].as_ptr(), + im[3].as_ptr(), + ]; + let name_ptrs: [*const c_char; 1] = [names[0].as_ptr()]; + + let label = cstr("phase_like"); + let arg = cstr("0.5"); + let arg_p = param_parse(arg.as_ptr()); + let args: [*const CParameter; 1] = [arg_p]; + let targets = [0u32]; + + let circuit = circuit_new(1); + assert_eq!( + circuit_unitary_with_params( + circuit, + label.as_ptr(), + 1, + re_ptrs.as_ptr(), + im_ptrs.as_ptr(), + name_ptrs.as_ptr(), + 1, + targets.as_ptr(), + 1, + args.as_ptr(), + 1 + ), + 0 + ); + + // The numeric matrix path only accepts fixed parameter values; the + // constant argument is interned as an index, so fold it first (NULL + // bindings keep true symbols symbolic but constant-fold expressions). + let resolved = circuit_assign_params(circuit, ptr::null()); + assert!(!resolved.is_null()); + let matrix = matrix_of(resolved); + assert!((matrix[0] - 1.0).abs() < 1e-12); // [0][0].re + assert!(matrix[1].abs() < 1e-12); // [0][0].im + assert!((matrix[6] - 0.5f64.cos()).abs() < 1e-12); // [1][1].re + assert!((matrix[7] - 0.5f64.sin()).abs() < 1e-12); // [1][1].im + + // A NULL imaginary-part array is treated as all zeros: the matrix + // becomes diag(1, cos(theta)) and binding theta = 0 yields the identity. + let zero = cstr("0"); + let zero_p = param_parse(zero.as_ptr()); + let zero_args: [*const CParameter; 1] = [zero_p]; + let plain = circuit_new(1); + assert_eq!( + circuit_unitary_with_params( + plain, + label.as_ptr(), + 1, + re_ptrs.as_ptr(), + ptr::null(), + name_ptrs.as_ptr(), + 1, + targets.as_ptr(), + 1, + zero_args.as_ptr(), + 1 + ), + 0 + ); + let resolved_plain = circuit_assign_params(plain, ptr::null()); + assert!(!resolved_plain.is_null()); + let identity = matrix_of(resolved_plain); + assert!((identity[0] - 1.0).abs() < 1e-12); + assert!((identity[6] - 1.0).abs() < 1e-12); // [1][1].re, cos(0) = 1 + assert!(identity[7].abs() < 1e-12); // [1][1].im + + // Wrong parameter count -> -3. + assert_eq!( + circuit_unitary_with_params( + circuit, + label.as_ptr(), + 1, + re_ptrs.as_ptr(), + im_ptrs.as_ptr(), + name_ptrs.as_ptr(), + 1, + targets.as_ptr(), + 1, + ptr::null(), + 0 + ), + -3 + ); + + circuit_free(resolved_plain); + circuit_free(plain); + circuit_free(resolved); + circuit_free(circuit); + param_free(zero_p); + param_free(arg_p); +} + +#[test] +fn to_gate_circuit_gate_bell_roundtrip() { + let inner = circuit_new(2); + assert_eq!(circuit_h(inner, 0), 0); + assert_eq!(circuit_cx(inner, 0, 1), 0); + + let name = cstr("bell"); + let gate = circuit_to_gate(inner, name.as_ptr()); + assert!(!gate.is_null()); + + let gate_name = circuit_gate_name(gate); + assert_eq!( + unsafe { CStr::from_ptr(gate_name) }.to_str().unwrap(), + "bell" + ); + cqlib_string_free(gate_name); + assert_eq!(circuit_gate_num_qubits(gate), 2); + assert_eq!(circuit_gate_num_params(gate), 0); + assert_eq!(circuit_gate_signature_params_len(gate), 0); + + let outer = circuit_new(2); + let qubits = [0u32, 1]; + assert_eq!( + circuit_circuit_gate(outer, gate, qubits.as_ptr(), 2, ptr::null(), 0), + 0 + ); + // Applying the composite gate reproduces the original Bell circuit. + assert_matrices_close(&matrix_of(outer), &matrix_of(inner)); + + // Wrong qubit arity -> -3; out-of-bounds qubit -> -2. + let single = [0u32]; + assert_eq!( + circuit_circuit_gate(outer, gate, single.as_ptr(), 1, ptr::null(), 0), + -3 + ); + let oob = [0u32, 9]; + assert_eq!( + circuit_circuit_gate(outer, gate, oob.as_ptr(), 2, ptr::null(), 0), + -2 + ); + + circuit_free(outer); + circuit_gate_free(gate); + circuit_free(inner); +} + +#[test] +fn to_gate_circuit_gate_parametrized_roundtrip() { + // Template: RX(theta) on one qubit. + let template = circuit_new(1); + let theta = cstr("theta"); + let theta_p = param_parse(theta.as_ptr()); + assert_eq!(circuit_rx_param(template, 0, theta_p), 0); + + let name = cstr("rx_theta"); + let gate = circuit_to_gate(template, name.as_ptr()); + assert!(!gate.is_null()); + assert_eq!(circuit_gate_num_params(gate), 1); + assert_eq!(circuit_gate_signature_params_len(gate), 1); + let mut names: [*mut c_char; 1] = [ptr::null_mut(); 1]; + assert_eq!( + circuit_gate_signature_params(gate, names.as_mut_ptr(), 1), + 1 + ); + assert_eq!( + unsafe { CStr::from_ptr(names[0]) }.to_str().unwrap(), + "theta" + ); + cqlib_string_free(names[0]); + + // Apply with theta = 0.5 -> equivalent to RX(0.5). + let arg = cstr("0.5"); + let arg_p = param_parse(arg.as_ptr()); + let args: [*const CParameter; 1] = [arg_p]; + let qubits = [0u32]; + let outer = circuit_new(1); + assert_eq!( + circuit_circuit_gate(outer, gate, qubits.as_ptr(), 1, args.as_ptr(), 1), + 0 + ); + + let reference = circuit_new(1); + assert_eq!(circuit_rx(reference, 0, 0.5), 0); + // Fold the constant argument into a fixed parameter so the numeric + // matrix path can evaluate the gate. + let resolved = circuit_assign_params(outer, ptr::null()); + assert!(!resolved.is_null()); + assert_matrices_close(&matrix_of(resolved), &matrix_of(reference)); + + // Wrong parameter count -> -3. + assert_eq!( + circuit_circuit_gate(outer, gate, qubits.as_ptr(), 1, ptr::null(), 0), + -3 + ); + + circuit_free(reference); + circuit_free(resolved); + circuit_free(outer); + param_free(arg_p); + circuit_gate_free(gate); + circuit_free(template); + param_free(theta_p); +} + +#[test] +fn multi_control_matches_ccx() { + let circuit = circuit_new(3); + let gate = cstr("X"); + let controls = [0u32, 1]; + let targets = [2u32]; + assert_eq!( + circuit_multi_control( + circuit, + gate.as_ptr(), + controls.as_ptr(), + 2, + targets.as_ptr(), + 1, + ptr::null(), + 0 + ), + 0 + ); + + let reference = circuit_new(3); + assert_eq!(circuit_ccx(reference, 0, 1, 2), 0); + assert_matrices_close(&matrix_of(circuit), &matrix_of(reference)); + + // Unknown gate name -> -8; arity mismatch -> -3; OOB control -> -2. + let bogus = cstr("NOT_A_GATE"); + assert_eq!( + circuit_multi_control( + circuit, + bogus.as_ptr(), + controls.as_ptr(), + 2, + targets.as_ptr(), + 1, + ptr::null(), + 0 + ), + -8 + ); + let hadamard = cstr("H"); + assert_eq!( + circuit_multi_control( + circuit, + hadamard.as_ptr(), + controls.as_ptr(), + 2, + ptr::null(), + 0, + ptr::null(), + 0 + ), + -3 + ); + let oob = [0u32, 9]; + assert_eq!( + circuit_multi_control( + circuit, + gate.as_ptr(), + oob.as_ptr(), + 2, + targets.as_ptr(), + 1, + ptr::null(), + 0 + ), + -2 + ); + + circuit_free(reference); + circuit_free(circuit); +} + +#[test] +fn delay_and_index_snapshot() { + let circuit = circuit_new(2); + assert_eq!(circuit_delay(circuit, 1, 100.0), 0); + assert_eq!(circuit_delay(circuit, 5, 1.0), -2); + assert_eq!(circuit_delay(circuit, 0, f64::NAN), -3); + assert_eq!(circuit_delay(ptr::null_mut(), 0, 1.0), -1); + + let dt = cstr("dt"); + let dt_p = param_parse(dt.as_ptr()); + assert_eq!(circuit_delay_param(circuit, 0, dt_p), 0); + assert_eq!(circuit_delay_param(circuit, 0, ptr::null()), -1); + assert_eq!(circuit_num_operations(circuit), 2); + assert_eq!(circuit_num_parameters(circuit), 1); + + // Snapshot of the numeric delay. + let op = circuit_index(circuit, 0); + assert!(!op.is_null()); + let name = value_operation_name(op); + assert_eq!(unsafe { CStr::from_ptr(name) }.to_str().unwrap(), "delay"); + cqlib_string_free(name); + let ty = value_operation_instruction_type(op); + assert_eq!(unsafe { CStr::from_ptr(ty) }.to_str().unwrap(), "delay"); + cqlib_string_free(ty); + assert!(value_operation_label(op).is_null()); + assert_eq!(value_operation_num_qubits(op), 1); + let mut qubits = [0u32; 1]; + assert_eq!(value_operation_qubits(op, qubits.as_mut_ptr(), 1), 1); + assert_eq!(qubits[0], 1); + assert_eq!(value_operation_num_params(op), 1); + + let mut value = CParameterValue { + tag: 255, + value: 0.0, + param: ptr::null_mut(), + }; + assert_eq!(value_operation_param(op, 0, &mut value), 0); + assert_eq!(value.tag, PARAMETER_VALUE_TAG_FIXED); + assert!((value.value - 100.0).abs() < 1e-12); + assert_eq!(value_operation_param(op, 1, &mut value), -3); + value_operation_free(op); + + // Snapshot of the symbolic delay. + let symbolic = circuit_index(circuit, 1); + assert!(!symbolic.is_null()); + assert_eq!(value_operation_param(symbolic, 0, &mut value), 0); + assert_eq!(value.tag, PARAMETER_VALUE_TAG_PARAM); + assert!(!value.param.is_null()); + let bindings = cstr("dt:7"); + assert!((param_evaluate(value.param, bindings.as_ptr()) - 7.0).abs() < 1e-12); + param_free(value.param); + value_operation_free(symbolic); + + assert!(circuit_index(circuit, 99).is_null()); + assert!(circuit_index(ptr::null(), 0).is_null()); + + circuit_free(circuit); + param_free(dt_p); +} + +#[test] +fn remove_operations_batch() { + let circuit = circuit_new(1); + assert_eq!(circuit_h(circuit, 0), 0); + assert_eq!(circuit_x(circuit, 0), 0); + assert_eq!(circuit_phase(circuit, 0, 0.5), 0); + + let indices = [0usize, 2]; + assert_eq!(circuit_remove_operations(circuit, indices.as_ptr(), 2), 0); + assert_eq!(circuit_num_operations(circuit), 1); + + // The surviving operation is X. + let op = circuit_index(circuit, 0); + assert!(!op.is_null()); + let name = value_operation_name(op); + assert_eq!(unsafe { CStr::from_ptr(name) }.to_str().unwrap(), "X"); + cqlib_string_free(name); + value_operation_free(op); + + // Empty index list is a successful no-op. + assert_eq!(circuit_remove_operations(circuit, ptr::null(), 0), 0); + assert_eq!(circuit_num_operations(circuit), 1); + + // Out-of-bounds index -> -3; NULL circuit -> -1. + let bad = [0usize, 9]; + assert_eq!(circuit_remove_operations(circuit, bad.as_ptr(), 2), -3); + assert_eq!( + circuit_remove_operations(ptr::null_mut(), ptr::null(), 0), + -1 + ); + + circuit_free(circuit); +} + +#[test] +fn advanced_bindings_null_checks() { + assert_eq!(circuit_add_parameter(ptr::null_mut(), ptr::null()), -1); + assert_eq!(circuit_parameters_len(ptr::null()), 0); + assert_eq!(circuit_parameters(ptr::null(), ptr::null_mut(), 0), 0); + assert_eq!(circuit_symbols_len(ptr::null()), 0); + assert_eq!(circuit_symbols(ptr::null(), ptr::null_mut(), 0), 0); + assert_eq!(circuit_used_symbols_len(ptr::null()), 0); + assert_eq!(circuit_used_symbols(ptr::null(), ptr::null_mut(), 0), 0); + assert_eq!(circuit_uses_symbol(ptr::null(), ptr::null()), -1); + assert_eq!(circuit_global_phase(ptr::null(), ptr::null_mut()), -1); + assert_eq!( + circuit_map_param(ptr::null_mut(), ptr::null(), ptr::null_mut()), + -1 + ); + assert!(circuit_resolve_parameter(ptr::null(), ptr::null()).is_null()); + assert_eq!( + circuit_parameter_value(ptr::null(), ptr::null(), ptr::null_mut()), + -1 + ); + + assert_eq!( + circuit_unitary(ptr::null_mut(), ptr::null(), 0, ptr::null(), ptr::null(), 0), + -1 + ); + assert_eq!( + circuit_unitary_with_params( + ptr::null_mut(), + ptr::null(), + 0, + ptr::null(), + ptr::null(), + ptr::null(), + 0, + ptr::null(), + 0, + ptr::null(), + 0 + ), + -1 + ); + assert!(circuit_to_gate(ptr::null(), ptr::null()).is_null()); + assert_eq!( + circuit_circuit_gate(ptr::null_mut(), ptr::null(), ptr::null(), 0, ptr::null(), 0), + -1 + ); + assert_eq!( + circuit_multi_control( + ptr::null_mut(), + ptr::null(), + ptr::null(), + 0, + ptr::null(), + 0, + ptr::null(), + 0 + ), + -1 + ); + assert_eq!(circuit_delay(ptr::null_mut(), 0, 1.0), -1); + assert_eq!(circuit_delay_param(ptr::null_mut(), 0, ptr::null()), -1); + assert!(circuit_index(ptr::null(), 0).is_null()); + assert_eq!( + circuit_remove_operations(ptr::null_mut(), ptr::null(), 0), + -1 + ); + + // Handle accessors tolerate NULL. + assert!(circuit_gate_name(ptr::null()).is_null()); + assert_eq!(circuit_gate_num_qubits(ptr::null()), 0); + assert_eq!(circuit_gate_num_params(ptr::null()), 0); + assert_eq!(circuit_gate_signature_params_len(ptr::null()), 0); + assert_eq!( + circuit_gate_signature_params(ptr::null(), ptr::null_mut(), 0), + 0 + ); + assert!(value_operation_name(ptr::null()).is_null()); + assert!(value_operation_instruction_type(ptr::null()).is_null()); + assert!(value_operation_label(ptr::null()).is_null()); + assert_eq!(value_operation_num_qubits(ptr::null()), 0); + assert_eq!(value_operation_qubits(ptr::null(), ptr::null_mut(), 0), 0); + assert_eq!(value_operation_num_params(ptr::null()), 0); + assert_eq!(value_operation_param(ptr::null(), 0, ptr::null_mut()), -1); + circuit_gate_free(ptr::null_mut()); + value_operation_free(ptr::null_mut()); +} diff --git a/crates/binding-c/tests/circuit_ansatz_test.rs b/crates/binding-c/tests/circuit_ansatz_test.rs new file mode 100644 index 00000000..6f7fa37e --- /dev/null +++ b/crates/binding-c/tests/circuit_ansatz_test.rs @@ -0,0 +1,497 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! Rust FFI tests for parameterized ansatz generation (`circuit/ansatz.rs`). + +use binding_c::circuit::{ + ENTANGLEMENT_CIRCULAR, ENTANGLEMENT_CUSTOM, ENTANGLEMENT_FULL, ENTANGLEMENT_LINEAR, + EVOLUTION_STRATEGY_AUTO, EVOLUTION_STRATEGY_EXACT, EVOLUTION_STRATEGY_TROTTER, + TROTTER_FIRST_ORDER, TROTTER_RANDOMIZED, TROTTER_SECOND_ORDER, + basic_entangler_layers_build_circuit, basic_entangler_layers_entanglement_gate, + basic_entangler_layers_free, basic_entangler_layers_new, basic_entangler_layers_num_parameters, + basic_entangler_layers_num_qubits, basic_entangler_layers_reps, + basic_entangler_layers_rotation_gate, circuit_free, circuit_new, circuit_num_operations, + circuit_num_parameters, circuit_num_qubits, qaoa_ansatz_build_circuit, + qaoa_ansatz_evolution_strategy, qaoa_ansatz_free, qaoa_ansatz_initial_state, qaoa_ansatz_mixer, + qaoa_ansatz_new, qaoa_ansatz_num_parameters, qaoa_ansatz_num_qubits, qaoa_ansatz_reps, + qaoa_ansatz_validate, two_local_build_circuit, two_local_entanglement, + two_local_entanglement_gate, two_local_free, two_local_new, two_local_num_parameters, + two_local_num_qubits, two_local_reps, two_local_rotation_gates, + two_local_skip_final_rotation_layer, two_local_validate, +}; +use binding_c::qis::{ + CHamiltonian, hamiltonian_free, hamiltonian_from_pauli, hamiltonian_new, pauli_string_parse, +}; +use std::ffi::CString; +use std::os::raw::c_char; + +fn cstr(s: &str) -> CString { + CString::new(s).unwrap() +} + +/// Builds a Hamiltonian from a single Pauli term (coefficient = 1.0). +fn pauli_hamiltonian(text: &str) -> *mut CHamiltonian { + let pauli = pauli_string_parse(cstr(text).as_ptr()); + assert!(!pauli.is_null()); + // `hamiltonian_from_pauli` takes ownership of the pauli handle. + let ham = hamiltonian_from_pauli(pauli); + assert!(!ham.is_null()); + ham +} + +#[test] +fn two_local_structure() { + let cz = cstr("CZ"); + let ansatz = two_local_new(3); + assert!(!ansatz.is_null()); + assert_eq!(two_local_num_qubits(ansatz), 3); + assert_eq!(two_local_validate(ansatz), 0); + + // Defaults: reps = 3, RY rotations, final rotation layer kept => + // (3 + 1) layers * 3 qubits * 1 gate = 12 parameters. + assert_eq!(two_local_num_parameters(ansatz), 12); + + // reps = 2 => (2 + 1) * 3 * 1 = 9. + assert_eq!(two_local_reps(ansatz, 2), 0); + assert_eq!(two_local_num_parameters(ansatz), 9); + + // Skipping the final rotation layer leaves exactly `reps` layers. + assert_eq!(two_local_skip_final_rotation_layer(ansatz, true), 0); + assert_eq!(two_local_num_parameters(ansatz), 2 * 3); + + // The entangler choice never changes the parameter count. + assert_eq!(two_local_entanglement_gate(ansatz, cz.as_ptr()), 0); + assert_eq!(two_local_num_parameters(ansatz), 6); + assert_eq!(two_local_validate(ansatz), 0); + + // Two rotation gates per qubit: 2 layers * 3 qubits * 2 gates = 12. + let ry = cstr("RY"); + let rz = cstr("RZ"); + let names = [ry.as_ptr(), rz.as_ptr()]; + assert_eq!(two_local_rotation_gates(ansatz, names.as_ptr(), 2), 0); + assert_eq!(two_local_num_parameters(ansatz), 12); + + // Topologies are accepted and orthogonal to the parameter count. + assert_eq!( + two_local_entanglement(ansatz, ENTANGLEMENT_CIRCULAR, std::ptr::null(), 0), + 0 + ); + assert_eq!( + two_local_entanglement(ansatz, ENTANGLEMENT_FULL, std::ptr::null(), 0), + 0 + ); + assert_eq!( + two_local_entanglement(ansatz, ENTANGLEMENT_LINEAR, std::ptr::null(), 0), + 0 + ); + let pairs = [1u32, 2, 0, 2]; + assert_eq!( + two_local_entanglement(ansatz, ENTANGLEMENT_CUSTOM, pairs.as_ptr(), 4), + 0 + ); + assert_eq!(two_local_num_parameters(ansatz), 12); + assert_eq!(two_local_validate(ansatz), 0); + + two_local_free(ansatz); +} + +#[test] +fn two_local_invalid_inputs() { + // Gate-name parsing: unknown names, case sensitivity, invalid UTF-8. + let bad_name = cstr("NOT_A_GATE"); + let lowercase = cstr("cx"); + let invalid_utf8 = CString::new(vec![0xFF, 0xFF]).unwrap(); + let ansatz = two_local_new(3); + assert!(!ansatz.is_null()); + assert_eq!(two_local_entanglement_gate(ansatz, bad_name.as_ptr()), -8); + assert_eq!(two_local_entanglement_gate(ansatz, lowercase.as_ptr()), -8); + assert_eq!(two_local_entanglement_gate(ansatz, std::ptr::null()), -1); + assert_eq!( + two_local_entanglement_gate(ansatz, invalid_utf8.as_ptr()), + -4 + ); + + // A known single-qubit gate parses fine, but as an entangler it fails + // validate and build (the entanglement gate must be CX, CY or CZ). + let h = cstr("H"); + assert_eq!(two_local_entanglement_gate(ansatz, h.as_ptr()), 0); + assert_eq!(two_local_validate(ansatz), -3); + assert!(two_local_build_circuit(ansatz, cstr("t").as_ptr()).is_null()); + two_local_free(ansatz); + + // Zero-qubit templates are created but never valid. + let zero = two_local_new(0); + assert!(!zero.is_null()); + assert_eq!(two_local_num_qubits(zero), 0); + assert_eq!(two_local_validate(zero), -3); + assert!(two_local_build_circuit(zero, cstr("t").as_ptr()).is_null()); + two_local_free(zero); + + // reps = 0 is legal: exactly one rotation layer remains. + let sparse = two_local_new(3); + assert_eq!(two_local_reps(sparse, 0), 0); + assert_eq!(two_local_num_parameters(sparse), 3); + assert_eq!(two_local_validate(sparse), 0); + // Skipping the final layer removes those last parameters. + assert_eq!(two_local_skip_final_rotation_layer(sparse, true), 0); + assert_eq!(two_local_num_parameters(sparse), 0); + two_local_free(sparse); + + // rotation_gates argument validation. + let setter = two_local_new(2); + assert!(!setter.is_null()); + assert_eq!(two_local_rotation_gates(setter, std::ptr::null(), 1), -1); + let null_names: [*const c_char; 1] = [std::ptr::null()]; + assert_eq!(two_local_rotation_gates(setter, null_names.as_ptr(), 1), -1); + // SWAP parses as a standard gate but is rejected by validate as a + // rotation gate (must be RX, RY or RZ). + let swap = cstr("SWAP"); + let swap_names = [swap.as_ptr()]; + assert_eq!(two_local_rotation_gates(setter, swap_names.as_ptr(), 1), 0); + assert_eq!(two_local_validate(setter), -3); + two_local_free(setter); + + // Topology argument validation. + let topo = two_local_new(3); + assert_eq!(two_local_entanglement(topo, 99, std::ptr::null(), 0), -8); + let pairs = [0u32, 1]; + assert_eq!( + two_local_entanglement(topo, ENTANGLEMENT_CUSTOM, pairs.as_ptr(), 3), + -8 + ); + assert_eq!( + two_local_entanglement(topo, ENTANGLEMENT_CUSTOM, std::ptr::null(), 2), + -1 + ); + // A self-loop pair is stored but fails validate. + let self_loop = [1u32, 1]; + assert_eq!( + two_local_entanglement(topo, ENTANGLEMENT_CUSTOM, self_loop.as_ptr(), 2), + 0 + ); + assert_eq!(two_local_validate(topo), -3); + two_local_free(topo); + + // build_circuit prefix validation. + let builder = two_local_new(2); + assert!(two_local_build_circuit(builder, std::ptr::null()).is_null()); + assert!(two_local_build_circuit(builder, invalid_utf8.as_ptr()).is_null()); + two_local_free(builder); +} + +#[test] +fn basic_entangler_layers_structure() { + let ry = cstr("RY"); + let cz = cstr("CZ"); + let layers = basic_entangler_layers_new(3); + assert!(!layers.is_null()); + assert_eq!(basic_entangler_layers_num_qubits(layers), 3); + + // Defaults: reps = 1, one rotation per qubit per layer => 3 parameters. + assert_eq!(basic_entangler_layers_num_parameters(layers), 3); + + // reps = 2 => 6. + assert_eq!(basic_entangler_layers_reps(layers, 2), 0); + assert_eq!(basic_entangler_layers_num_parameters(layers), 6); + + // Gate swaps keep the one-parameter-per-qubit-per-layer count. + assert_eq!(basic_entangler_layers_rotation_gate(layers, ry.as_ptr()), 0); + assert_eq!( + basic_entangler_layers_entanglement_gate(layers, cz.as_ptr()), + 0 + ); + assert_eq!(basic_entangler_layers_num_parameters(layers), 6); + + // Bad gate names: unknown, non-UTF-8 and NULL. + let bad_name = cstr("NOT_A_GATE"); + let invalid_utf8 = CString::new(vec![0xFF]).unwrap(); + assert_eq!( + basic_entangler_layers_rotation_gate(layers, bad_name.as_ptr()), + -8 + ); + assert_eq!( + basic_entangler_layers_entanglement_gate(layers, invalid_utf8.as_ptr()), + -4 + ); + assert_eq!( + basic_entangler_layers_rotation_gate(std::ptr::null_mut(), ry.as_ptr()), + -1 + ); + assert_eq!( + basic_entangler_layers_entanglement_gate(std::ptr::null_mut(), cz.as_ptr()), + -1 + ); + // The failed setters leave the template configured. + assert_eq!(basic_entangler_layers_num_parameters(layers), 6); + + basic_entangler_layers_free(layers); +} + +#[test] +fn qaoa_ansatz_structure() { + // Cost Hamiltonian: a single ZZ term on 2 qubits. + let cost = pauli_hamiltonian("ZZ"); + let ansatz = qaoa_ansatz_new(cost); + assert!(!ansatz.is_null()); + + // Qubit count is inferred from the cost operator; default reps = 1 + // gives one gamma and one beta angle. + assert_eq!(qaoa_ansatz_num_qubits(ansatz), 2); + assert_eq!(qaoa_ansatz_num_parameters(ansatz), 2); + assert_eq!(qaoa_ansatz_validate(ansatz), 0); + + // reps = 2 doubles the parameter count. + assert_eq!(qaoa_ansatz_reps(ansatz, 2), 0); + assert_eq!(qaoa_ansatz_num_parameters(ansatz), 4); + + // A same-width mixer override succeeds and stays valid. + let mixer = pauli_hamiltonian("XX"); + assert_eq!(qaoa_ansatz_mixer(ansatz, mixer), 0); + assert_eq!(qaoa_ansatz_validate(ansatz), 0); + assert_eq!(qaoa_ansatz_num_parameters(ansatz), 4); + + // Mixers on a different qubit count are rejected. + let wide = pauli_hamiltonian("XXX"); + assert_eq!(qaoa_ansatz_mixer(ansatz, wide), -3); + assert_eq!(qaoa_ansatz_mixer(ansatz, std::ptr::null()), -1); + + // Initial-state circuits must match the qubit count. + let init = circuit_new(2); + assert_eq!(qaoa_ansatz_initial_state(ansatz, init), 0); + circuit_free(init); + let big = circuit_new(3); + assert_eq!(qaoa_ansatz_initial_state(ansatz, big), -3); + circuit_free(big); + assert_eq!(qaoa_ansatz_initial_state(ansatz, std::ptr::null()), -1); + + // Evolution strategy selection. ZZ and XX are single terms, so the + // exact decomposition is valid; Trotter accepts any Hermitian input. + assert_eq!( + qaoa_ansatz_evolution_strategy(ansatz, EVOLUTION_STRATEGY_AUTO, TROTTER_FIRST_ORDER, 1, 0), + 0 + ); + assert_eq!( + qaoa_ansatz_evolution_strategy(ansatz, EVOLUTION_STRATEGY_EXACT, TROTTER_FIRST_ORDER, 0, 0), + 0 + ); + assert_eq!(qaoa_ansatz_validate(ansatz), 0); + assert_eq!( + qaoa_ansatz_evolution_strategy( + ansatz, + EVOLUTION_STRATEGY_TROTTER, + TROTTER_SECOND_ORDER, + 2, + 7 + ), + 0 + ); + assert_eq!(qaoa_ansatz_validate(ansatz), 0); + assert_eq!( + qaoa_ansatz_evolution_strategy( + ansatz, + EVOLUTION_STRATEGY_TROTTER, + TROTTER_RANDOMIZED, + 2, + 42 + ), + 0 + ); + assert_eq!(qaoa_ansatz_validate(ansatz), 0); + + // Unknown strategy or Trotter-mode tags are rejected. + assert_eq!( + qaoa_ansatz_evolution_strategy(ansatz, EVOLUTION_STRATEGY_TROTTER, 99, 1, 0), + -8 + ); + assert_eq!( + qaoa_ansatz_evolution_strategy(ansatz, 99, TROTTER_FIRST_ORDER, 1, 0), + -8 + ); + + // Zero-step strategies are stored but fail validation until restored. + assert_eq!( + qaoa_ansatz_evolution_strategy(ansatz, EVOLUTION_STRATEGY_AUTO, TROTTER_FIRST_ORDER, 0, 0), + 0 + ); + assert_eq!(qaoa_ansatz_validate(ansatz), -3); + assert_eq!( + qaoa_ansatz_evolution_strategy(ansatz, EVOLUTION_STRATEGY_AUTO, TROTTER_FIRST_ORDER, 1, 0), + 0 + ); + assert_eq!(qaoa_ansatz_validate(ansatz), 0); + + qaoa_ansatz_free(ansatz); + hamiltonian_free(mixer); + hamiltonian_free(wide); + hamiltonian_free(cost); + + // The constructor rejects NULL, empty and identity-only cost operators. + assert!(qaoa_ansatz_new(std::ptr::null()).is_null()); + let empty = hamiltonian_new(2); + assert!(qaoa_ansatz_new(empty).is_null()); + hamiltonian_free(empty); + let identity = pauli_hamiltonian("II"); + assert!(qaoa_ansatz_new(identity).is_null()); + hamiltonian_free(identity); +} + +#[test] +fn ansatz_null_handles() { + // TwoLocal: every exported entry point tolerates a NULL handle. + assert_eq!(two_local_reps(std::ptr::null_mut(), 1), -1); + assert_eq!( + two_local_rotation_gates(std::ptr::null_mut(), std::ptr::null(), 0), + -1 + ); + assert_eq!( + two_local_entanglement_gate(std::ptr::null_mut(), std::ptr::null()), + -1 + ); + assert_eq!( + two_local_entanglement( + std::ptr::null_mut(), + ENTANGLEMENT_LINEAR, + std::ptr::null(), + 0 + ), + -1 + ); + assert_eq!( + two_local_skip_final_rotation_layer(std::ptr::null_mut(), true), + -1 + ); + assert_eq!(two_local_validate(std::ptr::null()), -1); + assert_eq!(two_local_num_parameters(std::ptr::null()), 0); + assert_eq!(two_local_num_qubits(std::ptr::null()), 0); + assert!(two_local_build_circuit(std::ptr::null(), cstr("t").as_ptr()).is_null()); + + // BasicEntanglerLayers. + assert_eq!(basic_entangler_layers_reps(std::ptr::null_mut(), 1), -1); + assert_eq!( + basic_entangler_layers_rotation_gate(std::ptr::null_mut(), std::ptr::null()), + -1 + ); + assert_eq!( + basic_entangler_layers_entanglement_gate(std::ptr::null_mut(), std::ptr::null()), + -1 + ); + assert_eq!(basic_entangler_layers_num_parameters(std::ptr::null()), 0); + assert_eq!(basic_entangler_layers_num_qubits(std::ptr::null()), 0); + assert!(basic_entangler_layers_build_circuit(std::ptr::null(), cstr("t").as_ptr()).is_null()); + + // QAOAAnsatz. + assert!(qaoa_ansatz_new(std::ptr::null()).is_null()); + assert_eq!(qaoa_ansatz_reps(std::ptr::null_mut(), 1), -1); + assert_eq!( + qaoa_ansatz_mixer(std::ptr::null_mut(), std::ptr::null()), + -1 + ); + assert_eq!( + qaoa_ansatz_initial_state(std::ptr::null_mut(), std::ptr::null()), + -1 + ); + assert_eq!( + qaoa_ansatz_evolution_strategy( + std::ptr::null_mut(), + EVOLUTION_STRATEGY_EXACT, + TROTTER_FIRST_ORDER, + 1, + 0 + ), + -1 + ); + assert_eq!(qaoa_ansatz_validate(std::ptr::null()), -1); + assert_eq!(qaoa_ansatz_num_parameters(std::ptr::null()), 0); + assert_eq!(qaoa_ansatz_num_qubits(std::ptr::null()), 0); + assert!(qaoa_ansatz_build_circuit(std::ptr::null(), cstr("p").as_ptr()).is_null()); + + // Freeing NULL is a no-op everywhere. + two_local_free(std::ptr::null_mut()); + basic_entangler_layers_free(std::ptr::null_mut()); + qaoa_ansatz_free(std::ptr::null_mut()); + hamiltonian_free(std::ptr::null_mut()); +} + +#[test] +fn ansatz_build_circuit_smoke() { + let prefix = cstr("t"); + + // TwoLocal: reps = 2, RY, CX, linear. Three rotation layers of 3 RY + // plus two entanglement layers of 2 CX => 13 operations, 9 parameters. + let ansatz = two_local_new(3); + assert_eq!(two_local_reps(ansatz, 2), 0); + let circuit = two_local_build_circuit(ansatz, prefix.as_ptr()); + assert!(!circuit.is_null()); + assert_eq!(circuit_num_qubits(circuit), 3); + assert_eq!(circuit_num_operations(circuit), 13); + assert_eq!(circuit_num_parameters(circuit), 9); + circuit_free(circuit); + + // Skipping the final rotation layer drops the last 3 RY gates. + assert_eq!(two_local_skip_final_rotation_layer(ansatz, true), 0); + let skipped = two_local_build_circuit(ansatz, prefix.as_ptr()); + assert!(!skipped.is_null()); + assert_eq!(circuit_num_operations(skipped), 2 * 3 + 2 * 2); + assert_eq!(circuit_num_parameters(skipped), 6); + circuit_free(skipped); + + // Full topology: C(3,2) = 3 CX per entanglement layer => 15 operations. + assert_eq!(two_local_skip_final_rotation_layer(ansatz, false), 0); + assert_eq!( + two_local_entanglement(ansatz, ENTANGLEMENT_FULL, std::ptr::null(), 0), + 0 + ); + let full = two_local_build_circuit(ansatz, prefix.as_ptr()); + assert!(!full.is_null()); + assert_eq!(circuit_num_operations(full), 9 + 2 * 3); + circuit_free(full); + two_local_free(ansatz); + + // BasicEntanglerLayers: reps = 2 on a 3-qubit ring => per layer + // 3 rotations + 3 entanglers => 12 operations, 6 parameters. + let layers = basic_entangler_layers_new(3); + assert_eq!(basic_entangler_layers_reps(layers, 2), 0); + let ring = basic_entangler_layers_build_circuit(layers, prefix.as_ptr()); + assert!(!ring.is_null()); + assert_eq!(circuit_num_qubits(ring), 3); + assert_eq!(circuit_num_operations(ring), 12); + assert_eq!(circuit_num_parameters(ring), 6); + assert!(basic_entangler_layers_build_circuit(layers, std::ptr::null()).is_null()); + circuit_free(ring); + basic_entangler_layers_free(layers); + + // A single qubit has no entanglement: only the rotations remain. + let single = basic_entangler_layers_new(1); + assert_eq!(basic_entangler_layers_reps(single, 2), 0); + let solo = basic_entangler_layers_build_circuit(single, prefix.as_ptr()); + assert!(!solo.is_null()); + assert_eq!(circuit_num_operations(solo), 2); + assert_eq!(circuit_num_parameters(solo), 2); + circuit_free(solo); + basic_entangler_layers_free(single); + + // QAOA: the H layer plus cost/mixer evolution for one rep. The exact + // operation count depends on the Pauli-evolution lowering, so only + // smoke-check the floor while pinning the shape. + let cost = pauli_hamiltonian("ZZ"); + let qaoa = qaoa_ansatz_new(cost); + assert!(!qaoa.is_null()); + let qc = qaoa_ansatz_build_circuit(qaoa, prefix.as_ptr()); + assert!(!qc.is_null()); + assert_eq!(circuit_num_qubits(qc), 2); + assert_eq!(circuit_num_parameters(qc), 2); + assert!(circuit_num_operations(qc) >= 2); + assert!(qaoa_ansatz_build_circuit(qaoa, std::ptr::null()).is_null()); + circuit_free(qc); + qaoa_ansatz_free(qaoa); + hamiltonian_free(cost); +} diff --git a/crates/binding-c/tests/circuit_cfg_test.rs b/crates/binding-c/tests/circuit_cfg_test.rs new file mode 100644 index 00000000..39e3ed1b --- /dev/null +++ b/crates/binding-c/tests/circuit_cfg_test.rs @@ -0,0 +1,1029 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! Rust FFI tests for circuit control-flow graphs (`circuit/cfg.rs`). + +use binding_c::circuit::CqlibBodyFn; +use binding_c::circuit::{ + CCircuit, CClassicalExpr, CClassicalType, CClassicalVar, CFG_FLOW_FALSE_BRANCH, + CFG_FLOW_TRUE_BRANCH, CFG_FLOW_UNCONDITIONAL, CFG_REGION_FOR, CFG_REGION_IF, CFG_REGION_SWITCH, + CFG_REGION_WHILE, CFG_TERMINATOR_BRANCH, CFG_TERMINATOR_BREAK, CFG_TERMINATOR_CONTINUE, + CFG_TERMINATOR_FOR_LOOP, CFG_TERMINATOR_JUMP, CFG_TERMINATOR_RETURN, COperation, + CQLIB_CLASSICAL_TYPE_BIT, CQLIB_CLASSICAL_TYPE_BIT_VEC, CQLIB_CLASSICAL_TYPE_BOOL, + CQLIB_CLASSICAL_TYPE_UINT, circuit_cfg_add_block, circuit_cfg_add_edge, + circuit_cfg_block_has_terminator, circuit_cfg_block_is_empty, circuit_cfg_block_label, + circuit_cfg_block_operations, circuit_cfg_block_operations_len, + circuit_cfg_block_terminator_tag, circuit_cfg_blocks, circuit_cfg_classical_values, + circuit_cfg_classical_values_len, circuit_cfg_classical_vars, circuit_cfg_classical_vars_len, + circuit_cfg_entry_block, circuit_cfg_free, circuit_cfg_from_circuit, circuit_cfg_from_qubits, + circuit_cfg_is_loop_header, circuit_cfg_new, circuit_cfg_num_blocks, circuit_cfg_num_qubits, + circuit_cfg_outgoing_edges, circuit_cfg_outgoing_edges_len, circuit_cfg_push_operation, + circuit_cfg_qubits, circuit_cfg_region_if, circuit_cfg_region_loop, circuit_cfg_region_switch, + circuit_cfg_region_switch_cases_len, circuit_cfg_region_tag, circuit_cfg_set_entry_block, + circuit_cfg_set_for_region, circuit_cfg_set_if_region, circuit_cfg_set_switch_region, + circuit_cfg_set_terminator_break, circuit_cfg_set_terminator_continue, + circuit_cfg_set_terminator_jump, circuit_cfg_set_terminator_return, + circuit_cfg_set_while_region, circuit_cfg_terminator_condition, + circuit_cfg_terminator_for_info, circuit_cfg_terminator_target, circuit_cfg_to_circuit, + circuit_cfg_validate, circuit_classical_vars_len, circuit_cx, circuit_for_uint, circuit_free, + circuit_h, circuit_measure_bits, circuit_new, circuit_num_operations, circuit_num_qubits, + circuit_var, circuit_while, classical_expr_free, classical_expr_uint_literal, + classical_expr_var, classical_var_free, operation_free, operation_new, +}; +use binding_c::cqlib_string_free; +use std::ffi::{CStr, CString}; +use std::os::raw::c_void; + +fn cstr(s: &str) -> CString { + CString::new(s).unwrap() +} + +/// Loop-body callback appending one gate; `user_data` is ignored. +extern "C" fn add_h(circuit: *mut CCircuit, _user_data: *mut c_void) -> i32 { + circuit_h(circuit, 0) +} + +#[test] +fn cfg_from_circuit_roundtrip() { + let circuit = circuit_new(2); + assert_eq!(circuit_h(circuit, 0), 0); + assert_eq!(circuit_cx(circuit, 0, 1), 0); + assert_eq!(circuit_num_operations(circuit), 2); + + // A straight-line circuit expands into a single entry block. + let cfg = circuit_cfg_from_circuit(circuit); + assert!(!cfg.is_null()); + assert_eq!(circuit_cfg_validate(cfg), 0); + assert_eq!(circuit_cfg_num_qubits(cfg), 2); + assert_eq!(circuit_cfg_num_blocks(cfg), 1); + assert_eq!(circuit_cfg_entry_block(cfg), 0); + + let mut ids = [9u32; 2]; + assert_eq!(circuit_cfg_qubits(cfg, ids.as_mut_ptr(), 2), 0); + assert_eq!(ids, [0, 1]); + + let entry_label = circuit_cfg_block_label(cfg, 0); + assert!(!entry_label.is_null()); + assert_eq!( + unsafe { CStr::from_ptr(entry_label) }.to_str().unwrap(), + "entry" + ); + cqlib_string_free(entry_label); + + // Entry block: two ordinary operations plus a `Return` terminator. + assert_eq!(circuit_cfg_block_is_empty(cfg, 0), 0); + assert_eq!(circuit_cfg_block_operations_len(cfg, 0), 2); + assert_eq!(circuit_cfg_block_has_terminator(cfg, 0), 1); + assert_eq!( + circuit_cfg_block_terminator_tag(cfg, 0), + CFG_TERMINATOR_RETURN as i32 + ); + assert_eq!(circuit_cfg_terminator_target(cfg, 0), u32::MAX); + assert!(circuit_cfg_terminator_condition(cfg, 0).is_null()); + + // Cloned operation handles are independent of the cfg and of each other. + let mut cloned: [*mut COperation; 2] = [std::ptr::null_mut(); 2]; + assert_eq!( + circuit_cfg_block_operations(cfg, 0, cloned.as_mut_ptr(), 2), + 0 + ); + assert!(!cloned[0].is_null()); + assert!(!cloned[1].is_null()); + operation_free(cloned[0]); + operation_free(cloned[1]); + + // An undersized buffer is rejected before any write. + assert_eq!( + circuit_cfg_block_operations(cfg, 0, cloned.as_mut_ptr(), 1), + -8 + ); + + // Straight-line circuits carry no classical state. + assert_eq!(circuit_cfg_classical_vars_len(cfg), 0); + assert_eq!(circuit_cfg_classical_values_len(cfg), 0); + + // Round trip: qubit count and operation count are preserved. + let lowered = circuit_cfg_to_circuit(cfg); + assert!(!lowered.is_null()); + assert_eq!(circuit_num_qubits(lowered), 2); + assert_eq!(circuit_num_operations(lowered), 2); + + // The source circuit stays alive and unchanged. + assert_eq!(circuit_num_operations(circuit), 2); + + circuit_free(lowered); + circuit_cfg_free(cfg); + circuit_free(circuit); +} + +#[test] +fn cfg_layout_and_blocks() { + let h_name = cstr("H"); + let zero = [0u32]; + let op = operation_new(h_name.as_ptr(), zero.as_ptr(), 1, std::ptr::null(), 0); + assert!(!op.is_null()); + + // A fresh CFG is empty and has no designated entry. + let cfg = circuit_cfg_new(2); + assert!(!cfg.is_null()); + assert_eq!(circuit_cfg_num_qubits(cfg), 2); + assert_eq!(circuit_cfg_num_blocks(cfg), 0); + assert_eq!(circuit_cfg_entry_block(cfg), u32::MAX); + assert_eq!(circuit_cfg_blocks(cfg, std::ptr::null_mut(), 0), 0); + + // Blocks are appended with stable sequential indices. + let b0 = circuit_cfg_add_block(cfg); + let b1 = circuit_cfg_add_block(cfg); + assert_eq!(b0, 0); + assert_eq!(b1, 1); + assert_eq!(circuit_cfg_num_blocks(cfg), 2); + let b2 = circuit_cfg_add_block(cfg); + assert_eq!(b2, 2); + assert_eq!(circuit_cfg_num_blocks(cfg), 3); + + // Block enumeration into the caller-supplied buffer. + let mut nodes = [9u32; 3]; + assert_eq!(circuit_cfg_blocks(cfg, nodes.as_mut_ptr(), 3), 0); + assert_eq!(nodes, [0, 1, 2]); + assert_eq!(circuit_cfg_blocks(cfg, nodes.as_mut_ptr(), 2), -8); + assert_eq!(circuit_cfg_blocks(cfg, std::ptr::null_mut(), 3), -1); + + // Entry switching accepts any non-sentinel index (`validate` checks it). + assert_eq!(circuit_cfg_set_entry_block(cfg, b1), 0); + assert_eq!(circuit_cfg_entry_block(cfg), b1); + assert_eq!(circuit_cfg_set_entry_block(cfg, u32::MAX), -2); + + // Fresh blocks are empty and unlabeled. + assert_eq!(circuit_cfg_block_is_empty(cfg, b2), 1); + assert!(circuit_cfg_block_label(cfg, b2).is_null()); + assert_eq!(circuit_cfg_block_operations_len(cfg, b2), 0); + assert_eq!( + circuit_cfg_block_operations(cfg, b2, std::ptr::null_mut(), 0), + 0 + ); + + // Semantic edges between existing blocks; flow tags are checked first. + assert_eq!( + circuit_cfg_add_edge(cfg, b0, b1, CFG_FLOW_UNCONDITIONAL, 0, 0), + 0 + ); + assert_eq!( + circuit_cfg_add_edge(cfg, b0, 3, CFG_FLOW_UNCONDITIONAL, 0, 0), + -2 + ); + assert_eq!( + circuit_cfg_add_edge(cfg, u32::MAX, b1, CFG_FLOW_UNCONDITIONAL, 0, 0), + -2 + ); + assert_eq!(circuit_cfg_add_edge(cfg, b0, 3, 99, 0, 0), -8); + assert_eq!(circuit_cfg_add_edge(cfg, b0, b1, 99, 0, 0), -8); + + let mut targets = [9u32; 1]; + let mut tags = [9u32; 1]; + let mut lo = [9u64; 1]; + let mut hi = [9u64; 1]; + assert_eq!(circuit_cfg_outgoing_edges_len(cfg, b0), 1); + assert_eq!( + circuit_cfg_outgoing_edges( + cfg, + b0, + targets.as_mut_ptr(), + tags.as_mut_ptr(), + lo.as_mut_ptr(), + hi.as_mut_ptr(), + 1 + ), + 0 + ); + assert_eq!(targets, [b1]); + assert_eq!(tags, [CFG_FLOW_UNCONDITIONAL]); + assert_eq!(lo, [0]); + assert_eq!(hi, [0]); + // Undersized then NULL parallel buffers. + assert_eq!( + circuit_cfg_outgoing_edges( + cfg, + b0, + targets.as_mut_ptr(), + tags.as_mut_ptr(), + lo.as_mut_ptr(), + hi.as_mut_ptr(), + 0 + ), + -8 + ); + assert_eq!( + circuit_cfg_outgoing_edges( + cfg, + b0, + std::ptr::null_mut(), + tags.as_mut_ptr(), + lo.as_mut_ptr(), + hi.as_mut_ptr(), + 1 + ), + -1 + ); + + // Pushing one operation flips `block_is_empty` and grows the count. + assert_eq!(circuit_cfg_push_operation(cfg, b2, op), 0); + assert_eq!(circuit_cfg_block_is_empty(cfg, b2), 0); + assert_eq!(circuit_cfg_block_operations_len(cfg, b2), 1); + let mut single: [*mut COperation; 1] = [std::ptr::null_mut(); 1]; + assert_eq!( + circuit_cfg_block_operations(cfg, b2, single.as_mut_ptr(), 1), + 0 + ); + assert!(!single[0].is_null()); + operation_free(single[0]); + assert_eq!( + circuit_cfg_block_operations(cfg, b2, single.as_mut_ptr(), 0), + -8 + ); + assert_eq!( + circuit_cfg_block_operations(cfg, b2, std::ptr::null_mut(), 1), + -1 + ); + + // Unknown blocks are rejected by mutation entry points. + assert_eq!(circuit_cfg_push_operation(cfg, 7, op), -2); + assert_eq!(circuit_cfg_push_operation(cfg, u32::MAX, op), -2); + assert_eq!(circuit_cfg_push_operation(cfg, b2, std::ptr::null()), -1); + + circuit_cfg_free(cfg); + operation_free(op); +} + +#[test] +fn cfg_terminators() { + let cfg = circuit_cfg_new(1); + let b0 = circuit_cfg_add_block(cfg); + let b1 = circuit_cfg_add_block(cfg); + let b2 = circuit_cfg_add_block(cfg); + + // Setters succeed and the getter round-trips each terminator flavor. + assert_eq!(circuit_cfg_set_terminator_jump(cfg, b0, b1), 0); + assert_eq!(circuit_cfg_set_terminator_break(cfg, b1, b0), 0); + assert_eq!(circuit_cfg_set_terminator_continue(cfg, b2, b0), 0); + + assert_eq!(circuit_cfg_block_has_terminator(cfg, b0), 1); + assert_eq!( + circuit_cfg_block_terminator_tag(cfg, b0), + CFG_TERMINATOR_JUMP as i32 + ); + assert_eq!(circuit_cfg_terminator_target(cfg, b0), b1); + assert_eq!( + circuit_cfg_block_terminator_tag(cfg, b1), + CFG_TERMINATOR_BREAK as i32 + ); + assert_eq!(circuit_cfg_terminator_target(cfg, b1), b0); + assert_eq!( + circuit_cfg_block_terminator_tag(cfg, b2), + CFG_TERMINATOR_CONTINUE as i32 + ); + assert_eq!(circuit_cfg_terminator_target(cfg, b2), b0); + + // A terminator without operations still makes the block non-empty. + assert_eq!(circuit_cfg_block_is_empty(cfg, b0), 0); + assert_eq!(circuit_cfg_block_operations_len(cfg, b0), 0); + + // Jump/Break/Continue carry no condition and no loop info. + assert!(circuit_cfg_terminator_condition(cfg, b0).is_null()); + let mut var: *mut CClassicalVar = std::ptr::null_mut(); + let mut start: *mut CClassicalExpr = std::ptr::null_mut(); + let mut stop: *mut CClassicalExpr = std::ptr::null_mut(); + let mut step: *mut CClassicalExpr = std::ptr::null_mut(); + assert_eq!( + circuit_cfg_terminator_for_info(cfg, b0, &mut var, &mut start, &mut stop, &mut step), + -8 + ); + + // `Return` has no target and overwrites the previous terminator. + assert_eq!(circuit_cfg_set_terminator_return(cfg, b2), 0); + assert_eq!( + circuit_cfg_block_terminator_tag(cfg, b2), + CFG_TERMINATOR_RETURN as i32 + ); + assert_eq!(circuit_cfg_terminator_target(cfg, b2), u32::MAX); + + // NULL and unknown blocks. + assert_eq!( + circuit_cfg_set_terminator_jump(std::ptr::null_mut(), 0, 0), + -1 + ); + assert_eq!( + circuit_cfg_set_terminator_return(std::ptr::null_mut(), 0), + -1 + ); + assert_eq!( + circuit_cfg_set_terminator_break(std::ptr::null_mut(), 0, 0), + -1 + ); + assert_eq!( + circuit_cfg_set_terminator_continue(std::ptr::null_mut(), 0, 0), + -1 + ); + assert_eq!(circuit_cfg_set_terminator_jump(cfg, 7, 0), -2); + assert_eq!(circuit_cfg_set_terminator_jump(cfg, u32::MAX, 0), -2); + + circuit_cfg_free(cfg); +} + +#[test] +fn cfg_classical_state() { + // Circuit carrying two variables plus one measured value. + let circuit = circuit_new(2); + let bit_var = circuit_var(circuit, CQLIB_CLASSICAL_TYPE_BIT, 0); + let uint_var = circuit_var(circuit, CQLIB_CLASSICAL_TYPE_UINT, 8); + assert!(!bit_var.is_null()); + assert!(!uint_var.is_null()); + + let measured = [0u32, 1]; + let measured_expr = circuit_measure_bits(circuit, measured.as_ptr(), 2); + assert!(!measured_expr.is_null()); + assert_eq!(circuit_classical_vars_len(circuit), 2); + + let cfg = circuit_cfg_from_circuit(circuit); + assert!(!cfg.is_null()); + + // The CFG snapshots the circuit's static classical tables. + assert_eq!(circuit_cfg_classical_vars_len(cfg), 2); + assert_eq!(circuit_cfg_classical_values_len(cfg), 1); + + let mut vars = [ + CClassicalType { tag: 9, width: 9 }, + CClassicalType { tag: 9, width: 9 }, + ]; + assert_eq!(circuit_cfg_classical_vars(cfg, vars.as_mut_ptr(), 2), 0); + // `CClassicalType` is a repr(C) struct without derives, so compare per field. + assert_eq!(vars[0].tag, CQLIB_CLASSICAL_TYPE_BIT); + assert_eq!(vars[0].width, 1); + assert_eq!(vars[1].tag, CQLIB_CLASSICAL_TYPE_UINT); + assert_eq!(vars[1].width, 8); + + let mut values = [CClassicalType { tag: 9, width: 9 }]; + assert_eq!(circuit_cfg_classical_values(cfg, values.as_mut_ptr(), 1), 0); + assert_eq!(values[0].tag, CQLIB_CLASSICAL_TYPE_BIT_VEC); + assert_eq!(values[0].width, 2); + + // Undersized then NULL buffers (len >= count is allowed). + assert_eq!(circuit_cfg_classical_vars(cfg, vars.as_mut_ptr(), 1), -8); + assert_eq!(circuit_cfg_classical_vars(cfg, std::ptr::null_mut(), 2), -1); + assert_eq!( + circuit_cfg_classical_values(cfg, values.as_mut_ptr(), 0), + -8 + ); + assert_eq!( + circuit_cfg_classical_values(cfg, std::ptr::null_mut(), 1), + -1 + ); + + classical_expr_free(measured_expr); + classical_var_free(uint_var); + classical_var_free(bit_var); + circuit_cfg_free(cfg); + + // A circuit without classical state produces empty CFG tables. + let plain = circuit_new(1); + let plain_cfg = circuit_cfg_from_circuit(plain); + assert!(!plain_cfg.is_null()); + assert_eq!(circuit_cfg_classical_vars_len(plain_cfg), 0); + assert_eq!(circuit_cfg_classical_values_len(plain_cfg), 0); + assert_eq!( + circuit_cfg_classical_vars(plain_cfg, std::ptr::null_mut(), 0), + 0 + ); + assert_eq!( + circuit_cfg_classical_values(plain_cfg, std::ptr::null_mut(), 0), + 0 + ); + circuit_cfg_free(plain_cfg); + circuit_free(plain); + + // A hand-built CFG starts with empty tables as well. + let empty = circuit_cfg_new(0); + assert_eq!(circuit_cfg_classical_vars_len(empty), 0); + assert_eq!(circuit_cfg_classical_values_len(empty), 0); + assert_eq!( + circuit_cfg_classical_vars(empty, std::ptr::null_mut(), 0), + 0 + ); + assert_eq!( + circuit_cfg_classical_values(empty, std::ptr::null_mut(), 0), + 0 + ); + circuit_cfg_free(empty); + + circuit_free(circuit); +} + +#[test] +fn cfg_loop_regions() { + // `while (flag) { h(0); }` expands into entry / cond / body / exit. + let circuit = circuit_new(2); + let condition_var = circuit_var(circuit, CQLIB_CLASSICAL_TYPE_BOOL, 0); + assert!(!condition_var.is_null()); + let condition = classical_expr_var(condition_var); + assert!(!condition.is_null()); + let body: CqlibBodyFn = Some(add_h); + assert_eq!( + circuit_while(circuit, condition, body, std::ptr::null_mut()), + 0 + ); + classical_expr_free(condition); + classical_var_free(condition_var); + + let cfg = circuit_cfg_from_circuit(circuit); + assert!(!cfg.is_null()); + assert_eq!(circuit_cfg_validate(cfg), 0); + assert_eq!(circuit_cfg_num_blocks(cfg), 4); + assert_eq!(circuit_cfg_entry_block(cfg), 0); + + // Entry jumps to the condition header, which owns the While region. + assert_eq!( + circuit_cfg_block_terminator_tag(cfg, 0), + CFG_TERMINATOR_JUMP as i32 + ); + assert_eq!(circuit_cfg_terminator_target(cfg, 0), 1); + assert_eq!(circuit_cfg_region_tag(cfg, 1), CFG_REGION_WHILE as i32); + assert_eq!(circuit_cfg_is_loop_header(cfg, 1), 1); + assert_eq!(circuit_cfg_region_tag(cfg, 0), 0); + assert_eq!(circuit_cfg_is_loop_header(cfg, 0), 0); + assert_eq!( + circuit_cfg_block_terminator_tag(cfg, 1), + CFG_TERMINATOR_BRANCH as i32 + ); + assert_eq!(circuit_cfg_terminator_target(cfg, 1), u32::MAX); + + // Structured labels are attached for diagnostics. + let body_label = circuit_cfg_block_label(cfg, 2); + assert!(!body_label.is_null()); + let body_text = unsafe { CStr::from_ptr(body_label) } + .to_str() + .unwrap() + .to_owned(); + cqlib_string_free(body_label); + assert!(body_text.starts_with("while_body_")); + + // The Branch condition is exposed as a fresh expression handle. + let branch_condition = circuit_cfg_terminator_condition(cfg, 1); + assert!(!branch_condition.is_null()); + classical_expr_free(branch_condition); + + // The loop layout reads back body and exit blocks. + let mut body_block = 9u32; + let mut exit_block = 9u32; + assert_eq!( + circuit_cfg_region_loop(cfg, 1, &mut body_block, &mut exit_block), + 0 + ); + assert_eq!(body_block, 2); + assert_eq!(exit_block, 3); + // Non-loop blocks have no region and are rejected. + assert_eq!( + circuit_cfg_region_loop(cfg, 0, &mut body_block, &mut exit_block), + -8 + ); + assert_eq!( + circuit_cfg_terminator_for_info( + cfg, + 1, + std::ptr::null_mut(), + std::ptr::null_mut(), + std::ptr::null_mut(), + std::ptr::null_mut() + ), + -8 + ); + + // Header edges: TrueBranch into the body, FalseBranch into the exit. + let mut targets = [9u32; 2]; + let mut flow_tags = [9u32; 2]; + let mut lo = [9u64; 2]; + let mut hi = [9u64; 2]; + assert_eq!(circuit_cfg_outgoing_edges_len(cfg, 1), 2); + assert_eq!( + circuit_cfg_outgoing_edges( + cfg, + 1, + targets.as_mut_ptr(), + flow_tags.as_mut_ptr(), + lo.as_mut_ptr(), + hi.as_mut_ptr(), + 2 + ), + 0 + ); + assert_eq!(targets, [2, 3]); + assert_eq!(flow_tags, [CFG_FLOW_TRUE_BRANCH, CFG_FLOW_FALSE_BRANCH]); + + // The body jumps back to the header; the exit returns. + assert_eq!(circuit_cfg_block_operations_len(cfg, 2), 1); + assert_eq!(circuit_cfg_terminator_target(cfg, 2), 1); + assert_eq!( + circuit_cfg_block_terminator_tag(cfg, 3), + CFG_TERMINATOR_RETURN as i32 + ); + + circuit_cfg_free(cfg); + circuit_free(circuit); + + // `for` loops expose their loop variable through `terminator_for_info`. + let for_circuit = circuit_new(1); + let counter = circuit_var(for_circuit, CQLIB_CLASSICAL_TYPE_UINT, 8); + assert!(!counter.is_null()); + let start = classical_expr_uint_literal(8, 0, 0); + let stop = classical_expr_uint_literal(8, 4, 0); + let step = classical_expr_uint_literal(8, 1, 0); + assert!(!start.is_null()); + assert!(!stop.is_null()); + assert!(!step.is_null()); + assert_eq!( + circuit_for_uint( + for_circuit, + counter, + start, + stop, + step, + Some(add_h), + std::ptr::null_mut() + ), + 0 + ); + + let for_cfg = circuit_cfg_from_circuit(for_circuit); + assert!(!for_cfg.is_null()); + assert_eq!(circuit_cfg_num_blocks(for_cfg), 4); + assert_eq!( + circuit_cfg_block_terminator_tag(for_cfg, 1), + CFG_TERMINATOR_FOR_LOOP as i32 + ); + assert_eq!(circuit_cfg_region_tag(for_cfg, 1), CFG_REGION_FOR as i32); + assert_eq!(circuit_cfg_is_loop_header(for_cfg, 1), 1); + assert_eq!( + circuit_cfg_region_loop(for_cfg, 1, &mut body_block, &mut exit_block), + 0 + ); + assert_eq!(body_block, 2); + assert_eq!(exit_block, 3); + + let mut var: *mut CClassicalVar = std::ptr::null_mut(); + let mut for_start: *mut CClassicalExpr = std::ptr::null_mut(); + let mut for_stop: *mut CClassicalExpr = std::ptr::null_mut(); + let mut for_step: *mut CClassicalExpr = std::ptr::null_mut(); + assert_eq!( + circuit_cfg_terminator_for_info( + for_cfg, + 1, + &mut var, + &mut for_start, + &mut for_stop, + &mut for_step + ), + 0 + ); + assert!(!var.is_null()); + assert!(!for_start.is_null()); + assert!(!for_stop.is_null()); + assert!(!for_step.is_null()); + classical_var_free(var); + classical_expr_free(for_step); + classical_expr_free(for_stop); + classical_expr_free(for_start); + + // NULL out-parameters on a present ForLoop header fail before writing. + assert_eq!( + circuit_cfg_terminator_for_info( + for_cfg, + 1, + std::ptr::null_mut(), + &mut for_start, + &mut for_stop, + &mut for_step + ), + -1 + ); + // Non-loop blocks and unknown blocks report their failure mode. + assert_eq!( + circuit_cfg_terminator_for_info( + for_cfg, + 0, + &mut var, + &mut for_start, + &mut for_stop, + &mut for_step + ), + -8 + ); + assert_eq!( + circuit_cfg_terminator_for_info( + for_cfg, + u32::MAX, + &mut var, + &mut for_start, + &mut for_stop, + &mut for_step + ), + -2 + ); + + circuit_cfg_free(for_cfg); + classical_expr_free(step); + classical_expr_free(stop); + classical_expr_free(start); + classical_var_free(counter); + circuit_free(for_circuit); + + // Region setters: manual attachment and replacement on a bare CFG. + let manual = circuit_cfg_new(1); + let header = circuit_cfg_add_block(manual); + let body_b = circuit_cfg_add_block(manual); + let exit_b = circuit_cfg_add_block(manual); + let merge_b = circuit_cfg_add_block(manual); + + assert_eq!( + circuit_cfg_set_while_region(manual, header, body_b, exit_b, std::ptr::null()), + 0 + ); + assert_eq!( + circuit_cfg_region_tag(manual, header), + CFG_REGION_WHILE as i32 + ); + assert_eq!(circuit_cfg_is_loop_header(manual, header), 1); + + // Replacing the region with a For region keeps it a loop header. + assert_eq!( + circuit_cfg_set_for_region(manual, header, body_b, exit_b, std::ptr::null()), + 0 + ); + assert_eq!( + circuit_cfg_region_tag(manual, header), + CFG_REGION_FOR as i32 + ); + assert_eq!(circuit_cfg_is_loop_header(manual, header), 1); + assert_eq!( + circuit_cfg_region_loop(manual, header, &mut body_block, &mut exit_block), + 0 + ); + assert_eq!(body_block, body_b); + assert_eq!(exit_block, exit_b); + + // An If region is not a loop header and carries the four-way layout. + assert_eq!( + circuit_cfg_set_if_region(manual, header, body_b, exit_b, merge_b, 0, std::ptr::null()), + 0 + ); + assert_eq!(circuit_cfg_region_tag(manual, header), CFG_REGION_IF as i32); + assert_eq!(circuit_cfg_is_loop_header(manual, header), 0); + let mut then_entry = 9u32; + let mut else_entry = 9u32; + let mut merge_block = 9u32; + let mut has_else = 9i32; + assert_eq!( + circuit_cfg_region_if( + manual, + header, + &mut then_entry, + &mut else_entry, + &mut merge_block, + &mut has_else + ), + 0 + ); + assert_eq!(then_entry, body_b); + assert_eq!(else_entry, exit_b); + assert_eq!(merge_block, merge_b); + assert_eq!(has_else, 0); + assert_eq!( + circuit_cfg_region_loop(manual, header, &mut body_block, &mut exit_block), + -8 + ); + + // A Switch region enumerates its match values and entries. + let case_lo: [u64; 1] = [7]; + let case_hi: [u64; 1] = [0]; + let case_entries: [u32; 1] = [body_b]; + assert_eq!( + circuit_cfg_set_switch_region( + manual, + header, + case_lo.as_ptr(), + case_hi.as_ptr(), + case_entries.as_ptr(), + 1, + exit_b, + merge_b, + 1, + std::ptr::null() + ), + 0 + ); + assert_eq!( + circuit_cfg_region_tag(manual, header), + CFG_REGION_SWITCH as i32 + ); + assert_eq!(circuit_cfg_is_loop_header(manual, header), 0); + assert_eq!(circuit_cfg_region_switch_cases_len(manual, header), 1); + assert_eq!( + circuit_cfg_region_switch( + manual, + header, + lo.as_mut_ptr(), + hi.as_mut_ptr(), + targets.as_mut_ptr(), + 1, + &mut exit_block, + &mut merge_block, + &mut has_else + ), + 0 + ); + assert_eq!(lo[0], 7); + assert_eq!(hi[0], 0); + assert_eq!(targets[0], body_b); + assert_eq!(exit_block, exit_b); + assert_eq!(merge_block, merge_b); + assert_eq!(has_else, 1); + + // Setter failure modes: NULL handles, sentinel, and unknown headers. + assert_eq!( + circuit_cfg_set_while_region(std::ptr::null_mut(), 0, 0, 0, std::ptr::null()), + -1 + ); + assert_eq!( + circuit_cfg_set_if_region(std::ptr::null_mut(), 0, 0, 0, 0, 0, std::ptr::null()), + -1 + ); + assert_eq!( + circuit_cfg_set_for_region(std::ptr::null_mut(), 0, 0, 0, std::ptr::null()), + -1 + ); + assert_eq!( + circuit_cfg_set_switch_region( + std::ptr::null_mut(), + 0, + std::ptr::null(), + std::ptr::null(), + std::ptr::null(), + 0, + 0, + 0, + 0, + std::ptr::null() + ), + -1 + ); + assert_eq!( + circuit_cfg_set_switch_region( + manual, + header, + std::ptr::null(), + std::ptr::null(), + std::ptr::null(), + 1, + exit_b, + merge_b, + 1, + std::ptr::null() + ), + -1 + ); + assert_eq!( + circuit_cfg_set_while_region(manual, 7, 0, 0, std::ptr::null()), + -2 + ); + assert_eq!( + circuit_cfg_set_while_region(manual, u32::MAX, 0, 0, std::ptr::null()), + -2 + ); + + // Hand-attached regions point at real blocks everywhere. + let region_targets: [u32; 1] = [header]; + assert_eq!( + circuit_cfg_set_switch_region( + manual, + header, + case_lo.as_ptr(), + case_hi.as_ptr(), + region_targets.as_ptr(), + 1, + exit_b, + merge_b, + 1, + std::ptr::null() + ), + 0 + ); + assert_eq!(circuit_cfg_region_switch_cases_len(manual, header), 1); + + circuit_cfg_free(manual); +} + +#[test] +fn cfg_null_and_error_paths() { + let h_name = cstr("H"); + let zero = [0u32]; + let op = operation_new(h_name.as_ptr(), zero.as_ptr(), 1, std::ptr::null(), 0); + assert!(!op.is_null()); + let mut var: *mut CClassicalVar = std::ptr::null_mut(); + let mut start: *mut CClassicalExpr = std::ptr::null_mut(); + let mut stop: *mut CClassicalExpr = std::ptr::null_mut(); + let mut step: *mut CClassicalExpr = std::ptr::null_mut(); + let mut entry = 9u32; + + // Every exported function tolerates a NULL handle. + assert!(circuit_cfg_from_circuit(std::ptr::null()).is_null()); + assert!(circuit_cfg_from_qubits_probe_null()); + assert!(circuit_cfg_to_circuit(std::ptr::null()).is_null()); + assert_eq!(circuit_cfg_validate(std::ptr::null()), -1); + assert_eq!(circuit_cfg_num_qubits(std::ptr::null()), 0); + assert_eq!(circuit_cfg_num_blocks(std::ptr::null()), 0); + assert_eq!( + circuit_cfg_qubits(std::ptr::null(), std::ptr::null_mut(), 0), + -1 + ); + assert_eq!(circuit_cfg_classical_vars_len(std::ptr::null()), 0); + assert_eq!(circuit_cfg_classical_values_len(std::ptr::null()), 0); + assert_eq!( + circuit_cfg_classical_vars(std::ptr::null(), std::ptr::null_mut(), 0), + -1 + ); + assert_eq!( + circuit_cfg_classical_values(std::ptr::null(), std::ptr::null_mut(), 0), + -1 + ); + assert_eq!(circuit_cfg_entry_block(std::ptr::null()), u32::MAX); + assert_eq!(circuit_cfg_add_block(std::ptr::null_mut()), u32::MAX); + assert_eq!( + circuit_cfg_add_edge(std::ptr::null_mut(), 0, 0, CFG_FLOW_UNCONDITIONAL, 0, 0), + -1 + ); + assert_eq!(circuit_cfg_set_entry_block(std::ptr::null_mut(), 0), -1); + assert_eq!(circuit_cfg_push_operation(std::ptr::null_mut(), 0, op), -1); + assert_eq!( + circuit_cfg_blocks(std::ptr::null(), std::ptr::null_mut(), 0), + -1 + ); + assert!(circuit_cfg_block_label(std::ptr::null(), 0).is_null()); + assert_eq!(circuit_cfg_block_is_empty(std::ptr::null(), 0), -1); + assert_eq!(circuit_cfg_block_operations_len(std::ptr::null(), 0), 0); + assert_eq!( + circuit_cfg_block_operations(std::ptr::null(), 0, std::ptr::null_mut(), 0), + -1 + ); + assert_eq!(circuit_cfg_block_has_terminator(std::ptr::null(), 0), -1); + assert_eq!(circuit_cfg_block_terminator_tag(std::ptr::null(), 0), -1); + assert_eq!(circuit_cfg_terminator_target(std::ptr::null(), 0), u32::MAX); + assert!(circuit_cfg_terminator_condition(std::ptr::null(), 0).is_null()); + assert_eq!( + circuit_cfg_terminator_for_info( + std::ptr::null(), + 0, + &mut var, + &mut start, + &mut stop, + &mut step + ), + -1 + ); + assert_eq!(circuit_cfg_outgoing_edges_len(std::ptr::null(), 0), 0); + assert_eq!( + circuit_cfg_outgoing_edges( + std::ptr::null(), + 0, + std::ptr::null_mut(), + std::ptr::null_mut(), + std::ptr::null_mut(), + std::ptr::null_mut(), + 0 + ), + -1 + ); + assert_eq!(circuit_cfg_region_tag(std::ptr::null(), 0), -1); + assert_eq!(circuit_cfg_is_loop_header(std::ptr::null(), 0), -1); + assert_eq!( + circuit_cfg_region_if( + std::ptr::null(), + 0, + &mut entry, + &mut entry, + &mut entry, + std::ptr::null_mut() + ), + -1 + ); + assert_eq!( + circuit_cfg_region_loop(std::ptr::null(), 0, &mut entry, &mut entry), + -1 + ); + assert_eq!(circuit_cfg_region_switch_cases_len(std::ptr::null(), 0), 0); + assert_eq!( + circuit_cfg_region_switch( + std::ptr::null(), + 0, + std::ptr::null_mut(), + std::ptr::null_mut(), + std::ptr::null_mut(), + 0, + &mut entry, + &mut entry, + std::ptr::null_mut() + ), + -1 + ); + assert_eq!( + circuit_cfg_set_terminator_jump(std::ptr::null_mut(), 0, 0), + -1 + ); + assert_eq!( + circuit_cfg_set_terminator_return(std::ptr::null_mut(), 0), + -1 + ); + circuit_cfg_free(std::ptr::null_mut()); + + // Out-of-range and sentinel node ids fail every block-level query. + let cfg = circuit_cfg_new(1); + assert_eq!(circuit_cfg_add_block(cfg), 0); + for invalid in [7u32, u32::MAX] { + assert_eq!(circuit_cfg_block_is_empty(cfg, invalid), -2); + assert!(circuit_cfg_block_label(cfg, invalid).is_null()); + assert_eq!(circuit_cfg_block_operations_len(cfg, invalid), 0); + assert_eq!(circuit_cfg_block_has_terminator(cfg, invalid), -2); + assert_eq!(circuit_cfg_block_terminator_tag(cfg, invalid), -2); + assert_eq!(circuit_cfg_terminator_target(cfg, invalid), u32::MAX); + assert!(circuit_cfg_terminator_condition(cfg, invalid).is_null()); + assert_eq!( + circuit_cfg_block_operations(cfg, invalid, std::ptr::null_mut(), 1), + -2 + ); + assert_eq!(circuit_cfg_outgoing_edges_len(cfg, invalid), 0); + assert_eq!( + circuit_cfg_outgoing_edges( + cfg, + invalid, + std::ptr::null_mut(), + std::ptr::null_mut(), + std::ptr::null_mut(), + std::ptr::null_mut(), + 1 + ), + -2 + ); + assert_eq!(circuit_cfg_region_tag(cfg, invalid), -2); + assert_eq!(circuit_cfg_is_loop_header(cfg, invalid), -2); + assert_eq!( + circuit_cfg_region_if( + cfg, + invalid, + &mut entry, + &mut entry, + &mut entry, + std::ptr::null_mut() + ), + -2 + ); + assert_eq!( + circuit_cfg_region_loop(cfg, invalid, &mut entry, &mut entry), + -2 + ); + assert_eq!(circuit_cfg_region_switch_cases_len(cfg, invalid), 0); + assert_eq!( + circuit_cfg_region_switch( + cfg, + invalid, + std::ptr::null_mut(), + std::ptr::null_mut(), + std::ptr::null_mut(), + 0, + &mut entry, + &mut entry, + std::ptr::null_mut() + ), + -2 + ); + assert_eq!(circuit_cfg_push_operation(cfg, invalid, op), -2); + assert_eq!(circuit_cfg_set_terminator_return(cfg, invalid), -2); + assert_eq!( + circuit_cfg_set_while_region(cfg, invalid, 0, 0, std::ptr::null()), + -2 + ); + } + assert_eq!(circuit_cfg_set_entry_block(cfg, u32::MAX), -2); + + circuit_cfg_free(cfg); + operation_free(op); +} + +/// `circuit_cfg_from_qubits` with a NULL buffer plus a positive length. +fn circuit_cfg_from_qubits_probe_null() -> bool { + circuit_cfg_from_qubits(std::ptr::null(), 1).is_null() +} diff --git a/crates/binding-c/tests/circuit_classical_test.rs b/crates/binding-c/tests/circuit_classical_test.rs new file mode 100644 index 00000000..8f24759f --- /dev/null +++ b/crates/binding-c/tests/circuit_classical_test.rs @@ -0,0 +1,596 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +use binding_c::circuit::{ + CClassicalExpr, CClassicalType, CClassicalValueInfo, CClassicalVar, + CQLIB_CLASSICAL_EXPR_BINARY, CQLIB_CLASSICAL_EXPR_BIT_LITERAL, + CQLIB_CLASSICAL_EXPR_BIT_VEC_LITERAL, CQLIB_CLASSICAL_EXPR_BOOL_LITERAL, + CQLIB_CLASSICAL_EXPR_CAST, CQLIB_CLASSICAL_EXPR_COMPARE, CQLIB_CLASSICAL_EXPR_CONCAT, + CQLIB_CLASSICAL_EXPR_EXTRACT_BIT, CQLIB_CLASSICAL_EXPR_EXTRACT_BITS, + CQLIB_CLASSICAL_EXPR_PACK_BITS, CQLIB_CLASSICAL_EXPR_SELECT, CQLIB_CLASSICAL_EXPR_UINT_LITERAL, + CQLIB_CLASSICAL_EXPR_UNARY, CQLIB_CLASSICAL_EXPR_VALUE, CQLIB_CLASSICAL_EXPR_VAR, + CQLIB_CLASSICAL_TYPE_BIT, CQLIB_CLASSICAL_TYPE_BIT_VEC, CQLIB_CLASSICAL_TYPE_BOOL, + CQLIB_CLASSICAL_TYPE_UINT, circuit_classical_vars, circuit_classical_vars_len, circuit_free, + circuit_measure_bits, circuit_measure_bits_into, circuit_measure_into, circuit_new, + circuit_num_operations, circuit_store, circuit_validate, circuit_validate_classical_expr, + circuit_validate_classical_var, circuit_var, classical_expr_and, classical_expr_bit_literal, + classical_expr_bit_to_bool, classical_expr_bit_vec_literal, classical_expr_bit_vec_to_uint, + classical_expr_bool_literal, classical_expr_concat, classical_expr_eq, + classical_expr_extract_bit, classical_expr_extract_bits, classical_expr_free, + classical_expr_ge, classical_expr_gt, classical_expr_kind, classical_expr_le, + classical_expr_lt, classical_expr_ne, classical_expr_not, classical_expr_or, + classical_expr_pack_bits, classical_expr_select, classical_expr_simplified, + classical_expr_to_bool, classical_expr_to_uint, classical_expr_ty, classical_expr_uint_literal, + classical_expr_values, classical_expr_values_len, classical_expr_var, classical_expr_vars, + classical_expr_vars_len, classical_expr_xor, classical_var_expr, classical_var_free, + classical_var_id, classical_var_index, classical_var_ty, +}; + +/// Coerces an owned handle to the borrowed pointer type used by array inputs. +fn c(ptr: *mut CClassicalExpr) -> *const CClassicalExpr { + ptr +} + +fn expr_ty_parts(ptr: *const CClassicalExpr) -> (u32, u32) { + let mut tag = u32::MAX; + let mut width = u32::MAX; + assert_eq!(classical_expr_ty(ptr, &mut tag, &mut width), 0); + (tag, width) +} + +fn expr_kind(ptr: *const CClassicalExpr) -> u32 { + let mut kind = u32::MAX; + assert_eq!(classical_expr_kind(ptr, &mut kind), 0); + kind +} + +#[test] +fn classical_var_lifecycle_and_queries() { + let circuit = circuit_new(2); + + let bit = circuit_var(circuit, CQLIB_CLASSICAL_TYPE_BIT, 0); + let boolean = circuit_var(circuit, CQLIB_CLASSICAL_TYPE_BOOL, 0); + let uint = circuit_var(circuit, CQLIB_CLASSICAL_TYPE_UINT, 8); + let bitvec = circuit_var(circuit, CQLIB_CLASSICAL_TYPE_BIT_VEC, 5); + assert!(!bit.is_null()); + assert!(!boolean.is_null()); + assert!(!uint.is_null()); + assert!(!bitvec.is_null()); + + assert_eq!(classical_var_id(bit), 0); + assert_eq!(classical_var_index(bit), 0); + assert_eq!(classical_var_id(bitvec), 3); + + let (mut tag, mut width) = (u32::MAX, u32::MAX); + assert_eq!(classical_var_ty(bit, &mut tag, &mut width), 0); + assert_eq!((tag, width), (CQLIB_CLASSICAL_TYPE_BIT, 1)); + assert_eq!(classical_var_ty(uint, &mut tag, &mut width), 0); + assert_eq!((tag, width), (CQLIB_CLASSICAL_TYPE_UINT, 8)); + assert_eq!(classical_var_ty(bitvec, &mut tag, &mut width), 0); + assert_eq!((tag, width), (CQLIB_CLASSICAL_TYPE_BIT_VEC, 5)); + + // var -> expression + let expr = classical_var_expr(uint); + assert!(!expr.is_null()); + assert_eq!(expr_kind(expr), CQLIB_CLASSICAL_EXPR_VAR); + assert_eq!(expr_ty_parts(expr), (CQLIB_CLASSICAL_TYPE_UINT, 8)); + + // type table snapshot + assert_eq!(circuit_classical_vars_len(circuit), 4); + let mut buffer = [ + CClassicalType { tag: 9, width: 9 }, + CClassicalType { tag: 9, width: 9 }, + CClassicalType { tag: 9, width: 9 }, + CClassicalType { tag: 9, width: 9 }, + ]; + assert_eq!(circuit_classical_vars(circuit, buffer.as_mut_ptr(), 4), 0); + assert_eq!(buffer[0].tag, CQLIB_CLASSICAL_TYPE_BIT); + assert_eq!(buffer[0].width, 1); + assert_eq!(buffer[2].tag, CQLIB_CLASSICAL_TYPE_UINT); + assert_eq!(buffer[2].width, 8); + assert_eq!(buffer[3].tag, CQLIB_CLASSICAL_TYPE_BIT_VEC); + assert_eq!(buffer[3].width, 5); + + classical_expr_free(expr); + classical_var_free(bit); + classical_var_free(boolean); + classical_var_free(uint); + classical_var_free(bitvec); + circuit_free(circuit); +} + +#[test] +fn classical_var_error_paths() { + let circuit = circuit_new(1); + assert!(circuit_var(std::ptr::null_mut(), CQLIB_CLASSICAL_TYPE_BIT, 0).is_null()); + assert!(circuit_var(circuit, 99, 0).is_null()); // unknown tag + assert!(circuit_var(circuit, CQLIB_CLASSICAL_TYPE_UINT, 0).is_null()); // zero width + assert!(circuit_var(circuit, CQLIB_CLASSICAL_TYPE_BIT_VEC, 0).is_null()); + + assert_eq!(classical_var_id(std::ptr::null()), u32::MAX); + assert_eq!(classical_var_index(std::ptr::null()), u32::MAX); + assert_eq!(classical_var_ty(std::ptr::null(), &mut 0, &mut 0), -1); + assert!(classical_var_expr(std::ptr::null()).is_null()); + classical_var_free(std::ptr::null_mut()); // no-op + + let var = circuit_var(circuit, CQLIB_CLASSICAL_TYPE_BIT, 0); + let mut tag = 0u32; + assert_eq!(classical_var_ty(var, std::ptr::null_mut(), &mut tag), -1); + assert_eq!(classical_var_ty(var, &mut tag, std::ptr::null_mut()), -1); + + // len mismatch in type snapshot + assert_eq!(circuit_classical_vars_len(circuit), 1); + let mut buffer = [CClassicalType { tag: 0, width: 0 }]; + assert_eq!(circuit_classical_vars(circuit, buffer.as_mut_ptr(), 2), -8); + assert_eq!(circuit_classical_vars(circuit, std::ptr::null_mut(), 1), -1); + assert_eq!( + circuit_classical_vars(std::ptr::null(), buffer.as_mut_ptr(), 1), + -1 + ); + + classical_var_free(var); + circuit_free(circuit); +} + +#[test] +fn measure_bits_returns_bitvec_value_expr() { + let circuit = circuit_new(3); + let qubits = [2u32, 0, 1]; + let expr = circuit_measure_bits(circuit, qubits.as_ptr(), 3); + assert!(!expr.is_null()); + assert_eq!(expr_kind(expr), CQLIB_CLASSICAL_EXPR_VALUE); + assert_eq!(expr_ty_parts(expr), (CQLIB_CLASSICAL_TYPE_BIT_VEC, 3)); + assert_eq!(classical_expr_vars_len(expr), 0); + assert_eq!(classical_expr_values_len(expr), 1); + + let mut infos = [CClassicalValueInfo { + index: u32::MAX, + tag: u32::MAX, + width: u32::MAX, + }]; + assert_eq!(classical_expr_values(expr, infos.as_mut_ptr(), 1), 0); + assert_eq!(infos[0].index, 0); + assert_eq!(infos[0].tag, CQLIB_CLASSICAL_TYPE_BIT_VEC); + assert_eq!(infos[0].width, 3); + assert_eq!(classical_expr_values(expr, infos.as_mut_ptr(), 2), -8); + assert_eq!( + classical_expr_values(std::ptr::null(), infos.as_mut_ptr(), 1), + -1 + ); + + assert_eq!(circuit_validate_classical_expr(circuit, expr), 0); + assert_eq!(circuit_num_operations(circuit), 1); + + classical_expr_free(expr); + circuit_free(circuit); +} + +#[test] +fn measure_error_paths() { + let circuit = circuit_new(2); + assert!(circuit_measure_bits(std::ptr::null_mut(), std::ptr::null(), 0).is_null()); + assert!(circuit_measure_bits(circuit, std::ptr::null(), 0).is_null()); // empty list + let empty: [u32; 0] = []; + assert!(circuit_measure_bits(circuit, empty.as_ptr(), 0).is_null()); + let oob = [0u32, 5]; + assert!(circuit_measure_bits(circuit, oob.as_ptr(), 2).is_null()); // qubit out of bounds + + let target = circuit_var(circuit, CQLIB_CLASSICAL_TYPE_BIT, 0); + assert!(circuit_measure_into(std::ptr::null_mut(), 0, target).is_null()); + assert!(circuit_measure_into(circuit, 0, std::ptr::null()).is_null()); + assert!(circuit_measure_into(circuit, 9, target).is_null()); // out of bounds + + let bits_target = circuit_var(circuit, CQLIB_CLASSICAL_TYPE_BIT_VEC, 2); + let qubits = [0u32, 1]; + let expr = circuit_measure_bits_into(circuit, qubits.as_ptr(), 2, bits_target); + assert!(!expr.is_null()); + assert!(circuit_measure_bits_into(circuit, qubits.as_ptr(), 2, std::ptr::null()).is_null()); + assert!( + circuit_measure_bits_into(std::ptr::null_mut(), qubits.as_ptr(), 2, bits_target).is_null() + ); + + classical_expr_free(expr); + classical_var_free(bits_target); + classical_var_free(target); + circuit_free(circuit); +} + +#[test] +fn measure_into_appends_measure_and_store() { + let circuit = circuit_new(2); + let target = circuit_var(circuit, CQLIB_CLASSICAL_TYPE_BIT, 0); + let expr = circuit_measure_into(circuit, 0, target); + assert!(!expr.is_null()); + assert_eq!(expr_kind(expr), CQLIB_CLASSICAL_EXPR_VALUE); + assert_eq!(expr_ty_parts(expr), (CQLIB_CLASSICAL_TYPE_BIT, 1)); + assert_eq!(circuit_num_operations(circuit), 2); // measure + store + assert_eq!(circuit_validate(circuit), 0); + assert_eq!(circuit_validate_classical_var(circuit, target), 0); + assert_eq!(circuit_validate_classical_expr(circuit, expr), 0); + + // wrong target type: Bool var cannot receive a Bit measurement + let bad = circuit_var(circuit, CQLIB_CLASSICAL_TYPE_BOOL, 0); + assert!(circuit_measure_into(circuit, 1, bad).is_null()); + classical_var_free(bad); + + classical_expr_free(expr); + classical_var_free(target); + circuit_free(circuit); +} + +#[test] +fn measure_bits_into_appends_measure_and_store() { + let circuit = circuit_new(3); + let target = circuit_var(circuit, CQLIB_CLASSICAL_TYPE_BIT_VEC, 2); + let qubits = [1u32, 0]; + let expr = circuit_measure_bits_into(circuit, qubits.as_ptr(), 2, target); + assert!(!expr.is_null()); + assert_eq!(expr_ty_parts(expr), (CQLIB_CLASSICAL_TYPE_BIT_VEC, 2)); + assert_eq!(circuit_num_operations(circuit), 2); + assert_eq!(circuit_validate(circuit), 0); + + // width mismatch: 2-qubit measurement into a 3-wide variable + let wide = circuit_var(circuit, CQLIB_CLASSICAL_TYPE_BIT_VEC, 3); + assert!(circuit_measure_bits_into(circuit, qubits.as_ptr(), 2, wide).is_null()); + classical_var_free(wide); + + classical_expr_free(expr); + classical_var_free(target); + circuit_free(circuit); +} + +#[test] +fn validate_rejects_foreign_handles() { + let circuit_a = circuit_new(1); + let circuit_b = circuit_new(1); + let var_a = circuit_var(circuit_a, CQLIB_CLASSICAL_TYPE_BIT, 0); + let expr_a = classical_var_expr(var_a); + + assert_eq!(circuit_validate_classical_var(circuit_a, var_a), 0); + assert_eq!(circuit_validate_classical_var(circuit_b, var_a), -3); + assert_eq!(circuit_validate_classical_var(std::ptr::null(), var_a), -1); + assert_eq!( + circuit_validate_classical_var(circuit_a, std::ptr::null()), + -1 + ); + + assert_eq!(circuit_validate_classical_expr(circuit_a, expr_a), 0); + assert_eq!(circuit_validate_classical_expr(circuit_b, expr_a), -3); + assert_eq!( + circuit_validate_classical_expr(std::ptr::null(), expr_a), + -1 + ); + assert_eq!( + circuit_validate_classical_expr(circuit_a, std::ptr::null()), + -1 + ); + + classical_expr_free(expr_a); + classical_var_free(var_a); + circuit_free(circuit_a); + circuit_free(circuit_b); +} + +#[test] +fn literal_expressions() { + let bit = classical_expr_bit_literal(true); + assert_eq!(expr_kind(bit), CQLIB_CLASSICAL_EXPR_BIT_LITERAL); + assert_eq!(expr_ty_parts(bit), (CQLIB_CLASSICAL_TYPE_BIT, 1)); + + let boolean = classical_expr_bool_literal(false); + assert_eq!(expr_kind(boolean), CQLIB_CLASSICAL_EXPR_BOOL_LITERAL); + assert_eq!(expr_ty_parts(boolean), (CQLIB_CLASSICAL_TYPE_BOOL, 1)); + + let uint = classical_expr_uint_literal(8, 42, 0); + assert!(!uint.is_null()); + assert_eq!(expr_kind(uint), CQLIB_CLASSICAL_EXPR_UINT_LITERAL); + assert_eq!(expr_ty_parts(uint), (CQLIB_CLASSICAL_TYPE_UINT, 8)); + + // 128-bit literal split into lo/hi halves: 2^64 + let wide = classical_expr_uint_literal(128, 0, 1); + assert!(!wide.is_null()); + assert_eq!(expr_ty_parts(wide), (CQLIB_CLASSICAL_TYPE_UINT, 128)); + + let bitvec = classical_expr_bit_vec_literal(4, 0b1011, 0); + assert!(!bitvec.is_null()); + assert_eq!(expr_kind(bitvec), CQLIB_CLASSICAL_EXPR_BIT_VEC_LITERAL); + assert_eq!(expr_ty_parts(bitvec), (CQLIB_CLASSICAL_TYPE_BIT_VEC, 4)); + + // invalid literals + assert!(classical_expr_uint_literal(0, 0, 0).is_null()); // zero width + assert!(classical_expr_uint_literal(8, 256, 0).is_null()); // value overflow + assert!(classical_expr_uint_literal(129, 0, 0).is_null()); // width over 128 + assert!(classical_expr_bit_vec_literal(0, 0, 0).is_null()); + assert!(classical_expr_bit_vec_literal(3, 8, 0).is_null()); + + classical_expr_free(bit); + classical_expr_free(boolean); + classical_expr_free(uint); + classical_expr_free(wide); + classical_expr_free(bitvec); + classical_expr_free(std::ptr::null_mut()); // no-op +} + +#[test] +fn logic_and_comparison_expressions() { + let a = classical_expr_bool_literal(true); + let b = classical_expr_bool_literal(false); + let bit = classical_expr_bit_literal(true); + let u8lit = classical_expr_uint_literal(8, 0, 0); + + let not = classical_expr_not(a); + assert_eq!(expr_kind(not), CQLIB_CLASSICAL_EXPR_UNARY); + assert_eq!(expr_ty_parts(not), (CQLIB_CLASSICAL_TYPE_BOOL, 1)); + + let and = classical_expr_and(a, b); + assert_eq!(expr_kind(and), CQLIB_CLASSICAL_EXPR_BINARY); + assert_eq!(expr_ty_parts(and), (CQLIB_CLASSICAL_TYPE_BOOL, 1)); + + let or = classical_expr_or(a, b); + assert_eq!(expr_ty_parts(or), (CQLIB_CLASSICAL_TYPE_BOOL, 1)); + let xor = classical_expr_xor(bit, bit); + assert_eq!(expr_ty_parts(xor), (CQLIB_CLASSICAL_TYPE_BIT, 1)); + + // type mismatches + assert!(classical_expr_and(a, bit).is_null()); // Bool vs Bit + assert!(classical_expr_not(u8lit).is_null()); // UInt operand + assert!(classical_expr_or(std::ptr::null(), b).is_null()); + assert!(classical_expr_xor(a, std::ptr::null()).is_null()); + + let x = classical_expr_uint_literal(8, 10, 0); + let y = classical_expr_uint_literal(8, 20, 0); + type BinaryBuilder = + extern "C" fn(*const CClassicalExpr, *const CClassicalExpr) -> *mut CClassicalExpr; + let builders: [(BinaryBuilder, &str); 6] = [ + (classical_expr_eq, "eq"), + (classical_expr_ne, "ne"), + (classical_expr_lt, "lt"), + (classical_expr_le, "le"), + (classical_expr_gt, "gt"), + (classical_expr_ge, "ge"), + ]; + for (builder, name) in builders { + let expr = builder(x, y); + assert!(!expr.is_null(), "{name} must succeed on UInt operands"); + assert_eq!(expr_kind(expr), CQLIB_CLASSICAL_EXPR_COMPARE); + assert_eq!(expr_ty_parts(expr), (CQLIB_CLASSICAL_TYPE_BOOL, 1)); + classical_expr_free(expr); + } + + // eq works on all matching types; ordered comparisons need UInt + let eq_bits = classical_expr_eq(bit, bit); + assert!(!eq_bits.is_null()); + classical_expr_free(eq_bits); + assert!(classical_expr_lt(a, b).is_null()); // Bool operands + assert!(classical_expr_gt(a, b).is_null()); + assert!(classical_expr_eq(a, bit).is_null()); // type mismatch + assert!(classical_expr_ge(std::ptr::null(), y).is_null()); + + classical_expr_free(not); + classical_expr_free(and); + classical_expr_free(or); + classical_expr_free(xor); + classical_expr_free(a); + classical_expr_free(b); + classical_expr_free(bit); + classical_expr_free(u8lit); + classical_expr_free(x); + classical_expr_free(y); +} + +#[test] +fn select_extract_pack_concat_expressions() { + let cond = classical_expr_bool_literal(true); + let ten = classical_expr_uint_literal(8, 10, 0); + let twenty = classical_expr_uint_literal(8, 20, 0); + + let selected = classical_expr_select(cond, ten, twenty); + assert_eq!(expr_kind(selected), CQLIB_CLASSICAL_EXPR_SELECT); + assert_eq!(expr_ty_parts(selected), (CQLIB_CLASSICAL_TYPE_UINT, 8)); + assert!(classical_expr_select(ten, ten, twenty).is_null()); // non-Bool condition + assert!(classical_expr_select(cond, ten, cond).is_null()); // branch type mismatch + assert!(classical_expr_select(std::ptr::null(), ten, twenty).is_null()); + + let eighth = classical_expr_uint_literal(8, 0xFF, 0); + let bit = classical_expr_extract_bit(eighth, 3); + assert_eq!(expr_kind(bit), CQLIB_CLASSICAL_EXPR_EXTRACT_BIT); + assert_eq!(expr_ty_parts(bit), (CQLIB_CLASSICAL_TYPE_BIT, 1)); + assert!(classical_expr_extract_bit(eighth, 8).is_null()); // out of bounds + assert!(classical_expr_extract_bit(cond, 0).is_null()); // Bool operand + assert!(classical_expr_extract_bit(std::ptr::null(), 0).is_null()); + + let nibble = classical_expr_extract_bits(eighth, 4, 4); + assert_eq!(expr_kind(nibble), CQLIB_CLASSICAL_EXPR_EXTRACT_BITS); + assert_eq!(expr_ty_parts(nibble), (CQLIB_CLASSICAL_TYPE_BIT_VEC, 4)); + assert!(classical_expr_extract_bits(eighth, 4, 5).is_null()); // range overflow + assert!(classical_expr_extract_bits(eighth, 0, 0).is_null()); // zero width + + let bit0 = classical_expr_bit_literal(true); + let bit1 = classical_expr_bit_literal(false); + let packed = classical_expr_pack_bits([c(bit0), c(bit1)].as_ptr(), 2); + assert_eq!(expr_kind(packed), CQLIB_CLASSICAL_EXPR_PACK_BITS); + assert_eq!(expr_ty_parts(packed), (CQLIB_CLASSICAL_TYPE_BIT_VEC, 2)); + let empty: [*const CClassicalExpr; 0] = []; + assert!(classical_expr_pack_bits(empty.as_ptr(), 0).is_null()); + assert!(classical_expr_pack_bits([c(cond)].as_ptr(), 1).is_null()); // non-Bit operand + let null_entry: [*const CClassicalExpr; 1] = [std::ptr::null()]; + assert!(classical_expr_pack_bits(null_entry.as_ptr(), 1).is_null()); + + let low = classical_expr_bit_vec_literal(2, 0b01, 0); + let concatenated = classical_expr_concat([c(low), c(bit0)].as_ptr(), 2); + assert_eq!(expr_kind(concatenated), CQLIB_CLASSICAL_EXPR_CONCAT); + assert_eq!( + expr_ty_parts(concatenated), + (CQLIB_CLASSICAL_TYPE_BIT_VEC, 3) + ); + assert!(classical_expr_concat(empty.as_ptr(), 0).is_null()); + assert!(classical_expr_concat([c(cond)].as_ptr(), 1).is_null()); // Bool part + + classical_expr_free(selected); + classical_expr_free(bit); + classical_expr_free(nibble); + classical_expr_free(packed); + classical_expr_free(concatenated); + classical_expr_free(cond); + classical_expr_free(ten); + classical_expr_free(twenty); + classical_expr_free(eighth); + classical_expr_free(bit0); + classical_expr_free(bit1); + classical_expr_free(low); +} + +#[test] +fn cast_expressions() { + let bit = classical_expr_bit_literal(true); + let to_bool = classical_expr_bit_to_bool(bit); + assert_eq!(expr_kind(to_bool), CQLIB_CLASSICAL_EXPR_CAST); + assert_eq!(expr_ty_parts(to_bool), (CQLIB_CLASSICAL_TYPE_BOOL, 1)); + assert!(classical_expr_bit_to_bool(to_bool).is_null()); // Bool operand + assert!(classical_expr_bit_to_bool(std::ptr::null()).is_null()); + + let alias_bool = classical_expr_to_bool(bit); + assert!(!alias_bool.is_null()); + assert_eq!(expr_ty_parts(alias_bool), (CQLIB_CLASSICAL_TYPE_BOOL, 1)); + + let bitvec = classical_expr_bit_vec_literal(6, 0b101010, 0); + let to_uint = classical_expr_bit_vec_to_uint(bitvec); + assert_eq!(expr_ty_parts(to_uint), (CQLIB_CLASSICAL_TYPE_UINT, 6)); + assert!(classical_expr_bit_vec_to_uint(bit).is_null()); // Bit operand + assert!(classical_expr_bit_vec_to_uint(std::ptr::null()).is_null()); + + let alias_uint = classical_expr_to_uint(bitvec); + assert!(!alias_uint.is_null()); + assert_eq!(expr_ty_parts(alias_uint), (CQLIB_CLASSICAL_TYPE_UINT, 6)); + + classical_expr_free(to_bool); + classical_expr_free(alias_bool); + classical_expr_free(to_uint); + classical_expr_free(alias_uint); + classical_expr_free(bit); + classical_expr_free(bitvec); +} + +#[test] +fn simplified_expression_folds_literals() { + // and(b, true) simplifies to b + let b = classical_expr_bool_literal(false); + let t = classical_expr_bool_literal(true); + let expr = classical_expr_and(b, t); + let simplified = classical_expr_simplified(expr); + assert!(!simplified.is_null()); + assert_eq!(expr_kind(simplified), CQLIB_CLASSICAL_EXPR_BOOL_LITERAL); + assert_eq!(expr_ty_parts(simplified), (CQLIB_CLASSICAL_TYPE_BOOL, 1)); + assert!(classical_expr_simplified(std::ptr::null()).is_null()); + + // kind/ty error paths + let mut tag = 0u32; + assert_eq!(classical_expr_kind(std::ptr::null(), &mut tag), -1); + assert_eq!(classical_expr_kind(b, std::ptr::null_mut()), -1); + assert_eq!(classical_expr_ty(std::ptr::null(), &mut tag, &mut tag), -1); + + classical_expr_free(simplified); + classical_expr_free(expr); + classical_expr_free(b); + classical_expr_free(t); +} + +#[test] +fn vars_collection_returns_owned_handles() { + let circuit = circuit_new(2); + let a = circuit_var(circuit, CQLIB_CLASSICAL_TYPE_BOOL, 0); + let b = circuit_var(circuit, CQLIB_CLASSICAL_TYPE_BOOL, 0); + let expr_a = classical_expr_var(a); + let expr_b = classical_var_expr(b); + assert!(!expr_a.is_null()); + assert_eq!(expr_kind(expr_a), CQLIB_CLASSICAL_EXPR_VAR); + assert!(classical_expr_var(std::ptr::null()).is_null()); + let combined = classical_expr_and(expr_a, expr_b); + + assert_eq!(classical_expr_vars_len(combined), 2); + assert_eq!(classical_expr_vars_len(std::ptr::null()), 0); + + let mut handles: [*mut CClassicalVar; 2] = [std::ptr::null_mut(); 2]; + assert_eq!(classical_expr_vars(combined, handles.as_mut_ptr(), 2), 0); + // BTreeSet order: ascending id, so a (id 0) then b (id 1) + assert_eq!(classical_var_id(handles[0]), 0); + assert_eq!(classical_var_id(handles[1]), 1); + + // collected handles behave like the originals + assert_eq!(circuit_validate_classical_var(circuit, handles[0]), 0); + let reread = classical_var_expr(handles[1]); + assert_eq!(expr_kind(reread), CQLIB_CLASSICAL_EXPR_VAR); + + // error paths + assert_eq!(classical_expr_vars(combined, handles.as_mut_ptr(), 1), -8); + assert_eq!( + classical_expr_vars(std::ptr::null(), handles.as_mut_ptr(), 2), + -1 + ); + assert_eq!(classical_expr_vars(combined, std::ptr::null_mut(), 2), -1); + + classical_expr_free(reread); + for handle in handles { + classical_var_free(handle); + } + classical_expr_free(combined); + classical_expr_free(expr_a); + classical_expr_free(expr_b); + classical_var_free(a); + classical_var_free(b); + circuit_free(circuit); +} + +#[test] +fn circuit_store_roundtrip() { + let circuit = circuit_new(2); + + // store a Bool literal into a Bool variable + let flag = circuit_var(circuit, CQLIB_CLASSICAL_TYPE_BOOL, 0); + let value = classical_expr_bool_literal(true); + assert_eq!(circuit_store(circuit, flag, value), 0); + assert_eq!(circuit_num_operations(circuit), 1); + assert_eq!(circuit_validate(circuit), 0); + + // store a measured bit (converted to Bool) into another Bool variable + let bit_target = circuit_var(circuit, CQLIB_CLASSICAL_TYPE_BIT, 0); + let measured = circuit_measure_into(circuit, 0, bit_target); + assert!(!measured.is_null()); + let condition = classical_expr_bit_to_bool(measured); + let other = circuit_var(circuit, CQLIB_CLASSICAL_TYPE_BOOL, 0); + assert_eq!(circuit_store(circuit, other, condition), 0); + assert_eq!(circuit_num_operations(circuit), 4); // store + measure + store + store + assert_eq!(circuit_validate(circuit), 0); + + // error paths + assert_eq!(circuit_store(std::ptr::null_mut(), flag, value), -1); + assert_eq!(circuit_store(circuit, std::ptr::null(), value), -1); + assert_eq!(circuit_store(circuit, flag, std::ptr::null()), -1); + // type mismatch: Bit variable cannot receive a Bool expression + let bit_target2 = circuit_var(circuit, CQLIB_CLASSICAL_TYPE_BIT, 0); + assert_eq!(circuit_store(circuit, bit_target2, value), -3); + // foreign variable + let other_circuit = circuit_new(1); + let foreign = circuit_var(other_circuit, CQLIB_CLASSICAL_TYPE_BOOL, 0); + assert_eq!(circuit_store(circuit, foreign, value), -3); + + classical_expr_free(measured); + classical_expr_free(condition); + classical_expr_free(value); + classical_var_free(flag); + classical_var_free(other); + classical_var_free(bit_target); + classical_var_free(bit_target2); + classical_var_free(foreign); + circuit_free(other_circuit); + circuit_free(circuit); +} diff --git a/crates/binding-c/tests/circuit_control_test.rs b/crates/binding-c/tests/circuit_control_test.rs new file mode 100644 index 00000000..0c82a91a --- /dev/null +++ b/crates/binding-c/tests/circuit_control_test.rs @@ -0,0 +1,565 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! FFI tests for structured classical control flow (callback-based bodies). + +use binding_c::circuit::{ + CCircuit, CClassicalExpr, CClassicalVar, circuit_append_control, circuit_break_loop, + circuit_continue_loop, circuit_depth, circuit_for_uint, circuit_free, circuit_h, circuit_if, + circuit_if_else, circuit_measure, circuit_new, circuit_num_operations, circuit_switch, + circuit_validate, circuit_while, circuit_x, circuit_z, +}; +use cqlib_core::circuit::{ClassicalExpr, ClassicalType}; +use std::os::raw::c_void; + +fn bool_expr(value: bool) -> *mut CClassicalExpr { + Box::into_raw(Box::new(CClassicalExpr { + inner: ClassicalExpr::bool_literal(value), + })) +} + +fn uint_expr(width: u32, value: u128) -> *mut CClassicalExpr { + Box::into_raw(Box::new(CClassicalExpr { + inner: ClassicalExpr::uint_literal(width, value).unwrap(), + })) +} + +fn drop_expr(ptr: *mut CClassicalExpr) { + if !ptr.is_null() { + drop(unsafe { Box::from_raw(ptr) }); + } +} + +/// Counting callback context shared through `user_data`. +struct BodyCtx { + calls: u32, +} + +extern "C" fn add_h_then_x(circuit: *mut CCircuit, user_data: *mut c_void) -> i32 { + bump(user_data); + let rc = circuit_h(circuit, 0); + if rc != 0 { + return rc; + } + circuit_x(circuit, 0) +} + +extern "C" fn add_z(circuit: *mut CCircuit, user_data: *mut c_void) -> i32 { + bump(user_data); + circuit_z(circuit, 1) +} + +extern "C" fn add_measure_then_x(circuit: *mut CCircuit, user_data: *mut c_void) -> i32 { + bump(user_data); + let rc = circuit_measure(circuit, 0); + if rc != 0 { + return rc; + } + circuit_x(circuit, 0) +} + +extern "C" fn loop_body_with_continue(circuit: *mut CCircuit, user_data: *mut c_void) -> i32 { + bump(user_data); + let rc = circuit_h(circuit, 0); + if rc != 0 { + return rc; + } + let rc = circuit_x(circuit, 0); + if rc != 0 { + return rc; + } + // Terminal continue: legal as the final operation of a loop body. + circuit_continue_loop(circuit) +} + +extern "C" fn loop_body_with_break(circuit: *mut CCircuit, user_data: *mut c_void) -> i32 { + bump(user_data); + let rc = circuit_h(circuit, 1); + if rc != 0 { + return rc; + } + // Terminal break: legal as the final operation of a loop body. + circuit_break_loop(circuit) +} + +extern "C" fn loop_body_non_terminal_continue( + circuit: *mut CCircuit, + user_data: *mut c_void, +) -> i32 { + bump(user_data); + let rc = circuit_continue_loop(circuit); + if rc != 0 { + return rc; + } + circuit_x(circuit, 0) +} + +extern "C" fn failing_body(circuit: *mut CCircuit, user_data: *mut c_void) -> i32 { + bump(user_data); + circuit_h(circuit, 0); + circuit_h(circuit, 99) // qubit out of bounds -> -2 +} + +extern "C" fn nested_if_body(circuit: *mut CCircuit, user_data: *mut c_void) -> i32 { + bump(user_data); + let condition = bool_expr(true); + let rc = circuit_if(circuit, condition, Some(add_h_then_x), std::ptr::null_mut()); + drop_expr(condition); + rc +} + +fn bump(user_data: *mut c_void) { + if !user_data.is_null() { + unsafe { (*(user_data as *mut BodyCtx)).calls += 1 }; + } +} + +fn ctx_ptr(ctx: &mut BodyCtx) -> *mut c_void { + ctx as *mut BodyCtx as *mut c_void +} + +#[test] +fn circuit_if_builds_body_from_callback() { + let circuit = circuit_new(2); + let condition = bool_expr(true); + let mut ctx = BodyCtx { calls: 0 }; + + assert_eq!( + circuit_if(circuit, condition, Some(add_h_then_x), ctx_ptr(&mut ctx)), + 0 + ); + assert_eq!(ctx.calls, 1); + // Only the control operation itself is visible at the top level. + assert_eq!(circuit_num_operations(circuit), 1); + // Recursive depth: the control op is a scheduling boundary, so + // 1 + body depth (h(0) then x(0) -> 2) = 3. + assert_eq!(circuit_depth(circuit, true), 3); + assert_eq!(circuit_validate(circuit), 0); + + drop_expr(condition); + circuit_free(circuit); +} + +#[test] +fn circuit_if_else_runs_both_callbacks() { + let circuit = circuit_new(2); + let condition = bool_expr(false); + let mut ctx = BodyCtx { calls: 0 }; + + assert_eq!( + circuit_if_else( + circuit, + condition, + Some(add_h_then_x), + Some(add_z), + ctx_ptr(&mut ctx) + ), + 0 + ); + assert_eq!(ctx.calls, 2); + assert_eq!(circuit_num_operations(circuit), 1); + // 1 + max(then: h(0)+x(0) = 2, else: z(1) = 1) = 3. + assert_eq!(circuit_depth(circuit, true), 3); + assert_eq!(circuit_validate(circuit), 0); + + drop_expr(condition); + circuit_free(circuit); +} + +#[test] +fn circuit_while_allows_terminal_break_and_continue() { + let circuit = circuit_new(2); + let var = unsafe { (*circuit).inner.var(ClassicalType::Bool) }; + let condition = Box::into_raw(Box::new(CClassicalExpr { + inner: ClassicalExpr::var(var), + })); + let mut ctx = BodyCtx { calls: 0 }; + + assert_eq!( + circuit_while( + circuit, + condition, + Some(loop_body_with_continue), + ctx_ptr(&mut ctx) + ), + 0 + ); + assert_eq!( + circuit_while( + circuit, + condition, + Some(loop_body_with_break), + ctx_ptr(&mut ctx) + ), + 0 + ); + assert_eq!(ctx.calls, 2); + assert_eq!(circuit_num_operations(circuit), 2); + // 1 + body once: max(continue body h+x = 2, break body h = 1) + 1 = 3. + assert_eq!(circuit_depth(circuit, true), 3); + assert_eq!(circuit_validate(circuit), 0); + + // break/continue outside any loop body is rejected. + assert_eq!(circuit_break_loop(circuit), -3); + assert_eq!(circuit_continue_loop(circuit), -3); + + drop_expr(condition); + circuit_free(circuit); +} + +#[test] +fn circuit_while_rejects_non_terminal_control_transfer() { + let circuit = circuit_new(2); + let var = unsafe { (*circuit).inner.var(ClassicalType::Bool) }; + let condition = Box::into_raw(Box::new(CClassicalExpr { + inner: ClassicalExpr::var(var), + })); + let mut ctx = BodyCtx { calls: 0 }; + + // A continue followed by another operation is rejected and rolled back. + assert_eq!( + circuit_while( + circuit, + condition, + Some(loop_body_non_terminal_continue), + ctx_ptr(&mut ctx) + ), + -3 + ); + assert_eq!(ctx.calls, 1); + assert_eq!(circuit_num_operations(circuit), 0); + + drop_expr(condition); + circuit_free(circuit); +} + +#[test] +fn circuit_for_uint_builds_range_loop() { + let circuit = circuit_new(1); + let var = unsafe { (*circuit).inner.var(ClassicalType::uint(8).unwrap()) }; + let var_handle = Box::into_raw(Box::new(CClassicalVar { inner: var })); + let start = uint_expr(8, 0); + let stop = uint_expr(8, 4); + let step = uint_expr(8, 1); + let mut ctx = BodyCtx { calls: 0 }; + + assert_eq!( + circuit_for_uint( + circuit, + var_handle, + start, + stop, + step, + Some(add_h_then_x), + ctx_ptr(&mut ctx) + ), + 0 + ); + assert_eq!(ctx.calls, 1); + assert_eq!(circuit_num_operations(circuit), 1); + // Static range [0, 4) step 1 with body h(0)+x(0) unrolls to + // 1 + 4 * 2 = 9. + assert_eq!(circuit_depth(circuit, true), 9); + assert_eq!(circuit_validate(circuit), 0); + + drop_expr(start); + drop_expr(stop); + drop_expr(step); + drop(unsafe { Box::from_raw(var_handle) }); + circuit_free(circuit); +} + +extern "C" fn switch_case_zero(circuit: *mut CCircuit, user_data: *mut c_void) -> i32 { + bump(user_data); + circuit_x(circuit, 0) +} + +extern "C" fn switch_case_one(circuit: *mut CCircuit, user_data: *mut c_void) -> i32 { + bump(user_data); + circuit_h(circuit, 1) +} + +extern "C" fn switch_default(circuit: *mut CCircuit, user_data: *mut c_void) -> i32 { + bump(user_data); + circuit_z(circuit, 0) +} + +#[test] +fn circuit_switch_builds_cases_and_default() { + let circuit = circuit_new(2); + let target = uint_expr(2, 1); + let values_lo: [u64; 3] = [0, 1, 2]; + let values_hi: [u64; 3] = [0, 0, 0]; + let bodies: [binding_c::circuit::CqlibBodyFn; 3] = [ + Some(switch_case_zero), + Some(switch_case_one), + Some(switch_case_one), + ]; + let mut ctx = BodyCtx { calls: 0 }; + + assert_eq!( + circuit_switch( + circuit, + target, + values_lo.as_ptr(), + values_hi.as_ptr(), + bodies.as_ptr(), + 3, + Some(switch_default), + ctx_ptr(&mut ctx) + ), + 0 + ); + // Three case bodies plus the default body all ran. + assert_eq!(ctx.calls, 4); + assert_eq!(circuit_num_operations(circuit), 1); + // 1 + max(x(0) = 1, h(1) = 1, h(1) = 1, z(0) = 1) = 2. + assert_eq!(circuit_depth(circuit, true), 2); + assert_eq!(circuit_validate(circuit), 0); + + drop_expr(target); + circuit_free(circuit); +} + +#[test] +fn circuit_switch_rejects_invalid_cases() { + let circuit = circuit_new(2); + + // Duplicate case values. + let target = uint_expr(2, 0); + let values_lo: [u64; 2] = [1, 1]; + let values_hi: [u64; 2] = [0, 0]; + let bodies: [binding_c::circuit::CqlibBodyFn; 2] = + [Some(switch_case_zero), Some(switch_case_one)]; + assert_eq!( + circuit_switch( + circuit, + target, + values_lo.as_ptr(), + values_hi.as_ptr(), + bodies.as_ptr(), + 2, + None, + std::ptr::null_mut() + ), + -3 + ); + + // Case value does not fit the UInt(2) target. + let target = uint_expr(2, 0); + let values_lo: [u64; 1] = [4]; + let values_hi: [u64; 1] = [0]; + let bodies: [binding_c::circuit::CqlibBodyFn; 1] = [Some(switch_case_zero)]; + assert_eq!( + circuit_switch( + circuit, + target, + values_lo.as_ptr(), + values_hi.as_ptr(), + bodies.as_ptr(), + 1, + None, + std::ptr::null_mut() + ), + -3 + ); + + // Non-UInt target. + let target = bool_expr(true); + assert_eq!( + circuit_switch( + circuit, + target, + std::ptr::null(), + std::ptr::null(), + std::ptr::null(), + 0, + None, + std::ptr::null_mut() + ), + -3 + ); + + // Nothing was appended by the failed attempts. + assert_eq!(circuit_num_operations(circuit), 0); + + drop_expr(target); + circuit_free(circuit); +} + +#[test] +fn nested_control_flow_and_measurements() { + let circuit = circuit_new(2); + let condition = bool_expr(true); + let mut ctx = BodyCtx { calls: 0 }; + + // An `if` whose body measures a qubit and then nests another `if`. + assert_eq!( + circuit_if( + circuit, + condition, + Some(add_measure_then_x), + ctx_ptr(&mut ctx) + ), + 0 + ); + assert_eq!(ctx.calls, 1); + + // Nested: while (var) { if (true) { h(0); x(0) } }. + let var = unsafe { (*circuit).inner.var(ClassicalType::Bool) }; + let while_condition = Box::into_raw(Box::new(CClassicalExpr { + inner: ClassicalExpr::var(var), + })); + assert_eq!( + circuit_while( + circuit, + while_condition, + Some(nested_if_body), + ctx_ptr(&mut ctx) + ), + 0 + ); + assert_eq!(ctx.calls, 2); + + assert_eq!(circuit_num_operations(circuit), 2); + assert_eq!(circuit_validate(circuit), 0); + + drop_expr(condition); + drop_expr(while_condition); + circuit_free(circuit); +} + +#[test] +fn callback_errors_roll_back_the_control_region() { + let circuit = circuit_new(2); + assert_eq!(circuit_h(circuit, 1), 0); + let condition = bool_expr(true); + let mut ctx = BodyCtx { calls: 0 }; + + // The callback appends h(0) and then fails with -2; everything it + // appended must be rolled back and the status propagated. + assert_eq!( + circuit_if(circuit, condition, Some(failing_body), ctx_ptr(&mut ctx)), + -2 + ); + assert_eq!(ctx.calls, 1); + assert_eq!(circuit_num_operations(circuit), 1); + assert_eq!(circuit_validate(circuit), 0); + + drop_expr(condition); + circuit_free(circuit); +} + +#[test] +fn null_arguments_return_null_ptr_error() { + let circuit = circuit_new(1); + let condition = bool_expr(true); + + assert_eq!( + circuit_if( + std::ptr::null_mut(), + condition, + Some(add_h_then_x), + std::ptr::null_mut() + ), + -1 + ); + assert_eq!( + circuit_if( + circuit, + std::ptr::null(), + Some(add_h_then_x), + std::ptr::null_mut() + ), + -1 + ); + assert_eq!( + circuit_if(circuit, condition, None, std::ptr::null_mut()), + -1 + ); + assert_eq!(circuit_break_loop(std::ptr::null_mut()), -1); + assert_eq!(circuit_continue_loop(std::ptr::null_mut()), -1); + assert_eq!(circuit_append_control(std::ptr::null_mut(), circuit, 0), -1); + assert_eq!(circuit_append_control(circuit, std::ptr::null(), 0), -1); + assert_eq!(circuit_num_operations(circuit), 0); + + drop_expr(condition); + circuit_free(circuit); +} + +#[test] +fn foreign_classical_handles_are_rejected() { + let first = circuit_new(1); + let second = circuit_new(1); + let var = unsafe { (*first).inner.var(ClassicalType::Bool) }; + let condition = Box::into_raw(Box::new(CClassicalExpr { + inner: ClassicalExpr::var(var), + })); + let mut ctx = BodyCtx { calls: 0 }; + + // The condition belongs to `first`, so `second` must reject it. + assert_eq!( + circuit_if(second, condition, Some(add_h_then_x), ctx_ptr(&mut ctx)), + -3 + ); + assert_eq!(ctx.calls, 0); + assert_eq!(circuit_num_operations(second), 0); + + drop_expr(condition); + circuit_free(first); + circuit_free(second); +} + +#[test] +fn circuit_append_control_copies_existing_control_op() { + let source = circuit_new(2); + let target = circuit_new(2); + let condition = bool_expr(true); + let mut ctx = BodyCtx { calls: 0 }; + + assert_eq!( + circuit_if(source, condition, Some(add_h_then_x), ctx_ptr(&mut ctx)), + 0 + ); + assert_eq!(circuit_num_operations(source), 1); + + // Copy the control operation into another circuit. The body has no + // classical handles, so it stays valid for `target`. + assert_eq!(circuit_append_control(target, source, 0), 0); + assert_eq!(circuit_num_operations(target), 1); + assert_eq!(circuit_depth(target, true), 3); + assert_eq!(circuit_validate(target), 0); + + // Duplicating within the same circuit is allowed too. + assert_eq!(circuit_append_control(source, source, 0), 0); + assert_eq!(circuit_num_operations(source), 2); + + drop_expr(condition); + circuit_free(source); + circuit_free(target); +} + +#[test] +fn circuit_append_control_rejects_non_control_operations() { + let circuit = circuit_new(2); + let other = circuit_new(2); + assert_eq!(circuit_h(other, 0), 0); + + // Index out of range. + assert_eq!(circuit_append_control(circuit, other, 5), -8); + // Index 0 is a plain gate, not a control-flow operation. + assert_eq!(circuit_append_control(circuit, other, 0), -8); + assert_eq!(circuit_num_operations(circuit), 0); + + circuit_free(circuit); + circuit_free(other); +} diff --git a/crates/binding-c/tests/circuit_dag_test.rs b/crates/binding-c/tests/circuit_dag_test.rs new file mode 100644 index 00000000..dc6b9a51 --- /dev/null +++ b/crates/binding-c/tests/circuit_dag_test.rs @@ -0,0 +1,632 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! Rust FFI tests for circuit dependency DAGs (`circuit/dag.rs`). + +use binding_c::circuit::{ + COperation, DAG_WIRE_CLASSICAL_VAR, DAG_WIRE_GLOBAL_ORDER, DAG_WIRE_QUBIT, circuit_cx, + circuit_dag_apply_operation_back, circuit_dag_apply_operation_front, circuit_dag_depth, + circuit_dag_free, circuit_dag_from_circuit, circuit_dag_from_operations, + circuit_dag_front_layer, circuit_dag_front_layer_len, circuit_dag_has_control_flow, + circuit_dag_has_measurement, circuit_dag_has_wire, circuit_dag_is_empty, + circuit_dag_is_operation, circuit_dag_is_wire_idle, circuit_dag_layers, circuit_dag_layers_len, + circuit_dag_node_kind, circuit_dag_node_layers, circuit_dag_node_layers_len, + circuit_dag_nodes_on_wire, circuit_dag_nodes_on_wire_len, circuit_dag_num_ops, + circuit_dag_num_qubits, circuit_dag_op_nodes, circuit_dag_op_nodes_len, circuit_dag_operation, + circuit_dag_operation_count_by_name, circuit_dag_operation_count_by_name_len, + circuit_dag_parameters, circuit_dag_parameters_len, circuit_dag_predecessors, + circuit_dag_predecessors_len, circuit_dag_quantum_predecessors, + circuit_dag_quantum_predecessors_len, circuit_dag_qubits, circuit_dag_qubits_len, + circuit_dag_remove_op_node, circuit_dag_substitute_node, circuit_dag_successors, + circuit_dag_successors_len, circuit_dag_symbols, circuit_dag_symbols_len, + circuit_dag_to_circuit, circuit_dag_topological_op_nodes, circuit_dag_topological_op_nodes_len, + circuit_dag_validate, circuit_dag_wire_in, circuit_dag_wire_out, circuit_dag_wires, + circuit_dag_wires_len, circuit_free, circuit_h, circuit_measure, circuit_new, + circuit_num_operations, circuit_num_parameters, circuit_num_qubits, circuit_rx_param, + operation_free, operation_name, operation_new, param_free, param_parse, +}; +use binding_c::cqlib_string_free; +use std::ffi::{CStr, CString}; +use std::os::raw::c_char; + +fn cstr(s: &str) -> CString { + CString::new(s).unwrap() +} + +fn cstr_of(ptr: *mut c_char) -> &'static str { + unsafe { CStr::from_ptr(ptr) }.to_str().unwrap() +} + +#[test] +fn dag_from_circuit_reflects_structure() { + let circuit = circuit_new(2); + assert_eq!(circuit_h(circuit, 0), 0); + assert_eq!(circuit_cx(circuit, 0, 1), 0); + assert_eq!(circuit_num_operations(circuit), 2); + + let dag = circuit_dag_from_circuit(circuit); + assert!(!dag.is_null()); + assert_eq!(circuit_dag_validate(dag), 0); + assert_eq!(circuit_dag_is_empty(dag), 0); + assert_eq!(circuit_dag_num_qubits(dag), 2); + assert_eq!(circuit_dag_num_ops(dag), 2); + assert_eq!(circuit_dag_qubits_len(dag), 2); + + let mut ids = [9u32; 2]; + assert_eq!(circuit_dag_qubits(dag, ids.as_mut_ptr(), 2), 0); + assert_eq!(ids, [0, 1]); + + // Wire inventory: the global-order resource first, then qubit timelines. + assert_eq!(circuit_dag_wires_len(dag), 3); + let mut tags = [99u32; 3]; + let mut wire_ids = [99u32; 3]; + assert_eq!( + circuit_dag_wires(dag, tags.as_mut_ptr(), wire_ids.as_mut_ptr(), 3), + 0 + ); + assert_eq!( + tags, + [DAG_WIRE_GLOBAL_ORDER, DAG_WIRE_QUBIT, DAG_WIRE_QUBIT] + ); + assert_eq!(wire_ids, [0, 0, 1]); + + // Wire membership probes. + assert_eq!(circuit_dag_has_wire(dag, DAG_WIRE_QUBIT, 0), 1); + assert_eq!(circuit_dag_has_wire(dag, DAG_WIRE_QUBIT, 1), 1); + assert_eq!(circuit_dag_has_wire(dag, DAG_WIRE_QUBIT, 5), 0); + assert_eq!(circuit_dag_has_wire(dag, DAG_WIRE_GLOBAL_ORDER, 0), 1); + + // Round trip back to a circuit of identical shape. The source handle + // stays alive and unchanged. + let lowered = circuit_dag_to_circuit(dag); + assert!(!lowered.is_null()); + assert_eq!(circuit_num_qubits(lowered), 2); + assert_eq!(circuit_num_operations(lowered), 2); + + circuit_free(lowered); + circuit_dag_free(dag); + assert_eq!(circuit_num_operations(circuit), 2); + circuit_free(circuit); +} + +#[test] +fn dag_from_operations_build() { + let rx_name = cstr("RX"); + let cx_name = cstr("CX"); + let h_name = cstr("H"); + let zero = [0u32]; + let one = [1u32]; + let pair = [0u32, 1]; + let half = [0.5f64]; + + let rx = operation_new(rx_name.as_ptr(), zero.as_ptr(), 1, half.as_ptr(), 1); + let cx = operation_new(cx_name.as_ptr(), pair.as_ptr(), 2, std::ptr::null(), 0); + let h = operation_new(h_name.as_ptr(), one.as_ptr(), 1, std::ptr::null(), 0); + assert!(!rx.is_null()); + assert!(!cx.is_null()); + assert!(!h.is_null()); + + let qubits = [0u32, 1]; + let ops: [*const COperation; 3] = [rx, cx, h]; + let dag = circuit_dag_from_operations(qubits.as_ptr(), 2, ops.as_ptr(), 3); + assert!(!dag.is_null()); + assert_eq!(circuit_dag_validate(dag), 0); + assert_eq!(circuit_dag_num_qubits(dag), 2); + assert_eq!(circuit_dag_num_ops(dag), 3); + assert_eq!(circuit_dag_is_empty(dag), 0); + + // The narrow constructor has no parameter table: numeric angles yield + // neither parameters nor symbols. + assert_eq!(circuit_dag_parameters_len(dag), 0); + assert_eq!(circuit_dag_symbols_len(dag), 0); + + // Round trip keeps all three operations. + let lowered = circuit_dag_to_circuit(dag); + assert!(!lowered.is_null()); + assert_eq!(circuit_num_qubits(lowered), 2); + assert_eq!(circuit_num_operations(lowered), 3); + + circuit_free(lowered); + circuit_dag_free(dag); + operation_free(rx); + operation_free(cx); + operation_free(h); +} + +#[test] +fn dag_builder_rejects_invalid_inputs() { + let h_name = cstr("H"); + let zero = [0u32]; + let h = operation_new(h_name.as_ptr(), zero.as_ptr(), 1, std::ptr::null(), 0); + assert!(!h.is_null()); + let ops: [*const COperation; 1] = [h]; + + // NULL qubit buffer with a non-zero length. + assert!(circuit_dag_from_operations(std::ptr::null(), 1, std::ptr::null(), 0).is_null()); + + // NULL op buffer with a non-zero length. + let qubits = [0u32]; + assert!(circuit_dag_from_operations(qubits.as_ptr(), 1, std::ptr::null(), 1).is_null()); + + // Duplicate qubit ids fail the build. + let dup = [3u32, 3]; + assert!(circuit_dag_from_operations(dup.as_ptr(), 2, ops.as_ptr(), 1).is_null()); + + // Operations acting on qubits that were never registered. + let sparse = [7u32, 8]; + assert!(circuit_dag_from_operations(sparse.as_ptr(), 2, ops.as_ptr(), 1).is_null()); + + // NULL entries inside the op pointer list. + let mixed: [*const COperation; 2] = [h, std::ptr::null()]; + assert!(circuit_dag_from_operations(qubits.as_ptr(), 1, mixed.as_ptr(), 2).is_null()); + + // Building from a NULL circuit is rejected too. + assert!(circuit_dag_from_circuit(std::ptr::null()).is_null()); + + operation_free(h); +} + +#[test] +fn dag_reports_parameters_and_symbols() { + let theta = cstr("theta"); + let theta_ptr = param_parse(theta.as_ptr()); + assert!(!theta_ptr.is_null()); + + let circuit = circuit_new(1); + assert_eq!(circuit_rx_param(circuit, 0, theta_ptr), 0); + assert_eq!(circuit_num_parameters(circuit), 1); + + let dag = circuit_dag_from_circuit(circuit); + assert!(!dag.is_null()); + + // Parameter names in table order. + assert_eq!(circuit_dag_parameters_len(dag), 1); + let mut names = [std::ptr::null_mut(); 1]; + assert_eq!(circuit_dag_parameters(dag, names.as_mut_ptr(), 1), 0); + assert!(!names[0].is_null()); + assert_eq!( + unsafe { CStr::from_ptr(names[0]) }.to_str().unwrap(), + "theta" + ); + cqlib_string_free(names[0]); + + // Free-symbol inventory mirrors the parameter expression. + assert_eq!(circuit_dag_symbols_len(dag), 1); + let mut symbols = [std::ptr::null_mut(); 1]; + assert_eq!(circuit_dag_symbols(dag, symbols.as_mut_ptr(), 1), 0); + assert!(!symbols[0].is_null()); + assert_eq!( + unsafe { CStr::from_ptr(symbols[0]) }.to_str().unwrap(), + "theta" + ); + cqlib_string_free(symbols[0]); + + // Undersized or NULL output buffers. + assert_eq!(circuit_dag_parameters(dag, names.as_mut_ptr(), 0), -8); + assert_eq!(circuit_dag_parameters(dag, std::ptr::null_mut(), 1), -1); + assert_eq!(circuit_dag_symbols(dag, symbols.as_mut_ptr(), 0), -8); + assert_eq!(circuit_dag_symbols(dag, std::ptr::null_mut(), 1), -1); + + circuit_dag_free(dag); + circuit_free(circuit); + param_free(theta_ptr); +} + +#[test] +fn dag_null_and_error_paths() { + // Every exported function tolerates a NULL handle. + assert!(circuit_dag_from_circuit(std::ptr::null()).is_null()); + assert!(circuit_dag_to_circuit(std::ptr::null()).is_null()); + assert_eq!(circuit_dag_validate(std::ptr::null()), -1); + assert_eq!(circuit_dag_is_empty(std::ptr::null()), -1); + assert_eq!(circuit_dag_num_qubits(std::ptr::null()), 0); + assert_eq!(circuit_dag_num_ops(std::ptr::null()), 0); + assert_eq!(circuit_dag_qubits_len(std::ptr::null()), 0); + assert_eq!(circuit_dag_parameters_len(std::ptr::null()), 0); + assert_eq!(circuit_dag_symbols_len(std::ptr::null()), 0); + assert_eq!(circuit_dag_wires_len(std::ptr::null()), 0); + assert_eq!( + circuit_dag_qubits(std::ptr::null(), std::ptr::null_mut(), 0), + -1 + ); + assert_eq!( + circuit_dag_parameters(std::ptr::null(), std::ptr::null_mut(), 0), + -1 + ); + assert_eq!( + circuit_dag_symbols(std::ptr::null(), std::ptr::null_mut(), 0), + -1 + ); + assert_eq!( + circuit_dag_wires( + std::ptr::null(), + std::ptr::null_mut(), + std::ptr::null_mut(), + 0 + ), + -1 + ); + assert_eq!( + circuit_dag_has_wire(std::ptr::null(), DAG_WIRE_QUBIT, 0), + -1 + ); + circuit_dag_free(std::ptr::null_mut()); + + // An empty DAG carries only the global-order wire. + let dag = circuit_dag_from_operations(std::ptr::null(), 0, std::ptr::null(), 0); + assert!(!dag.is_null()); + assert_eq!(circuit_dag_validate(dag), 0); + assert_eq!(circuit_dag_is_empty(dag), 1); + assert_eq!(circuit_dag_num_qubits(dag), 0); + assert_eq!(circuit_dag_num_ops(dag), 0); + assert_eq!(circuit_dag_qubits_len(dag), 0); + assert_eq!(circuit_dag_wires_len(dag), 1); + assert_eq!(circuit_dag_has_wire(dag, DAG_WIRE_GLOBAL_ORDER, 0), 1); + assert_eq!(circuit_dag_has_wire(dag, DAG_WIRE_QUBIT, 0), 0); + + // Unknown or unsupported wire tags are rejected, not dereferenced. + assert_eq!(circuit_dag_has_wire(dag, DAG_WIRE_CLASSICAL_VAR, 0), -8); + assert_eq!(circuit_dag_has_wire(dag, 99, 0), -8); + + // Wire buffers: undersized then NULL. + let mut tags = [99u32; 1]; + let mut ids = [99u32; 1]; + assert_eq!( + circuit_dag_wires(dag, tags.as_mut_ptr(), ids.as_mut_ptr(), 0), + -8 + ); + assert_eq!( + circuit_dag_wires(dag, std::ptr::null_mut(), ids.as_mut_ptr(), 1), + -1 + ); + assert_eq!( + circuit_dag_wires(dag, tags.as_mut_ptr(), std::ptr::null_mut(), 1), + -1 + ); + assert_eq!( + circuit_dag_wires(dag, tags.as_mut_ptr(), ids.as_mut_ptr(), 1), + 0 + ); + assert_eq!(tags, [DAG_WIRE_GLOBAL_ORDER]); + assert_eq!(ids, [0]); + + // A qubit buffer is fine to omit when the DAG has no qubits; the empty + // parameter/symbol readers also accept len = 0 with no buffer. + assert_eq!(circuit_dag_qubits(dag, std::ptr::null_mut(), 0), 0); + assert_eq!(circuit_dag_parameters(dag, std::ptr::null_mut(), 0), 0); + assert_eq!(circuit_dag_symbols(dag, std::ptr::null_mut(), 0), 0); + + circuit_dag_free(dag); +} + +#[test] +fn dag_node_identification_and_traversal() { + // H(0) -> CX(0,1) -> H(1): a chain on qubit 0 with a tail on qubit 1. + let circuit = circuit_new(2); + assert_eq!(circuit_h(circuit, 0), 0); + assert_eq!(circuit_cx(circuit, 0, 1), 0); + assert_eq!(circuit_h(circuit, 1), 0); + + let dag = circuit_dag_from_circuit(circuit); + assert!(!dag.is_null()); + assert_eq!(circuit_dag_depth(dag), 3); + + // Operation nodes in source order; a too-small buffer reports -8. + assert_eq!(circuit_dag_op_nodes_len(dag), 3); + let mut ops = [9u32; 3]; + assert_eq!(circuit_dag_op_nodes(dag, ops.as_mut_ptr(), 3), 0); + assert_eq!(circuit_dag_op_nodes(dag, ops.as_mut_ptr(), 2), -8); + + // Every op node reports the operation kind; the middle node holds CX. + for node in ops { + assert_eq!(circuit_dag_is_operation(dag, node), 1); + let kind = circuit_dag_node_kind(dag, node); + assert_eq!(cstr_of(kind), "operation"); + cqlib_string_free(kind); + } + let op = circuit_dag_operation(dag, ops[1]); + assert!(!op.is_null()); + let name = operation_name(op); + assert_eq!(cstr_of(name), "CX"); + cqlib_string_free(name); + operation_free(op); + + // Topological order matches source order for this chain. + assert_eq!(circuit_dag_topological_op_nodes_len(dag), 3); + let mut topo = [9u32; 3]; + assert_eq!( + circuit_dag_topological_op_nodes(dag, topo.as_mut_ptr(), 3), + 0 + ); + assert_eq!(topo, ops); + + // Predecessors of CX: H(0) plus the qubit-1 input sentinel; quantum + // neighbors filter the sentinel out. + assert_eq!(circuit_dag_predecessors_len(dag, ops[1]), 2); + let mut preds = [9u32; 2]; + assert_eq!( + circuit_dag_predecessors(dag, ops[1], preds.as_mut_ptr(), 2), + 0 + ); + assert!(preds.contains(&ops[0])); + + assert_eq!(circuit_dag_quantum_predecessors_len(dag, ops[1]), 1); + let mut qpreds = [9u32; 1]; + assert_eq!( + circuit_dag_quantum_predecessors(dag, ops[1], qpreds.as_mut_ptr(), 1), + 0 + ); + assert_eq!(qpreds[0], ops[0]); + + // Successors of CX: H(1) plus the qubit-0 output sentinel. + assert_eq!(circuit_dag_successors_len(dag, ops[1]), 2); + let mut succs = [9u32; 2]; + assert_eq!( + circuit_dag_successors(dag, ops[1], succs.as_mut_ptr(), 2), + 0 + ); + assert!(succs.contains(&ops[2])); + + // Unknown node ids and NULL handles are reported, not dereferenced. + assert_eq!(circuit_dag_is_operation(dag, 999), -2); + assert!(circuit_dag_node_kind(dag, 999).is_null()); + assert!(circuit_dag_operation(dag, 999).is_null()); + assert_eq!(circuit_dag_predecessors_len(std::ptr::null(), 0), 0); + assert_eq!( + circuit_dag_predecessors(std::ptr::null(), 0, std::ptr::null_mut(), 0), + -1 + ); + + circuit_dag_free(dag); + circuit_free(circuit); +} + +#[test] +fn dag_layers_and_depth() { + // Disjoint H(0), H(1) share the front layer; CX depends on both. + let circuit = circuit_new(2); + assert_eq!(circuit_h(circuit, 0), 0); + assert_eq!(circuit_h(circuit, 1), 0); + assert_eq!(circuit_cx(circuit, 0, 1), 0); + + let dag = circuit_dag_from_circuit(circuit); + assert!(!dag.is_null()); + assert_eq!(circuit_dag_depth(dag), 2); + assert_eq!(circuit_dag_depth(std::ptr::null()), -1); + + // Front layer holds the two disjoint roots. + assert_eq!(circuit_dag_front_layer_len(dag), 2); + let mut front = [9u32; 2]; + assert_eq!(circuit_dag_front_layer(dag, front.as_mut_ptr(), 2), 0); + assert_eq!(circuit_dag_front_layer(dag, front.as_mut_ptr(), 1), -8); + + // ASAP layers in CSR style: two nodes in layer 0, CX in layer 1. + assert_eq!(circuit_dag_layers_len(dag), 2); + let mut nodes = [9u32; 3]; + let mut offsets = [9usize; 3]; + assert_eq!( + circuit_dag_layers(dag, nodes.as_mut_ptr(), 3, offsets.as_mut_ptr(), 3), + 0 + ); + assert_eq!(offsets, [0, 2, 3]); + let layer0 = [nodes[0], nodes[1]]; + assert!(layer0.contains(&front[0]) && layer0.contains(&front[1])); + + // Per-node layer entries agree: the H nodes sit in layer 0, CX in 1. + assert_eq!(circuit_dag_node_layers_len(dag), 3); + let mut pair_nodes = [9u32; 3]; + let mut pair_layers = [9usize; 3]; + assert_eq!( + circuit_dag_node_layers(dag, pair_nodes.as_mut_ptr(), pair_layers.as_mut_ptr(), 3), + 0 + ); + let layer_of = |node: u32| -> usize { + pair_nodes + .iter() + .zip(pair_layers.iter()) + .find(|(n, _)| **n == node) + .map(|(_, l)| *l) + .unwrap() + }; + assert_eq!(layer_of(front[0]), 0); + assert_eq!(layer_of(front[1]), 0); + assert_eq!(layer_of(nodes[2]), 1); + + circuit_dag_free(dag); + circuit_free(circuit); +} + +#[test] +fn dag_wire_timelines() { + // Only qubit 0 is driven; qubit 1 stays idle. + let circuit = circuit_new(2); + assert_eq!(circuit_h(circuit, 0), 0); + + let dag = circuit_dag_from_circuit(circuit); + assert!(!dag.is_null()); + + assert_eq!(circuit_dag_is_wire_idle(dag, DAG_WIRE_QUBIT, 0), 0); + assert_eq!(circuit_dag_is_wire_idle(dag, DAG_WIRE_QUBIT, 1), 1); + // Unregistered qubits and unsupported tags are rejected. + assert_eq!(circuit_dag_is_wire_idle(dag, DAG_WIRE_QUBIT, 5), -3); + assert_eq!(circuit_dag_is_wire_idle(dag, DAG_WIRE_CLASSICAL_VAR, 0), -8); + + // The qubit-0 timeline holds exactly the H node, in wire order. + assert_eq!(circuit_dag_nodes_on_wire_len(dag, DAG_WIRE_QUBIT, 0), 1); + let mut wire_ops = [9u32; 1]; + assert_eq!( + circuit_dag_nodes_on_wire(dag, DAG_WIRE_QUBIT, 0, wire_ops.as_mut_ptr(), 1), + 0 + ); + assert_eq!( + circuit_dag_nodes_on_wire(dag, DAG_WIRE_QUBIT, 0, wire_ops.as_mut_ptr(), 0), + -8 + ); + assert_eq!(circuit_dag_nodes_on_wire_len(dag, DAG_WIRE_QUBIT, 5), 0); + + // Sentinel endpoints report their node kinds. + let input = circuit_dag_wire_in(dag, DAG_WIRE_QUBIT, 0); + let output = circuit_dag_wire_out(dag, DAG_WIRE_QUBIT, 0); + assert!(input >= 0 && output >= 0 && input != output); + assert_eq!(circuit_dag_is_operation(dag, input as u32), 0); + let kind = circuit_dag_node_kind(dag, input as u32); + assert_eq!(cstr_of(kind), "wire_in"); + cqlib_string_free(kind); + let kind = circuit_dag_node_kind(dag, output as u32); + assert_eq!(cstr_of(kind), "wire_out"); + cqlib_string_free(kind); + assert_eq!(circuit_dag_wire_in(dag, DAG_WIRE_QUBIT, 5), -3); + + circuit_dag_free(dag); + circuit_free(circuit); +} + +#[test] +fn dag_operation_counts_and_measurement_flag() { + let circuit = circuit_new(2); + assert_eq!(circuit_h(circuit, 0), 0); + assert_eq!(circuit_cx(circuit, 0, 1), 0); + assert_eq!(circuit_h(circuit, 1), 0); + assert_eq!(circuit_measure(circuit, 0), 0); + + let dag = circuit_dag_from_circuit(circuit); + assert!(!dag.is_null()); + assert_eq!(circuit_dag_has_measurement(dag), 1); + assert_eq!(circuit_dag_has_measurement(std::ptr::null()), -1); + assert_eq!(circuit_dag_has_control_flow(dag), 0); + + // Top-level counts by instruction name, in insertion order. + assert_eq!(circuit_dag_operation_count_by_name_len(dag), 3); + let mut names = [std::ptr::null_mut(); 3]; + let mut counts = [0usize; 3]; + assert_eq!( + circuit_dag_operation_count_by_name(dag, names.as_mut_ptr(), counts.as_mut_ptr(), 3), + 0 + ); + let totals: Vec<(String, usize)> = names + .iter() + .zip(counts.iter()) + .map(|(name, count)| (cstr_of(*name).to_string(), *count)) + .collect(); + for name in names { + cqlib_string_free(name); + } + assert_eq!( + totals, + vec![ + ("H".to_string(), 2), + ("CX".to_string(), 1), + ("measure_bit".to_string(), 1), + ] + ); + + // NULL and undersized output buffers. + assert_eq!( + circuit_dag_operation_count_by_name(dag, std::ptr::null_mut(), counts.as_mut_ptr(), 3), + -1 + ); + assert_eq!( + circuit_dag_operation_count_by_name(dag, names.as_mut_ptr(), counts.as_mut_ptr(), 2), + -8 + ); + assert_eq!(circuit_dag_operation_count_by_name_len(std::ptr::null()), 0); + + circuit_dag_free(dag); + circuit_free(circuit); +} + +#[test] +fn dag_mutations() { + let z_name = cstr("Z"); + let x_name = cstr("X"); + let zero = [0u32]; + let one = [1u32]; + let z_op = operation_new(z_name.as_ptr(), zero.as_ptr(), 1, std::ptr::null(), 0); + let x_op = operation_new(x_name.as_ptr(), one.as_ptr(), 1, std::ptr::null(), 0); + assert!(!z_op.is_null()); + assert!(!x_op.is_null()); + + let circuit = circuit_new(2); + assert_eq!(circuit_h(circuit, 0), 0); + assert_eq!(circuit_cx(circuit, 0, 1), 0); + + let dag = circuit_dag_from_circuit(circuit); + assert!(!dag.is_null()); + assert_eq!(circuit_dag_num_ops(dag), 2); + + // Append Z(0) at the back: it lands last in source order. + let mut appended = 9u32; + assert_eq!( + circuit_dag_apply_operation_back(dag, z_op, &mut appended), + 0 + ); + assert_eq!(circuit_dag_num_ops(dag), 3); + let mut ops = [9u32; 3]; + assert_eq!(circuit_dag_op_nodes(dag, ops.as_mut_ptr(), 3), 0); + assert_eq!(ops[2], appended); + + // Prepend X(1) at the front: it lands first in source order. + let mut prepended = 9u32; + assert_eq!( + circuit_dag_apply_operation_front(dag, x_op, &mut prepended), + 0 + ); + assert_eq!(circuit_dag_num_ops(dag), 4); + let mut ops = [9u32; 4]; + assert_eq!(circuit_dag_op_nodes(dag, ops.as_mut_ptr(), 4), 0); + assert_eq!(ops[0], prepended); + + // Remove the front node: the removed operation comes back out. + let removed = circuit_dag_remove_op_node(dag, prepended); + assert!(!removed.is_null()); + let name = operation_name(removed); + assert_eq!(cstr_of(name), "X"); + cqlib_string_free(name); + operation_free(removed); + assert_eq!(circuit_dag_num_ops(dag), 3); + + // Substitute the leading H with Z(0); the op count is unchanged and the + // node id may be reassigned by the rebuild, so re-read the position. + let mut ops = [9u32; 3]; + assert_eq!(circuit_dag_op_nodes(dag, ops.as_mut_ptr(), 3), 0); + assert_eq!(circuit_dag_substitute_node(dag, ops[0], z_op), 0); + assert_eq!(circuit_dag_num_ops(dag), 3); + let mut ops = [9u32; 3]; + assert_eq!(circuit_dag_op_nodes(dag, ops.as_mut_ptr(), 3), 0); + let op = circuit_dag_operation(dag, ops[0]); + assert!(!op.is_null()); + let name = operation_name(op); + assert_eq!(cstr_of(name), "Z"); + cqlib_string_free(name); + operation_free(op); + assert_eq!(circuit_dag_validate(dag), 0); + + // Error paths: unknown node, NULL operation, and a wire sentinel. + assert_eq!(circuit_dag_substitute_node(dag, 9999, z_op), -2); + assert_eq!( + circuit_dag_substitute_node(dag, ops[0], std::ptr::null()), + -1 + ); + let sentinel = circuit_dag_wire_in(dag, DAG_WIRE_QUBIT, 0); + assert_eq!(circuit_dag_substitute_node(dag, sentinel as u32, z_op), -2); + assert_eq!( + circuit_dag_apply_operation_back(dag, std::ptr::null(), std::ptr::null_mut()), + -1 + ); + assert!(circuit_dag_remove_op_node(dag, 9999).is_null()); + assert!(circuit_dag_remove_op_node(std::ptr::null_mut(), 0).is_null()); + + circuit_dag_free(dag); + circuit_free(circuit); + operation_free(z_op); + operation_free(x_op); +} diff --git a/crates/binding-c/tests/circuit_evolution_test.rs b/crates/binding-c/tests/circuit_evolution_test.rs new file mode 100644 index 00000000..c4276aab --- /dev/null +++ b/crates/binding-c/tests/circuit_evolution_test.rs @@ -0,0 +1,103 @@ +//! Integration tests for the Pauli evolution C ABI. + +#![allow(dead_code)] + +use binding_c::circuit::{ + circuit_free, circuit_new, circuit_num_operations, circuit_pauli_evolution, +}; +use binding_c::qis::{pauli_string_free, pauli_string_parse}; + +fn parse_pauli(text: &std::ffi::CStr) -> *mut binding_c::qis::CPauliString { + let ptr = pauli_string_parse(text.as_ptr()); + assert!(!ptr.is_null(), "pauli_string_parse should succeed"); + ptr +} + +#[test] +fn test_pauli_evolution_xz() { + let circuit = circuit_new(2); + assert!(!circuit.is_null()); + let pauli = parse_pauli(c"XZ"); + + let qubits: [u32; 2] = [0, 1]; + let before = circuit_num_operations(circuit); + assert_eq!(before, 0); + + // exp(-i * pi/4 * XZ) decomposes into basis changes, a CNOT ladder, + // one RZ, and the inverse ladder, so the operation count must grow. + assert_eq!( + circuit_pauli_evolution( + circuit, + pauli, + std::f64::consts::FRAC_PI_2, + qubits.as_ptr(), + 2 + ), + 0 + ); + let after = circuit_num_operations(circuit); + assert!( + after > before, + "expected operations to grow: {before} -> {after}" + ); + + pauli_string_free(pauli); + circuit_free(circuit); +} + +#[test] +fn test_pauli_evolution_null_arguments() { + let circuit = circuit_new(2); + assert!(!circuit.is_null()); + let pauli = parse_pauli(c"XZ"); + let qubits: [u32; 2] = [0, 1]; + + assert_eq!( + circuit_pauli_evolution(std::ptr::null_mut(), pauli, 1.0, qubits.as_ptr(), 2), + -1 + ); + assert_eq!( + circuit_pauli_evolution(circuit, std::ptr::null(), 1.0, qubits.as_ptr(), 2), + -1 + ); + // A NULL qubit buffer with a positive length is rejected. + assert_eq!( + circuit_pauli_evolution(circuit, pauli, 1.0, std::ptr::null(), 2), + -1 + ); + + pauli_string_free(pauli); + circuit_free(circuit); +} + +#[test] +fn test_pauli_evolution_error_codes() { + let circuit = circuit_new(2); + assert!(!circuit.is_null()); + + // Out-of-bounds qubit index. + let pauli = parse_pauli(c"XZ"); + let out_of_bounds: [u32; 2] = [0, 5]; + assert_eq!( + circuit_pauli_evolution(circuit, pauli, 1.0, out_of_bounds.as_ptr(), 2), + -2 + ); + + // Position count mismatch: a 3-position Pauli string on 2 positions. + let big = parse_pauli(c"XXX"); + let qubits: [u32; 2] = [0, 1]; + assert_eq!( + circuit_pauli_evolution(circuit, big, 1.0, qubits.as_ptr(), 2), + -3 + ); + + // Non-finite angle. + assert_eq!( + circuit_pauli_evolution(circuit, pauli, f64::NAN, qubits.as_ptr(), 2), + -8 + ); + + pauli_string_free(big); + pauli_string_free(pauli); + circuit_free(circuit); +} diff --git a/crates/binding-c/tests/circuit_feature_map_test.rs b/crates/binding-c/tests/circuit_feature_map_test.rs new file mode 100644 index 00000000..631c77e5 --- /dev/null +++ b/crates/binding-c/tests/circuit_feature_map_test.rs @@ -0,0 +1,888 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! Rust FFI tests for feature maps, facade constructors, entanglement +//! topology helpers, `StronglyEntanglingLayers` and the +//! `PauliEvolutionAnsatz` time-parameter naming (`circuit/ansatz.rs`). + +use binding_c::circuit::{ + ENTANGLEMENT_CIRCULAR, ENTANGLEMENT_CUSTOM, ENTANGLEMENT_FULL, ENTANGLEMENT_LINEAR, + circuit_free, circuit_num_operations, circuit_num_parameters, circuit_num_qubits, + efficient_su2, entanglement_generate_k_tuples, entanglement_generate_k_tuples_len, + entanglement_generate_pairs, entanglement_generate_pairs_len, iqp_feature_map_build_circuit, + iqp_feature_map_entanglement, iqp_feature_map_free, iqp_feature_map_new, + iqp_feature_map_num_parameters, iqp_feature_map_num_qubits, iqp_feature_map_reps, + iqp_feature_map_validate, pauli_evolution_ansatz_free, pauli_evolution_ansatz_new, + pauli_evolution_ansatz_with_time_param_name, pauli_feature_map_build, + pauli_feature_map_build_circuit, pauli_feature_map_entanglement, pauli_feature_map_free, + pauli_feature_map_new, pauli_feature_map_num_parameters, pauli_feature_map_num_qubits, + pauli_feature_map_parameter_prefix, pauli_feature_map_paulis, pauli_feature_map_reps, + pauli_feature_map_validate, real_amplitudes, strongly_entangling_layers_build_circuit, + strongly_entangling_layers_entanglement_gate, strongly_entangling_layers_free, + strongly_entangling_layers_new, strongly_entangling_layers_num_parameters, + strongly_entangling_layers_num_qubits, strongly_entangling_layers_ranges, + strongly_entangling_layers_reps, strongly_entangling_layers_validate, two_local_free, + two_local_num_parameters, two_local_num_qubits, two_local_validate, + z_feature_map_build_circuit, z_feature_map_free, z_feature_map_new, + z_feature_map_num_parameters, z_feature_map_num_qubits, z_feature_map_reps, + z_feature_map_validate, zz_feature_map_build, zz_feature_map_build_circuit, + zz_feature_map_entanglement, zz_feature_map_free, zz_feature_map_new, + zz_feature_map_num_parameters, zz_feature_map_num_qubits, zz_feature_map_reps, + zz_feature_map_validate, +}; +use binding_c::qis::{ + CHamiltonian, hamiltonian_free, hamiltonian_from_pauli, hamiltonian_new, pauli_string_parse, +}; +use std::ffi::CString; +use std::os::raw::c_char; + +fn cstr(s: &str) -> CString { + CString::new(s).unwrap() +} + +/// Builds a Hamiltonian from a single Pauli term (coefficient = 1.0). +fn pauli_hamiltonian(text: &str) -> *mut CHamiltonian { + let pauli = pauli_string_parse(cstr(text).as_ptr()); + assert!(!pauli.is_null()); + // `hamiltonian_from_pauli` takes ownership of the pauli handle. + let ham = hamiltonian_from_pauli(pauli); + assert!(!ham.is_null()); + ham +} + +#[test] +fn z_feature_map_structure() { + let map = z_feature_map_new(3); + assert!(!map.is_null()); + assert_eq!(z_feature_map_num_qubits(map), 3); + assert_eq!(z_feature_map_validate(map), 0); + + // Defaults: reps = 2, one parameter per qubit. + assert_eq!(z_feature_map_num_parameters(map), 3); + assert_eq!(z_feature_map_reps(map, 3), 0); + assert_eq!(z_feature_map_num_parameters(map), 3); + + // Each rep is one H layer plus one RZ layer: 3 reps * (3 + 3) = 18 ops. + let circuit = z_feature_map_build_circuit(map, cstr("x").as_ptr()); + assert!(!circuit.is_null()); + assert_eq!(circuit_num_qubits(circuit), 3); + assert_eq!(circuit_num_operations(circuit), 18); + assert_eq!(circuit_num_parameters(circuit), 3); + circuit_free(circuit); + + // reps = 0 is legal and yields an empty circuit. + assert_eq!(z_feature_map_reps(map, 0), 0); + assert_eq!(z_feature_map_num_parameters(map), 0); + let empty = z_feature_map_build_circuit(map, cstr("x").as_ptr()); + assert!(!empty.is_null()); + assert_eq!(circuit_num_operations(empty), 0); + circuit_free(empty); + + // Prefix validation. + assert!(z_feature_map_build_circuit(map, std::ptr::null()).is_null()); + let invalid_utf8 = CString::new(vec![0xFF, 0xFE]).unwrap(); + assert!(z_feature_map_build_circuit(map, invalid_utf8.as_ptr()).is_null()); + + // Zero-qubit maps are created but never valid. + let zero = z_feature_map_new(0); + assert!(!zero.is_null()); + assert_eq!(z_feature_map_num_qubits(zero), 0); + assert_eq!(z_feature_map_validate(zero), -3); + assert!(z_feature_map_build_circuit(zero, cstr("x").as_ptr()).is_null()); + z_feature_map_free(zero); + + // NULL-handle tolerance. + assert_eq!(z_feature_map_reps(std::ptr::null_mut(), 1), -1); + assert_eq!(z_feature_map_validate(std::ptr::null()), -1); + assert_eq!(z_feature_map_num_parameters(std::ptr::null()), 0); + assert_eq!(z_feature_map_num_qubits(std::ptr::null()), 0); + assert!(z_feature_map_build_circuit(std::ptr::null(), cstr("x").as_ptr()).is_null()); + z_feature_map_free(std::ptr::null_mut()); + + z_feature_map_free(map); +} + +#[test] +fn zz_feature_map_structure() { + let map = zz_feature_map_new(2); + assert!(!map.is_null()); + assert_eq!(zz_feature_map_num_qubits(map), 2); + assert_eq!(zz_feature_map_num_parameters(map), 2); + assert_eq!(zz_feature_map_validate(map), 0); + + // Full topology on 2 qubits: one evolution per layer. The evolution + // lowering decides the exact op count, so smoke-check the floor. + let circuit = zz_feature_map_build_circuit(map, cstr("x").as_ptr()); + assert!(!circuit.is_null()); + assert_eq!(circuit_num_qubits(circuit), 2); + // 2 first-order RZ angles plus one ZZ angle expression per edge; `pi` + // appears inside the ZZ angle expression `4 * (pi - x_i) * (pi - x_j)`. + assert_eq!(circuit_num_parameters(circuit), 3); + assert!(circuit_num_operations(circuit) >= 2 * (2 + 2 + 1)); + circuit_free(circuit); + + // Custom topology validation: self-loops, out-of-bounds and duplicate + // edges are stored by the setter but rejected by validate. + assert_eq!( + zz_feature_map_entanglement(map, ENTANGLEMENT_CUSTOM, [0u32, 0].as_ptr(), 2), + 0 + ); + assert_eq!(zz_feature_map_validate(map), -3); + assert!(zz_feature_map_build_circuit(map, cstr("x").as_ptr()).is_null()); + + assert_eq!( + zz_feature_map_entanglement(map, ENTANGLEMENT_CUSTOM, [0u32, 5].as_ptr(), 2), + 0 + ); + assert_eq!(zz_feature_map_validate(map), -3); + + assert_eq!( + zz_feature_map_entanglement(map, ENTANGLEMENT_CUSTOM, [0u32, 1, 1, 0].as_ptr(), 4), + 0 + ); + assert_eq!(zz_feature_map_validate(map), -3); + + // A valid custom pair restores validity. + assert_eq!( + zz_feature_map_entanglement(map, ENTANGLEMENT_CUSTOM, [1u32, 0].as_ptr(), 2), + 0 + ); + assert_eq!(zz_feature_map_validate(map), 0); + + // Setter argument validation. + assert_eq!( + zz_feature_map_entanglement(map, 99, std::ptr::null(), 0), + -8 + ); + assert_eq!( + zz_feature_map_entanglement(map, ENTANGLEMENT_CUSTOM, [0u32, 1].as_ptr(), 3), + -8 + ); + assert_eq!( + zz_feature_map_entanglement(map, ENTANGLEMENT_CUSTOM, std::ptr::null(), 2), + -1 + ); + assert_eq!( + zz_feature_map_entanglement( + std::ptr::null_mut(), + ENTANGLEMENT_LINEAR, + std::ptr::null(), + 0 + ), + -1 + ); + + // NULL-handle tolerance. + assert_eq!(zz_feature_map_reps(std::ptr::null_mut(), 1), -1); + assert_eq!(zz_feature_map_validate(std::ptr::null()), -1); + assert_eq!(zz_feature_map_num_parameters(std::ptr::null()), 0); + assert_eq!(zz_feature_map_num_qubits(std::ptr::null()), 0); + assert!(zz_feature_map_build_circuit(std::ptr::null(), cstr("x").as_ptr()).is_null()); + zz_feature_map_free(std::ptr::null_mut()); + + zz_feature_map_free(map); +} + +#[test] +fn iqp_feature_map_structure() { + let map = iqp_feature_map_new(3); + assert!(!map.is_null()); + assert_eq!(iqp_feature_map_num_qubits(map), 3); + assert_eq!(iqp_feature_map_num_parameters(map), 3); + assert_eq!(iqp_feature_map_validate(map), 0); + + // IQP is a named ZZ wrapper: one smoke-built circuit with reps = 1. + assert_eq!(iqp_feature_map_reps(map, 1), 0); + assert_eq!( + iqp_feature_map_entanglement(map, ENTANGLEMENT_CIRCULAR, std::ptr::null(), 0), + 0 + ); + let circuit = iqp_feature_map_build_circuit(map, cstr("x").as_ptr()); + assert!(!circuit.is_null()); + assert_eq!(circuit_num_qubits(circuit), 3); + // Interned parameter expressions: 3 first-order RZ angles plus one ZZ + // angle expression per circular edge (3 edges); `pi` appears inside the + // ZZ expressions, not as its own entry. + assert_eq!(circuit_num_parameters(circuit), 6); + // Per layer: 3 H + 3 RZ + 3 circular evolutions (reps = 1). + assert!(circuit_num_operations(circuit) >= 3 + 3 + 3); + circuit_free(circuit); + + // Topology argument validation. + assert_eq!( + iqp_feature_map_entanglement(map, 42, std::ptr::null(), 0), + -8 + ); + assert_eq!( + iqp_feature_map_entanglement(std::ptr::null_mut(), ENTANGLEMENT_FULL, std::ptr::null(), 0), + -1 + ); + + // NULL-handle tolerance. + assert_eq!(iqp_feature_map_reps(std::ptr::null_mut(), 1), -1); + assert_eq!(iqp_feature_map_validate(std::ptr::null()), -1); + assert_eq!(iqp_feature_map_num_parameters(std::ptr::null()), 0); + assert_eq!(iqp_feature_map_num_qubits(std::ptr::null()), 0); + assert!(iqp_feature_map_build_circuit(std::ptr::null(), cstr("x").as_ptr()).is_null()); + iqp_feature_map_free(std::ptr::null_mut()); + + iqp_feature_map_free(map); +} + +#[test] +fn pauli_feature_map_structure() { + let map = pauli_feature_map_new(2); + assert!(!map.is_null()); + assert_eq!(pauli_feature_map_num_qubits(map), 2); + assert_eq!(pauli_feature_map_num_parameters(map), 2); + assert_eq!(pauli_feature_map_validate(map), 0); + + // Defaults (Z + ZZ, full) build a parameterized circuit. + assert_eq!(pauli_feature_map_reps(map, 1), 0); + let circuit = pauli_feature_map_build_circuit(map, cstr("x").as_ptr()); + assert!(!circuit.is_null()); + assert_eq!(circuit_num_qubits(circuit), 2); + // 2 first-order RZ angles plus one ZZ angle expression for the full + // topology's single edge. + assert_eq!(circuit_num_parameters(circuit), 3); + assert!(circuit_num_operations(circuit) > 2 + 2); + circuit_free(circuit); + + // Custom Pauli templates. + let z = cstr("Z"); + let zz = cstr("ZZ"); + let texts = [z.as_ptr(), zz.as_ptr()]; + let labels = [z.as_ptr(), zz.as_ptr()]; + assert_eq!( + pauli_feature_map_paulis(map, texts.as_ptr(), labels.as_ptr(), 2), + 0 + ); + assert_eq!(pauli_feature_map_validate(map), 0); + + // An empty prefix falls back to the configured parameter prefix. + assert_eq!( + pauli_feature_map_parameter_prefix(map, cstr("p").as_ptr()), + 0 + ); + let prefixed = pauli_feature_map_build_circuit(map, cstr("").as_ptr()); + assert!(!prefixed.is_null()); + // 2 first-order RZ angles plus one ZZ angle expression for the full + // topology's single edge. + assert_eq!(circuit_num_parameters(prefixed), 3); + circuit_free(prefixed); + + // Pauli text / label argument validation. + let bad = cstr("NOT_A_PAULI"); + let bad_texts = [bad.as_ptr()]; + assert_eq!( + pauli_feature_map_paulis(map, bad_texts.as_ptr(), labels.as_ptr(), 1), + -8 + ); + let invalid_utf8 = CString::new(vec![0xFF]).unwrap(); + let utf8_texts = [invalid_utf8.as_ptr()]; + assert_eq!( + pauli_feature_map_paulis(map, utf8_texts.as_ptr(), labels.as_ptr(), 1), + -4 + ); + let null_texts: [*const c_char; 1] = [std::ptr::null()]; + assert_eq!( + pauli_feature_map_paulis(map, null_texts.as_ptr(), labels.as_ptr(), 1), + -1 + ); + assert_eq!( + pauli_feature_map_paulis(map, std::ptr::null(), labels.as_ptr(), 1), + -1 + ); + assert_eq!( + pauli_feature_map_paulis(map, texts.as_ptr(), std::ptr::null(), 1), + -1 + ); + assert_eq!( + pauli_feature_map_paulis(std::ptr::null_mut(), texts.as_ptr(), labels.as_ptr(), 1), + -1 + ); + // The failed setters left the valid configuration in place. + assert_eq!(pauli_feature_map_validate(map), 0); + + // A Pauli string longer than the qubit count fails validation. + let zzz = cstr("ZZZ"); + let wide_texts = [zzz.as_ptr()]; + let wide_labels = [zzz.as_ptr()]; + assert_eq!( + pauli_feature_map_paulis(map, wide_texts.as_ptr(), wide_labels.as_ptr(), 1), + 0 + ); + assert_eq!(pauli_feature_map_validate(map), -3); + assert!(pauli_feature_map_build_circuit(map, cstr("x").as_ptr()).is_null()); + + // Topology and prefix argument validation. + assert_eq!( + pauli_feature_map_entanglement(map, 7, std::ptr::null(), 0), + -8 + ); + assert_eq!( + pauli_feature_map_entanglement( + std::ptr::null_mut(), + ENTANGLEMENT_LINEAR, + std::ptr::null(), + 0 + ), + -1 + ); + assert_eq!( + pauli_feature_map_parameter_prefix(map, std::ptr::null()), + -1 + ); + assert_eq!( + pauli_feature_map_parameter_prefix(map, invalid_utf8.as_ptr()), + -4 + ); + assert_eq!( + pauli_feature_map_parameter_prefix(std::ptr::null_mut(), cstr("p").as_ptr()), + -1 + ); + + // NULL-handle tolerance. + assert_eq!(pauli_feature_map_reps(std::ptr::null_mut(), 1), -1); + assert_eq!(pauli_feature_map_validate(std::ptr::null()), -1); + assert_eq!(pauli_feature_map_num_parameters(std::ptr::null()), 0); + assert_eq!(pauli_feature_map_num_qubits(std::ptr::null()), 0); + assert!(pauli_feature_map_build_circuit(std::ptr::null(), cstr("x").as_ptr()).is_null()); + pauli_feature_map_free(std::ptr::null_mut()); + + pauli_feature_map_free(map); +} + +#[test] +fn facade_constructors() { + // EfficientSU2: [RY, RZ] + CX => (1 + 1) * 2 * 2 = 8 parameters. + let su2 = efficient_su2(2, 1, ENTANGLEMENT_FULL, std::ptr::null(), 0); + assert!(!su2.is_null()); + assert_eq!(two_local_num_qubits(su2), 2); + assert_eq!(two_local_num_parameters(su2), 8); + assert_eq!(two_local_validate(su2), 0); + two_local_free(su2); + + // Custom topology passes straight through. + let pairs = [0u32, 1, 1, 2]; + let custom = efficient_su2(3, 1, ENTANGLEMENT_CUSTOM, pairs.as_ptr(), 4); + assert!(!custom.is_null()); + assert_eq!(two_local_validate(custom), 0); + two_local_free(custom); + + // RealAmplitudes: [RY] + CX => (2 + 1) * 3 * 1 = 9 parameters. + let ra = real_amplitudes(3, 2, ENTANGLEMENT_LINEAR, std::ptr::null(), 0); + assert!(!ra.is_null()); + assert_eq!(two_local_num_parameters(ra), 9); + assert_eq!(two_local_validate(ra), 0); + two_local_free(ra); + + // Facade argument validation returns NULL. + assert!(efficient_su2(2, 1, 99, std::ptr::null(), 0).is_null()); + assert!(efficient_su2(2, 1, ENTANGLEMENT_CUSTOM, pairs.as_ptr(), 3).is_null()); + assert!(real_amplitudes(2, 1, ENTANGLEMENT_CUSTOM, std::ptr::null(), 2).is_null()); + + // ZZ feature map facade. + let zz = zz_feature_map_build(3, 2, ENTANGLEMENT_CIRCULAR, std::ptr::null(), 0); + assert!(!zz.is_null()); + assert_eq!(zz_feature_map_num_qubits(zz), 3); + assert_eq!(zz_feature_map_num_parameters(zz), 3); + assert_eq!(zz_feature_map_validate(zz), 0); + zz_feature_map_free(zz); + assert!(zz_feature_map_build(3, 2, 99, std::ptr::null(), 0).is_null()); + assert!(zz_feature_map_build(3, 1, ENTANGLEMENT_CUSTOM, std::ptr::null(), 2).is_null()); + + // Pauli feature map facade. + let z = cstr("Z"); + let zz_text = cstr("ZZ"); + let texts = [z.as_ptr(), zz_text.as_ptr()]; + let labels = [z.as_ptr(), zz_text.as_ptr()]; + let pf = pauli_feature_map_build( + 2, + 1, + texts.as_ptr(), + labels.as_ptr(), + 2, + ENTANGLEMENT_FULL, + std::ptr::null(), + 0, + ); + assert!(!pf.is_null()); + assert_eq!(pauli_feature_map_num_parameters(pf), 2); + assert_eq!(pauli_feature_map_validate(pf), 0); + pauli_feature_map_free(pf); + + // Facade failure paths return NULL. + let bad_text = cstr("NOT_A_PAULI"); + let bad_texts = [bad_text.as_ptr()]; + assert!( + pauli_feature_map_build( + 2, + 1, + bad_texts.as_ptr(), + labels.as_ptr(), + 1, + ENTANGLEMENT_FULL, + std::ptr::null(), + 0 + ) + .is_null() + ); + assert!( + pauli_feature_map_build( + 2, + 1, + std::ptr::null(), + labels.as_ptr(), + 1, + ENTANGLEMENT_FULL, + std::ptr::null(), + 0 + ) + .is_null() + ); + assert!( + pauli_feature_map_build( + 2, + 1, + texts.as_ptr(), + labels.as_ptr(), + 2, + 99, + std::ptr::null(), + 0 + ) + .is_null() + ); +} + +#[test] +fn entanglement_topology_helpers() { + let mut out = [0u32; 16]; + + // Linear: (0,1), (1,2), (2,3). + assert_eq!( + entanglement_generate_pairs_len(ENTANGLEMENT_LINEAR, std::ptr::null(), 0, 4), + 6 + ); + assert_eq!( + entanglement_generate_pairs( + ENTANGLEMENT_LINEAR, + std::ptr::null(), + 0, + 4, + out.as_mut_ptr(), + 6 + ), + 0 + ); + assert_eq!(&out[..6], &[0, 1, 1, 2, 2, 3]); + + // Circular adds the wrap-around edge (3, 0). + assert_eq!( + entanglement_generate_pairs_len(ENTANGLEMENT_CIRCULAR, std::ptr::null(), 0, 4), + 8 + ); + assert_eq!( + entanglement_generate_pairs( + ENTANGLEMENT_CIRCULAR, + std::ptr::null(), + 0, + 4, + out.as_mut_ptr(), + 8 + ), + 0 + ); + assert_eq!(&out[..8], &[0, 1, 1, 2, 2, 3, 3, 0]); + + // Full on 3 qubits: C(3,2) = 3 pairs. + assert_eq!( + entanglement_generate_pairs_len(ENTANGLEMENT_FULL, std::ptr::null(), 0, 3), + 6 + ); + assert_eq!( + entanglement_generate_pairs( + ENTANGLEMENT_FULL, + std::ptr::null(), + 0, + 3, + out.as_mut_ptr(), + 6 + ), + 0 + ); + assert_eq!(&out[..6], &[0, 1, 0, 2, 1, 2]); + + // Custom pairs are echoed verbatim; num_qubits is ignored. + let custom = [1u32, 2, 0, 2]; + assert_eq!( + entanglement_generate_pairs_len(ENTANGLEMENT_CUSTOM, custom.as_ptr(), 4, 3), + 4 + ); + assert_eq!( + entanglement_generate_pairs( + ENTANGLEMENT_CUSTOM, + custom.as_ptr(), + 4, + 3, + out.as_mut_ptr(), + 4 + ), + 0 + ); + assert_eq!(&out[..4], &[1, 2, 0, 2]); + + // Pair argument validation. + assert_eq!( + entanglement_generate_pairs( + ENTANGLEMENT_LINEAR, + std::ptr::null(), + 0, + 4, + out.as_mut_ptr(), + 5 + ), + -8 + ); + assert_eq!( + entanglement_generate_pairs( + ENTANGLEMENT_LINEAR, + std::ptr::null(), + 0, + 2, + std::ptr::null_mut(), + 2 + ), + -1 + ); + assert_eq!( + entanglement_generate_pairs(99, std::ptr::null(), 0, 4, out.as_mut_ptr(), 6), + -8 + ); + assert_eq!( + entanglement_generate_pairs_len(99, std::ptr::null(), 0, 4), + 0 + ); + assert_eq!( + entanglement_generate_pairs_len(ENTANGLEMENT_CUSTOM, custom.as_ptr(), 3, 3), + 0 + ); + assert_eq!( + entanglement_generate_pairs( + ENTANGLEMENT_CUSTOM, + custom.as_ptr(), + 3, + 3, + out.as_mut_ptr(), + 4 + ), + -8 + ); + assert_eq!( + entanglement_generate_pairs( + ENTANGLEMENT_CUSTOM, + std::ptr::null(), + 2, + 3, + out.as_mut_ptr(), + 2 + ), + -1 + ); + + // k-tuples, full topology: C(4,3) = 4 lexicographic triples. + assert_eq!( + entanglement_generate_k_tuples_len(ENTANGLEMENT_FULL, std::ptr::null(), 0, 3, 4), + 12 + ); + assert_eq!( + entanglement_generate_k_tuples( + ENTANGLEMENT_FULL, + std::ptr::null(), + 0, + 3, + 4, + out.as_mut_ptr(), + 12 + ), + 0 + ); + assert_eq!(&out[..12], &[0, 1, 2, 0, 1, 3, 0, 2, 3, 1, 2, 3]); + + // Linear k-tuples are consecutive windows. + assert_eq!( + entanglement_generate_k_tuples_len(ENTANGLEMENT_LINEAR, std::ptr::null(), 0, 3, 4), + 6 + ); + assert_eq!( + entanglement_generate_k_tuples( + ENTANGLEMENT_LINEAR, + std::ptr::null(), + 0, + 3, + 4, + out.as_mut_ptr(), + 6 + ), + 0 + ); + assert_eq!(&out[..6], &[0, 1, 2, 1, 2, 3]); + + // Circular k-tuples are rotational windows (mod n). + assert_eq!( + entanglement_generate_k_tuples_len(ENTANGLEMENT_CIRCULAR, std::ptr::null(), 0, 3, 4), + 12 + ); + assert_eq!( + entanglement_generate_k_tuples( + ENTANGLEMENT_CIRCULAR, + std::ptr::null(), + 0, + 3, + 4, + out.as_mut_ptr(), + 12 + ), + 0 + ); + assert_eq!(&out[..12], &[0, 1, 2, 1, 2, 3, 2, 3, 0, 3, 0, 1]); + + // Custom topologies only define pairs: k > 2 falls back to full. + assert_eq!( + entanglement_generate_k_tuples_len(ENTANGLEMENT_CUSTOM, custom.as_ptr(), 4, 3, 4), + 12 + ); + + // k = 1 yields each qubit on its own. + assert_eq!( + entanglement_generate_k_tuples_len(ENTANGLEMENT_LINEAR, std::ptr::null(), 0, 1, 3), + 3 + ); + assert_eq!( + entanglement_generate_k_tuples( + ENTANGLEMENT_LINEAR, + std::ptr::null(), + 0, + 1, + 3, + out.as_mut_ptr(), + 3 + ), + 0 + ); + assert_eq!(&out[..3], &[0, 1, 2]); + + // Degenerate and invalid inputs. + assert_eq!( + entanglement_generate_k_tuples_len(ENTANGLEMENT_FULL, std::ptr::null(), 0, 0, 3), + 0 + ); + assert_eq!( + entanglement_generate_k_tuples_len(ENTANGLEMENT_FULL, std::ptr::null(), 0, 4, 3), + 0 + ); + assert_eq!( + entanglement_generate_k_tuples_len(ENTANGLEMENT_FULL, std::ptr::null(), 0, 3, 0), + 0 + ); + assert_eq!( + entanglement_generate_k_tuples( + ENTANGLEMENT_FULL, + std::ptr::null(), + 0, + 3, + 4, + out.as_mut_ptr(), + 11 + ), + -8 + ); + assert_eq!( + entanglement_generate_k_tuples(99, std::ptr::null(), 0, 3, 4, out.as_mut_ptr(), 12), + -8 + ); + assert_eq!( + entanglement_generate_k_tuples_len(99, std::ptr::null(), 0, 3, 4), + 0 + ); +} + +#[test] +fn strongly_entangling_layers_structure() { + let layers = strongly_entangling_layers_new(3); + assert!(!layers.is_null()); + assert_eq!(strongly_entangling_layers_num_qubits(layers), 3); + assert_eq!(strongly_entangling_layers_num_parameters(layers), 9); + assert_eq!(strongly_entangling_layers_validate(layers), 0); + + // reps = 2 => 2 * 3 * 3 = 18 parameters. + assert_eq!(strongly_entangling_layers_reps(layers, 2), 0); + assert_eq!(strongly_entangling_layers_num_parameters(layers), 18); + + // Explicit ranges are reused cyclically across layers. + let ranges = [1usize, 2]; + assert_eq!( + strongly_entangling_layers_ranges(layers, ranges.as_ptr(), 2), + 0 + ); + assert_eq!(strongly_entangling_layers_validate(layers), 0); + + // Entangler selection. + let cz = cstr("CZ"); + let bad_name = cstr("NOT_A_GATE"); + let invalid_utf8 = CString::new(vec![0xFF]).unwrap(); + assert_eq!( + strongly_entangling_layers_entanglement_gate(layers, cz.as_ptr()), + 0 + ); + assert_eq!( + strongly_entangling_layers_entanglement_gate(layers, bad_name.as_ptr()), + -8 + ); + assert_eq!( + strongly_entangling_layers_entanglement_gate(layers, invalid_utf8.as_ptr()), + -4 + ); + assert_eq!( + strongly_entangling_layers_entanglement_gate(std::ptr::null_mut(), cz.as_ptr()), + -1 + ); + assert_eq!(strongly_entangling_layers_validate(layers), 0); + + // Per layer: 3 U gates + 3 entanglers => 12 operations, 18 parameters. + let circuit = strongly_entangling_layers_build_circuit(layers, cstr("t").as_ptr()); + assert!(!circuit.is_null()); + assert_eq!(circuit_num_qubits(circuit), 3); + assert_eq!(circuit_num_operations(circuit), 12); + assert_eq!(circuit_num_parameters(circuit), 18); + circuit_free(circuit); + + // Range validation: zero and out-of-range distances fail. + let zero_range = [0usize]; + assert_eq!( + strongly_entangling_layers_ranges(layers, zero_range.as_ptr(), 1), + 0 + ); + assert_eq!(strongly_entangling_layers_validate(layers), -3); + assert!(strongly_entangling_layers_build_circuit(layers, cstr("t").as_ptr()).is_null()); + + let big_range = [3usize]; + assert_eq!( + strongly_entangling_layers_ranges(layers, big_range.as_ptr(), 1), + 0 + ); + assert_eq!(strongly_entangling_layers_validate(layers), -3); + + // An empty explicit list also fails validation until replaced. + assert_eq!( + strongly_entangling_layers_ranges(layers, std::ptr::null(), 0), + 0 + ); + assert_eq!(strongly_entangling_layers_validate(layers), -3); + assert_eq!( + strongly_entangling_layers_ranges(layers, ranges.as_ptr(), 2), + 0 + ); + assert_eq!(strongly_entangling_layers_validate(layers), 0); + + // ranges NULL array with entries is rejected. + assert_eq!( + strongly_entangling_layers_ranges(layers, std::ptr::null(), 1), + -1 + ); + assert_eq!( + strongly_entangling_layers_ranges(std::ptr::null_mut(), ranges.as_ptr(), 2), + -1 + ); + + // Prefix validation. + assert!(strongly_entangling_layers_build_circuit(layers, std::ptr::null()).is_null()); + assert!(strongly_entangling_layers_build_circuit(layers, invalid_utf8.as_ptr()).is_null()); + + // A single qubit has no entanglement: only the U gate remains. + let solo = strongly_entangling_layers_new(1); + assert!(!solo.is_null()); + assert_eq!(strongly_entangling_layers_num_parameters(solo), 3); + assert_eq!(strongly_entangling_layers_validate(solo), 0); + let solo_circuit = strongly_entangling_layers_build_circuit(solo, cstr("t").as_ptr()); + assert!(!solo_circuit.is_null()); + assert_eq!(circuit_num_operations(solo_circuit), 1); + assert_eq!(circuit_num_parameters(solo_circuit), 3); + circuit_free(solo_circuit); + strongly_entangling_layers_free(solo); + + // Zero qubits are never valid. + let zero = strongly_entangling_layers_new(0); + assert!(!zero.is_null()); + assert_eq!(strongly_entangling_layers_validate(zero), -3); + strongly_entangling_layers_free(zero); + + // NULL-handle tolerance. + assert_eq!(strongly_entangling_layers_reps(std::ptr::null_mut(), 1), -1); + assert_eq!(strongly_entangling_layers_validate(std::ptr::null()), -1); + assert_eq!( + strongly_entangling_layers_num_parameters(std::ptr::null()), + 0 + ); + assert_eq!(strongly_entangling_layers_num_qubits(std::ptr::null()), 0); + assert!( + strongly_entangling_layers_build_circuit(std::ptr::null(), cstr("t").as_ptr()).is_null() + ); + strongly_entangling_layers_free(std::ptr::null_mut()); + + strongly_entangling_layers_free(layers); +} + +#[test] +fn pauli_evolution_ansatz_time_param_name() { + // Non-commuting Hamiltonian H = X + Z on one qubit. + let ham = pauli_hamiltonian("X"); + let ansatz = pauli_evolution_ansatz_new(ham); + assert!(!ansatz.is_null()); + + // Overrides and reset behave per the core semantics. + assert_eq!( + pauli_evolution_ansatz_with_time_param_name(ansatz, cstr("tau").as_ptr()), + 0 + ); + assert_eq!( + pauli_evolution_ansatz_with_time_param_name(ansatz, cstr("").as_ptr()), + 0 + ); + + // Argument validation. + assert_eq!( + pauli_evolution_ansatz_with_time_param_name(ansatz, std::ptr::null()), + -1 + ); + let invalid_utf8 = CString::new(vec![0xFF]).unwrap(); + assert_eq!( + pauli_evolution_ansatz_with_time_param_name(ansatz, invalid_utf8.as_ptr()), + -4 + ); + assert_eq!( + pauli_evolution_ansatz_with_time_param_name(std::ptr::null_mut(), cstr("tau").as_ptr()), + -1 + ); + + pauli_evolution_ansatz_free(ansatz); + hamiltonian_free(ham); + + // Constructor rejects NULL and invalid Hamiltonians. + assert!(pauli_evolution_ansatz_new(std::ptr::null()).is_null()); + let empty = hamiltonian_new(2); + assert!(pauli_evolution_ansatz_new(empty).is_null()); + hamiltonian_free(empty); + + pauli_evolution_ansatz_free(std::ptr::null_mut()); +} diff --git a/crates/binding-c/tests/circuit_missing_test.rs b/crates/binding-c/tests/circuit_missing_test.rs new file mode 100644 index 00000000..de749f0c --- /dev/null +++ b/crates/binding-c/tests/circuit_missing_test.rs @@ -0,0 +1,568 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! Rust FFI tests for the previously missing circuit FFI surface: +//! operation kind discriminants, CFG block editing, structural +//! construction/queries, and checkpoint transactions. + +use binding_c::circuit::{ + CCircuitCFG, CClassicalValueInfo, COperation, CQLIB_CLASSICAL_TYPE_BIT, CValueOperation, + OPERATION_DIRECTIVE_BARRIER, checkpoint_free, circuit_append_value_operation, circuit_barrier, + circuit_begin, circuit_cfg_add_block, circuit_cfg_block_label, circuit_cfg_block_operations, + circuit_cfg_block_operations_len, circuit_cfg_extend_operations, circuit_cfg_free, + circuit_cfg_from_circuit, circuit_cfg_new, circuit_cfg_with_label, circuit_checkpoint, + circuit_commit, circuit_contains_qubit, circuit_delay, circuit_free, circuit_from_operations, + circuit_from_qubits, circuit_h, circuit_has_same_qubits, circuit_index, circuit_measure_into, + circuit_new, circuit_num_operations, circuit_num_qubits, circuit_operations_structurally_equal, + circuit_parameters_len, circuit_rollback_control_body_transaction, circuit_rollback_to, + circuit_rx, circuit_store, circuit_var, classical_expr_bit_literal, classical_expr_free, + classical_var_free, operation_as_instruction, operation_directive, operation_free, + operation_gate_arity, operation_is_circuit_gate, operation_is_classical_control, + operation_is_classical_data, operation_is_delay, operation_is_directive, + operation_is_instruction, operation_is_mcgate, operation_is_quantum_gate, + operation_is_standard_gate, operation_is_unitary, operation_name, operation_new, + operation_reads_value, operation_result, value_operation_free, +}; +use binding_c::cqlib_string_free; +use std::ffi::{CStr, CString}; + +fn cstr(s: &str) -> CString { + CString::new(s).unwrap() +} + +/// Builds a fixed-parameter standard-gate operation handle. +fn op_new(name: &str, qubits: &[u32], params: &[f64]) -> *mut COperation { + let gate = cstr(name); + operation_new( + gate.as_ptr(), + qubits.as_ptr(), + qubits.len(), + params.as_ptr(), + params.len(), + ) +} + +/// Copies `count` operation handles out of the entry block of `cfg`. +fn block_ops(cfg: *const CCircuitCFG, node: u32, count: usize) -> Vec<*mut COperation> { + let mut ops: Vec<*mut COperation> = vec![std::ptr::null_mut(); count]; + assert_eq!( + circuit_cfg_block_operations(cfg, node, ops.as_mut_ptr(), count), + 0 + ); + assert!(ops.iter().all(|op| !op.is_null())); + ops +} + +fn op_name(op: *const COperation) -> String { + let name = operation_name(op); + assert!(!name.is_null()); + let text = unsafe { CStr::from_ptr(name) } + .to_str() + .unwrap() + .to_string(); + cqlib_string_free(name); + text +} + +#[test] +fn operation_kind_predicates_for_standard_gate() { + let op = op_new("H", &[0], &[]); + + // An H gate is a standard gate and a quantum gate, nothing else. + assert_eq!(operation_is_standard_gate(op), 1); + assert_eq!(operation_is_quantum_gate(op), 1); + assert_eq!(operation_is_instruction(op), 1); + assert_eq!(operation_is_unitary(op), 0); + assert_eq!(operation_is_mcgate(op), 0); + assert_eq!(operation_is_circuit_gate(op), 0); + assert_eq!(operation_is_directive(op), 0); + assert_eq!(operation_is_delay(op), 0); + assert_eq!(operation_is_classical_control(op), 0); + assert_eq!(operation_is_classical_data(op), 0); + + // NULL input is reported as -1 by every predicate. + let null = std::ptr::null::(); + assert_eq!(operation_is_standard_gate(null), -1); + assert_eq!(operation_is_quantum_gate(null), -1); + assert_eq!(operation_is_instruction(null), -1); + assert_eq!(operation_is_unitary(null), -1); + assert_eq!(operation_is_mcgate(null), -1); + assert_eq!(operation_is_circuit_gate(null), -1); + assert_eq!(operation_is_directive(null), -1); + assert_eq!(operation_is_delay(null), -1); + assert_eq!(operation_is_classical_control(null), -1); + assert_eq!(operation_is_classical_data(null), -1); + + operation_free(op); +} + +#[test] +fn operation_gate_arity_reports_fixed_arity() { + let mut qubits: usize = 9; + let mut params: usize = 9; + + let h = op_new("H", &[0], &[]); + assert_eq!(operation_gate_arity(h, &mut qubits, &mut params), 0); + assert_eq!((qubits, params), (1, 0)); + + let rx = op_new("RX", &[0], &[0.5]); + assert_eq!(operation_gate_arity(rx, &mut qubits, &mut params), 0); + assert_eq!((qubits, params), (1, 1)); + + let cx = op_new("CX", &[0, 1], &[]); + assert_eq!(operation_gate_arity(cx, &mut qubits, &mut params), 0); + assert_eq!((qubits, params), (2, 0)); + + // NULL inputs, and barriers whose arity is variable, are rejected. + assert_eq!( + operation_gate_arity(std::ptr::null(), &mut qubits, &mut params), + -1 + ); + assert_eq!( + operation_gate_arity(h, std::ptr::null_mut(), &mut params), + -1 + ); + operation_free(h); + operation_free(rx); + operation_free(cx); +} + +#[test] +fn operation_as_instruction_clones_plain_operation() { + let op = op_new("H", &[0], &[]); + let cloned = operation_as_instruction(op); + assert!(!cloned.is_null()); + assert_eq!(op_name(cloned), "H"); + operation_free(cloned); + operation_free(op); + + assert!(operation_as_instruction(std::ptr::null()).is_null()); +} + +#[test] +fn operation_directive_and_delay_flags_via_cfg() { + let circuit = circuit_new(2); + let var = circuit_var(circuit, CQLIB_CLASSICAL_TYPE_BIT, 1); + assert!(!var.is_null()); + assert_eq!(circuit_barrier(circuit, std::ptr::null(), 0), 0); + assert_eq!(circuit_delay(circuit, 0, 1.5), 0); + let meas = circuit_measure_into(circuit, 1, var); + assert!(!meas.is_null()); + classical_expr_free(meas); + + let cfg = circuit_cfg_from_circuit(circuit); + assert!(!cfg.is_null()); + // Storage order: [barrier, delay, MeasureBit, Store]; measure_into + // appends both the measurement and its store. + assert_eq!(circuit_cfg_block_operations_len(cfg, 0), 4); + let ops = block_ops(cfg, 0, 4); + + // Barrier: directive, non-gate, variable arity. + assert_eq!(operation_is_directive(ops[0]), 1); + assert_eq!( + operation_directive(ops[0]), + OPERATION_DIRECTIVE_BARRIER as i32 + ); + assert_eq!(operation_is_quantum_gate(ops[0]), 0); + assert_eq!(operation_is_instruction(ops[0]), 1); + let mut qubits: usize = 0; + let mut params: usize = 0; + assert_eq!(operation_gate_arity(ops[0], &mut qubits, &mut params), -3); + + // Delay: delay instruction with fixed (1, 1) arity, not a gate. + assert_eq!(operation_is_delay(ops[1]), 1); + assert_eq!(operation_is_quantum_gate(ops[1]), 0); + assert_eq!(operation_directive(ops[1]), 0); + assert_eq!(operation_gate_arity(ops[1], &mut qubits, &mut params), 0); + assert_eq!((qubits, params), (1, 1)); + + // MeasureBit: classical data with fixed (1, 0) arity. + assert_eq!(operation_is_classical_data(ops[2]), 1); + assert_eq!(operation_is_directive(ops[2]), 0); + assert_eq!(operation_is_quantum_gate(ops[2]), 0); + assert_eq!(operation_gate_arity(ops[2], &mut qubits, &mut params), 0); + assert_eq!((qubits, params), (1, 0)); + + for op in &ops { + operation_free(*op); + } + circuit_cfg_free(cfg); + classical_var_free(var); + circuit_free(circuit); +} + +#[test] +fn operation_result_reports_measurement_value() { + let circuit = circuit_new(1); + let var = circuit_var(circuit, CQLIB_CLASSICAL_TYPE_BIT, 1); + let meas = circuit_measure_into(circuit, 0, var); + assert!(!meas.is_null()); + classical_expr_free(meas); + + let cfg = circuit_cfg_from_circuit(circuit); + // Storage order: [MeasureBit, Store]; measure_into appends both. + assert_eq!(circuit_cfg_block_operations_len(cfg, 0), 2); + let ops = block_ops(cfg, 0, 2); + let meas_op = ops[0]; + + let mut info = CClassicalValueInfo { + index: u32::MAX, + tag: u32::MAX, + width: u32::MAX, + }; + assert_eq!(operation_result(meas_op, &mut info), 0); + assert_eq!(info.index, 0); + assert_eq!(info.tag, CQLIB_CLASSICAL_TYPE_BIT); + assert_eq!(info.width, 1); + + // Non-measurement operations and NULL inputs are rejected. + let h = op_new("H", &[0], &[]); + assert_eq!(operation_result(h, &mut info), -3); + assert_eq!(operation_result(std::ptr::null(), &mut info), -1); + assert_eq!(operation_result(meas_op, std::ptr::null_mut()), -1); + + operation_free(h); + for op in &ops { + operation_free(*op); + } + circuit_cfg_free(cfg); + classical_var_free(var); + circuit_free(circuit); +} + +#[test] +fn operation_reads_value_matches_store_expression() { + let circuit = circuit_new(1); + let var = circuit_var(circuit, CQLIB_CLASSICAL_TYPE_BIT, 1); + let meas = circuit_measure_into(circuit, 0, var); + assert!(!meas.is_null()); + let target = circuit_var(circuit, CQLIB_CLASSICAL_TYPE_BIT, 1); + assert_eq!(circuit_store(circuit, target, meas), 0); + + // Storage order: [MeasureBit, Store(measure_into), Store(explicit)]. + let cfg = circuit_cfg_from_circuit(circuit); + assert_eq!(circuit_cfg_block_operations_len(cfg, 0), 3); + let ops = block_ops(cfg, 0, 3); + assert_eq!(op_name(ops[0]), "measure_bit"); + assert_eq!(op_name(ops[1]), "store"); + assert_eq!(op_name(ops[2]), "store"); + let meas_op = ops[0]; + let store_op = ops[1]; + + // The store operations recursively read the measured value; the + // measurement itself does not. + assert_eq!(operation_reads_value(store_op, meas), 1); + assert_eq!(operation_reads_value(ops[2], meas), 1); + assert_eq!(operation_reads_value(meas_op, meas), 0); + + // Non-value expressions and NULL inputs are rejected. + let literal = classical_expr_bit_literal(true); + assert_eq!(operation_reads_value(store_op, literal), -8); + assert_eq!(operation_reads_value(std::ptr::null(), meas), -1); + assert_eq!(operation_reads_value(store_op, std::ptr::null()), -1); + + classical_expr_free(literal); + classical_expr_free(meas); + for op in &ops { + operation_free(*op); + } + circuit_cfg_free(cfg); + classical_var_free(var); + classical_var_free(target); + circuit_free(circuit); +} + +#[test] +fn circuit_cfg_extend_operations_appends_batch() { + let cfg = circuit_cfg_new(2); + let node = circuit_cfg_add_block(cfg); + assert_ne!(node, u32::MAX); + + let h = op_new("H", &[0], &[]); + let cx = op_new("CX", &[0, 1], &[]); + let batch: [*const COperation; 2] = [h as *const _, cx as *const _]; + assert_eq!( + circuit_cfg_extend_operations(cfg, node, batch.as_ptr(), 2), + 0 + ); + + // The two-step pattern agrees with the batch length. + assert_eq!(circuit_cfg_block_operations_len(cfg, node), 2); + let ops = block_ops(cfg, node, 2); + assert_eq!(op_name(ops[0]), "H"); + assert_eq!(op_name(ops[1]), "CX"); + for op in &ops { + operation_free(*op); + } + + // Error paths: NULL cfg, sentinel/unknown node, NULL arrays and entries. + assert_eq!( + circuit_cfg_extend_operations(std::ptr::null_mut(), node, batch.as_ptr(), 2), + -1 + ); + assert_eq!( + circuit_cfg_extend_operations(cfg, u32::MAX, batch.as_ptr(), 2), + -2 + ); + assert_eq!( + circuit_cfg_extend_operations(cfg, 99, batch.as_ptr(), 2), + -2 + ); + assert_eq!( + circuit_cfg_extend_operations(cfg, node, std::ptr::null(), 2), + -1 + ); + let with_null: [*const COperation; 2] = [h as *const _, std::ptr::null()]; + assert_eq!( + circuit_cfg_extend_operations(cfg, node, with_null.as_ptr(), 2), + -1 + ); + + operation_free(h); + operation_free(cx); + circuit_cfg_free(cfg); +} + +#[test] +fn circuit_cfg_with_label_sets_and_replaces() { + let cfg = circuit_cfg_new(1); + let node = circuit_cfg_add_block(cfg); + + assert_eq!( + circuit_cfg_with_label(cfg, node, cstr("loop-body").as_ptr()), + 0 + ); + let label = circuit_cfg_block_label(cfg, node); + assert!(!label.is_null()); + assert_eq!( + unsafe { CStr::from_ptr(label) }.to_str().unwrap(), + "loop-body" + ); + cqlib_string_free(label); + + // Setting again replaces the previous label. + assert_eq!(circuit_cfg_with_label(cfg, node, cstr("exit").as_ptr()), 0); + let label = circuit_cfg_block_label(cfg, node); + assert_eq!(unsafe { CStr::from_ptr(label) }.to_str().unwrap(), "exit"); + cqlib_string_free(label); + + // Error paths: NULL inputs, unknown block, invalid UTF-8. + assert_eq!( + circuit_cfg_with_label(std::ptr::null_mut(), node, cstr("x").as_ptr()), + -1 + ); + assert_eq!(circuit_cfg_with_label(cfg, node, std::ptr::null()), -1); + assert_eq!( + circuit_cfg_with_label(cfg, u32::MAX, cstr("x").as_ptr()), + -2 + ); + let invalid = CString::new(vec![0xffu8, 0xfe]).unwrap(); + assert_eq!(circuit_cfg_with_label(cfg, node, invalid.as_ptr()), -4); + + circuit_cfg_free(cfg); +} + +#[test] +fn circuit_from_operations_rebuilds_structurally_equal_circuit() { + let source = circuit_new(2); + assert_eq!(circuit_h(source, 0), 0); + assert_eq!(circuit_rx(source, 1, 0.25), 0); + + let op0 = circuit_index(source, 0); + let op1 = circuit_index(source, 1); + assert!(!op0.is_null() && !op1.is_null()); + let ops = [op0, op1]; + let ops: Vec<*const CValueOperation> = ops.into_iter().map(|p| p as *const _).collect(); + let qubits = [0u32, 1]; + + let rebuilt = circuit_from_operations(qubits.as_ptr(), 2, ops.as_ptr(), 2); + assert!(!rebuilt.is_null()); + assert_eq!(circuit_num_qubits(rebuilt), 2); + assert_eq!(circuit_num_operations(rebuilt), 2); + assert_eq!(circuit_operations_structurally_equal(source, rebuilt), 1); + assert_eq!(circuit_has_same_qubits(source, rebuilt), 1); + assert_eq!(circuit_contains_qubit(rebuilt, 1), 1); + assert_eq!(circuit_contains_qubit(rebuilt, 9), 0); + + // Error paths: NULL arrays, NULL entries, duplicate qubits. + assert!(circuit_from_operations(qubits.as_ptr(), 2, std::ptr::null(), 2).is_null()); + let with_null: [*const CValueOperation; 2] = [op0 as *const _, std::ptr::null()]; + assert!(circuit_from_operations(qubits.as_ptr(), 2, with_null.as_ptr(), 2).is_null()); + let dup = [0u32, 0]; + assert!(circuit_from_operations(dup.as_ptr(), 2, std::ptr::null(), 0).is_null()); + + // An empty construction (no qubits, no operations) is valid. + let empty = circuit_from_operations(std::ptr::null(), 0, std::ptr::null(), 0); + assert!(!empty.is_null()); + assert_eq!(circuit_num_qubits(empty), 0); + assert_eq!(circuit_num_operations(empty), 0); + circuit_free(empty); + + // NULL inputs to the query helpers. + assert_eq!( + circuit_operations_structurally_equal(std::ptr::null(), rebuilt), + -1 + ); + assert_eq!( + circuit_operations_structurally_equal(source, std::ptr::null()), + -1 + ); + assert_eq!(circuit_has_same_qubits(std::ptr::null(), rebuilt), -1); + assert_eq!(circuit_contains_qubit(std::ptr::null(), 0), -1); + + value_operation_free(op0); + value_operation_free(op1); + circuit_free(rebuilt); + circuit_free(source); +} + +#[test] +fn circuit_append_value_operation_appends_snapshot() { + let source = circuit_new(2); + assert_eq!(circuit_h(source, 1), 0); + let op = circuit_index(source, 0); + assert!(!op.is_null()); + + let target = circuit_new(2); + assert_eq!(circuit_append_value_operation(target, op), 0); + assert_eq!(circuit_num_operations(target), 1); + assert_eq!(circuit_operations_structurally_equal(source, target), 1); + + // Appending to a circuit that misses the operation's qubit fails. + let narrow = circuit_new(1); + assert_eq!(circuit_append_value_operation(narrow, op), -3); + assert_eq!(circuit_num_operations(narrow), 0); + + assert_eq!(circuit_append_value_operation(std::ptr::null_mut(), op), -1); + assert_eq!(circuit_append_value_operation(target, std::ptr::null()), -1); + + value_operation_free(op); + circuit_free(narrow); + circuit_free(target); + circuit_free(source); +} + +#[test] +fn circuit_qubit_domain_queries() { + let qubits = [3u32, 7]; + let a = circuit_from_qubits(qubits.as_ptr(), 2); + assert!(!a.is_null()); + let b = circuit_from_qubits(qubits.as_ptr(), 2); + let other = circuit_new(2); + + assert_eq!(circuit_contains_qubit(a, 3), 1); + assert_eq!(circuit_contains_qubit(a, 7), 1); + assert_eq!(circuit_contains_qubit(a, 0), 0); + assert_eq!(circuit_has_same_qubits(a, b), 1); + assert_eq!(circuit_has_same_qubits(a, other), 0); + + circuit_free(other); + circuit_free(b); + circuit_free(a); +} + +#[test] +fn circuit_checkpoint_commit_keeps_operations() { + let circuit = circuit_new(2); + + let checkpoint = circuit_checkpoint(circuit); + assert!(!checkpoint.is_null()); + assert_eq!(circuit_h(circuit, 0), 0); + assert_eq!(circuit_commit(circuit, checkpoint), 0); + assert_eq!(circuit_num_operations(circuit), 1); + + // NULL handling: construction fails, and commit rejects NULL without + // consuming the token. + assert!(circuit_checkpoint(std::ptr::null()).is_null()); + assert_eq!(circuit_commit(circuit, std::ptr::null_mut()), -1); + let spare = circuit_new(1); + let checkpoint = circuit_checkpoint(spare); + assert_eq!(circuit_commit(std::ptr::null_mut(), checkpoint), -1); + checkpoint_free(checkpoint); + + // An unused token can always be released. + let checkpoint = circuit_checkpoint(spare); + checkpoint_free(checkpoint); + checkpoint_free(std::ptr::null_mut()); + + circuit_free(spare); + circuit_free(circuit); +} + +#[test] +fn circuit_rollback_to_restores_state() { + let circuit = circuit_new(2); + + let checkpoint = circuit_begin(circuit); + assert!(!checkpoint.is_null()); + assert_eq!(circuit_h(circuit, 0), 0); + assert_eq!(circuit_rx(circuit, 0, 0.5), 0); + assert_eq!(circuit_num_operations(circuit), 2); + assert_eq!(circuit_rollback_to(circuit, checkpoint), 0); + assert_eq!(circuit_num_operations(circuit), 0); + assert_eq!(circuit_parameters_len(circuit), 0); + + // The explicit control-body transaction rollback consumes its token too. + let other = circuit_new(1); + let checkpoint = circuit_checkpoint(other); + assert_eq!(circuit_h(other, 0), 0); + assert_eq!( + circuit_rollback_control_body_transaction(other, checkpoint), + 0 + ); + assert_eq!(circuit_num_operations(other), 0); + + // NULL inputs are rejected without consuming anything. + assert_eq!( + circuit_rollback_to(std::ptr::null_mut(), std::ptr::null_mut()), + -1 + ); + let checkpoint = circuit_checkpoint(other); + assert_eq!(circuit_rollback_to(other, std::ptr::null_mut()), -1); + checkpoint_free(checkpoint); + + circuit_free(other); + circuit_free(circuit); +} + +#[test] +fn cfg_from_structurally_rebuilt_circuit_roundtrips() { + // End-to-end: rebuild a circuit from snapshots and confirm the CFG view + // still expands, including a label set through circuit_cfg_with_label. + let source = circuit_new(2); + assert_eq!(circuit_h(source, 0), 0); + let op = circuit_index(source, 0); + let ops = [op]; + let ops: Vec<*const CValueOperation> = ops.into_iter().map(|p| p as *const _).collect(); + let qubits = [0u32, 1]; + let rebuilt = circuit_from_operations(qubits.as_ptr(), 2, ops.as_ptr(), 1); + assert!(!rebuilt.is_null()); + + let cfg = circuit_cfg_from_circuit(rebuilt); + assert!(!cfg.is_null()); + assert_eq!(circuit_cfg_block_operations_len(cfg, 0), 1); + assert_eq!( + circuit_cfg_with_label(cfg, 0, cstr("rebuilt-entry").as_ptr()), + 0 + ); + let label = circuit_cfg_block_label(cfg, 0); + assert_eq!( + unsafe { CStr::from_ptr(label) }.to_str().unwrap(), + "rebuilt-entry" + ); + cqlib_string_free(label); + + circuit_cfg_free(cfg); + circuit_free(rebuilt); + value_operation_free(op); + circuit_free(source); +} diff --git a/crates/binding-c/tests/circuit_operations_test.rs b/crates/binding-c/tests/circuit_operations_test.rs new file mode 100644 index 00000000..cf33ce38 --- /dev/null +++ b/crates/binding-c/tests/circuit_operations_test.rs @@ -0,0 +1,232 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! Rust FFI tests for standalone circuit-operation handles +//! (`circuit/operations.rs`). + +use binding_c::circuit::{ + COperation, OPERATION_PARAM_FIXED, OPERATION_PARAM_INDEX, operation_free, + operation_is_standard_gate, operation_label, operation_matrix, operation_matrix_dims, + operation_matrix_len, operation_name, operation_new, operation_num_qubits, operation_params, + operation_params_len, operation_qubits, operation_qubits_len, +}; +use binding_c::cqlib_string_free; +use cqlib_core::circuit::gate::instruction::Instruction; +use cqlib_core::circuit::gate::standard_gate::StandardGate; +use cqlib_core::circuit::{CircuitParam, Operation, Qubit}; +use std::ffi::{CStr, CString}; + +fn cstr(s: &str) -> CString { + CString::new(s).unwrap() +} + +#[test] +fn fixed_param_operation_roundtrip() { + // RX(0.5) acting on qubit 0. + let name = cstr("RX"); + let qubits = [0u32]; + let params = [0.5f64]; + let op = operation_new(name.as_ptr(), qubits.as_ptr(), 1, params.as_ptr(), 1); + assert!(!op.is_null()); + + // Instruction name. + let text = operation_name(op); + assert!(!text.is_null()); + assert_eq!(unsafe { CStr::from_ptr(text) }.to_str().unwrap(), "RX"); + cqlib_string_free(text); + + // Standard gate, arity, and qubit list. + assert_eq!(operation_is_standard_gate(op), 1); + assert_eq!(operation_num_qubits(op), 1); + assert_eq!(operation_qubits_len(op), 1); + let mut ids = [9u32]; + assert_eq!(operation_qubits(op, ids.as_mut_ptr(), 1), 0); + assert_eq!(ids, [0]); + + // Parameters: fixed value written inline. + assert_eq!(operation_params_len(op), 1); + let mut tags = [255u32]; + let mut values = [0.0f64]; + assert_eq!( + operation_params(op, tags.as_mut_ptr(), values.as_mut_ptr(), 1), + 0 + ); + assert_eq!(tags, [OPERATION_PARAM_FIXED]); + assert!((values[0] - 0.5).abs() < 1e-12); + + // Numeric unitary: 2x2 complex elements. + assert_eq!(operation_matrix_len(op), 4); + let mut rows = 0usize; + let mut cols = 0usize; + assert_eq!(operation_matrix_dims(op, &mut rows, &mut cols), 0); + assert_eq!((rows, cols), (2, 2)); + let mut out = [0.0f64; 8]; + assert_eq!(operation_matrix(op, out.as_mut_ptr(), out.len()), 4); + // RX(0.5) = [[cos(0.25), -i*sin(0.25)], [-i*sin(0.25), cos(0.25)]]. + let c = 0.25f64.cos(); + let s = 0.25f64.sin(); + assert!((out[0] - c).abs() < 1e-12); + assert!(out[1].abs() < 1e-12); + assert!(out[2].abs() < 1e-12); + assert!((out[3] + s).abs() < 1e-12); + assert!(out[4].abs() < 1e-12); + assert!((out[5] + s).abs() < 1e-12); + assert!((out[6] - c).abs() < 1e-12); + assert!(out[7].abs() < 1e-12); + + // Operations built by `operation_new` never carry a label. + assert!(operation_label(op).is_null()); + + // Buffer too small is rejected everywhere. + assert_eq!(operation_qubits(op, ids.as_mut_ptr(), 0), -8); + assert_eq!( + operation_params(op, tags.as_mut_ptr(), values.as_mut_ptr(), 0), + -8 + ); + assert_eq!(operation_matrix(op, out.as_mut_ptr(), 7), 0); + + operation_free(op); +} + +#[test] +fn parameterless_gate_operation() { + // CX with an empty parameter list. + let name = cstr("CX"); + let qubits = [0u32, 1]; + let op = operation_new(name.as_ptr(), qubits.as_ptr(), 2, std::ptr::null(), 0); + assert!(!op.is_null()); + + assert_eq!(operation_qubits_len(op), 2); + let mut ids = [0u32; 2]; + assert_eq!(operation_qubits(op, ids.as_mut_ptr(), 2), 0); + assert_eq!(ids, [0, 1]); + + // Zero parameters: `operation_params` succeeds with len = 0. + assert_eq!(operation_params_len(op), 0); + assert_eq!( + operation_params(op, std::ptr::null_mut(), std::ptr::null_mut(), 0), + 0 + ); + + // The 4x4 matrix is returned as 16 interleaved doubles. + assert_eq!(operation_matrix_len(op), 16); + let mut out = [0.0f64; 32]; + assert_eq!(operation_matrix(op, out.as_mut_ptr(), out.len()), 16); + + operation_free(op); +} + +#[test] +fn index_params_report_parameter_table_index() { + // An operation whose parameter refers to a parameter-table index (as + // produced by `circuit_rx_param`) reports the INDEX tag and carries the + // index as the payload value. Such handles only originate from + // expanded circuit bodies; construct one directly here. + let op = Box::into_raw(Box::new(COperation { + inner: Operation { + instruction: Instruction::Standard(StandardGate::RX), + qubits: vec![Qubit::new(0)].into(), + params: vec![CircuitParam::Index(3)].into(), + label: None, + }, + })); + assert_eq!(operation_is_standard_gate(op), 1); + + let mut tags = [255u32]; + let mut values = [0.0f64]; + assert_eq!(operation_params_len(op), 1); + assert_eq!( + operation_params(op, tags.as_mut_ptr(), values.as_mut_ptr(), 1), + 0 + ); + assert_eq!(tags, [OPERATION_PARAM_INDEX]); + assert_eq!(values[0], 3.0); + + // An unresolved index parameter has no numeric matrix. + assert_eq!(operation_matrix_len(op), 0); + let mut rows = 1usize; + let mut cols = 1usize; + assert_eq!(operation_matrix_dims(op, &mut rows, &mut cols), -3); + + operation_free(op); +} + +#[test] +fn operation_new_error_codes() { + // NULL gate name. + let qubits = [0u32]; + assert!(operation_new(std::ptr::null(), qubits.as_ptr(), 1, std::ptr::null(), 0).is_null()); + + // Unknown gate name. + let bogus = cstr("NOT_A_GATE"); + assert!(operation_new(bogus.as_ptr(), std::ptr::null(), 0, std::ptr::null(), 0).is_null()); + + // Non-null qubit length with a NULL buffer. + let h = cstr("H"); + assert!(operation_new(h.as_ptr(), std::ptr::null(), 1, std::ptr::null(), 0).is_null()); + + // Non-null parameter length with a NULL buffer. + assert!(operation_new(h.as_ptr(), qubits.as_ptr(), 1, std::ptr::null(), 1).is_null()); +} + +#[test] +fn accessors_tolerate_null_handles() { + assert!(operation_name(std::ptr::null()).is_null()); + assert_eq!(operation_is_standard_gate(std::ptr::null()), -1); + assert_eq!(operation_num_qubits(std::ptr::null()), 0); + assert_eq!(operation_qubits_len(std::ptr::null()), 0); + assert_eq!( + operation_qubits(std::ptr::null(), std::ptr::null_mut(), 0), + -1 + ); + assert_eq!(operation_params_len(std::ptr::null()), 0); + assert_eq!( + operation_params( + std::ptr::null(), + std::ptr::null_mut(), + std::ptr::null_mut(), + 0 + ), + -1 + ); + assert!(operation_label(std::ptr::null()).is_null()); + assert_eq!(operation_matrix_len(std::ptr::null()), 0); + assert_eq!( + operation_matrix_dims(std::ptr::null(), std::ptr::null_mut(), std::ptr::null_mut()), + -1 + ); + assert_eq!( + operation_matrix(std::ptr::null(), std::ptr::null_mut(), 0), + 0 + ); + + // Buffer arguments may not be NULL on success paths. + let name = cstr("H"); + let qubits = [0u32]; + let op = operation_new(name.as_ptr(), qubits.as_ptr(), 1, std::ptr::null(), 0); + assert!(!op.is_null()); + assert_eq!(operation_qubits(op, std::ptr::null_mut(), 1), -1); + let mut rows = 0usize; + let mut cols = 0usize; + assert_eq!( + operation_matrix_dims(op, std::ptr::null_mut(), &mut cols), + -1 + ); + assert_eq!( + operation_matrix_dims(op, &mut rows, std::ptr::null_mut()), + -1 + ); + assert_eq!(operation_matrix(op, std::ptr::null_mut(), 8), 0); + + operation_free(op); + operation_free(std::ptr::null_mut()); +} diff --git a/crates/binding-c/tests/circuit_symbolic_matrix_test.rs b/crates/binding-c/tests/circuit_symbolic_matrix_test.rs new file mode 100644 index 00000000..53badd84 --- /dev/null +++ b/crates/binding-c/tests/circuit_symbolic_matrix_test.rs @@ -0,0 +1,483 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! Rust FFI tests for symbolic matrices (`circuit/symbolic_matrix.rs`). + +use binding_c::circuit::{ + CParameter, circuit_cx, circuit_free, circuit_h, circuit_new, circuit_rx, circuit_rx_param, + circuit_to_symbolic_matrix, circuit_x, circuits_equivalent, param_free, param_parse, + standard_gate_symbolic_matrix, symbolic_eye, symbolic_matrices_equivalent, + symbolic_matrix_cols, symbolic_matrix_controlled, symbolic_matrix_element_str, + symbolic_matrix_evaluate, symbolic_matrix_evaluate_len, symbolic_matrix_free, + symbolic_matrix_rows, symbolic_matrix_simplify, symbolic_matrix_substitute, +}; +use binding_c::cqlib_string_free; +use num_complex::Complex64; +use std::ffi::{CStr, CString}; +use std::os::raw::c_char; + +fn cstr(s: &str) -> CString { + CString::new(s).unwrap() +} + +#[test] +fn symbolic_eye_shape() { + // NULL tolerance for the shape readers and the free routine. + assert_eq!(symbolic_matrix_rows(std::ptr::null()), 0); + assert_eq!(symbolic_matrix_cols(std::ptr::null()), 0); + assert_eq!(symbolic_matrix_evaluate_len(std::ptr::null()), 0); + assert!(symbolic_matrix_element_str(std::ptr::null(), 0, 0).is_null()); + assert!(symbolic_matrix_simplify(std::ptr::null()).is_null()); + symbolic_matrix_free(std::ptr::null_mut()); + + let eye = symbolic_eye(2); + assert!(!eye.is_null()); + assert_eq!(symbolic_matrix_rows(eye), 2); + assert_eq!(symbolic_matrix_cols(eye), 2); + assert_eq!(symbolic_matrix_evaluate_len(eye), 4); + + // Element strings: both diagonal entries match (and are non-empty), both + // off-diagonal entries match, and the two groups differ. + let d00 = symbolic_matrix_element_str(eye, 0, 0); + let d11 = symbolic_matrix_element_str(eye, 1, 1); + assert!(!d00.is_null()); + assert!(!d11.is_null()); + let d00_text = unsafe { CStr::from_ptr(d00) }.to_str().unwrap().to_string(); + let d11_text = unsafe { CStr::from_ptr(d11) }.to_str().unwrap().to_string(); + cqlib_string_free(d00); + cqlib_string_free(d11); + assert!(!d00_text.is_empty()); + assert_eq!(d00_text, d11_text); + + let o01 = symbolic_matrix_element_str(eye, 0, 1); + let o10 = symbolic_matrix_element_str(eye, 1, 0); + assert!(!o01.is_null()); + assert!(!o10.is_null()); + let o01_text = unsafe { CStr::from_ptr(o01) }.to_str().unwrap().to_string(); + let o10_text = unsafe { CStr::from_ptr(o10) }.to_str().unwrap().to_string(); + cqlib_string_free(o01); + cqlib_string_free(o10); + assert_eq!(o01_text, o10_text); + assert_ne!(d00_text, o01_text); + assert_ne!(d11_text, o10_text); + + // Out-of-bounds indices return NULL. + assert!(symbolic_matrix_element_str(eye, 2, 0).is_null()); + + // Simplification keeps the shape. + let simplified = symbolic_matrix_simplify(eye); + assert!(!simplified.is_null()); + assert_eq!(symbolic_matrix_rows(simplified), 2); + assert_eq!(symbolic_matrix_cols(simplified), 2); + + // A constant matrix evaluates with no bindings at all. + let mut buf = [Complex64::new(0.0, 0.0); 4]; + assert_eq!( + symbolic_matrix_evaluate(eye, std::ptr::null(), buf.as_mut_ptr(), 4), + 0 + ); + assert_eq!(buf[0].re, 1.0); + assert_eq!(buf[0].im, 0.0); + assert_eq!(buf[1].re, 0.0); + assert_eq!(buf[1].im, 0.0); + + symbolic_matrix_free(simplified); + symbolic_matrix_free(eye); +} + +#[test] +fn standard_gate_matrix_evaluation() { + // RX(theta) as a symbolic standard-gate matrix. + let theta_expr = cstr("theta"); + let theta_ptr = param_parse(theta_expr.as_ptr()); + assert!(!theta_ptr.is_null()); + let rx_name = cstr("RX"); + let params = [theta_ptr as *const CParameter; 1]; + let rx_mat = standard_gate_symbolic_matrix(rx_name.as_ptr(), params.as_ptr(), 1); + assert!(!rx_mat.is_null()); + assert_eq!(symbolic_matrix_rows(rx_mat), 2); + assert_eq!(symbolic_matrix_cols(rx_mat), 2); + assert_eq!(symbolic_matrix_evaluate_len(rx_mat), 4); + + // The diagonal entry renders as a cos(theta/2) expression. + let diag = symbolic_matrix_element_str(rx_mat, 0, 0); + assert!(!diag.is_null()); + assert!( + unsafe { CStr::from_ptr(diag) } + .to_str() + .unwrap() + .contains("cos") + ); + cqlib_string_free(diag); + + // Bound evaluation: RX(0.5) = [[cos(0.25), -i*sin(0.25)], + // [-i*sin(0.25), cos(0.25)]] in row-major order. + let bindings = cstr("theta:0.5"); + let mut buf = [Complex64::new(0.0, 0.0); 4]; + assert_eq!( + symbolic_matrix_evaluate(rx_mat, bindings.as_ptr(), buf.as_mut_ptr(), 4), + 0 + ); + assert!((buf[0].re - (0.25f64).cos()).abs() < 1e-12); + assert_eq!(buf[0].im, 0.0); + assert_eq!(buf[1].re, 0.0); + assert!((buf[1].im + (0.25f64).sin()).abs() < 1e-12); + assert!((buf[2].re - buf[1].re).abs() < 1e-12); + assert!((buf[2].im - buf[1].im).abs() < 1e-12); + assert!((buf[3].re - buf[0].re).abs() < 1e-12); + assert!((buf[3].im - buf[0].im).abs() < 1e-12); + + // Cross-check against the matrix of a numeric RX(0.5) circuit. + let circuit = circuit_new(1); + assert_eq!(circuit_rx(circuit, 0, 0.5), 0); + let circ_mat = circuit_to_symbolic_matrix(circuit, std::ptr::null(), 0); + assert!(!circ_mat.is_null()); + let mut buf2 = [Complex64::new(0.0, 0.0); 4]; + assert_eq!( + symbolic_matrix_evaluate(circ_mat, std::ptr::null(), buf2.as_mut_ptr(), 4), + 0 + ); + for (a, b) in buf.iter().zip(buf2.iter()) { + assert!((a.re - b.re).abs() < 1e-12); + assert!((a.im - b.im).abs() < 1e-12); + } + + symbolic_matrix_free(circ_mat); + symbolic_matrix_free(rx_mat); + circuit_free(circuit); + param_free(theta_ptr); +} + +#[test] +fn standard_gate_matrix_invalid_inputs() { + // The parameterless X gate builds with no params buffer at all. + let x_name = cstr("X"); + let x_mat = standard_gate_symbolic_matrix(x_name.as_ptr(), std::ptr::null(), 0); + assert!(!x_mat.is_null()); + symbolic_matrix_free(x_mat); + + let theta_expr = cstr("theta"); + let theta_ptr = param_parse(theta_expr.as_ptr()); + assert!(!theta_ptr.is_null()); + let params = [theta_ptr as *const CParameter; 1]; + let rx_name = cstr("RX"); + + // NULL gate name. + assert!(standard_gate_symbolic_matrix(std::ptr::null(), std::ptr::null(), 0).is_null()); + + // Invalid UTF-8 gate name. + let invalid_utf8 = CString::new(vec![0xFF]).unwrap(); + assert!(standard_gate_symbolic_matrix(invalid_utf8.as_ptr(), std::ptr::null(), 0).is_null()); + + // Unknown gate name. + let unknown = cstr("NOT_A_GATE"); + assert!(standard_gate_symbolic_matrix(unknown.as_ptr(), std::ptr::null(), 0).is_null()); + + // RX with a NULL params buffer but a non-zero length. + assert!(standard_gate_symbolic_matrix(rx_name.as_ptr(), std::ptr::null(), 1).is_null()); + + // RX with zero parameters. + assert!(standard_gate_symbolic_matrix(rx_name.as_ptr(), std::ptr::null(), 0).is_null()); + + // X with one parameter too many (parameter-count mismatch). + assert!(standard_gate_symbolic_matrix(x_name.as_ptr(), params.as_ptr(), 1).is_null()); + + // A NULL entry inside the params array. + let null_params: [*const CParameter; 1] = [std::ptr::null()]; + assert!(standard_gate_symbolic_matrix(rx_name.as_ptr(), null_params.as_ptr(), 1).is_null()); + + param_free(theta_ptr); +} + +#[test] +fn circuit_matrix_conversion_and_order() { + // A 1q parametric circuit yields a 2x2 matrix with a free symbol. + let theta_expr = cstr("theta"); + let theta_ptr = param_parse(theta_expr.as_ptr()); + let rx_circuit = circuit_new(1); + assert_eq!(circuit_rx_param(rx_circuit, 0, theta_ptr), 0); + let rx_mat = circuit_to_symbolic_matrix(rx_circuit, std::ptr::null(), 0); + assert!(!rx_mat.is_null()); + assert_eq!(symbolic_matrix_rows(rx_mat), 2); + assert_eq!(symbolic_matrix_cols(rx_mat), 2); + + // The free symbol blocks evaluation without bindings. + let mut buf = [Complex64::new(0.0, 0.0); 4]; + assert_eq!( + symbolic_matrix_evaluate(rx_mat, std::ptr::null(), buf.as_mut_ptr(), 4), + -3 + ); + + // A constant 2q circuit yields a 4x4 matrix of 16 elements. + let circuit = circuit_new(2); + assert_eq!(circuit_h(circuit, 0), 0); + assert_eq!(circuit_cx(circuit, 0, 1), 0); + let mat = circuit_to_symbolic_matrix(circuit, std::ptr::null(), 0); + assert!(!mat.is_null()); + assert_eq!(symbolic_matrix_rows(mat), 4); + assert_eq!(symbolic_matrix_cols(mat), 4); + assert_eq!(symbolic_matrix_evaluate_len(mat), 16); + let mut buf16 = [Complex64::new(0.0, 0.0); 16]; + assert_eq!( + symbolic_matrix_evaluate(mat, std::ptr::null(), buf16.as_mut_ptr(), 16), + 0 + ); + + // Order handling: a NULL order buffer with a non-zero length is rejected. + assert!(circuit_to_symbolic_matrix(circuit, std::ptr::null(), 1).is_null()); + + // A full order for the 2q qubit set works. + let order = [0u32, 1]; + let ordered = circuit_to_symbolic_matrix(circuit, order.as_ptr(), 2); + assert!(!ordered.is_null()); + + // A one-entry order misses qubit 1 of the 2q set. + let partial = [0u32]; + assert!(circuit_to_symbolic_matrix(circuit, partial.as_ptr(), 1).is_null()); + + // A two-entry order references a qubit the 1q circuit does not have. + assert!(circuit_to_symbolic_matrix(rx_circuit, order.as_ptr(), 2).is_null()); + + symbolic_matrix_free(ordered); + symbolic_matrix_free(mat); + symbolic_matrix_free(rx_mat); + circuit_free(circuit); + circuit_free(rx_circuit); + param_free(theta_ptr); +} + +#[test] +fn matrix_evaluate_error_codes() { + let theta_expr = cstr("theta"); + let theta_ptr = param_parse(theta_expr.as_ptr()); + let rx_name = cstr("RX"); + let params = [theta_ptr as *const CParameter; 1]; + let rx_mat = standard_gate_symbolic_matrix(rx_name.as_ptr(), params.as_ptr(), 1); + assert!(!rx_mat.is_null()); + + let good = cstr("theta:0.5"); + let mut buf = [Complex64::new(0.0, 0.0); 4]; + + // NULL matrix handle. + assert_eq!( + symbolic_matrix_evaluate(std::ptr::null(), good.as_ptr(), buf.as_mut_ptr(), 4), + -1 + ); + + // NULL output buffer (valid bindings, sufficient capacity). + assert_eq!( + symbolic_matrix_evaluate(rx_mat, good.as_ptr(), std::ptr::null_mut(), 4), + -1 + ); + + // Undersized buffer. + assert_eq!( + symbolic_matrix_evaluate(rx_mat, good.as_ptr(), buf.as_mut_ptr(), 3), + -8 + ); + + // Malformed bindings value. + let bad_value = cstr("theta:abc"); + assert_eq!( + symbolic_matrix_evaluate(rx_mat, bad_value.as_ptr(), buf.as_mut_ptr(), 4), + -4 + ); + + // Bindings for an unrelated symbol leave theta unevaluable. + let ghost = cstr("ghost:1.0"); + assert_eq!( + symbolic_matrix_evaluate(rx_mat, ghost.as_ptr(), buf.as_mut_ptr(), 4), + -3 + ); + + // No bindings at all. + assert_eq!( + symbolic_matrix_evaluate(rx_mat, std::ptr::null(), buf.as_mut_ptr(), 4), + -3 + ); + + // Constant matrices evaluate without any bindings. + let eye = symbolic_eye(2); + assert_eq!( + symbolic_matrix_evaluate(eye, std::ptr::null(), buf.as_mut_ptr(), 4), + 0 + ); + + symbolic_matrix_free(eye); + symbolic_matrix_free(rx_mat); + param_free(theta_ptr); +} + +#[test] +fn equivalence_checks() { + // Matrix-level equivalence: a NULL side is an error. + let x_name = cstr("X"); + let x_gate = standard_gate_symbolic_matrix(x_name.as_ptr(), std::ptr::null(), 0); + assert!(!x_gate.is_null()); + assert_eq!(symbolic_matrices_equivalent(std::ptr::null(), x_gate), -1); + assert_eq!(symbolic_matrices_equivalent(x_gate, std::ptr::null()), -1); + + let x_circuit = circuit_new(1); + assert_eq!(circuit_x(x_circuit, 0), 0); + let x_mat = circuit_to_symbolic_matrix(x_circuit, std::ptr::null(), 0); + let h_circuit = circuit_new(1); + assert_eq!(circuit_h(h_circuit, 0), 0); + let h_mat = circuit_to_symbolic_matrix(h_circuit, std::ptr::null(), 0); + + // The X gate matrix matches the X circuit matrix. + assert_eq!(symbolic_matrices_equivalent(x_gate, x_mat), 1); + // X and H differ by more than a global phase. + assert_eq!(symbolic_matrices_equivalent(x_mat, h_mat), 0); + + // Different shapes (2x2 identity vs 4x4 controlled-X) are not equivalent. + let eye = symbolic_eye(2); + let cnot = symbolic_matrix_controlled(x_gate, 1); + assert!(!cnot.is_null()); + assert_eq!(symbolic_matrices_equivalent(eye, cnot), 0); + + // Circuit-level equivalence: RX(pi) equals X up to a global phase. + let rx_pi = circuit_new(1); + assert_eq!(circuit_rx(rx_pi, 0, std::f64::consts::PI), 0); + assert_eq!( + circuits_equivalent(x_circuit, rx_pi, std::ptr::null(), 0), + 1 + ); + assert_eq!( + circuits_equivalent(x_circuit, x_circuit, std::ptr::null(), 0), + 1 + ); + assert_eq!( + circuits_equivalent(x_circuit, h_circuit, std::ptr::null(), 0), + 0 + ); + + // NULL circuits. + assert_eq!( + circuits_equivalent(std::ptr::null(), x_circuit, std::ptr::null(), 0), + -1 + ); + assert_eq!( + circuits_equivalent(x_circuit, std::ptr::null(), std::ptr::null(), 0), + -1 + ); + + // A NULL order buffer with a non-zero length. + assert_eq!( + circuits_equivalent(x_circuit, rx_pi, std::ptr::null(), 1), + -8 + ); + + // An order that does not cover the 2q qubit set fails matrix construction. + let empty_a = circuit_new(2); + let empty_b = circuit_new(2); + let order = [0u32]; + assert_eq!(circuits_equivalent(empty_a, empty_b, order.as_ptr(), 1), -3); + + symbolic_matrix_free(cnot); + symbolic_matrix_free(eye); + symbolic_matrix_free(h_mat); + symbolic_matrix_free(x_mat); + symbolic_matrix_free(x_gate); + circuit_free(empty_b); + circuit_free(empty_a); + circuit_free(rx_pi); + circuit_free(h_circuit); + circuit_free(x_circuit); +} + +#[test] +fn matrix_substitute_and_controlled() { + // Substitute theta -> 0.5 in the RX(theta) matrix. + let theta_expr = cstr("theta"); + let theta_ptr = param_parse(theta_expr.as_ptr()); + let rx_name = cstr("RX"); + let params = [theta_ptr as *const CParameter; 1]; + let rx_mat = standard_gate_symbolic_matrix(rx_name.as_ptr(), params.as_ptr(), 1); + assert!(!rx_mat.is_null()); + + let half_expr = cstr("0.5"); + let half = param_parse(half_expr.as_ptr()); + assert!(!half.is_null()); + let names = [theta_expr.as_ptr()]; + let repl = [half as *const CParameter; 1]; + let substituted = symbolic_matrix_substitute(rx_mat, names.as_ptr(), repl.as_ptr(), 1); + assert!(!substituted.is_null()); + + // The substituted matrix evaluates without bindings... + let mut buf = [Complex64::new(0.0, 0.0); 4]; + assert_eq!( + symbolic_matrix_evaluate(substituted, std::ptr::null(), buf.as_mut_ptr(), 4), + 0 + ); + assert!((buf[0].re - (0.25f64).cos()).abs() < 1e-12); + + // ...while the original matrix still carries the free symbol. + assert_eq!( + symbolic_matrix_evaluate(rx_mat, std::ptr::null(), buf.as_mut_ptr(), 4), + -3 + ); + + // Controlled-X embeds the X matrix in the bottom-right of a 4x4 identity. + let x_name = cstr("X"); + let x_mat = standard_gate_symbolic_matrix(x_name.as_ptr(), std::ptr::null(), 0); + assert!(!x_mat.is_null()); + let cnot = symbolic_matrix_controlled(x_mat, 1); + assert!(!cnot.is_null()); + assert_eq!(symbolic_matrix_rows(cnot), 4); + assert_eq!(symbolic_matrix_cols(cnot), 4); + let mut buf4 = [Complex64::new(0.0, 0.0); 16]; + assert_eq!( + symbolic_matrix_evaluate(cnot, std::ptr::null(), buf4.as_mut_ptr(), 16), + 0 + ); + // Top-left identity block. + assert_eq!(buf4[0].re, 1.0); + assert_eq!(buf4[5].re, 1.0); + // Bottom-right X block (CNOT pattern). + assert_eq!(buf4[6].re, 0.0); + assert_eq!(buf4[7].re, 0.0); + assert_eq!(buf4[10].re, 0.0); + assert_eq!(buf4[11].re, 1.0); + assert_eq!(buf4[14].re, 1.0); + assert_eq!(buf4[15].re, 0.0); + + // One more control level doubles the dimension again. + let ccx = symbolic_matrix_controlled(cnot, 1); + assert!(!ccx.is_null()); + assert_eq!(symbolic_matrix_rows(ccx), 8); + assert_eq!(symbolic_matrix_cols(ccx), 8); + + // Error paths. + assert!(symbolic_matrix_controlled(std::ptr::null(), 1).is_null()); + assert!( + symbolic_matrix_substitute(std::ptr::null(), names.as_ptr(), repl.as_ptr(), 1).is_null() + ); + // names = NULL with a non-zero length. + assert!(symbolic_matrix_substitute(rx_mat, std::ptr::null(), repl.as_ptr(), 1).is_null()); + // params = NULL with a non-zero length. + assert!(symbolic_matrix_substitute(rx_mat, names.as_ptr(), std::ptr::null(), 1).is_null()); + // A NULL entry inside the names array. + let null_names: [*const c_char; 1] = [std::ptr::null()]; + assert!(symbolic_matrix_substitute(rx_mat, null_names.as_ptr(), repl.as_ptr(), 1).is_null()); + // Invalid UTF-8 symbol name. + let invalid_utf8 = CString::new(vec![0xFF]).unwrap(); + let bad_names = [invalid_utf8.as_ptr()]; + assert!(symbolic_matrix_substitute(rx_mat, bad_names.as_ptr(), repl.as_ptr(), 1).is_null()); + + symbolic_matrix_free(ccx); + symbolic_matrix_free(cnot); + symbolic_matrix_free(x_mat); + symbolic_matrix_free(substituted); + symbolic_matrix_free(rx_mat); + param_free(half); + param_free(theta_ptr); +} diff --git a/crates/binding-c/tests/circuit_test.rs b/crates/binding-c/tests/circuit_test.rs new file mode 100644 index 00000000..1490794c --- /dev/null +++ b/crates/binding-c/tests/circuit_test.rs @@ -0,0 +1,309 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +use binding_c::circuit::{ + circuit_add_qubits, circuit_assign_params, circuit_barrier, circuit_ccx, circuit_compose, + circuit_crx, circuit_cry, circuit_crz, circuit_crz_param, circuit_cx, circuit_cy, circuit_cz, + circuit_decompose, circuit_depth, circuit_free, circuit_fsim, circuit_h, circuit_i, + circuit_inverse, circuit_measure, circuit_new, circuit_num_operations, circuit_num_parameters, + circuit_num_qubits, circuit_phase, circuit_phase_param, circuit_qubits, circuit_qubits_len, + circuit_remove_operation, circuit_reset, circuit_rx, circuit_rx_param, circuit_rxx, + circuit_rxx_param, circuit_rxy, circuit_rxy_param, circuit_ry, circuit_ry_param, circuit_ryy, + circuit_rz, circuit_rz_param, circuit_rzx, circuit_rzz, circuit_rzz_param, circuit_s, + circuit_sdg, circuit_set_global_phase, circuit_set_global_phase_param, circuit_swap, circuit_t, + circuit_tdg, circuit_to_matrix, circuit_to_matrix_len, circuit_u, circuit_u_param, + circuit_validate, circuit_width, circuit_x, circuit_x2m, circuit_x2p, circuit_xy, circuit_xy2m, + circuit_xy2p, circuit_y, circuit_y2m, circuit_y2p, circuit_z, param_evaluate, param_free, + param_parse, +}; +use std::ffi::CString; + +#[test] +fn circuit_lifecycle_and_basic_gates() { + let circuit = circuit_new(2); + assert!(!circuit.is_null()); + assert_eq!(circuit_num_qubits(circuit), 2); + assert_eq!(circuit_num_operations(circuit), 0); + + assert_eq!(circuit_h(circuit, 0), 0); + assert_eq!(circuit_x(circuit, 1), 0); + assert_eq!(circuit_y(circuit, 0), 0); + assert_eq!(circuit_z(circuit, 1), 0); + assert_eq!(circuit_rx(circuit, 0, 0.25), 0); + assert_eq!(circuit_ry(circuit, 1, 0.5), 0); + assert_eq!(circuit_rz(circuit, 0, 0.75), 0); + assert_eq!(circuit_cx(circuit, 0, 1), 0); + assert_eq!(circuit_cz(circuit, 1, 0), 0); + assert_eq!(circuit_measure(circuit, 0), 0); + assert_eq!(circuit_reset(circuit, 1), 0); + + assert_eq!(circuit_num_operations(circuit), 11); + assert_eq!(circuit_validate(circuit), 0); + circuit_free(circuit); +} + +#[test] +fn circuit_error_codes_are_stable() { + assert_eq!(circuit_num_qubits(std::ptr::null()), 0); + assert_eq!(circuit_num_operations(std::ptr::null()), 0); + assert_eq!(circuit_num_parameters(std::ptr::null()), 0); + assert_eq!(circuit_validate(std::ptr::null()), -1); + assert_eq!(circuit_h(std::ptr::null_mut(), 0), -1); + circuit_free(std::ptr::null_mut()); + + let circuit = circuit_new(1); + assert_eq!(circuit_h(circuit, 1), -2); + assert_eq!(circuit_cx(circuit, 0, 1), -2); + assert_eq!(circuit_rx(circuit, 0, f64::NAN), -3); + circuit_free(circuit); +} + +#[test] +fn symbolic_parameters_can_be_evaluated_and_assigned() { + let theta = CString::new("theta").unwrap(); + let phi = CString::new("phi").unwrap(); + let theta_ptr = param_parse(theta.as_ptr()); + let phi_ptr = param_parse(phi.as_ptr()); + assert!(!theta_ptr.is_null()); + assert!(!phi_ptr.is_null()); + + let bindings = CString::new("theta:0.5,phi:1.25").unwrap(); + assert!((param_evaluate(theta_ptr, bindings.as_ptr()) - 0.5).abs() < 1e-12); + assert!((param_evaluate(phi_ptr, bindings.as_ptr()) - 1.25).abs() < 1e-12); + + let circuit = circuit_new(2); + assert_eq!(circuit_rx_param(circuit, 0, theta_ptr), 0); + assert_eq!(circuit_ry_param(circuit, 1, phi_ptr), 0); + assert_eq!(circuit_rz_param(circuit, 0, theta_ptr), 0); + assert_eq!(circuit_cx(circuit, 0, 1), 0); + assert_eq!(circuit_num_operations(circuit), 4); + assert_eq!(circuit_num_parameters(circuit), 2); + + let assigned = circuit_assign_params(circuit, bindings.as_ptr()); + assert!(!assigned.is_null()); + assert_eq!(circuit_num_operations(assigned), 4); + assert_eq!(circuit_num_parameters(assigned), 0); + assert_eq!(circuit_validate(assigned), 0); + + circuit_free(assigned); + circuit_free(circuit); + param_free(theta_ptr); + param_free(phi_ptr); +} + +#[test] +fn invalid_parameters_return_null_or_error() { + assert!(param_parse(std::ptr::null()).is_null()); + assert_eq!(param_evaluate(std::ptr::null(), std::ptr::null()), 0.0); + + let circuit = circuit_new(1); + assert_eq!(circuit_rx_param(circuit, 0, std::ptr::null()), -1); + assert!(circuit_assign_params(std::ptr::null(), std::ptr::null()).is_null()); + circuit_free(circuit); + param_free(std::ptr::null_mut()); +} + +#[test] +fn single_qubit_no_param_gates() { + let circuit = circuit_new(2); + assert_eq!(circuit_i(circuit, 0), 0); + assert_eq!(circuit_s(circuit, 1), 0); + assert_eq!(circuit_sdg(circuit, 0), 0); + assert_eq!(circuit_t(circuit, 1), 0); + assert_eq!(circuit_tdg(circuit, 0), 0); + assert_eq!(circuit_x2p(circuit, 1), 0); + assert_eq!(circuit_x2m(circuit, 0), 0); + assert_eq!(circuit_y2p(circuit, 1), 0); + assert_eq!(circuit_y2m(circuit, 0), 0); + assert_eq!(circuit_num_operations(circuit), 9); + assert_eq!(circuit_validate(circuit), 0); + assert_eq!(circuit_i(circuit, 5), -2); // out of bounds + circuit_free(circuit); +} + +#[test] +fn single_qubit_param_gates_numeric() { + let circuit = circuit_new(1); + assert_eq!(circuit_phase(circuit, 0, 0.5), 0); + assert_eq!(circuit_u(circuit, 0, 0.1, 0.2, 0.3), 0); + assert_eq!(circuit_xy(circuit, 0, 0.4), 0); + assert_eq!(circuit_xy2p(circuit, 0, 0.5), 0); + assert_eq!(circuit_xy2m(circuit, 0, 0.6), 0); + assert_eq!(circuit_rxy(circuit, 0, 0.7, 0.8), 0); + assert_eq!(circuit_num_operations(circuit), 6); + assert_eq!(circuit_u(circuit, 0, f64::NAN, 0.0, 0.0), -3); + circuit_free(circuit); +} + +#[test] +fn single_qubit_param_gates_symbolic() { + let theta = CString::new("theta").unwrap(); + let phi = CString::new("phi").unwrap(); + let lambda = CString::new("lambda").unwrap(); + let t = param_parse(theta.as_ptr()); + let p = param_parse(phi.as_ptr()); + let l = param_parse(lambda.as_ptr()); + assert!(!t.is_null() && !p.is_null() && !l.is_null()); + + let circuit = circuit_new(1); + assert_eq!(circuit_phase_param(circuit, 0, t), 0); + assert_eq!(circuit_u_param(circuit, 0, t, p, l), 0); + assert_eq!(circuit_rxy_param(circuit, 0, t, p), 0); + assert_eq!(circuit_num_parameters(circuit), 3); + assert_eq!(circuit_u_param(circuit, 0, std::ptr::null(), p, l), -1); + circuit_free(circuit); + param_free(t); + param_free(p); + param_free(l); +} + +#[test] +fn two_and_three_qubit_gates() { + let circuit = circuit_new(3); + assert_eq!(circuit_cy(circuit, 0, 1), 0); + assert_eq!(circuit_swap(circuit, 1, 2), 0); + assert_eq!(circuit_rxx(circuit, 0, 1, 0.5), 0); + assert_eq!(circuit_ryy(circuit, 1, 2, 0.5), 0); + assert_eq!(circuit_rzz(circuit, 0, 2, 0.5), 0); + assert_eq!(circuit_rzx(circuit, 1, 0, 0.5), 0); + assert_eq!(circuit_crx(circuit, 0, 1, 0.5), 0); + assert_eq!(circuit_cry(circuit, 1, 2, 0.5), 0); + assert_eq!(circuit_crz(circuit, 0, 2, 0.5), 0); + assert_eq!(circuit_fsim(circuit, 1, 0, 0.3, 0.7), 0); + assert_eq!(circuit_ccx(circuit, 0, 1, 2), 0); + assert_eq!(circuit_num_operations(circuit), 11); + assert_eq!(circuit_ccx(circuit, 0, 1, 5), -2); // out of bounds + circuit_free(circuit); +} + +#[test] +fn two_qubit_symbolic_gates() { + let theta = CString::new("theta").unwrap(); + let t = param_parse(theta.as_ptr()); + assert!(!t.is_null()); + + let circuit = circuit_new(2); + assert_eq!(circuit_rxx_param(circuit, 0, 1, t), 0); + assert_eq!(circuit_rzz_param(circuit, 1, 0, t), 0); + assert_eq!(circuit_crz_param(circuit, 0, 1, t), 0); + assert_eq!(circuit_num_parameters(circuit), 1); + circuit_free(circuit); + param_free(t); +} + +#[test] +fn barrier_and_properties() { + let circuit = circuit_new(3); + assert_eq!(circuit_h(circuit, 0), 0); + assert_eq!(circuit_h(circuit, 1), 0); + assert_eq!(circuit_cx(circuit, 0, 2), 0); + + // Barrier over subset + let qubits = [0u32, 1]; + assert_eq!(circuit_barrier(circuit, qubits.as_ptr(), 2), 0); + + // Global barrier (NULL + count=0) + assert_eq!(circuit_barrier(circuit, std::ptr::null(), 0), 0); + assert_eq!(circuit_num_operations(circuit), 5); + + // Properties + assert_eq!(circuit_width(circuit), 3); + assert_eq!(circuit_qubits_len(circuit), 3); + let mut buf = [0u32; 3]; + assert_eq!(circuit_qubits(circuit, buf.as_mut_ptr(), 3), 3); + assert_eq!(buf, [0, 1, 2]); + let depth = circuit_depth(circuit, false); + assert!(depth > 0); + + // Remove operation + assert_eq!(circuit_remove_operation(circuit, 0), 0); + assert_eq!(circuit_num_operations(circuit), 4); + assert_eq!(circuit_remove_operation(circuit, 100), -3); + circuit_free(circuit); +} + +#[test] +fn inverse_and_decompose() { + let circuit = circuit_new(2); + assert_eq!(circuit_h(circuit, 0), 0); + assert_eq!(circuit_cx(circuit, 0, 1), 0); + + let inv = circuit_inverse(circuit); + assert!(!inv.is_null()); + assert_eq!(circuit_num_operations(inv), 2); + assert_eq!(circuit_validate(inv), 0); + + let dec = circuit_decompose(circuit); + assert!(!dec.is_null()); + assert_eq!(circuit_validate(dec), 0); + + circuit_free(inv); + circuit_free(dec); + circuit_free(circuit); +} + +#[test] +fn to_matrix_works() { + let circuit = circuit_new(2); + assert_eq!(circuit_h(circuit, 0), 0); + assert_eq!(circuit_cx(circuit, 0, 1), 0); + + let len = circuit_to_matrix_len(circuit, std::ptr::null(), 0); + assert_eq!(len, 4 * 4); // 2^2 * 2^2 = 16 complex elements + + let mut buf = vec![0.0f64; len * 2]; + let written = circuit_to_matrix(circuit, std::ptr::null(), 0, buf.as_mut_ptr(), buf.len()); + assert_eq!(written, len); + + circuit_free(circuit); +} + +#[test] +fn compose_and_add_qubits() { + let circuit = circuit_new(2); + assert_eq!(circuit_h(circuit, 0), 0); + + let other = circuit_new(1); + assert_eq!(circuit_x(other, 0), 0); + + // Compose with identity mapping: other's q0 -> circuit's q1 + let map = [1u32]; + assert_eq!(circuit_compose(circuit, other, map.as_ptr(), 1), 0); + assert_eq!(circuit_num_operations(circuit), 2); + + // Add qubits + let new_q = [3u32, 4]; + assert_eq!(circuit_add_qubits(circuit, new_q.as_ptr(), 2), 0); + assert_eq!(circuit_width(circuit), 4); + + circuit_free(other); + circuit_free(circuit); +} + +#[test] +fn global_phase_operations() { + let circuit = circuit_new(1); + assert_eq!(circuit_set_global_phase(circuit, 0.5), 0); + assert_eq!(circuit_set_global_phase(circuit, f64::NAN), -3); + + let expr = CString::new("alpha").unwrap(); + let p = param_parse(expr.as_ptr()); + assert!(!p.is_null()); + assert_eq!(circuit_set_global_phase_param(circuit, p), 0); + assert_eq!( + circuit_set_global_phase_param(circuit, std::ptr::null()), + -1 + ); + + circuit_free(circuit); + param_free(p); +} diff --git a/crates/binding-c/tests/compile_config_test.rs b/crates/binding-c/tests/compile_config_test.rs new file mode 100644 index 00000000..4e1f6720 --- /dev/null +++ b/crates/binding-c/tests/compile_config_test.rs @@ -0,0 +1,628 @@ +//! Integration tests for the compile-module configuration C ABI +//! (`CTransformConfig` in `compile::transform` and `CDecomposeUnitaryTarget` +//! in `compile::decompose`). + +#![allow(dead_code)] + +use binding_c::compile::*; +use binding_c::cqlib_string_free; +use std::ffi::{CStr, CString}; +use std::os::raw::c_char; + +/// Builds an owned C string array from Rust string literals. The returned +/// `Vec` must outlive every use of the pointer array. +fn name_array(names: &[&str]) -> (Vec<*const c_char>, Vec) { + let owned: Vec = names.iter().map(|n| CString::new(*n).unwrap()).collect(); + let ptrs: Vec<*const c_char> = owned.iter().map(|n| n.as_ptr()).collect(); + (ptrs, owned) +} + +fn ptr_to_string(ptr: *mut c_char) -> String { + assert!(!ptr.is_null(), "expected a heap-allocated name"); + let s = unsafe { CStr::from_ptr(ptr) } + .to_string_lossy() + .into_owned(); + cqlib_string_free(ptr); + s +} + +/// Reads a two-step name list and frees every entry. +fn read_name_list(total: usize, read: impl Fn(*mut *mut c_char, usize) -> usize) -> Vec { + assert_eq!( + total, + read(std::ptr::null_mut(), 0), + "NULL-out query mismatch" + ); + let mut buffer: Vec<*mut c_char> = vec![std::ptr::null_mut(); total]; + let written = read(buffer.as_mut_ptr(), total); + assert_eq!(written, total, "two-step length mismatch"); + buffer.into_iter().map(ptr_to_string).collect() +} + +// ===== CTransformConfig ===== + +/// Reads the enabled-kind tags of a config via the two-step pattern. +fn read_enabled_kinds(config: *const CTransformConfig, expected: usize) -> Vec { + assert_eq!(transform_config_enabled_kinds_len(config), expected); + let mut buffer = vec![0u8; expected]; + let written = transform_config_enabled_kinds(config, buffer.as_mut_ptr(), expected); + assert_eq!(written, expected); + buffer +} + +#[test] +fn test_transform_config_default_getters() { + let config = transform_config_default(); + assert!(!config.is_null()); + + assert_eq!(transform_config_mode(config), REWRITE_MODE_OPTIMIZE as i32); + assert_eq!(transform_config_max_rounds(config), 8); + assert_eq!(transform_config_max_window_ops(config), 16); + assert_eq!(transform_config_max_pattern_len(config), 8); + assert_eq!(transform_config_recurse_control_flow(config), 1); + assert_eq!(transform_config_skip_labeled_ops(config), 1); + assert_eq!(transform_config_enabled_kinds_len(config), 4); + assert_eq!(transform_config_target_instruction_basis_len(config), 0); + assert_eq!( + transform_config_target_instruction_basis(config, std::ptr::null_mut(), 0), + 0 + ); + assert_eq!( + read_enabled_kinds(config, 4), + vec![ + REWRITE_KIND_SIMPLIFY, + REWRITE_KIND_CANCEL, + REWRITE_KIND_MERGE, + REWRITE_KIND_CANONICALIZE, + ] + ); + + transform_config_free(config); +} + +#[test] +fn test_transform_config_lowering_getters() { + let config = transform_config_lowering(); + assert!(!config.is_null()); + + assert_eq!(transform_config_mode(config), REWRITE_MODE_LOWERING as i32); + assert_eq!(transform_config_enabled_kinds_len(config), 6); + let kinds = read_enabled_kinds(config, 6); + assert!(kinds.contains(&REWRITE_KIND_DECOMPOSE)); + assert!(kinds.contains(&REWRITE_KIND_HARDWARE_NATIVE)); + + transform_config_free(config); +} + +#[test] +fn test_transform_config_with_mode() { + let config = transform_config_default(); + assert_eq!(transform_config_with_mode(config, REWRITE_MODE_LOWERING), 0); + assert_eq!(transform_config_mode(config), REWRITE_MODE_LOWERING as i32); + assert_eq!(transform_config_with_mode(config, REWRITE_MODE_OPTIMIZE), 0); + assert_eq!(transform_config_mode(config), REWRITE_MODE_OPTIMIZE as i32); + // An unknown mode tag is rejected and leaves the mode unchanged. + assert_eq!(transform_config_with_mode(config, 99), -8); + assert_eq!(transform_config_mode(config), REWRITE_MODE_OPTIMIZE as i32); + transform_config_free(config); + + assert_eq!(transform_config_with_mode(std::ptr::null_mut(), 0), -1); + assert_eq!(transform_config_mode(std::ptr::null()), -1); +} + +#[test] +fn test_transform_config_scalar_setters() { + let config = transform_config_default(); + + assert_eq!(transform_config_with_max_rounds(config, 3), 0); + assert_eq!(transform_config_max_rounds(config), 3); + + assert_eq!(transform_config_with_max_window_ops(config, 32), 0); + assert_eq!(transform_config_max_window_ops(config), 32); + + assert_eq!(transform_config_with_max_pattern_len(config, 5), 0); + assert_eq!(transform_config_max_pattern_len(config), 5); + + transform_config_free(config); + + assert_eq!( + transform_config_with_max_rounds(std::ptr::null_mut(), 1), + -1 + ); + assert_eq!( + transform_config_with_max_window_ops(std::ptr::null_mut(), 1), + -1 + ); + assert_eq!( + transform_config_with_max_pattern_len(std::ptr::null_mut(), 1), + -1 + ); + assert_eq!(transform_config_max_rounds(std::ptr::null()), 0); + assert_eq!(transform_config_max_window_ops(std::ptr::null()), 0); + assert_eq!(transform_config_max_pattern_len(std::ptr::null()), 0); + assert_eq!(transform_config_recurse_control_flow(std::ptr::null()), -1); + assert_eq!(transform_config_skip_labeled_ops(std::ptr::null()), -1); +} + +#[test] +fn test_transform_config_with_enabled_kinds() { + let config = transform_config_default(); + + let tags = [REWRITE_KIND_CANCEL, REWRITE_KIND_MERGE]; + assert_eq!( + transform_config_with_enabled_kinds(config, tags.as_ptr(), tags.len()), + 0 + ); + assert_eq!(read_enabled_kinds(config, 2), tags.to_vec()); + + // Unknown tags are rejected and leave the configuration unchanged. + let bad = [REWRITE_KIND_CANCEL, 42]; + assert_eq!( + transform_config_with_enabled_kinds(config, bad.as_ptr(), bad.len()), + -8 + ); + assert_eq!(read_enabled_kinds(config, 2), tags.to_vec()); + + // An empty list disables every category. + assert_eq!( + transform_config_with_enabled_kinds(config, std::ptr::null(), 0), + 0 + ); + assert_eq!(transform_config_enabled_kinds_len(config), 0); + + // A NULL array with a positive length is rejected. + assert_eq!( + transform_config_with_enabled_kinds(config, std::ptr::null(), 1), + -1 + ); + + transform_config_free(config); + assert_eq!( + transform_config_with_enabled_kinds(std::ptr::null_mut(), std::ptr::null(), 0), + -1 + ); +} + +#[test] +fn test_transform_config_enabled_kinds_two_step_consistency() { + let config = transform_config_lowering(); + let total = transform_config_enabled_kinds_len(config); + assert_eq!(total, 6); + + // A truncated buffer only receives the first entries; the returned + // total is unaffected. + let mut small = [0u8; 2]; + assert_eq!( + transform_config_enabled_kinds(config, small.as_mut_ptr(), small.len()), + total + ); + assert_eq!(small[0], REWRITE_KIND_SIMPLIFY); + assert_eq!(small[1], REWRITE_KIND_CANCEL); + + // A NULL out pointer only queries the total. + assert_eq!( + transform_config_enabled_kinds(config, std::ptr::null_mut(), total), + total + ); + assert_eq!( + transform_config_enabled_kinds(std::ptr::null(), std::ptr::null_mut(), 0), + 0 + ); + + transform_config_free(config); +} + +#[test] +fn test_transform_config_with_target_instructions() { + let config = transform_config_default(); + + let (ptrs, _owned) = name_array(&["H", "RZ", "CX"]); + assert_eq!( + transform_config_with_target_instructions(config, ptrs.as_ptr(), 3), + 0 + ); + assert_eq!(transform_config_target_instruction_basis_len(config), 3); + let names = read_name_list(3, |out, len| { + transform_config_target_instruction_basis(config, out, len) + }); + assert_eq!(names, vec!["H", "RZ", "CX"]); + + // Unknown gate names map to -4 and leave the basis unchanged. + let (bad_ptrs, _bad_owned) = name_array(&["H", "NOPE"]); + assert_eq!( + transform_config_with_target_instructions(config, bad_ptrs.as_ptr(), 2), + -4 + ); + assert_eq!(transform_config_target_instruction_basis_len(config), 3); + + // An empty basis is rejected (-8). + assert_eq!( + transform_config_with_target_instructions(config, std::ptr::null(), 0), + -8 + ); + + // A NULL name entry is rejected (-1). + let with_null = [std::ptr::null()]; + assert_eq!( + transform_config_with_target_instructions(config, with_null.as_ptr(), 1), + -1 + ); + + transform_config_free(config); + assert_eq!( + transform_config_with_target_instructions(std::ptr::null_mut(), std::ptr::null(), 0), + -1 + ); +} + +#[test] +fn test_transform_config_with_target_instructions_invalid_utf8() { + let config = transform_config_default(); + let invalid = unsafe { CString::from_vec_unchecked(vec![0xFF, 0xFE]) }; + let ptrs = [invalid.as_ptr()]; + assert_eq!( + transform_config_with_target_instructions(config, ptrs.as_ptr(), 1), + -4 + ); + transform_config_free(config); +} + +#[test] +fn test_transform_config_target_basis_two_step_consistency() { + let config = transform_config_default(); + assert_eq!(transform_config_target_instruction_basis_len(config), 0); + assert_eq!( + transform_config_target_instruction_basis(config, std::ptr::null_mut(), 0), + 0 + ); + assert_eq!( + transform_config_target_instruction_basis(std::ptr::null(), std::ptr::null_mut(), 0), + 0 + ); + + let (ptrs, _owned) = name_array(&["H", "CX"]); + assert_eq!( + transform_config_with_target_instructions(config, ptrs.as_ptr(), 2), + 0 + ); + assert_eq!(transform_config_target_instruction_basis_len(config), 2); + + // A truncated buffer only receives the first entry; the total is + // unaffected. + let mut small: Vec<*mut c_char> = vec![std::ptr::null_mut(); 1]; + assert_eq!( + transform_config_target_instruction_basis(config, small.as_mut_ptr(), small.len()), + 2 + ); + assert_eq!(ptr_to_string(small[0]), "H"); + + transform_config_free(config); +} + +#[test] +fn test_transform_config_free_null() { + transform_config_free(std::ptr::null_mut()); +} + +// ===== CDecomposeUnitaryTarget ===== + +#[test] +fn test_decompose_unitary_target_unconstrained() { + let target = decompose_unitary_target_unconstrained(); + assert!(!target.is_null()); + + assert_eq!(decompose_unitary_target_native_1q_len(target), 0); + assert_eq!(decompose_unitary_target_native_2q_len(target), 0); + assert_eq!(decompose_unitary_target_basis_len(target), 0); + assert_eq!(decompose_unitary_target_fallback_pauli(target), 1); + assert_eq!( + decompose_unitary_target_basis(target, std::ptr::null_mut(), 0), + 0 + ); + + // Rebuilding the fallback flag needs a non-empty native basis. + assert_eq!(decompose_unitary_target_set_fallback_pauli(target, 0), -8); + assert_eq!(decompose_unitary_target_fallback_pauli(target), 1); + + // An unconstrained target has no basis to cost against. + let mut cost = CTargetBasisCost { + two_qubit_ops: 1, + depth: 1, + total_ops: 1, + parameterized_ops: 1, + }; + assert_eq!( + decompose_unitary_target_cost_of_fixed_operations( + target, + std::ptr::null(), + std::ptr::null(), + std::ptr::null(), + 0, + &mut cost + ), + -8 + ); + + decompose_unitary_target_free(target); + decompose_unitary_target_free(std::ptr::null_mut()); + assert_eq!( + decompose_unitary_target_fallback_pauli(std::ptr::null()), + -1 + ); + assert_eq!(decompose_unitary_target_native_1q_len(std::ptr::null()), 0); + assert_eq!(decompose_unitary_target_native_2q_len(std::ptr::null()), 0); + assert_eq!(decompose_unitary_target_basis_len(std::ptr::null()), 0); +} + +#[test] +fn test_decompose_unitary_target_from_standard_gates() { + let (oneq, _oneq_owned) = name_array(&["H", "RZ"]); + let (twoq, _twoq_owned) = name_array(&["CX", "RZZ"]); + let target = + decompose_unitary_target_from_standard_gates(oneq.as_ptr(), 2, twoq.as_ptr(), 2, 1); + assert!(!target.is_null()); + + assert_eq!(decompose_unitary_target_fallback_pauli(target), 1); + assert_eq!(decompose_unitary_target_native_1q_len(target), 2); + assert_eq!(decompose_unitary_target_native_2q_len(target), 2); + assert_eq!(decompose_unitary_target_basis_len(target), 4); + + let mut oneq_names = read_name_list(2, |out, len| { + decompose_unitary_target_native_1q(target, out, len) + }); + oneq_names.sort(); + assert_eq!(oneq_names, vec!["H", "RZ"]); + + let mut twoq_names = read_name_list(2, |out, len| { + decompose_unitary_target_native_2q(target, out, len) + }); + twoq_names.sort(); + assert_eq!(twoq_names, vec!["CX", "RZZ"]); + + // The combined basis contains every native gate. + let basis = read_name_list(4, |out, len| { + decompose_unitary_target_basis(target, out, len) + }); + assert!(basis.contains(&"H".to_string())); + assert!(basis.contains(&"RZ".to_string())); + assert!(basis.contains(&"CX".to_string())); + assert!(basis.contains(&"RZZ".to_string())); + + // Toggling the fallback flag rebuilds the target in place. + assert_eq!(decompose_unitary_target_set_fallback_pauli(target, 0), 0); + assert_eq!(decompose_unitary_target_fallback_pauli(target), 0); + assert_eq!(decompose_unitary_target_set_fallback_pauli(target, 1), 0); + assert_eq!(decompose_unitary_target_fallback_pauli(target), 1); + assert_eq!( + decompose_unitary_target_set_fallback_pauli(std::ptr::null_mut(), 1), + -1 + ); + + // Wrong arity is rejected by the core. + assert!( + decompose_unitary_target_from_standard_gates(twoq.as_ptr(), 1, twoq.as_ptr(), 1, 1) + .is_null() + ); + // Unknown gate names are rejected. + let (bad, _bad_owned) = name_array(&["NOT_A_GATE"]); + assert!( + decompose_unitary_target_from_standard_gates(bad.as_ptr(), 1, twoq.as_ptr(), 1, 1) + .is_null() + ); + + decompose_unitary_target_free(target); +} + +#[test] +fn test_decompose_unitary_target_from_instructions() { + let (ptrs, _owned) = name_array(&["H", "CX"]); + let target = decompose_unitary_target_from_instructions(ptrs.as_ptr(), 2); + assert!(!target.is_null()); + + assert_eq!(decompose_unitary_target_native_1q_len(target), 1); + assert_eq!(decompose_unitary_target_native_2q_len(target), 1); + assert_eq!(decompose_unitary_target_fallback_pauli(target), 1); + + decompose_unitary_target_free(target); + + // An empty basis cannot build a cost model. + assert!(decompose_unitary_target_from_instructions(std::ptr::null(), 0).is_null()); + // Unknown gate names are rejected. + let (bad, _bad_owned) = name_array(&["NOPE"]); + assert!(decompose_unitary_target_from_instructions(bad.as_ptr(), 1).is_null()); +} + +#[test] +fn test_decompose_unitary_target_basis_two_step_consistency() { + let (oneq, _oneq_owned) = name_array(&["H"]); + let (twoq, _twoq_owned) = name_array(&["CX"]); + let target = + decompose_unitary_target_from_standard_gates(oneq.as_ptr(), 1, twoq.as_ptr(), 1, 1); + assert!(!target.is_null()); + + let total = decompose_unitary_target_basis_len(target); + assert_eq!(total, 2); + + // A truncated buffer only receives the first entry; the total is + // unaffected. + let mut small: Vec<*mut c_char> = vec![std::ptr::null_mut(); 1]; + assert_eq!( + decompose_unitary_target_basis(target, small.as_mut_ptr(), small.len()), + total + ); + assert_eq!(ptr_to_string(small[0]), "H"); + + // A NULL out pointer only queries the total. + assert_eq!( + decompose_unitary_target_basis(target, std::ptr::null_mut(), total), + total + ); + + decompose_unitary_target_free(target); +} + +#[test] +fn test_decompose_unitary_target_cost_of_fixed_operations() { + let (oneq, _oneq_owned) = name_array(&["H", "RZ"]); + let (twoq, _twoq_owned) = name_array(&["CX"]); + let target = + decompose_unitary_target_from_standard_gates(oneq.as_ptr(), 2, twoq.as_ptr(), 1, 1); + assert!(!target.is_null()); + + // Ops: H(q0); CX(q0, q1); H(q1) -> already native, so the lowered cost + // is 3 ops, depth 3, one two-qubit op, no parameters. + let (ops, _ops_owned) = name_array(&["H", "CX", "H"]); + let qubit_ids = [0u32, 0, 1, 1]; + let qubit_counts = [1u32, 2, 1]; + let mut cost = CTargetBasisCost { + two_qubit_ops: 0, + depth: 0, + total_ops: 0, + parameterized_ops: 0, + }; + assert_eq!( + decompose_unitary_target_cost_of_fixed_operations( + target, + ops.as_ptr(), + qubit_ids.as_ptr(), + qubit_counts.as_ptr(), + 3, + &mut cost + ), + 0 + ); + assert_eq!(cost.total_ops, 3); + assert_eq!(cost.two_qubit_ops, 1); + assert_eq!(cost.parameterized_ops, 0); + assert_eq!(cost.depth, 3); + + // Parameterized gates are counted as such. + let (rz_ops, _rz_owned) = name_array(&["RZ"]); + let rz_counts = [1u32]; + assert_eq!( + decompose_unitary_target_cost_of_fixed_operations( + target, + rz_ops.as_ptr(), + qubit_ids.as_ptr(), + rz_counts.as_ptr(), + 1, + &mut cost + ), + 0 + ); + assert_eq!(cost.total_ops, 1); + assert_eq!(cost.parameterized_ops, 1); + + // An empty sequence costs nothing. + assert_eq!( + decompose_unitary_target_cost_of_fixed_operations( + target, + std::ptr::null(), + std::ptr::null(), + std::ptr::null(), + 0, + &mut cost + ), + 0 + ); + assert_eq!(cost.total_ops, 0); + + // Error paths: NULL out, unknown gate name, NULL config. + assert_eq!( + decompose_unitary_target_cost_of_fixed_operations( + target, + ops.as_ptr(), + qubit_ids.as_ptr(), + qubit_counts.as_ptr(), + 3, + std::ptr::null_mut() + ), + -1 + ); + let (bad_ops, _bad_owned) = name_array(&["NOT_A_GATE"]); + assert_eq!( + decompose_unitary_target_cost_of_fixed_operations( + target, + bad_ops.as_ptr(), + qubit_ids.as_ptr(), + qubit_counts.as_ptr(), + 1, + &mut cost + ), + -4 + ); + assert_eq!( + decompose_unitary_target_cost_of_fixed_operations( + std::ptr::null(), + ops.as_ptr(), + qubit_ids.as_ptr(), + qubit_counts.as_ptr(), + 3, + &mut cost + ), + -1 + ); + + decompose_unitary_target_free(target); +} + +// ===== CCanonicalizeConfig ===== + +#[test] +fn test_canonicalize_config_defaults_and_setters() { + let config = canonicalize_config_default(); + assert!(!config.is_null()); + assert_eq!(canonicalize_config_round_limit(config), 8); + assert_eq!(canonicalize_config_recurse_control_flow(config), 1); + assert_eq!(canonicalize_config_fold_gphase(config), 1); + assert_eq!(canonicalize_config_canonicalize_instruction_form(config), 1); + assert_eq!(canonicalize_config_drop_noops(config), 1); + assert_eq!(canonicalize_config_canonicalize_barriers(config), 1); + + // Setters chain on the current config instead of resetting to defaults. + assert_eq!(canonicalize_config_with_round_limit(config, 2), 0); + assert_eq!(canonicalize_config_with_drop_noops(config, 0), 0); + assert_eq!(canonicalize_config_with_recurse_control_flow(config, 0), 0); + assert_eq!(canonicalize_config_with_fold_gphase(config, 0), 0); + assert_eq!( + canonicalize_config_with_canonicalize_instruction_form(config, 0), + 0 + ); + assert_eq!(canonicalize_config_with_canonicalize_barriers(config, 0), 0); + assert_eq!(canonicalize_config_round_limit(config), 2); + assert_eq!(canonicalize_config_recurse_control_flow(config), 0); + assert_eq!(canonicalize_config_fold_gphase(config), 0); + assert_eq!(canonicalize_config_canonicalize_instruction_form(config), 0); + assert_eq!(canonicalize_config_drop_noops(config), 0); + assert_eq!(canonicalize_config_canonicalize_barriers(config), 0); + + assert_eq!( + canonicalize_config_with_round_limit(std::ptr::null_mut(), 1), + -1 + ); + assert_eq!(canonicalize_config_round_limit(std::ptr::null_mut()), 0); + assert_eq!(canonicalize_config_drop_noops(std::ptr::null_mut()), -1); + + canonicalize_config_free(config); + canonicalize_config_free(std::ptr::null_mut()); +} + +#[test] +fn test_canonicalize_circuit_with_config() { + use binding_c::circuit::{circuit_free, circuit_h, circuit_new}; + + let qc = circuit_new(2); + assert_eq!(circuit_h(qc, 0), 0); + + let config = canonicalize_config_default(); + let result = canonicalize_circuit_with_config(qc, config); + assert!(!result.is_null()); + assert!(canonicalize_result_rounds(result) >= 1); + canonicalize_result_free(result); + + // NULL inputs return NULL; the config stays owned by the caller. + assert!(canonicalize_circuit_with_config(std::ptr::null(), config).is_null()); + + canonicalize_config_free(config); + circuit_free(qc); +} diff --git a/crates/binding-c/tests/compile_knowledge_test.rs b/crates/binding-c/tests/compile_knowledge_test.rs new file mode 100644 index 00000000..452bd8cc --- /dev/null +++ b/crates/binding-c/tests/compile_knowledge_test.rs @@ -0,0 +1,669 @@ +//! Integration tests for the knowledge matching engine C ABI: rule listing +//! and lookup, transactional matching, condition checks, equivalence, +//! candidate/key filtering, free symbols, rule extension, and target +//! instantiation. + +use binding_c::circuit::{COperation, operation_free, operation_new}; +use binding_c::compile::*; +use binding_c::cqlib_string_free; +use std::ffi::{CStr, CString}; +use std::os::raw::c_char; + +const MERGE_DSL: &str = "rule merge_rz {\n match {\n RZ(a) 0\n RZ(b) 0\n }\n rewrite {\n RZ(a + b) 0\n }\n}"; +const CANCEL_H_DSL: &str = + "rule cancel_h {\n match {\n H 0\n H 0\n }\n rewrite {}\n}"; +const CANCEL_X_DSL: &str = + "rule cancel_x {\n match {\n X 0\n X 0\n }\n rewrite {}\n}"; +const CONDITION_DSL: &str = "rule cancel_rz_inverse {\n match { RZ(a) 0, RZ(b) 0 }\n require { a + b == 0 mod 4*π }\n rewrite {}\n}"; +const SHIFT_DSL: &str = + "rule shift_rz {\n match { RZ(a) 0, RZ(b) 1 }\n rewrite { RZ(a + b) 0, RZ(a + b) 1 }\n}"; + +fn name(s: &str) -> CString { + CString::new(s).unwrap() +} + +fn make_rule(dsl: &str) -> *mut CKnowledgeRule { + let source = name(dsl); + let mut rule: *mut CKnowledgeRule = std::ptr::null_mut(); + assert_eq!(knowledge_rule_from_dsl(source.as_ptr(), &mut rule), 0); + assert!(!rule.is_null()); + rule +} + +fn make_op(gate: &str, qubits: &[u32], params: &[f64]) -> *mut COperation { + let gate = name(gate); + let op = operation_new( + gate.as_ptr(), + if qubits.is_empty() { + std::ptr::null() + } else { + qubits.as_ptr() + }, + qubits.len(), + if params.is_empty() { + std::ptr::null() + } else { + params.as_ptr() + }, + params.len(), + ); + assert!(!op.is_null()); + op +} + +fn read_string(ptr: *mut c_char) -> String { + assert!(!ptr.is_null()); + let text = unsafe { CStr::from_ptr(ptr) } + .to_string_lossy() + .into_owned(); + cqlib_string_free(ptr); + text +} + +// ===== Bindings lifecycle ===== + +#[test] +fn test_knowledge_bindings_lifecycle() { + let bindings = knowledge_bindings_new(); + assert!(!bindings.is_null()); + knowledge_bindings_free(bindings); + knowledge_bindings_free(std::ptr::null_mut()); +} + +// ===== Rule listing and lookup ===== + +#[test] +fn test_knowledge_rules_listing() { + let source = name(&format!("{CANCEL_H_DSL}\n{CANCEL_X_DSL}")); + let mut library: *mut CKnowledgeLibrary = std::ptr::null_mut(); + assert_eq!( + knowledge_library_from_dsl_str(source.as_ptr(), RULE_KIND_CANCEL, &mut library), + 0 + ); + + assert_eq!(knowledge_rules_len(library), 2); + let mut ids = [99u32; 2]; + assert_eq!(knowledge_rules(library, ids.as_mut_ptr(), 2), 2); + assert_eq!(ids, [0, 1]); + + // Truncated buffer reports the full total but only writes the prefix. + let mut truncated = [99u32; 1]; + assert_eq!(knowledge_rules(library, truncated.as_mut_ptr(), 1), 2); + assert_eq!(truncated[0], 0); + + assert_eq!(knowledge_rules(library, std::ptr::null_mut(), 2), 2); + assert_eq!(knowledge_rules_len(std::ptr::null()), 0); + assert_eq!(knowledge_rules(std::ptr::null(), ids.as_mut_ptr(), 2), 0); + + knowledge_library_free(library); +} + +#[test] +fn test_knowledge_get_by_name() { + let source = name(&format!("{CANCEL_H_DSL}\n{CANCEL_X_DSL}")); + let mut library: *mut CKnowledgeLibrary = std::ptr::null_mut(); + assert_eq!( + knowledge_library_from_dsl_str(source.as_ptr(), RULE_KIND_CANCEL, &mut library), + 0 + ); + + let cancel_h = name("cancel_h"); + let rule = knowledge_get_by_name(library, cancel_h.as_ptr()); + assert!(!rule.is_null()); + let rule_name = knowledge_rule_name(rule); + assert_eq!(read_string(rule_name), "cancel_h"); + knowledge_rule_free(rule); + + let absent = name("no_such_rule"); + assert!(knowledge_get_by_name(library, absent.as_ptr()).is_null()); + assert!(knowledge_get_by_name(std::ptr::null(), cancel_h.as_ptr()).is_null()); + assert!(knowledge_get_by_name(library, std::ptr::null()).is_null()); + + knowledge_library_free(library); +} + +// ===== Matching ===== + +#[test] +fn test_knowledge_match_rule_item() { + let rule = make_rule(MERGE_DSL); + let op_a = make_op("RZ", &[7], &[0.25]); + let op_b = make_op("RZ", &[7], &[0.5]); + let op_conflict = make_op("RZ", &[9], &[0.5]); + let op_h = make_op("H", &[7], &[]); + + let bindings = knowledge_bindings_new(); + + // Both pattern items bind against the same window, transactionally. + assert_eq!(knowledge_match_rule_item(rule, 0, op_a, bindings), 1); + assert_eq!(knowledge_match_rule_item(rule, 1, op_b, bindings), 1); + + // A conflicting qubit fails without corrupting existing bindings. + assert_eq!(knowledge_match_rule_item(rule, 0, op_conflict, bindings), 0); + // A different gate never matches the RZ pattern item. + assert_eq!(knowledge_match_rule_item(rule, 0, op_h, bindings), 0); + + // The rolled-back item can be re-matched on the original qubit. + assert_eq!(knowledge_match_rule_item(rule, 1, op_b, bindings), 1); + + // Error paths. + assert_eq!(knowledge_match_rule_item(rule, 5, op_a, bindings), -2); + assert_eq!( + knowledge_match_rule_item(std::ptr::null(), 0, op_a, bindings), + -1 + ); + assert_eq!( + knowledge_match_rule_item(rule, 0, std::ptr::null(), bindings), + -1 + ); + assert_eq!( + knowledge_match_rule_item(rule, 0, op_a, std::ptr::null_mut()), + -1 + ); + + knowledge_bindings_free(bindings); + operation_free(op_h); + operation_free(op_conflict); + operation_free(op_b); + operation_free(op_a); + knowledge_rule_free(rule); +} + +#[test] +fn test_knowledge_rule_matches_operations() { + let rule = make_rule(CANCEL_H_DSL); + let op_h1 = make_op("H", &[7], &[]); + let op_h2 = make_op("H", &[7], &[]); + let op_x = make_op("X", &[7], &[]); + + let adjacent = [op_h1 as *const COperation, op_h2 as *const COperation]; + let interrupted = [op_h1 as *const COperation, op_x as *const COperation]; + + let mut bindings: *mut CKnowledgeBindings = std::ptr::null_mut(); + assert_eq!( + knowledge_rule_matches_operations(rule, adjacent.as_ptr(), 2, &mut bindings), + 1 + ); + assert!(!bindings.is_null()); + knowledge_bindings_free(bindings); + + let mut no_match: *mut CKnowledgeBindings = std::ptr::null_mut(); + assert_eq!( + knowledge_rule_matches_operations(rule, interrupted.as_ptr(), 2, &mut no_match), + 0 + ); + assert!(no_match.is_null()); + + // The operation count must equal the pattern length. + assert_eq!( + knowledge_rule_matches_operations(rule, adjacent.as_ptr(), 1, &mut bindings), + -8 + ); + assert_eq!( + knowledge_rule_matches_operations(std::ptr::null(), adjacent.as_ptr(), 2, &mut bindings), + -1 + ); + assert_eq!( + knowledge_rule_matches_operations(rule, std::ptr::null(), 2, &mut bindings), + -1 + ); + assert_eq!( + knowledge_rule_matches_operations(rule, adjacent.as_ptr(), 2, std::ptr::null_mut()), + -1 + ); + + operation_free(op_x); + operation_free(op_h2); + operation_free(op_h1); + knowledge_rule_free(rule); +} + +#[test] +fn test_knowledge_conditions_hold() { + let rule = make_rule(CONDITION_DSL); + let op_hold_a = make_op("RZ", &[3], &[0.0]); + let op_hold_b = make_op("RZ", &[3], &[0.0]); + let op_fail_a = make_op("RZ", &[3], &[0.1]); + let op_fail_b = make_op("RZ", &[3], &[0.2]); + + let holding = [ + op_hold_a as *const COperation, + op_hold_b as *const COperation, + ]; + let failing = [ + op_fail_a as *const COperation, + op_fail_b as *const COperation, + ]; + + let mut bindings: *mut CKnowledgeBindings = std::ptr::null_mut(); + assert_eq!( + knowledge_rule_matches_operations(rule, holding.as_ptr(), 2, &mut bindings), + 1 + ); + assert_eq!(knowledge_conditions_hold(rule, bindings), 1); + knowledge_bindings_free(bindings); + + let mut failing_bindings: *mut CKnowledgeBindings = std::ptr::null_mut(); + assert_eq!( + knowledge_rule_matches_operations(rule, failing.as_ptr(), 2, &mut failing_bindings), + 1 + ); + assert_eq!(knowledge_conditions_hold(rule, failing_bindings), 0); + knowledge_bindings_free(failing_bindings); + + assert_eq!( + knowledge_conditions_hold(std::ptr::null(), std::ptr::null()), + -1 + ); + + operation_free(op_fail_b); + operation_free(op_fail_a); + operation_free(op_hold_b); + operation_free(op_hold_a); + knowledge_rule_free(rule); +} + +#[test] +fn test_knowledge_equivalent_to() { + let cancel_h_1 = make_rule(CANCEL_H_DSL); + let cancel_h_2 = make_rule(CANCEL_H_DSL); + let cancel_x = make_rule(CANCEL_X_DSL); + let merge = make_rule(MERGE_DSL); + let condition = make_rule(CONDITION_DSL); + + // Identical rules have equivalent items. + assert_eq!(knowledge_equivalent_to(cancel_h_1, 0, cancel_h_2, 0), 1); + assert_eq!(knowledge_equivalent_to(cancel_h_1, 1, cancel_h_2, 1), 1); + // Different gates are not equivalent. + assert_eq!(knowledge_equivalent_to(cancel_h_1, 0, cancel_x, 0), 0); + // RZ(a) in the merge rule matches RZ(a) in the condition rule. + assert_eq!(knowledge_equivalent_to(merge, 0, condition, 0), 1); + + // Out-of-bounds item indices and NULL guards. + assert_eq!(knowledge_equivalent_to(cancel_h_1, 7, cancel_h_2, 0), -2); + assert_eq!(knowledge_equivalent_to(cancel_h_1, 0, cancel_h_2, 7), -2); + assert_eq!( + knowledge_equivalent_to(std::ptr::null(), 0, cancel_h_2, 0), + -1 + ); + + knowledge_rule_free(condition); + knowledge_rule_free(merge); + knowledge_rule_free(cancel_x); + knowledge_rule_free(cancel_h_2); + knowledge_rule_free(cancel_h_1); +} + +// ===== Candidate and key filtering ===== + +#[test] +fn test_knowledge_candidates_for_first_instruction() { + let library = knowledge_library_builtin(); + assert!(!library.is_null()); + + let h = name("H"); + let total = knowledge_candidates_for_first_instruction_len(library, h.as_ptr()); + assert!(total >= 1); + let mut ids = vec![0u32; total]; + assert_eq!( + knowledge_candidates_for_first_instruction(library, h.as_ptr(), ids.as_mut_ptr(), total), + total + ); + // cancel_h is a builtin rule starting with H. + assert!( + ids.contains(&(knowledge_library_id_by_name(library, name("cancel_h").as_ptr()) as u32)) + ); + + // Truncated copies still report the full total. + let mut truncated = [0u32; 1]; + assert_eq!( + knowledge_candidates_for_first_instruction(library, h.as_ptr(), truncated.as_mut_ptr(), 1), + total + ); + assert_eq!(truncated[0], ids[0]); + + // Unknown gate names and NULL inputs report no candidates. + let unknown = name("NOPE"); + assert_eq!( + knowledge_candidates_for_first_instruction_len(library, unknown.as_ptr()), + 0 + ); + assert_eq!( + knowledge_candidates_for_first_instruction_len(std::ptr::null(), h.as_ptr()), + 0 + ); + assert_eq!( + knowledge_candidates_for_first_instruction( + library, + std::ptr::null(), + std::ptr::null_mut(), + 0 + ), + 0 + ); + + knowledge_library_free(library); +} + +#[test] +fn test_knowledge_collect_free_symbols() { + let merge = make_rule(MERGE_DSL); + let cancel = make_rule(CANCEL_H_DSL); + + assert_eq!(knowledge_collect_free_symbols_len(merge), 2); + let mut symbols: [*mut c_char; 2] = [std::ptr::null_mut(); 2]; + assert_eq!( + knowledge_collect_free_symbols(merge, symbols.as_mut_ptr(), 2), + 2 + ); + assert_eq!(read_string(symbols[0]), "a"); + assert_eq!(read_string(symbols[1]), "b"); + + assert_eq!(knowledge_collect_free_symbols_len(cancel), 0); + assert_eq!( + knowledge_collect_free_symbols(cancel, std::ptr::null_mut(), 0), + 0 + ); + + assert_eq!(knowledge_collect_free_symbols_len(std::ptr::null()), 0); + assert_eq!( + knowledge_collect_free_symbols(std::ptr::null(), std::ptr::null_mut(), 0), + 0 + ); + + knowledge_rule_free(cancel); + knowledge_rule_free(merge); +} + +#[test] +fn test_knowledge_filter_rule_ids_by_instruction_keys() { + let source = name(MERGE_DSL); + let mut library: *mut CKnowledgeLibrary = std::ptr::null_mut(); + assert_eq!( + knowledge_library_from_dsl_str(source.as_ptr(), RULE_KIND_MERGE, &mut library), + 0 + ); + + let rz = name("RZ"); + let op_names = [rz.as_ptr()]; + let target_names = [rz.as_ptr()]; + + let total = knowledge_filter_rule_ids_by_instruction_keys_len( + library, + op_names.as_ptr(), + 1, + target_names.as_ptr(), + 1, + ); + assert_eq!(total, 1); + let mut ids = [0u32; 1]; + assert_eq!( + knowledge_filter_rule_ids_by_instruction_keys( + library, + op_names.as_ptr(), + 1, + target_names.as_ptr(), + 1, + ids.as_mut_ptr(), + 1 + ), + 1 + ); + assert_eq!(ids, [0]); + + // A match side that does not cover RZ filters everything out. + let h = name("H"); + let h_names = [h.as_ptr()]; + assert_eq!( + knowledge_filter_rule_ids_by_instruction_keys_len( + library, + h_names.as_ptr(), + 1, + target_names.as_ptr(), + 1 + ), + 0 + ); + + // Unknown gate names and NULL inputs report nothing. + let unknown = name("NOPE"); + let unknown_names = [unknown.as_ptr()]; + assert_eq!( + knowledge_filter_rule_ids_by_instruction_keys_len( + library, + unknown_names.as_ptr(), + 1, + target_names.as_ptr(), + 1 + ), + 0 + ); + assert_eq!( + knowledge_filter_rule_ids_by_instruction_keys_len( + std::ptr::null(), + op_names.as_ptr(), + 1, + target_names.as_ptr(), + 1 + ), + 0 + ); + + knowledge_library_free(library); +} + +// ===== Rule extension and target queries ===== + +#[test] +fn test_knowledge_extend_rules() { + let cancel_h = make_rule(CANCEL_H_DSL); + let cancel_x = make_rule(CANCEL_X_DSL); + let rules = [ + cancel_h as *const CKnowledgeRule, + cancel_x as *const CKnowledgeRule, + ]; + + let library = knowledge_library_new(); + let mut ids = [0u32; 2]; + assert_eq!( + knowledge_extend_rules( + library, + rules.as_ptr(), + 2, + RULE_KIND_CANCEL, + ids.as_mut_ptr(), + 2 + ), + 0 + ); + assert_eq!(ids, [0, 1]); + assert_eq!(knowledge_library_len(library), 2); + assert_eq!( + knowledge_library_rule_kind(library, 0), + RULE_KIND_CANCEL as i32 + ); + + // Duplicate names are rejected and the library stays unchanged. + assert_eq!( + knowledge_extend_rules( + library, + rules.as_ptr(), + 2, + RULE_KIND_CANCEL, + ids.as_mut_ptr(), + 2 + ), + -4 + ); + assert_eq!(knowledge_library_len(library), 2); + + // Unknown kind tags and small buffers are rejected before insertion. + assert_eq!( + knowledge_extend_rules(library, rules.as_ptr(), 2, 99, ids.as_mut_ptr(), 2), + -8 + ); + assert_eq!( + knowledge_extend_rules( + library, + rules.as_ptr(), + 2, + RULE_KIND_CANCEL, + ids.as_mut_ptr(), + 1 + ), + -8 + ); + + // NULL guards. + assert_eq!( + knowledge_extend_rules( + std::ptr::null_mut(), + rules.as_ptr(), + 2, + RULE_KIND_CANCEL, + ids.as_mut_ptr(), + 2 + ), + -1 + ); + assert_eq!( + knowledge_extend_rules( + library, + std::ptr::null(), + 2, + RULE_KIND_CANCEL, + ids.as_mut_ptr(), + 2 + ), + -1 + ); + + knowledge_library_free(library); + knowledge_rule_free(cancel_x); + knowledge_rule_free(cancel_h); +} + +#[test] +fn test_knowledge_target_qubits() { + let merge = make_rule(MERGE_DSL); + let cancel = make_rule(CANCEL_H_DSL); + let shift = make_rule(SHIFT_DSL); + + assert_eq!(knowledge_target_qubits_len(merge), 1); + let mut qubits = [0u32; 1]; + assert_eq!(knowledge_target_qubits(merge, qubits.as_mut_ptr(), 1), 1); + assert_eq!(qubits, [0]); + + // The shift rule rewrites on rule-local labels 0 and 1. + assert_eq!(knowledge_target_qubits_len(shift), 2); + let mut shift_qubits = [0u32; 2]; + assert_eq!( + knowledge_target_qubits(shift, shift_qubits.as_mut_ptr(), 2), + 2 + ); + assert_eq!(shift_qubits, [0, 1]); + + // An empty rewrite target has no qubits. + assert_eq!(knowledge_target_qubits_len(cancel), 0); + assert_eq!(knowledge_target_qubits(cancel, std::ptr::null_mut(), 0), 0); + + assert_eq!(knowledge_target_qubits_len(std::ptr::null()), 0); + assert_eq!( + knowledge_target_qubits(std::ptr::null(), std::ptr::null_mut(), 0), + 0 + ); + + knowledge_rule_free(shift); + knowledge_rule_free(cancel); + knowledge_rule_free(merge); +} + +// ===== Target instantiation ===== + +#[test] +fn test_knowledge_instantiate_target() { + let rule = make_rule(MERGE_DSL); + let op_a = make_op("RZ", &[7], &[0.25]); + let op_b = make_op("RZ", &[7], &[0.5]); + + // Match the full window, then instantiate the rewrite target. + let adjacent = [op_a as *const COperation, op_b as *const COperation]; + let mut bindings: *mut CKnowledgeBindings = std::ptr::null_mut(); + assert_eq!( + knowledge_rule_matches_operations(rule, adjacent.as_ptr(), 2, &mut bindings), + 1 + ); + + let mut replacements: *mut CKnowledgeReplacements = std::ptr::null_mut(); + assert_eq!( + knowledge_instantiate_target(rule, bindings, &mut replacements), + 0 + ); + assert!(!replacements.is_null()); + assert_eq!(knowledge_replacements_len(replacements), 1); + + assert_eq!( + read_string(knowledge_replacement_instruction(replacements, 0)), + "RZ" + ); + + assert_eq!(knowledge_replacement_qubits_len(replacements, 0), 1); + let mut qubits = [0u32; 1]; + assert_eq!( + knowledge_replacement_qubits(replacements, 0, qubits.as_mut_ptr(), 1), + 1 + ); + assert_eq!(qubits, [7]); + + assert_eq!(knowledge_replacement_params_len(replacements, 0), 1); + let mut params = [0.0f64; 1]; + assert_eq!( + knowledge_replacement_params(replacements, 0, params.as_mut_ptr(), 1), + 1 + ); + assert!((params[0] - 0.75).abs() < 1e-12); + + // Out-of-bounds replacement indices are benign. + assert!(knowledge_replacement_instruction(replacements, 3).is_null()); + assert_eq!(knowledge_replacement_qubits_len(replacements, 3), 0); + assert_eq!( + knowledge_replacement_qubits(replacements, 3, std::ptr::null_mut(), 0), + 0 + ); + assert_eq!(knowledge_replacement_params_len(replacements, 3), 0); + assert_eq!( + knowledge_replacement_params(replacements, 3, std::ptr::null_mut(), 0), + 0 + ); + knowledge_replacements_free(replacements); + + // Instantiating against empty bindings fails on the unbound qubit. + let empty = knowledge_bindings_new(); + let mut unbound: *mut CKnowledgeReplacements = std::ptr::null_mut(); + assert_eq!(knowledge_instantiate_target(rule, empty, &mut unbound), -2); + knowledge_bindings_free(empty); + + // NULL guards. + assert_eq!( + knowledge_instantiate_target(std::ptr::null(), bindings, &mut unbound), + -1 + ); + assert_eq!( + knowledge_instantiate_target(rule, std::ptr::null(), &mut unbound), + -1 + ); + assert_eq!( + knowledge_instantiate_target(rule, bindings, std::ptr::null_mut()), + -1 + ); + knowledge_bindings_free(bindings); + + // NULL-safe accessors and frees. + assert_eq!(knowledge_replacements_len(std::ptr::null()), 0); + assert!(knowledge_replacement_instruction(std::ptr::null(), 0).is_null()); + knowledge_replacements_free(std::ptr::null_mut()); + + operation_free(op_b); + operation_free(op_a); + knowledge_rule_free(rule); +} diff --git a/crates/binding-c/tests/compile_pass2_test.rs b/crates/binding-c/tests/compile_pass2_test.rs new file mode 100644 index 00000000..30edee93 --- /dev/null +++ b/crates/binding-c/tests/compile_pass2_test.rs @@ -0,0 +1,944 @@ +//! Integration tests for the compile pass C ABI: decomposition/resynthesis, +//! knowledge rules, ancillary resources, and commutation checks. + +#![allow(dead_code)] + +use binding_c::circuit::{ + circuit_circuit_gate, circuit_cx, circuit_free, circuit_gate_free, circuit_h, + circuit_multi_control, circuit_new, circuit_num_operations, circuit_num_qubits, circuit_t, + circuit_to_gate, circuit_unitary, +}; +use binding_c::compile::*; +use binding_c::cqlib_string_free; +use binding_c::device::{device_free, device_from_edges, device_with_native_gates}; +use num_complex::Complex64; +use std::ffi::{CStr, CString}; + +fn name(s: &str) -> CString { + CString::new(s).unwrap() +} + +fn zeroed() -> T { + unsafe { std::mem::zeroed() } +} + +// ===== Decomposition and resynthesis ===== + +#[test] +fn test_decompose_expand_definitions() { + let inner = circuit_new(1); + assert_eq!(circuit_h(inner, 0), 0); + let gate_name = name("my_h"); + let gate = circuit_to_gate(inner, gate_name.as_ptr()); + assert!(!gate.is_null()); + + let outer = circuit_new(2); + let qubits = [0u32]; + assert_eq!( + circuit_circuit_gate(outer, gate, qubits.as_ptr(), 1, std::ptr::null(), 0), + 0 + ); + assert_eq!(circuit_num_operations(outer), 1); + + // The circuit-gate definition is expanded back into plain operations. + let expanded = decompose_expand_definitions(outer); + assert!(!expanded.is_null()); + assert_eq!(circuit_num_operations(expanded), 1); + circuit_free(expanded); + + circuit_gate_free(gate); + circuit_free(outer); + circuit_free(inner); +} + +#[test] +fn test_decompose_unitaries() { + let h_matrix = [ + Complex64::new(1.0 / 2.0f64.sqrt(), 0.0), + Complex64::new(1.0 / 2.0f64.sqrt(), 0.0), + Complex64::new(1.0 / 2.0f64.sqrt(), 0.0), + Complex64::new(-1.0 / 2.0f64.sqrt(), 0.0), + ]; + let label = name("my_h"); + let targets = [0u32]; + let circuit = circuit_new(2); + assert_eq!( + circuit_unitary( + circuit, + label.as_ptr(), + 1, + h_matrix.as_ptr(), + targets.as_ptr(), + 1 + ), + 0 + ); + + // The matrix-backed unitary is synthesized into standard operations. + let out = decompose_unitaries(circuit, decompose_unitary_config_default()); + assert!(!out.is_null()); + assert!(circuit_num_operations(out) >= 1); + circuit_free(out); + + // The diagnostic form additionally reports rule-cache statistics. + let mut stats: CDecompositionRuleStats = zeroed(); + let out = decompose_unitaries_with_rule_stats( + circuit, + decompose_unitary_config_default(), + &mut stats, + ); + assert!(!out.is_null()); + circuit_free(out); + + circuit_free(circuit); +} + +#[test] +fn test_decompose_mc_gates() { + let gate = name("X"); + let controls = [0u32, 1]; + let targets = [2u32]; + let circuit = circuit_new(3); + assert_eq!( + circuit_multi_control( + circuit, + gate.as_ptr(), + controls.as_ptr(), + 2, + targets.as_ptr(), + 1, + std::ptr::null(), + 0 + ), + 0 + ); + + let out = decompose_mc_gates(circuit, mc_gate_config_default()); + assert!(!out.is_null()); + assert!(circuit_num_operations(out) >= 1); + circuit_free(out); + + let mut stats: CDecompositionRuleStats = zeroed(); + let out = decompose_mc_gates_with_rule_stats(circuit, mc_gate_config_default(), &mut stats); + assert!(!out.is_null()); + circuit_free(out); + + // Device-bounded form caps the decomposition at the usable width. + let dev_name = name("line-3"); + let edges = [0u32, 1, 1, 2]; + let device = device_from_edges(dev_name.as_ptr(), 3, edges.as_ptr(), 2); + assert!(!device.is_null()); + let out = decompose_mc_gates_for_device(circuit, device, std::ptr::null()); + assert!(!out.is_null()); + circuit_free(out); + device_free(device); + + circuit_free(circuit); +} + +#[test] +fn test_resynthesize_two_qubit_blocks() { + let circuit = circuit_new(2); + assert_eq!(circuit_h(circuit, 0), 0); + assert_eq!(circuit_cx(circuit, 0, 1), 0); + + let out = resynthesize_two_qubit_blocks(circuit, resynthesis_config_normal()); + assert!(!out.is_null()); + assert!(circuit_num_operations(out) >= 1); + circuit_free(out); + + let out = resynthesize_two_qubit_blocks(circuit, resynthesis_config_enhanced()); + assert!(!out.is_null()); + circuit_free(out); + + circuit_free(circuit); +} + +#[test] +fn test_numeric_unitary_synthesis() { + // 1q synthesis of the identity yields U(0, 0, 0) with zero global phase. + let identity2 = [ + Complex64::new(1.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(1.0, 0.0), + ]; + let mut one_q: COneQubitUnitaryDecomposition = zeroed(); + assert_eq!( + synthesize_numeric_1q_unitary(identity2.as_ptr(), &mut one_q), + 0 + ); + assert!(one_q.theta.abs() < 1e-10); + assert!(one_q.phi.abs() < 1e-10); + assert!(one_q.lambda.abs() < 1e-10); + assert!(one_q.global_phase.abs() < 1e-10); + + // 2q synthesis of the identity succeeds and exposes its operations. + let mut identity4 = [Complex64::new(0.0, 0.0); 16]; + for (i, slot) in identity4.iter_mut().enumerate() { + if i % 5 == 0 { + *slot = Complex64::new(1.0, 0.0); + } + } + let synthesis = synthesize_numeric_2q_unitary(identity4.as_ptr(), 0, 1, TWO_QUBIT_BASIS_CX); + assert!(!synthesis.is_null()); + assert!(two_qubit_synthesis_global_phase(synthesis).is_finite()); + for index in 0..two_qubit_synthesis_num_operations(synthesis) { + let op_name = two_qubit_synthesis_operation_name(synthesis, index); + assert!(!op_name.is_null()); + cqlib_string_free(op_name); + let qubits_len = two_qubit_synthesis_operation_qubits_len(synthesis, index); + assert!(qubits_len >= 1); + let mut qubits = vec![0u32; qubits_len]; + assert_eq!( + two_qubit_synthesis_operation_qubits(synthesis, index, qubits.as_mut_ptr(), qubits_len), + qubits_len + ); + } + two_qubit_synthesis_free(synthesis); + + // KAK decomposition of the identity has zero interaction coordinates. + let kak = kak_decompose(identity4.as_ptr()); + assert!(!kak.is_null()); + let (mut a, mut b, mut c) = (0.0f64, 0.0f64, 0.0f64); + assert_eq!( + kak_decomposition_coordinates(kak, &mut a, &mut b, &mut c), + 0 + ); + assert!(a.abs() < 1e-6); + assert!(b.abs() < 1e-6); + assert!(c.abs() < 1e-6); + assert!(kak_decomposition_global_phase(kak).abs() < 1e-6); + let mut factor = [Complex64::new(0.0, 0.0); 4]; + assert_eq!( + kak_decomposition_local_factor(kak, KAK_FACTOR_K1L, factor.as_mut_ptr()), + 0 + ); + // Unknown factor tags are rejected. + assert_eq!( + kak_decomposition_local_factor(kak, 99, factor.as_mut_ptr()), + -8 + ); + kak_decomposition_free(kak); +} + +#[test] +fn test_target_basis_lowering() { + let names = [name("H"), name("RZ"), name("CX")]; + let name_ptrs: Vec<*const std::os::raw::c_char> = names.iter().map(|s| s.as_ptr()).collect(); + let lowerer = target_basis_lowerer_new(name_ptrs.as_ptr(), 3); + assert!(!lowerer.is_null()); + assert_eq!(target_basis_lowerer_num_gates(lowerer), 3); + + // T is outside {H, RZ, CX} and requires lowering. + let circuit = circuit_new(1); + assert_eq!(circuit_t(circuit, 0), 0); + assert_eq!(target_basis_lowerer_requires_lowering(lowerer, circuit), 1); + let lowered = target_basis_lowerer_apply(lowerer, circuit); + assert!(!lowered.is_null()); + assert!(circuit_num_operations(lowered) >= 1); + circuit_free(lowered); + + // A circuit already in the basis needs no lowering. + let basis_circuit = circuit_new(2); + assert_eq!(circuit_h(basis_circuit, 0), 0); + assert_eq!(circuit_cx(basis_circuit, 0, 1), 0); + assert_eq!( + target_basis_lowerer_requires_lowering(lowerer, basis_circuit), + 0 + ); + circuit_free(basis_circuit); + + circuit_free(circuit); + target_basis_lowerer_free(lowerer); + + // Unknown gate names and empty bases are rejected. + let bad = [name("NOPE")]; + let bad_ptrs = [bad[0].as_ptr()]; + assert!(target_basis_lowerer_new(bad_ptrs.as_ptr(), 1).is_null()); + assert!(target_basis_lowerer_new(std::ptr::null(), 0).is_null()); +} + +#[test] +fn test_decompose_lower_to_device() { + let dev_name = name("line-2"); + let edges = [0u32, 1]; + let device = device_from_edges(dev_name.as_ptr(), 2, edges.as_ptr(), 1); + assert!(!device.is_null()); + let native = name("H,RZ,CX"); + assert_eq!(device_with_native_gates(device, native.as_ptr()), 0); + + // A circuit already native to the device passes through unchanged. + let circuit = circuit_new(2); + assert_eq!(circuit_h(circuit, 0), 0); + assert_eq!(circuit_cx(circuit, 0, 1), 0); + let lowered = decompose_lower_to_device(circuit, device); + assert!(!lowered.is_null()); + assert_eq!(circuit_num_operations(lowered), 2); + circuit_free(lowered); + + circuit_free(circuit); + device_free(device); +} + +// ===== Ancillary resources ===== + +#[test] +fn test_resource_manager_lifecycle() { + let circuit = circuit_new(2); + assert_eq!(circuit_h(circuit, 0), 0); + + let mut policy = resource_policy_default(); + policy.max_pre_layout_clean_ancillas = 2; + + let manager = resource_manager_from_circuit(circuit, &policy, std::ptr::null()); + assert!(!manager.is_null()); + assert_eq!(resource_manager_verify_consistency(manager), 0); + assert_eq!(resource_manager_verify_idle(manager), 0); + + // Invalid requirement tags and oversized requests are rejected. + assert!(resource_manager_preview(manager, 99, 1, std::ptr::null(), 0).is_null()); + assert!( + resource_manager_preview( + manager, + RESOURCE_REQUIREMENT_CLEAN_ZERO, + 5, + std::ptr::null(), + 0 + ) + .is_null() + ); + + // Preview plans one fresh clean ancilla beyond the input width. + let plan = resource_manager_preview( + manager, + RESOURCE_REQUIREMENT_CLEAN_ZERO, + 1, + std::ptr::null(), + 0, + ); + assert!(!plan.is_null()); + assert_eq!(resource_plan_qubits_len(plan), 1); + assert_eq!(resource_plan_num_new_qubits(plan), 1); + assert_eq!( + resource_plan_requirement(plan), + RESOURCE_REQUIREMENT_CLEAN_ZERO as i32 + ); + let mut planned = [7u32]; + assert_eq!(resource_plan_qubits(plan, planned.as_mut_ptr(), 1), 1); + assert_eq!(planned[0], 2); + + // Commit mutates the manager's working circuit and reserves the qubit. + let lease = resource_manager_commit(manager, plan); + assert!(!lease.is_null()); + assert_eq!(resource_lease_qubits_len(lease), 1); + assert_eq!( + resource_lease_requirement(lease), + RESOURCE_REQUIREMENT_CLEAN_ZERO as i32 + ); + let working = resource_manager_circuit(manager); + assert!(!working.is_null()); + assert_eq!(circuit_num_qubits(working), 3); + circuit_free(working); + + // While the lease is active the manager is consistent but not idle. + assert_eq!(resource_manager_verify_consistency(manager), 0); + assert_eq!(resource_manager_verify_idle(manager), -6); + + // Release frees the lease; a second release is rejected. + assert_eq!(resource_manager_release(manager, lease), 0); + assert_eq!(resource_manager_verify_idle(manager), 0); + assert_eq!(resource_manager_release(manager, lease), -6); + + resource_lease_free(lease); + resource_manager_free(manager); + circuit_free(circuit); +} + +// ===== Knowledge rules ===== + +#[test] +fn test_knowledge_library_builtin() { + let builtin = knowledge_library_builtin(); + assert!(!builtin.is_null()); + assert!(knowledge_library_len(builtin) > 0); + assert_eq!(knowledge_library_is_empty(builtin), 0); + + // Builtin rules are registered under their compiler kinds. + assert!(knowledge_library_rules_by_kind_len(builtin, RULE_KIND_DECOMPOSE) > 0); + assert!(knowledge_library_rules_by_kind_len(builtin, RULE_KIND_CANCEL) > 0); + let decompose_len = knowledge_library_rules_by_kind_len(builtin, RULE_KIND_DECOMPOSE); + let mut ids = vec![0u32; decompose_len]; + assert_eq!( + knowledge_library_rules_by_kind( + builtin, + RULE_KIND_DECOMPOSE, + ids.as_mut_ptr(), + decompose_len + ), + decompose_len + ); + + // Metadata accessors cover every builtin rule. + for index in 0..knowledge_library_len(builtin) { + let rule_name = knowledge_library_rule_name(builtin, index); + assert!(!rule_name.is_null()); + cqlib_string_free(rule_name); + let kind = knowledge_library_rule_kind(builtin, index); + assert!(kind >= RULE_KIND_SIMPLIFY as i32); + assert!(kind <= RULE_KIND_OTHER as i32); + assert!(knowledge_library_rule_pattern_len(builtin, index) >= 1); + } + let first = knowledge_library_rule_first_instruction(builtin, 0); + assert!(!first.is_null()); + cqlib_string_free(first); + + // Out-of-bounds indices are rejected. + assert!(knowledge_library_rule_name(builtin, usize::MAX).is_null()); + assert_eq!(knowledge_library_rule_kind(builtin, usize::MAX), -8); + + knowledge_library_free(builtin); +} + +#[test] +fn test_knowledge_library_from_dsl() { + let source = name( + "rule merge_rz {\n match {\n RZ(a) 0\n RZ(b) 0\n }\n rewrite {\n RZ(a + b) 0\n }\n}", + ); + let mut library: *mut CKnowledgeLibrary = std::ptr::null_mut(); + assert_eq!( + knowledge_library_from_dsl_str(source.as_ptr(), RULE_KIND_MERGE, &mut library), + 0 + ); + assert!(!library.is_null()); + assert_eq!(knowledge_library_len(library), 1); + + let merge_name = name("merge_rz"); + assert_eq!(knowledge_library_contains(library, merge_name.as_ptr()), 1); + assert_eq!( + knowledge_library_id_by_name(library, merge_name.as_ptr()), + 0 + ); + assert_eq!(knowledge_library_rule_pattern_len(library, 0), 2); + assert_eq!(knowledge_library_rule_rewrite_len(library, 0), 1); + assert_eq!(knowledge_library_rule_qubit_count(library, 0), 1); + assert_eq!(knowledge_library_rule_cost_delta(library, 0), -1); + assert_eq!(knowledge_library_rule_has_conditions(library, 0), 0); + assert_eq!( + knowledge_library_rule_kind(library, 0), + RULE_KIND_MERGE as i32 + ); + let rule_name = knowledge_library_rule_name(library, 0); + assert!(!rule_name.is_null()); + unsafe { + assert_eq!(CStr::from_ptr(rule_name).to_str().unwrap(), "merge_rz"); + } + cqlib_string_free(rule_name); + + // Unknown names report absence, not errors. + let absent = name("no_such_rule"); + assert_eq!(knowledge_library_contains(library, absent.as_ptr()), 0); + assert_eq!(knowledge_library_id_by_name(library, absent.as_ptr()), -1); + + // Unknown kind tags are rejected without touching `out`. + assert_eq!( + knowledge_library_from_dsl_str(source.as_ptr(), 99, &mut library), + -8 + ); + + knowledge_library_free(library); + + // Malformed DSL text fails with the parse error code. + let bad = name("this is not a rule"); + let mut bad_library: *mut CKnowledgeLibrary = std::ptr::null_mut(); + assert_eq!( + knowledge_library_from_dsl_str(bad.as_ptr(), RULE_KIND_OTHER, &mut bad_library), + -4 + ); + + // Missing files fail with the I/O error code. + let missing = name("no_such_rule_file.rule"); + assert_eq!( + knowledge_library_from_dsl_file(missing.as_ptr(), RULE_KIND_OTHER, &mut bad_library), + -5 + ); + + // NULL guards. + assert_eq!( + knowledge_library_from_dsl_str(std::ptr::null(), RULE_KIND_OTHER, std::ptr::null_mut()), + -1 + ); + assert_eq!( + knowledge_library_from_dsl_file(std::ptr::null(), RULE_KIND_OTHER, std::ptr::null_mut()), + -1 + ); +} + +#[test] +fn test_knowledge_rule_add() { + let source = + name("rule cancel_h {\n match {\n H 0\n H 0\n }\n rewrite {}\n}"); + let mut rule: *mut CKnowledgeRule = std::ptr::null_mut(); + assert_eq!(knowledge_rule_from_dsl(source.as_ptr(), &mut rule), 0); + assert!(!rule.is_null()); + assert_eq!(knowledge_rule_validate(rule), 0); + assert_eq!(knowledge_rule_pattern_len(rule), 2); + assert_eq!(knowledge_rule_rewrite_len(rule), 0); + assert_eq!(knowledge_rule_num_qubits(rule), 1); + assert_eq!(knowledge_rule_has_conditions(rule), 0); + let rule_name = knowledge_rule_name(rule); + assert!(!rule_name.is_null()); + unsafe { + assert_eq!(CStr::from_ptr(rule_name).to_str().unwrap(), "cancel_h"); + } + cqlib_string_free(rule_name); + + // Insertion into a fresh library assigns id 0. + let library = knowledge_library_new(); + assert!(!library.is_null()); + assert_eq!(knowledge_library_is_empty(library), 1); + assert_eq!( + knowledge_library_add_rule(library, rule, RULE_KIND_CANCEL), + 0 + ); + assert_eq!(knowledge_library_len(library), 1); + assert_eq!( + knowledge_library_rule_kind(library, 0), + RULE_KIND_CANCEL as i32 + ); + + // Duplicate names and unknown kind tags are rejected. + assert_eq!( + knowledge_library_add_rule(library, rule, RULE_KIND_CANCEL), + -4 + ); + assert_eq!(knowledge_library_add_rule(library, rule, 99), -8); + + knowledge_library_free(library); + knowledge_rule_free(rule); + + // Malformed DSL text fails with the parse error code. + let bad = name("this is not a rule"); + let mut bad_rule: *mut CKnowledgeRule = std::ptr::null_mut(); + assert_eq!(knowledge_rule_from_dsl(bad.as_ptr(), &mut bad_rule), -4); + assert_eq!( + knowledge_rule_from_dsl(std::ptr::null(), std::ptr::null_mut()), + -1 + ); +} + +// ===== Commutation ===== + +#[test] +fn test_commutation_checks() { + let x = name("X"); + let z = name("Z"); + let rz = name("RZ"); + let q0 = [0u32]; + let q1 = [1u32]; + let angle_a = [0.3f64]; + let angle_b = [0.5f64]; + let mut phase = 0.0f64; + + // Disjoint supports commute exactly. + assert_eq!( + commutation_check( + x.as_ptr(), + q0.as_ptr(), + 1, + std::ptr::null(), + 0, + x.as_ptr(), + q1.as_ptr(), + 1, + std::ptr::null(), + 0, + &mut phase, + ), + COMMUTATION_RESULT_EXACT + ); + assert_eq!(phase, 0.0); + + // The same application commutes exactly. + assert_eq!( + commutation_check( + x.as_ptr(), + q0.as_ptr(), + 1, + std::ptr::null(), + 0, + x.as_ptr(), + q0.as_ptr(), + 1, + std::ptr::null(), + 0, + &mut phase, + ), + COMMUTATION_RESULT_EXACT + ); + + // X and Z on the same qubit anti-commute, proving commutation up to pi. + phase = 0.0; + assert_eq!( + commutation_check( + x.as_ptr(), + q0.as_ptr(), + 1, + std::ptr::null(), + 0, + z.as_ptr(), + q0.as_ptr(), + 1, + std::ptr::null(), + 0, + &mut phase, + ), + COMMUTATION_RESULT_UP_TO_GLOBAL_PHASE + ); + assert!((phase - std::f64::consts::PI).abs() < 1e-9); + + // Diagonal rotations commute exactly with numeric parameters. + assert_eq!( + commutation_check( + rz.as_ptr(), + q0.as_ptr(), + 1, + angle_a.as_ptr(), + 1, + rz.as_ptr(), + q0.as_ptr(), + 1, + angle_b.as_ptr(), + 1, + &mut phase, + ), + COMMUTATION_RESULT_EXACT + ); + + // The algebraic-only variant proves the same facts. + assert_eq!( + commutation_check_algebraic( + x.as_ptr(), + q0.as_ptr(), + 1, + std::ptr::null(), + 0, + x.as_ptr(), + q1.as_ptr(), + 1, + std::ptr::null(), + 0, + &mut phase, + ), + COMMUTATION_RESULT_EXACT + ); + phase = 0.0; + assert_eq!( + commutation_check_algebraic( + x.as_ptr(), + q0.as_ptr(), + 1, + std::ptr::null(), + 0, + z.as_ptr(), + q0.as_ptr(), + 1, + std::ptr::null(), + 0, + &mut phase, + ), + COMMUTATION_RESULT_UP_TO_GLOBAL_PHASE + ); + assert!((phase - std::f64::consts::PI).abs() < 1e-9); + + // Unknown gate names and NULL inputs are rejected. + let nope = name("NOPE"); + assert_eq!( + commutation_check( + nope.as_ptr(), + q0.as_ptr(), + 1, + std::ptr::null(), + 0, + x.as_ptr(), + q0.as_ptr(), + 1, + std::ptr::null(), + 0, + &mut phase, + ), + -8 + ); + assert_eq!( + commutation_check( + std::ptr::null(), + q0.as_ptr(), + 1, + std::ptr::null(), + 0, + x.as_ptr(), + q0.as_ptr(), + 1, + std::ptr::null(), + 0, + &mut phase, + ), + -1 + ); +} + +#[test] +fn test_commutation_checker() { + let config = commutation_config_default(); + assert_eq!(config.enable_rule_oracle, 1); + assert_eq!(config.enable_matrix_fallback, 1); + assert_eq!(config.max_matrix_qubits, 4); + + let x = name("X"); + let q0 = [0u32]; + let q1 = [1u32]; + let mut phase = 0.0f64; + + let checker = commutation_checker_new(config); + assert!(!checker.is_null()); + assert_eq!( + commutation_checker_check( + checker, + x.as_ptr(), + q0.as_ptr(), + 1, + std::ptr::null(), + 0, + x.as_ptr(), + q1.as_ptr(), + 1, + std::ptr::null(), + 0, + &mut phase, + ), + COMMUTATION_RESULT_EXACT + ); + assert_eq!( + commutation_checker_check( + std::ptr::null(), + x.as_ptr(), + q0.as_ptr(), + 1, + std::ptr::null(), + 0, + x.as_ptr(), + q1.as_ptr(), + 1, + std::ptr::null(), + 0, + &mut phase, + ), + -1 + ); + commutation_checker_free(checker); + + // A checker can draw its commutation rules from a knowledge library. + let library = knowledge_library_builtin(); + assert!(!library.is_null()); + let from_library = commutation_checker_from_library(library, commutation_config_default()); + assert!(!from_library.is_null()); + assert!( + commutation_checker_from_library(std::ptr::null(), commutation_config_default()).is_null() + ); + commutation_checker_free(from_library); + knowledge_library_free(library); +} + +// ===== NULL guards ===== + +#[test] +fn test_null_guards() { + // Decompose entry points. + assert!(decompose_expand_definitions(std::ptr::null()).is_null()); + assert!(decompose_unitaries(std::ptr::null(), decompose_unitary_config_default()).is_null()); + assert!( + decompose_unitaries_with_rule_stats( + std::ptr::null(), + decompose_unitary_config_default(), + std::ptr::null_mut() + ) + .is_null() + ); + assert!(decompose_mc_gates(std::ptr::null(), mc_gate_config_default()).is_null()); + assert!( + decompose_mc_gates_with_rule_stats( + std::ptr::null(), + mc_gate_config_default(), + std::ptr::null_mut() + ) + .is_null() + ); + assert!( + decompose_mc_gates_for_device(std::ptr::null(), std::ptr::null(), std::ptr::null()) + .is_null() + ); + assert!(resynthesize_two_qubit_blocks(std::ptr::null(), resynthesis_config_normal()).is_null()); + assert!(synthesize_numeric_2q_unitary(std::ptr::null(), 0, 1, TWO_QUBIT_BASIS_CX).is_null()); + assert!(kak_decompose(std::ptr::null()).is_null()); + assert_eq!( + synthesize_numeric_1q_unitary(std::ptr::null(), std::ptr::null_mut()), + -1 + ); + let (mut a, mut b, mut c) = (0.0f64, 0.0f64, 0.0f64); + assert_eq!( + kak_decomposition_coordinates(std::ptr::null(), &mut a, &mut b, &mut c), + -1 + ); + let mut factor = [Complex64::new(0.0, 0.0); 4]; + assert_eq!( + kak_decomposition_local_factor(std::ptr::null(), KAK_FACTOR_K1L, factor.as_mut_ptr()), + -1 + ); + assert_eq!(two_qubit_synthesis_num_operations(std::ptr::null()), 0); + assert_eq!(two_qubit_synthesis_global_phase(std::ptr::null()), 0.0); + assert!(two_qubit_synthesis_operation_name(std::ptr::null(), 0).is_null()); + assert_eq!( + two_qubit_synthesis_operation_qubits(std::ptr::null(), 0, std::ptr::null_mut(), 0), + 0 + ); + assert_eq!( + two_qubit_synthesis_operation_qubits_len(std::ptr::null(), 0), + 0 + ); + assert_eq!(kak_decomposition_global_phase(std::ptr::null()), 0.0); + assert!(target_basis_lowerer_apply(std::ptr::null(), std::ptr::null()).is_null()); + assert!(target_basis_lowerer_requires_lowering(std::ptr::null(), std::ptr::null()) == -1); + assert_eq!(target_basis_lowerer_num_gates(std::ptr::null()), 0); + assert!(decompose_lower_to_device(std::ptr::null(), std::ptr::null()).is_null()); + two_qubit_synthesis_free(std::ptr::null_mut()); + kak_decomposition_free(std::ptr::null_mut()); + target_basis_lowerer_free(std::ptr::null_mut()); + + // Resource entry points. + assert!( + resource_manager_from_circuit(std::ptr::null(), std::ptr::null(), std::ptr::null()) + .is_null() + ); + assert!( + resource_manager_preview( + std::ptr::null(), + RESOURCE_REQUIREMENT_CLEAN_ZERO, + 1, + std::ptr::null(), + 0 + ) + .is_null() + ); + assert!(resource_manager_commit(std::ptr::null_mut(), std::ptr::null_mut()).is_null()); + assert!(resource_manager_circuit(std::ptr::null()).is_null()); + assert_eq!( + resource_manager_release(std::ptr::null_mut(), std::ptr::null()), + -1 + ); + assert_eq!(resource_manager_enter_post_layout(std::ptr::null_mut()), -1); + assert_eq!(resource_manager_verify_consistency(std::ptr::null()), -1); + assert_eq!(resource_manager_verify_idle(std::ptr::null()), -1); + assert_eq!(resource_plan_qubits_len(std::ptr::null()), 0); + assert_eq!(resource_plan_num_new_qubits(std::ptr::null()), 0); + assert_eq!(resource_plan_requirement(std::ptr::null()), -1); + assert_eq!(resource_lease_qubits_len(std::ptr::null()), 0); + assert_eq!(resource_lease_requirement(std::ptr::null()), -1); + resource_manager_free(std::ptr::null_mut()); + resource_plan_free(std::ptr::null_mut()); + resource_lease_free(std::ptr::null_mut()); + + // Knowledge entry points. + assert_eq!(knowledge_library_len(std::ptr::null()), 0); + assert_eq!(knowledge_library_is_empty(std::ptr::null()), -1); + assert_eq!( + knowledge_library_contains(std::ptr::null(), std::ptr::null()), + -1 + ); + assert_eq!( + knowledge_library_id_by_name(std::ptr::null(), std::ptr::null()), + -1 + ); + assert_eq!( + knowledge_library_rules_by_kind_len(std::ptr::null(), RULE_KIND_OTHER), + 0 + ); + assert_eq!( + knowledge_library_rules_by_kind(std::ptr::null(), RULE_KIND_OTHER, std::ptr::null_mut(), 0), + 0 + ); + assert!(knowledge_library_rule_name(std::ptr::null(), 0).is_null()); + assert_eq!(knowledge_library_rule_kind(std::ptr::null(), 0), -1); + assert!(knowledge_library_rule_first_instruction(std::ptr::null(), 0).is_null()); + assert_eq!(knowledge_library_rule_pattern_len(std::ptr::null(), 0), 0); + assert_eq!(knowledge_library_rule_rewrite_len(std::ptr::null(), 0), 0); + assert_eq!(knowledge_library_rule_qubit_count(std::ptr::null(), 0), 0); + assert_eq!(knowledge_library_rule_cost_delta(std::ptr::null(), 0), 0); + assert_eq!( + knowledge_library_rule_has_conditions(std::ptr::null(), 0), + -1 + ); + assert_eq!( + knowledge_library_add_rule(std::ptr::null_mut(), std::ptr::null(), RULE_KIND_OTHER), + -1 + ); + assert!(knowledge_rule_name(std::ptr::null()).is_null()); + assert_eq!(knowledge_rule_num_qubits(std::ptr::null()), 0); + assert_eq!(knowledge_rule_pattern_len(std::ptr::null()), 0); + assert_eq!(knowledge_rule_rewrite_len(std::ptr::null()), 0); + assert_eq!(knowledge_rule_has_conditions(std::ptr::null()), -1); + assert_eq!(knowledge_rule_validate(std::ptr::null()), -1); + knowledge_library_free(std::ptr::null_mut()); + knowledge_rule_free(std::ptr::null_mut()); + + // Commutation entry points. + assert_eq!( + commutation_check( + std::ptr::null(), + std::ptr::null(), + 0, + std::ptr::null(), + 0, + std::ptr::null(), + std::ptr::null(), + 0, + std::ptr::null(), + 0, + std::ptr::null_mut(), + ), + -1 + ); + assert_eq!( + commutation_check_algebraic( + std::ptr::null(), + std::ptr::null(), + 0, + std::ptr::null(), + 0, + std::ptr::null(), + std::ptr::null(), + 0, + std::ptr::null(), + 0, + std::ptr::null_mut(), + ), + -1 + ); + assert_eq!( + commutation_checker_check( + std::ptr::null(), + std::ptr::null(), + std::ptr::null(), + 0, + std::ptr::null(), + 0, + std::ptr::null(), + std::ptr::null(), + 0, + std::ptr::null(), + 0, + std::ptr::null_mut(), + ), + -1 + ); + commutation_checker_free(std::ptr::null_mut()); +} diff --git a/crates/binding-c/tests/compile_pass_test.rs b/crates/binding-c/tests/compile_pass_test.rs new file mode 100644 index 00000000..17be787d --- /dev/null +++ b/crates/binding-c/tests/compile_pass_test.rs @@ -0,0 +1,724 @@ +//! Integration tests for the compile pass C ABI: layout, routing, transform, +//! and low-level SABRE entry points. + +#![allow(dead_code)] + +use binding_c::circuit::{ + circuit_cx, circuit_free, circuit_h, circuit_new, circuit_num_operations, circuit_s, + circuit_sdg, circuit_t, circuit_tdg, +}; +use binding_c::compile::*; +use binding_c::cqlib_string_free; +use binding_c::device::*; +use std::ffi::CString; + +fn name(s: &str) -> CString { + CString::new(s).unwrap() +} + +fn zeroed() -> T { + unsafe { std::mem::zeroed() } +} + +/// Directed line 0 -> 1 -> 2 built from an edge list. +fn line3_device(dev_name: &CString) -> *mut CDevice { + let edges: [u32; 4] = [0, 1, 1, 2]; + device_from_edges(dev_name.as_ptr(), 3, edges.as_ptr(), 2) +} + +fn identity_layout(n: usize) -> *mut CLayout { + let qubits: Vec = (0..n as u32).collect(); + layout_new(qubits.as_ptr(), n, qubits.as_ptr(), n) +} + +// ===== Layout entry points ===== + +#[test] +fn test_layout_entry_points() { + let dev_name = name("line-3"); + let device = line3_device(&dev_name); + assert!(!device.is_null()); + + let circuit = circuit_new(3); + assert_eq!(circuit_cx(circuit, 0, 2), 0); + + // Configuration constructors are callable and match the documented + // defaults. + assert_eq!(vf2_layout_config_default().candidate_limit, 10); + assert_eq!(sabre_config_default().routing_trials, 1); + + // Trivial layout maps logical i to physical i. + let trivial = trivial_layout(circuit, device, LAYOUT_OBJECTIVE_TOPOLOGY_ONLY); + assert!(!trivial.is_null()); + let layout = layout_result_layout(trivial); + assert!(!layout.is_null()); + assert_eq!(layout_num_logical(layout), 3); + assert_eq!(layout_get(layout, 0), 0); + assert_eq!(layout_get(layout, 2), 2); + assert!(layout_result_candidates_evaluated(trivial) >= 1); + // Every note is a valid C string. + for i in 0..layout_result_notes_len(trivial) { + let note = layout_result_note(trivial, i); + assert!(!note.is_null()); + cqlib_string_free(note); + } + layout_free(layout); + layout_result_free(trivial); + + // Greedy layout succeeds on the same circuit. + let greedy = greedy_layout(circuit, device, LAYOUT_OBJECTIVE_TOPOLOGY_ONLY); + assert!(!greedy.is_null()); + layout_result_free(greedy); + + // SABRE layout with the default config. + let sabre = sabre_layout( + circuit, + device, + LAYOUT_OBJECTIVE_TOPOLOGY_ONLY, + std::ptr::null(), + ); + assert!(!sabre.is_null()); + assert!(layout_result_candidates_evaluated(sabre) >= 1); + layout_result_free(sabre); + + // Unknown objective tag is rejected. + assert!(trivial_layout(circuit, device, 99).is_null()); + + circuit_free(circuit); + device_free(device); +} + +#[test] +fn test_vf2_perfect_layout() { + let dev_name = name("line-3"); + let device = line3_device(&dev_name); + assert!(!device.is_null()); + + // Interactions (0, 1) and (1, 2) fit the line perfectly. + let circuit = circuit_new(3); + assert_eq!(circuit_cx(circuit, 0, 1), 0); + assert_eq!(circuit_cx(circuit, 1, 2), 0); + + let result = vf2_perfect_layout( + circuit, + device, + LAYOUT_OBJECTIVE_TOPOLOGY_ONLY, + std::ptr::null(), + ); + assert!(!result.is_null()); + assert_eq!(layout_result_is_perfect(result), 1); + layout_result_free(result); + + circuit_free(circuit); + device_free(device); +} + +#[test] +fn test_layout_analysis_and_scoring() { + let dev_name = name("line-3"); + let device = line3_device(&dev_name); + assert!(!device.is_null()); + + let circuit = circuit_new(3); + assert_eq!(circuit_cx(circuit, 0, 2), 0); + assert_eq!(circuit_cx(circuit, 0, 2), 0); + + let analysis = analyze_circuit_for_layout(circuit); + assert!(!analysis.is_null()); + assert_eq!(circuit_layout_analysis_num_logical(analysis), 3); + let mut qubits = [0u32; 3]; + assert_eq!( + circuit_layout_analysis_logical_qubits(analysis, qubits.as_mut_ptr(), 3), + 3 + ); + assert_eq!(qubits, [0, 1, 2]); + // Both operations collapse into one weighted interaction pair. + assert_eq!(circuit_layout_analysis_interactions_len(analysis), 1); + let mut interaction: CInteraction = zeroed(); + assert_eq!( + circuit_layout_analysis_interaction(analysis, 0, &mut interaction), + 0 + ); + assert_eq!(interaction.left, 0); + assert_eq!(interaction.right, 2); + assert_eq!(interaction.weight, 2.0); + assert_eq!( + circuit_layout_analysis_interaction(analysis, 5, &mut interaction), + -8 + ); + + let graph = physical_layout_graph_from_device(device); + assert!(!graph.is_null()); + assert_eq!(physical_layout_graph_num_physical(graph), 3); + assert_eq!(physical_layout_graph_distance(graph, 0, 2), 2); + assert_eq!(physical_layout_graph_distance(graph, 0, 1), 1); + assert_eq!(physical_layout_graph_is_adjacent_undirected(graph, 0, 2), 0); + assert_eq!(physical_layout_graph_is_adjacent_undirected(graph, 1, 0), 1); + assert_eq!( + physical_layout_graph_supports_directed_coupling(graph, 0, 1), + 1 + ); + assert_eq!( + physical_layout_graph_supports_directed_coupling(graph, 1, 0), + 0 + ); + assert_eq!(physical_layout_graph_has_fidelity_data(graph), 0); + + let trivial = trivial_layout(circuit, device, LAYOUT_OBJECTIVE_TOPOLOGY_ONLY); + assert!(!trivial.is_null()); + let layout = layout_result_layout(trivial); + assert!(!layout.is_null()); + + let mut score: CLayoutScore = zeroed(); + assert_eq!( + score_layout( + LAYOUT_OBJECTIVE_TOPOLOGY_ONLY, + analysis, + graph, + layout, + &mut score + ), + 0 + ); + assert!(score.total > 0.0); + assert_eq!(score_layout(99, analysis, graph, layout, &mut score), -8); + + layout_free(layout); + layout_result_free(trivial); + physical_layout_graph_free(graph); + circuit_layout_analysis_free(analysis); + circuit_free(circuit); + device_free(device); +} + +#[test] +fn test_prepared_layout_pipeline() { + let dev_name = name("line-3"); + let device = line3_device(&dev_name); + assert!(!device.is_null()); + // Exact device-native SABRE requires the device to declare routing + // capability: native gates, or per-qubit/edge native instructions. + let gates = name("H,CX,SWAP"); + assert_eq!(device_with_native_gates(device, gates.as_ptr()), 0); + + let circuit = circuit_new(3); + assert_eq!(circuit_cx(circuit, 0, 2), 0); + + let analysis = analyze_circuit_for_layout(circuit); + assert!(!analysis.is_null()); + let graph = physical_layout_graph_from_device(device); + assert!(!graph.is_null()); + + let trivial = trivial_layout_prepared(analysis, graph, LAYOUT_OBJECTIVE_TOPOLOGY_ONLY); + assert!(!trivial.is_null()); + layout_result_free(trivial); + + // Prepared SABRE circuit/target pipeline. + let prepared = prepare_sabre_circuit(circuit); + assert!(!prepared.is_null()); + assert_eq!(prepared_sabre_circuit_logical_qubits_len(prepared), 3); + let mut logical = [0u32; 3]; + assert_eq!( + prepared_sabre_circuit_logical_qubits(prepared, logical.as_mut_ptr(), 3), + 3 + ); + assert_eq!(logical, [0, 1, 2]); + + let target = prepare_sabre_device_target(prepared, device); + assert!(!target.is_null()); + let target_graph = prepared_sabre_target_physical(target); + assert!(!target_graph.is_null()); + assert_eq!(physical_layout_graph_num_physical(target_graph), 3); + physical_layout_graph_free(target_graph); + + let sabre = sabre_layout_prepared( + prepared, + target, + LAYOUT_OBJECTIVE_TOPOLOGY_ONLY, + std::ptr::null(), + ); + assert!(!sabre.is_null()); + layout_result_free(sabre); + + prepared_sabre_target_free(target); + prepared_sabre_circuit_free(prepared); + physical_layout_graph_free(graph); + circuit_layout_analysis_free(analysis); + circuit_free(circuit); + device_free(device); +} + +// ===== Routing ===== + +#[test] +fn test_route_with_layout() { + let dev_name = name("line-3"); + let device = line3_device(&dev_name); + assert!(!device.is_null()); + + let circuit = circuit_new(3); + assert_eq!(circuit_cx(circuit, 0, 2), 0); + let layout = identity_layout(3); + assert!(!layout.is_null()); + + // Identity layout needs at least one SWAP for cx(0,2) on the line. + let routed = route_with_layout(circuit, device, layout, std::ptr::null()); + assert!(!routed.is_null()); + assert!(routed_circuit_swap_count(routed) >= 1); + assert_eq!(routed_circuit_changed(routed, circuit), 1); + + let final_layout = routed_circuit_final_layout(routed); + assert!(!final_layout.is_null()); + assert_eq!(layout_num_logical(final_layout), 3); + layout_free(final_layout); + + let initial_layout = routed_circuit_initial_layout(routed); + assert!(!initial_layout.is_null()); + layout_free(initial_layout); + + let physical = routed_circuit_circuit(routed); + assert!(!physical.is_null()); + assert!(circuit_num_operations(physical) >= 1); + circuit_free(physical); + + let mut diagnostics: CSabreRoutingDiagnostics = zeroed(); + assert_eq!(routed_circuit_diagnostics(routed, &mut diagnostics), 0); + assert!(diagnostics.trials_evaluated >= 1); + + routed_circuit_free(routed); + layout_free(layout); + circuit_free(circuit); + device_free(device); +} + +#[test] +fn test_route_sabre() { + let dev_name = name("line-3"); + let device = line3_device(&dev_name); + assert!(!device.is_null()); + + let circuit = circuit_new(3); + assert_eq!(circuit_cx(circuit, 0, 2), 0); + + let result = route_sabre( + circuit, + device, + LAYOUT_OBJECTIVE_TOPOLOGY_ONLY, + std::ptr::null(), + ); + assert!(!result.is_null()); + assert!(sabre_route_result_layout_candidates_evaluated(result) >= 1); + for i in 0..sabre_route_result_layout_notes_len(result) { + let note = sabre_route_result_layout_note(result, i); + assert!(!note.is_null()); + cqlib_string_free(note); + } + + // The layout score is recorded for the selected initial layout. + let mut score: CLayoutScore = zeroed(); + assert_eq!(sabre_route_result_layout_score(result, &mut score), 0); + + // The nested routed circuit exposes the same metadata. + let routed = sabre_route_result_routed(result); + assert!(!routed.is_null()); + assert_eq!( + sabre_route_result_swap_count(result), + routed_circuit_swap_count(routed) + ); + routed_circuit_free(routed); + + let physical = sabre_route_result_circuit(result); + assert!(!physical.is_null()); + circuit_free(physical); + + let mut diagnostics: CSabreRoutingDiagnostics = zeroed(); + assert_eq!(sabre_route_result_diagnostics(result, &mut diagnostics), 0); + + let final_layout = sabre_route_result_final_layout(result); + assert!(!final_layout.is_null()); + layout_free(final_layout); + + sabre_route_result_free(result); + circuit_free(circuit); + device_free(device); +} + +// ===== Low-level SABRE ===== + +#[test] +fn test_sabre_route_low_level() { + let dev_name = name("line-3"); + let device = line3_device(&dev_name); + assert!(!device.is_null()); + + let circuit = circuit_new(3); + assert_eq!(circuit_cx(circuit, 0, 2), 0); + let layout = identity_layout(3); + assert!(!layout.is_null()); + + let result = sabre_route(circuit, device, layout, std::ptr::null()); + assert!(!result.is_null()); + assert!(sabre_routing_result_swap_count(result) >= 1); + + let physical = sabre_routing_result_circuit(result); + assert!(!physical.is_null()); + assert!(circuit_num_operations(physical) >= 1); + circuit_free(physical); + + let initial = sabre_routing_result_initial_layout(result); + assert!(!initial.is_null()); + assert_eq!(layout_num_logical(initial), 3); + layout_free(initial); + + let final_layout = sabre_routing_result_final_layout(result); + assert!(!final_layout.is_null()); + layout_free(final_layout); + + let mut diagnostics: CSabreRoutingDiagnostics = zeroed(); + assert_eq!( + sabre_routing_result_diagnostics(result, &mut diagnostics), + 0 + ); + assert!(diagnostics.trials_evaluated >= 1); + + sabre_routing_result_free(result); + layout_free(layout); + circuit_free(circuit); + device_free(device); +} + +#[test] +fn test_normalize_and_validate_reachable() { + let dev_name = name("line-3"); + let device = line3_device(&dev_name); + assert!(!device.is_null()); + + let logical: [u32; 3] = [0, 1, 2]; + let layout = identity_layout(3); + assert!(!layout.is_null()); + + // normalize_initial_layout accepts an already-normal identity layout. + let normalized = normalize_initial_layout(logical.as_ptr(), 3, device, layout); + assert!(!normalized.is_null()); + assert_eq!(layout_num_logical(normalized), 3); + layout_free(normalized); + + // Adjacent interactions are reachable from the identity layout. + let adjacent = circuit_new(3); + assert_eq!(circuit_cx(adjacent, 0, 1), 0); + assert_eq!(validate_reachable_interactions(adjacent, device, layout), 0); + // Distant but connected interactions remain routable. + assert_eq!(circuit_cx(adjacent, 0, 2), 0); + assert_eq!(validate_reachable_interactions(adjacent, device, layout), 0); + circuit_free(adjacent); + + layout_free(layout); + device_free(device); + + // A qubit with no couplings cannot host a two-qubit interaction. + let iso_name = name("iso-4"); + let edges: [u32; 4] = [0, 1, 1, 2]; + let iso_device = device_from_edges(iso_name.as_ptr(), 4, edges.as_ptr(), 2); + assert!(!iso_device.is_null()); + let iso_layout = identity_layout(4); + let split = circuit_new(4); + assert_eq!(circuit_cx(split, 0, 3), 0); + assert_eq!( + validate_reachable_interactions(split, iso_device, iso_layout), + -6 + ); + circuit_free(split); + layout_free(iso_layout); + device_free(iso_device); +} + +// ===== Transforms ===== + +#[test] +fn test_canonicalize() { + let circuit = circuit_new(2); + assert_eq!(circuit_h(circuit, 0), 0); + assert_eq!(circuit_cx(circuit, 0, 1), 0); + + let result = canonicalize_circuit(circuit); + assert!(!result.is_null()); + assert!(canonicalize_result_rounds(result) >= 1); + let out = canonicalize_result_circuit(result); + assert!(!out.is_null()); + assert_eq!(circuit_num_operations(out), 2); + assert!(canonicalize_result_changed(result) == 0 || canonicalize_result_changed(result) == 1); + circuit_free(out); + canonicalize_result_free(result); + + // The transform wrapper returns a new circuit handle. + let wrapped = transform_canonicalize(circuit); + assert!(!wrapped.is_null()); + assert_eq!(circuit_num_operations(wrapped), 2); + circuit_free(wrapped); + + circuit_free(circuit); +} + +#[test] +fn test_rewrite_circuit() { + let circuit = circuit_new(1); + assert_eq!(circuit_h(circuit, 0), 0); + assert_eq!(circuit_h(circuit, 0), 0); + + let config = rewrite_config_default(); + assert_eq!(config.mode, REWRITE_MODE_OPTIMIZE); + let result = rewrite_circuit(circuit, config); + assert!(!result.is_null()); + + let changed = knowledge_rewrite_result_changed(result); + assert!(changed == 0 || changed == 1); + let out = knowledge_rewrite_result_circuit(result); + assert!(!out.is_null()); + if changed == 1 { + assert!(circuit_num_operations(out) <= 2); + } + + let mut stats: CKnowledgeRewriteStats = zeroed(); + assert_eq!(knowledge_rewrite_result_stats(result, &mut stats), 0); + let mut diagnostics: CKnowledgeRewriteDiagnostics = zeroed(); + assert_eq!( + knowledge_rewrite_result_diagnostics(result, &mut diagnostics), + 0 + ); + if changed == 1 { + assert!(stats.rules_applied >= 1); + } + + circuit_free(out); + knowledge_rewrite_result_free(result); + + // Lowering-mode config and the transform wrapper are callable. + let lowering = rewrite_config_lowering(); + assert_eq!(lowering.mode, REWRITE_MODE_LOWERING); + let wrapped = transform_knowledge_rewrite(circuit, rewrite_config_default()); + assert!(!wrapped.is_null()); + circuit_free(wrapped); + + circuit_free(circuit); +} + +#[test] +fn test_transform_wrappers() { + // A one-qubit run collapses: the optimized stream never grows. + let runs = circuit_new(1); + assert_eq!(circuit_h(runs, 0), 0); + assert_eq!(circuit_t(runs, 0), 0); + assert_eq!(circuit_tdg(runs, 0), 0); + assert_eq!(circuit_s(runs, 0), 0); + assert_eq!(circuit_sdg(runs, 0), 0); + assert_eq!(circuit_num_operations(runs), 5); + + let optimized = transform_optimize_one_qubit_runs(runs); + assert!(!optimized.is_null()); + assert!(circuit_num_operations(optimized) <= 5); + assert!(circuit_num_operations(optimized) >= 1); + circuit_free(optimized); + circuit_free(runs); + + // Commutative cancellation and routing-basis lowering smoke runs. + let circuit = circuit_new(2); + assert_eq!(circuit_h(circuit, 0), 0); + assert_eq!(circuit_cx(circuit, 0, 1), 0); + + let cancelled = transform_commutative_cancellation(circuit); + assert!(!cancelled.is_null()); + assert!(circuit_num_operations(cancelled) >= 1); + circuit_free(cancelled); + + let lowered = transform_lower_to_routing_basis(circuit); + assert!(!lowered.is_null()); + assert!(circuit_num_operations(lowered) >= 1); + circuit_free(lowered); + + circuit_free(circuit); +} + +#[test] +fn test_circuit_analyze() { + let circuit = circuit_new(2); + assert_eq!(circuit_h(circuit, 0), 0); + assert_eq!(circuit_cx(circuit, 0, 1), 0); + + let mut analysis: CCircuitAnalysis = zeroed(); + assert_eq!(circuit_analyze(circuit, &mut analysis), 0); + assert_eq!(analysis.has_classical_data, 0); + assert_eq!(analysis.has_classical_control, 0); + assert_eq!(analysis.has_measurement, 0); + assert_eq!(analysis.has_classical_values, 0); + assert_eq!(analysis.has_classical_vars, 0); + assert_eq!(analysis.has_runtime_classical, 0); + assert_eq!(analysis.needs_classical_handle_preservation, 0); + assert_eq!(analysis.has_circuit_gate_definitions, 0); + assert_eq!(analysis.has_unitary_circuit_definitions, 0); + assert_eq!(analysis.has_unitary_gates, 0); + assert_eq!(analysis.has_mc_gates, 0); + + circuit_free(circuit); +} + +// ===== NULL guards ===== + +#[test] +fn test_null_guards() { + // Layout entry points. + assert!(trivial_layout(std::ptr::null(), std::ptr::null(), 0).is_null()); + assert!(greedy_layout(std::ptr::null(), std::ptr::null(), 0).is_null()); + assert!(vf2_perfect_layout(std::ptr::null(), std::ptr::null(), 0, std::ptr::null()).is_null()); + assert!(sabre_layout(std::ptr::null(), std::ptr::null(), 0, std::ptr::null()).is_null()); + assert!(analyze_circuit_for_layout(std::ptr::null()).is_null()); + assert!(physical_layout_graph_from_device(std::ptr::null()).is_null()); + assert!(prepare_sabre_circuit(std::ptr::null()).is_null()); + assert!(prepare_sabre_device_target(std::ptr::null(), std::ptr::null()).is_null()); + assert!(trivial_layout_prepared(std::ptr::null(), std::ptr::null(), 0).is_null()); + assert!( + sabre_layout_prepared(std::ptr::null(), std::ptr::null(), 0, std::ptr::null()).is_null() + ); + assert_eq!( + score_layout( + 0, + std::ptr::null(), + std::ptr::null(), + std::ptr::null(), + std::ptr::null_mut() + ), + -1 + ); + + // Routing entry points. + assert!( + route_with_layout( + std::ptr::null(), + std::ptr::null(), + std::ptr::null(), + std::ptr::null() + ) + .is_null() + ); + assert!(route_sabre(std::ptr::null(), std::ptr::null(), 0, std::ptr::null()).is_null()); + assert!( + sabre_route( + std::ptr::null(), + std::ptr::null(), + std::ptr::null(), + std::ptr::null() + ) + .is_null() + ); + assert!( + normalize_initial_layout(std::ptr::null(), 0, std::ptr::null(), std::ptr::null()).is_null() + ); + assert_eq!( + validate_reachable_interactions(std::ptr::null(), std::ptr::null(), std::ptr::null()), + -1 + ); + + // Transform entry points. + assert!(canonicalize_circuit(std::ptr::null()).is_null()); + assert!(rewrite_circuit(std::ptr::null(), rewrite_config_default()).is_null()); + assert!(transform_canonicalize(std::ptr::null()).is_null()); + assert!(transform_knowledge_rewrite(std::ptr::null(), rewrite_config_default()).is_null()); + assert!(transform_optimize_one_qubit_runs(std::ptr::null()).is_null()); + assert!(transform_commutative_cancellation(std::ptr::null()).is_null()); + assert!(transform_lower_to_routing_basis(std::ptr::null()).is_null()); + assert_eq!(circuit_analyze(std::ptr::null(), std::ptr::null_mut()), -1); + + // Accessors on NULL handles. + assert!(layout_result_layout(std::ptr::null()).is_null()); + assert_eq!(layout_result_has_score(std::ptr::null()), -1); + assert_eq!(layout_result_is_perfect(std::ptr::null()), -1); + assert_eq!(layout_result_candidates_evaluated(std::ptr::null()), 0); + assert_eq!(layout_result_notes_len(std::ptr::null()), 0); + assert!(layout_result_note(std::ptr::null(), 0).is_null()); + assert!(routed_circuit_circuit(std::ptr::null()).is_null()); + assert!(routed_circuit_initial_layout(std::ptr::null()).is_null()); + assert!(routed_circuit_final_layout(std::ptr::null()).is_null()); + assert_eq!(routed_circuit_swap_count(std::ptr::null()), 0); + assert_eq!( + routed_circuit_diagnostics(std::ptr::null(), std::ptr::null_mut()), + -1 + ); + assert_eq!( + routed_circuit_changed(std::ptr::null(), std::ptr::null()), + -1 + ); + assert!(sabre_route_result_routed(std::ptr::null()).is_null()); + assert!(sabre_route_result_circuit(std::ptr::null()).is_null()); + assert!(sabre_route_result_initial_layout(std::ptr::null()).is_null()); + assert!(sabre_route_result_final_layout(std::ptr::null()).is_null()); + assert_eq!(sabre_route_result_swap_count(std::ptr::null()), 0); + assert_eq!( + sabre_route_result_changed(std::ptr::null(), std::ptr::null()), + -1 + ); + assert_eq!( + sabre_route_result_layout_score(std::ptr::null(), std::ptr::null_mut()), + -1 + ); + assert_eq!(sabre_route_result_layout_is_perfect(std::ptr::null()), -1); + assert_eq!( + sabre_route_result_layout_candidates_evaluated(std::ptr::null()), + 0 + ); + assert_eq!( + sabre_route_result_layout_used_fidelity(std::ptr::null()), + -1 + ); + assert_eq!(sabre_route_result_layout_notes_len(std::ptr::null()), 0); + assert!(sabre_route_result_layout_note(std::ptr::null(), 0).is_null()); + assert!(sabre_routing_result_circuit(std::ptr::null()).is_null()); + assert_eq!(sabre_routing_result_swap_count(std::ptr::null()), 0); + assert_eq!( + sabre_routing_result_diagnostics(std::ptr::null(), std::ptr::null_mut()), + -1 + ); + assert!(canonicalize_result_circuit(std::ptr::null()).is_null()); + assert_eq!(canonicalize_result_changed(std::ptr::null()), -1); + assert_eq!(canonicalize_result_rounds(std::ptr::null()), 0); + assert!(knowledge_rewrite_result_circuit(std::ptr::null()).is_null()); + assert_eq!(knowledge_rewrite_result_changed(std::ptr::null()), -1); + assert_eq!( + knowledge_rewrite_result_stats(std::ptr::null(), std::ptr::null_mut()), + -1 + ); + assert_eq!( + knowledge_rewrite_result_diagnostics(std::ptr::null(), std::ptr::null_mut()), + -1 + ); + assert_eq!(circuit_layout_analysis_num_logical(std::ptr::null()), 0); + assert_eq!( + circuit_layout_analysis_interactions_len(std::ptr::null()), + 0 + ); + assert_eq!( + circuit_layout_analysis_interaction(std::ptr::null(), 0, std::ptr::null_mut()), + -1 + ); + assert_eq!(physical_layout_graph_num_physical(std::ptr::null()), 0); + assert_eq!( + physical_layout_graph_distance(std::ptr::null(), 0, 1), + u32::MAX + ); + assert_eq!( + prepared_sabre_circuit_logical_qubits_len(std::ptr::null()), + 0 + ); + assert!(prepared_sabre_target_physical(std::ptr::null()).is_null()); + assert_eq!(sabre_config_validate(std::ptr::null()), -1); + + // Free functions accept NULL. + layout_result_free(std::ptr::null_mut()); + circuit_layout_analysis_free(std::ptr::null_mut()); + physical_layout_graph_free(std::ptr::null_mut()); + prepared_sabre_circuit_free(std::ptr::null_mut()); + prepared_sabre_target_free(std::ptr::null_mut()); + routed_circuit_free(std::ptr::null_mut()); + sabre_route_result_free(std::ptr::null_mut()); + sabre_routing_result_free(std::ptr::null_mut()); + canonicalize_result_free(std::ptr::null_mut()); + knowledge_rewrite_result_free(std::ptr::null_mut()); +} diff --git a/crates/binding-c/tests/compile_test.rs b/crates/binding-c/tests/compile_test.rs new file mode 100644 index 00000000..5fe1f134 --- /dev/null +++ b/crates/binding-c/tests/compile_test.rs @@ -0,0 +1,308 @@ +//! Integration tests for compile module C ABI. + +#![allow(dead_code)] + +use binding_c::circuit::{circuit_cx, circuit_free, circuit_h, circuit_new, circuit_num_qubits}; +use binding_c::compile::*; +use binding_c::cqlib_string_free; +use binding_c::device::{ + device_bidirectional_line, device_free, device_with_native_gates, layout_free, + layout_num_logical, +}; +use std::ffi::CStr; +use std::os::raw::c_char; + +fn cstr_to_string(ptr: *mut c_char) -> String { + if ptr.is_null() { + return String::new(); + } + unsafe { CStr::from_ptr(ptr) } + .to_string_lossy() + .into_owned() +} + +fn build_bell_circuit() -> *mut binding_c::circuit::CCircuit { + let circuit = circuit_new(2); + assert!(!circuit.is_null()); + assert_eq!(circuit_h(circuit, 0), 0); + assert_eq!(circuit_cx(circuit, 0, 1), 0); + circuit +} + +#[test] +fn test_compile_logical_normal() { + let circuit = build_bell_circuit(); + let config = CompileConfigC { + mode: COMPILE_MODE_NORMAL, + target: COMPILE_TARGET_LOGICAL, + allow_dirty_ancilla: 0, + allow_clean_ancilla: 0, + _reserved: [0; 4], + }; + + let result = compile(circuit, config); + assert!(!result.is_null(), "compile should succeed"); + + assert!(compile_result_num_steps(result) > 0); + + let mode = compile_result_mode(result); + assert_eq!(mode, 0); + + let changed = compile_result_changed(result); + assert!(changed == 0 || changed == 1); + + let optimized = compile_result_circuit(result); + assert!(!optimized.is_null()); + assert_eq!(circuit_num_qubits(optimized), 2); + + circuit_free(optimized); + compile_result_free(result); + circuit_free(circuit); +} + +#[test] +fn test_compile_enhanced_mode() { + let circuit = build_bell_circuit(); + let config = CompileConfigC { + mode: COMPILE_MODE_ENHANCED, + target: COMPILE_TARGET_LOGICAL, + allow_dirty_ancilla: 0, + allow_clean_ancilla: 0, + _reserved: [0; 4], + }; + + let result = compile(circuit, config); + assert!(!result.is_null()); + + assert_eq!(compile_result_mode(result), 1); + compile_result_free(result); + circuit_free(circuit); +} + +#[test] +fn test_compile_null_circuit() { + let config = CompileConfigC { + mode: COMPILE_MODE_NORMAL, + target: COMPILE_TARGET_LOGICAL, + allow_dirty_ancilla: 0, + allow_clean_ancilla: 0, + _reserved: [0; 4], + }; + let result = compile(std::ptr::null(), config); + assert!(result.is_null(), "NULL circuit should return NULL"); +} + +#[test] +fn test_compile_result_step_name() { + let circuit = build_bell_circuit(); + let config = CompileConfigC { + mode: COMPILE_MODE_NORMAL, + target: COMPILE_TARGET_LOGICAL, + allow_dirty_ancilla: 0, + allow_clean_ancilla: 0, + _reserved: [0; 4], + }; + let result = compile(circuit, config); + assert!(!result.is_null()); + + let n = compile_result_num_steps(result); + assert!(n > 0); + for i in 0..n { + let ptr = compile_result_step_name(result, i); + let name = cstr_to_string(ptr); + cqlib_string_free(ptr); + assert!(!name.is_empty(), "step {i} should have a non-empty name"); + } + + let bad = compile_result_step_name(result, n + 10); + assert!(bad.is_null(), "out-of-range index should return NULL"); + + compile_result_free(result); + circuit_free(circuit); +} + +#[test] +fn test_compile_result_step_changed() { + let circuit = build_bell_circuit(); + let config = CompileConfigC { + mode: COMPILE_MODE_NORMAL, + target: COMPILE_TARGET_LOGICAL, + allow_dirty_ancilla: 0, + allow_clean_ancilla: 0, + _reserved: [0; 4], + }; + let result = compile(circuit, config); + assert!(!result.is_null()); + + let n = compile_result_num_steps(result); + for i in 0..n { + let v = compile_result_step_changed(result, i); + assert!(v == 0 || v == 1); + } + + assert_eq!( + compile_result_step_changed(result, n + 10), + -8, + "out-of-range index should return InvalidParam" + ); + + compile_result_free(result); + circuit_free(circuit); +} + +#[test] +fn test_compile_result_free_null() { + compile_result_free(std::ptr::null_mut()); +} + +#[test] +fn test_compile_result_accessors_null() { + assert_eq!(compile_result_changed(std::ptr::null()), -1); + assert_eq!(compile_result_mode(std::ptr::null()), -1); + assert_eq!(compile_result_num_steps(std::ptr::null()), 0); + assert!(compile_result_step_name(std::ptr::null(), 0).is_null()); + assert_eq!(compile_result_step_changed(std::ptr::null(), 0), -1); + assert!(compile_result_circuit(std::ptr::null()).is_null()); +} + +// ===== compile_result_step / device-target compilation (checklist section 4) ===== + +#[test] +fn test_compile_result_step_by_name() { + let circuit = build_bell_circuit(); + let config = CompileConfigC { + mode: COMPILE_MODE_ENHANCED, + target: COMPILE_TARGET_LOGICAL, + allow_dirty_ancilla: 0, + allow_clean_ancilla: 0, + _reserved: [0; 4], + }; + let result = compile(circuit, config); + assert!(!result.is_null()); + + // Every reported step name must be findable by name. + let n = compile_result_num_steps(result); + assert!(n > 0); + let mut changed = 0; + let mut skipped = 0; + for i in 0..n { + let name = compile_result_step_name(result, i); + assert!(!name.is_null()); + assert_eq!( + compile_result_step(result, name, &mut changed, &mut skipped), + 1 + ); + assert!(changed == 0 || changed == 1); + assert!(skipped == 0 || skipped == 1); + cqlib_string_free(name); + } + + // A logical-target workflow reports route.sabre as skipped. + assert_eq!( + compile_result_step(result, c"route.sabre".as_ptr(), &mut changed, &mut skipped), + 1 + ); + assert_eq!(skipped, 1); + + // Unknown step names report "not found". + assert_eq!( + compile_result_step(result, c"no.such_step".as_ptr(), &mut changed, &mut skipped), + 0 + ); + + // NULL handling. + assert_eq!( + compile_result_step( + std::ptr::null(), + c"route.sabre".as_ptr(), + &mut changed, + &mut skipped + ), + -1 + ); + assert_eq!( + compile_result_step(result, std::ptr::null(), &mut changed, &mut skipped), + -1 + ); + + compile_result_free(result); + circuit_free(circuit); +} + +#[test] +fn test_compile_with_device_and_layouts() { + let circuit = build_bell_circuit(); + + let device = device_bidirectional_line(c"line-2".as_ptr(), 2); + assert!(!device.is_null()); + assert_eq!(device_with_native_gates(device, c"H,CX".as_ptr()), 0); + + // Device-target compilation routes on the topology and records layouts. + let result = compile_with_device(circuit, COMPILE_MODE_NORMAL, device, std::ptr::null(), 7); + assert!(!result.is_null(), "device compilation should succeed"); + + let initial = compile_result_initial_layout(result); + assert!( + !initial.is_null(), + "device compile records an initial layout" + ); + assert_eq!(layout_num_logical(initial), 2); + + let final_layout = compile_result_final_layout(result); + assert!( + !final_layout.is_null(), + "device compile records a final layout" + ); + + layout_free(initial); + layout_free(final_layout); + + // The compiled circuit remains a valid 2-qubit circuit. + let optimized = compile_result_circuit(result); + assert!(!optimized.is_null()); + assert_eq!(circuit_num_qubits(optimized), 2); + circuit_free(optimized); + + // Logical-target compilation carries no device metadata. + let config = CompileConfigC { + mode: COMPILE_MODE_NORMAL, + target: COMPILE_TARGET_LOGICAL, + allow_dirty_ancilla: 0, + allow_clean_ancilla: 0, + _reserved: [0; 4], + }; + let logical = compile(circuit, config); + assert!(!logical.is_null()); + assert!(compile_result_initial_layout(logical).is_null()); + assert!(compile_result_final_layout(logical).is_null()); + + // NULL and invalid-argument handling. + assert!( + compile_with_device( + std::ptr::null(), + COMPILE_MODE_NORMAL, + device, + std::ptr::null(), + 7 + ) + .is_null() + ); + assert!( + compile_with_device( + circuit, + COMPILE_MODE_NORMAL, + std::ptr::null(), + std::ptr::null(), + 7 + ) + .is_null() + ); + assert!(compile_with_device(circuit, 42, device, std::ptr::null(), 7).is_null()); + assert!(compile_result_initial_layout(std::ptr::null()).is_null()); + assert!(compile_result_final_layout(std::ptr::null()).is_null()); + + compile_result_free(result); + compile_result_free(logical); + device_free(device); + circuit_free(circuit); +} diff --git a/crates/binding-c/tests/compile_workflow_test.rs b/crates/binding-c/tests/compile_workflow_test.rs new file mode 100644 index 00000000..afd320e4 --- /dev/null +++ b/crates/binding-c/tests/compile_workflow_test.rs @@ -0,0 +1,219 @@ +//! Integration tests for the compiler workflow C ABI. + +#![allow(dead_code)] + +use binding_c::circuit::{circuit_cx, circuit_free, circuit_h, circuit_new, circuit_num_qubits}; +use binding_c::compile::*; +use binding_c::device::layout_free; + +fn logical_config(mode: u8) -> CompileConfigC { + CompileConfigC { + mode, + target: COMPILE_TARGET_LOGICAL, + allow_dirty_ancilla: 0, + allow_clean_ancilla: 0, + _reserved: [0; 4], + } +} + +fn build_bell_circuit() -> *mut binding_c::circuit::CCircuit { + let circuit = circuit_new(2); + assert!(!circuit.is_null()); + assert_eq!(circuit_h(circuit, 0), 0); + assert_eq!(circuit_cx(circuit, 0, 1), 0); + circuit +} + +#[test] +fn test_workflow_new_and_free() { + let workflow = compiler_workflow_new(logical_config(COMPILE_MODE_NORMAL)); + assert!( + !workflow.is_null(), + "logical config should build a workflow" + ); + compiler_workflow_free(workflow); + + // Enhanced mode is also accepted. + let workflow = compiler_workflow_new(logical_config(COMPILE_MODE_ENHANCED)); + assert!(!workflow.is_null()); + compiler_workflow_free(workflow); + + // Invalid mode and unsupported target tags return NULL. + assert!(compiler_workflow_new(logical_config(42)).is_null()); + + let mut config = logical_config(COMPILE_MODE_NORMAL); + config.target = COMPILE_TARGET_DEVICE; + assert!(compiler_workflow_new(config).is_null()); + + // Freeing NULL is allowed. + compiler_workflow_free(std::ptr::null_mut()); +} + +#[test] +fn test_workflow_run_logical() { + let circuit = build_bell_circuit(); + let workflow = compiler_workflow_new(logical_config(COMPILE_MODE_ENHANCED)); + assert!(!workflow.is_null()); + + let result = compiler_workflow_run(workflow, circuit); + assert!(!result.is_null(), "workflow run should succeed"); + assert_eq!(compile_result_mode(result), COMPILE_MODE_ENHANCED as i32); + assert!(compile_result_num_steps(result) > 0); + + let optimized = compile_result_circuit(result); + assert!(!optimized.is_null()); + assert_eq!(circuit_num_qubits(optimized), 2); + circuit_free(optimized); + + // The workflow is reusable: a second run on the same circuit works. + let second = compiler_workflow_run(workflow, circuit); + assert!(!second.is_null()); + compile_result_free(second); + + compile_result_free(result); + compiler_workflow_free(workflow); + circuit_free(circuit); +} + +#[test] +fn test_workflow_run_null_arguments() { + let circuit = build_bell_circuit(); + let workflow = compiler_workflow_new(logical_config(COMPILE_MODE_NORMAL)); + assert!(!workflow.is_null()); + + assert!(compiler_workflow_run(std::ptr::null(), circuit).is_null()); + assert!(compiler_workflow_run(workflow, std::ptr::null()).is_null()); + + compiler_workflow_free(workflow); + circuit_free(circuit); +} + +#[test] +fn test_device_metadata_accessors_logical_compile_is_empty() { + let circuit = build_bell_circuit(); + let workflow = compiler_workflow_new(logical_config(COMPILE_MODE_NORMAL)); + assert!(!workflow.is_null()); + let result = compiler_workflow_run(workflow, circuit); + assert!(!result.is_null()); + + // A logical-target compile carries no device metadata: numeric + // accessors return 0, handle accessors return NULL, and query + // functions report absence. + assert_eq!(compile_result_has_device_metadata(result), 0); + assert!(compile_result_device_metadata_initial_layout(result).is_null()); + assert!(compile_result_device_metadata_final_layout(result).is_null()); + assert_eq!(compile_result_device_metadata_permutation_len(result), 0); + assert_eq!( + compile_result_device_metadata_permutation( + result, + std::ptr::null_mut(), + std::ptr::null_mut(), + 0 + ), + 0 + ); + // A positive length with no metadata is a mismatch. + let mut original: u32 = 0; + let mut rewritten: u32 = 0; + assert_eq!( + compile_result_device_metadata_permutation(result, &mut original, &mut rewritten, 1), + -8 + ); + + compile_result_free(result); + compiler_workflow_free(workflow); + circuit_free(circuit); +} + +#[test] +fn test_device_metadata_accessors_null_result() { + assert_eq!(compile_result_has_device_metadata(std::ptr::null()), -1); + assert!(compile_result_device_metadata_initial_layout(std::ptr::null()).is_null()); + assert!(compile_result_device_metadata_final_layout(std::ptr::null()).is_null()); + assert_eq!( + compile_result_device_metadata_permutation_len(std::ptr::null()), + 0 + ); + assert_eq!( + compile_result_device_metadata_permutation( + std::ptr::null(), + std::ptr::null_mut(), + std::ptr::null_mut(), + 0 + ), + -1 + ); +} + +#[test] +fn test_device_metadata_layouts_present_after_device_compile() { + use binding_c::device::{ + device_bidirectional_line, device_free, device_with_native_gates, layout_num_logical, + }; + + let circuit = build_bell_circuit(); + let device = device_bidirectional_line(c"line-2".as_ptr(), 2); + assert!(!device.is_null()); + // Device-target compilation requires declared native capabilities. + assert_eq!(device_with_native_gates(device, c"H,CX".as_ptr()), 0); + + let result = compile_with_device(circuit, COMPILE_MODE_NORMAL, device, std::ptr::null(), 7); + assert!(!result.is_null()); + + // A device-target compile records metadata for every accessor. + assert_eq!(compile_result_has_device_metadata(result), 1); + + let initial = compile_result_device_metadata_initial_layout(result); + assert!(!initial.is_null()); + assert_eq!(layout_num_logical(initial), 2); + + let final_layout = compile_result_device_metadata_final_layout(result); + assert!(!final_layout.is_null()); + + // The permutation is reported with the two-step array protocol. + let len = compile_result_device_metadata_permutation_len(result); + assert_eq!(len, 2); + let mut original = [0u32; 2]; + let mut rewritten = [0u32; 2]; + assert_eq!( + compile_result_device_metadata_permutation( + result, + original.as_mut_ptr(), + rewritten.as_mut_ptr(), + len + ), + 0 + ); + for i in 0..len { + assert!( + original[i] < 2 && rewritten[i] < 2, + "permutation ids must be in range" + ); + } + + // Length mismatch is rejected. + assert_eq!( + compile_result_device_metadata_permutation( + result, + original.as_mut_ptr(), + rewritten.as_mut_ptr(), + len + 1 + ), + -8 + ); + assert_eq!( + compile_result_device_metadata_permutation( + result, + std::ptr::null_mut(), + std::ptr::null_mut(), + len + ), + -1 + ); + + layout_free(initial); + layout_free(final_layout); + compile_result_free(result); + device_free(device); + circuit_free(circuit); +} diff --git a/crates/binding-c/tests/device_missing_test.rs b/crates/binding-c/tests/device_missing_test.rs new file mode 100644 index 00000000..f371c08b --- /dev/null +++ b/crates/binding-c/tests/device_missing_test.rs @@ -0,0 +1,479 @@ +//! Integration tests for the device-module FFI covering per-qubit property +//! setters/getters, layout-result helpers, execution-result status/probability +//! helpers, and noise-model queries. + +#![allow(dead_code)] + +use binding_c::circuit::{circuit_cx, circuit_free, circuit_new}; +use binding_c::compile::layout::{ + analyze_circuit_for_layout, circuit_layout_analysis_free, circuit_layout_analysis_num_logical, + physical_layout_graph_free, physical_layout_graph_from_device, prepare_sabre_circuit, + prepared_sabre_circuit_free, +}; +use binding_c::cqlib_string_free; +use binding_c::device::*; +use num_complex::Complex64; +use std::ffi::{CStr, CString}; +use std::os::raw::c_char; + +fn cstr_to_string(ptr: *mut c_char) -> String { + if ptr.is_null() { + return String::new(); + } + unsafe { CStr::from_ptr(ptr) } + .to_string_lossy() + .into_owned() +} + +fn name_ptr(name: &str) -> CString { + CString::new(name).unwrap() +} + +/// Builds a `CQubitPropInput` with only the readout error set. +fn bare_input(readout: f64) -> CQubitPropInput { + CQubitPropInput { + readout_error: readout, + t1: f64::NAN, + t2: f64::NAN, + prob_meas0_prep1: f64::NAN, + prob_meas1_prep0: f64::NAN, + frequency: f64::NAN, + native_gate_names: std::ptr::null(), + native_error_rates: std::ptr::null(), + native_lengths: std::ptr::null(), + num_native_instructions: 0, + } +} + +#[test] +fn test_device_qubit_property_setters_and_getters() { + let name = name_ptr("prop-line"); + let device = device_line(name.as_ptr(), 3); + assert!(!device.is_null()); + + // Record full properties for qubit 0: readout 0.01, t1 20, t2 30, + // prob01 0.02, prob10 0.03, freq 5.0, native H 0.001/50, X 0.002/60. + let gates = [name_ptr("H"), name_ptr("X")]; + let gate_ptrs = [gates[0].as_ptr(), gates[1].as_ptr()]; + let rates = [0.001, 0.002]; + let lengths = [50.0, 60.0]; + let input = CQubitPropInput { + readout_error: 0.01, + t1: 20.0, + t2: 30.0, + prob_meas0_prep1: 0.02, + prob_meas1_prep0: 0.03, + frequency: 5.0, + native_gate_names: gate_ptrs.as_ptr(), + native_error_rates: rates.as_ptr(), + native_lengths: lengths.as_ptr(), + num_native_instructions: 2, + }; + assert_eq!(device_add_qubit_properties(device, 0, &input), 0); + + // Getters read back the stored values. + let mut v = f64::NAN; + assert_eq!(device_get_frequency(device, 0, &mut v), 0); + assert_eq!(v, 5.0); + assert_eq!(device_get_prob_meas0_prep1(device, 0, &mut v), 0); + assert_eq!(v, 0.02); + assert_eq!(device_get_prob_meas1_prep0(device, 0, &mut v), 0); + assert_eq!(v, 0.03); + assert_eq!(device_get_error_rate(device, 0, 1, &mut v), 0); + assert_eq!(v, 0.002); + assert_eq!(device_get_length(device, 0, 0, &mut v), 0); + assert_eq!(v, 50.0); + assert_eq!(device_get_t1(device, 0, &mut v), 0); + assert_eq!(v, 20.0); + + // Getters fail for a qubit without recorded properties. + assert_eq!(device_get_frequency(device, 1, &mut v), -8); + assert_eq!(device_get_error_rate(device, 1, 0, &mut v), -8); + + // NULL handling. + assert_eq!(device_get_frequency(std::ptr::null(), 0, &mut v), -1); + assert_eq!(device_set_frequency(std::ptr::null_mut(), 0, 1.0), -1); + + // Setters update one field and preserve the others; all reads happen + // after the last setter to verify the rebuild keeps every field. + assert_eq!(device_set_t1(device, 0, 21.0), 0); + assert_eq!(device_set_t2(device, 0, 31.0), 0); + assert_eq!(device_set_frequency(device, 0, 5.5), 0); + assert_eq!(device_set_prob_meas0_prep1(device, 0, 0.021), 0); + assert_eq!(device_set_prob_meas1_prep0(device, 0, 0.031), 0); + assert_eq!(device_get_t1(device, 0, &mut v), 0); + assert_eq!(v, 21.0); + assert_eq!(device_get_t2(device, 0, &mut v), 0); + assert_eq!(v, 31.0); + assert_eq!(device_get_frequency(device, 0, &mut v), 0); + assert_eq!(v, 5.5); + assert_eq!(device_get_prob_meas0_prep1(device, 0, &mut v), 0); + assert_eq!(v, 0.021); + assert_eq!(device_get_prob_meas1_prep0(device, 0, &mut v), 0); + assert_eq!(v, 0.031); + // Native instructions survive the property rebuilds. + assert_eq!(device_get_error_rate(device, 0, 1, &mut v), 0); + assert_eq!(v, 0.002); + + // Setters fail for a qubit without recorded properties. + assert_eq!(device_set_t1(device, 2, 1.0), -8); + assert_eq!(device_set_prob_meas0_prep1(device, 2, 0.1), -8); + + device_free(device); +} + +#[test] +fn test_device_native_instruction_editors() { + let name = name_ptr("gate-line"); + let device = device_line(name.as_ptr(), 2); + assert!(!device.is_null()); + + let gates = [name_ptr("H"), name_ptr("X")]; + let gate_ptrs = [gates[0].as_ptr(), gates[1].as_ptr()]; + let rates = [0.001, 0.002]; + let lengths = [50.0, 60.0]; + let input = CQubitPropInput { + readout_error: 0.01, + t1: f64::NAN, + t2: f64::NAN, + prob_meas0_prep1: f64::NAN, + prob_meas1_prep0: f64::NAN, + frequency: f64::NAN, + native_gate_names: gate_ptrs.as_ptr(), + native_error_rates: rates.as_ptr(), + native_lengths: lengths.as_ptr(), + num_native_instructions: 2, + }; + assert_eq!(device_add_qubit_properties(device, 0, &input), 0); + + let mut v = f64::NAN; + // Edit error rate and duration in place. + assert_eq!(device_set_error_rate(device, 0, 1, 0.009), 0); + assert_eq!(device_set_length(device, 0, 0, 70.0), 0); + assert_eq!(device_get_error_rate(device, 0, 1, &mut v), 0); + assert_eq!(v, 0.009); + assert_eq!(device_get_length(device, 0, 0, &mut v), 0); + assert_eq!(v, 70.0); + // Untouched entries keep their values. + assert_eq!(device_get_error_rate(device, 0, 0, &mut v), 0); + assert_eq!(v, 0.001); + assert_eq!(device_get_length(device, 0, 1, &mut v), 0); + assert_eq!(v, 60.0); + + // Out-of-bounds index handling. + assert_eq!(device_set_error_rate(device, 0, 5, 0.1), -8); + assert_eq!(device_set_length(device, 0, 5, 1.0), -8); + assert_eq!(device_get_error_rate(device, 0, 5, &mut v), -8); + assert_eq!(device_set_error_rate(device, 1, 0, 0.1), -8); + + // Replace the carried gate of entry 0 (H -> X) and verify the snapshot. + let x = name_ptr("X"); + assert_eq!(device_set_instruction(device, 0, 0, x.as_ptr()), 0); + let mut inst = CNativeInstruction { + name: std::ptr::null_mut(), + error_rate: 0.0, + length: 0.0, + }; + assert_eq!( + device_qubit_prop_native_instruction(device, 0, 0, &mut inst), + 0 + ); + assert_eq!(cstr_to_string(inst.name), "X"); + cqlib_string_free(inst.name); + assert_eq!(inst.error_rate, 0.001); + assert_eq!(inst.length, 70.0); + + // Append a new native instruction. + let z = name_ptr("Z"); + assert_eq!( + device_set_native_instruction(device, 0, z.as_ptr(), 0.003, 80.0), + 0 + ); + let mut snap = CQubitProp { + readout_error: 0.0, + t1: 0.0, + t2: 0.0, + prob_meas0_prep1: 0.0, + prob_meas1_prep0: 0.0, + frequency: 0.0, + num_native_instructions: 0, + }; + assert_eq!(device_qubit_properties(device, 0, &mut snap), 0); + assert_eq!(snap.num_native_instructions, 3); + let mut third = CNativeInstruction { + name: std::ptr::null_mut(), + error_rate: 0.0, + length: 0.0, + }; + assert_eq!( + device_qubit_prop_native_instruction(device, 0, 2, &mut third), + 0 + ); + assert_eq!(cstr_to_string(third.name), "Z"); + cqlib_string_free(third.name); + assert_eq!(third.error_rate, 0.003); + assert_eq!(third.length, 80.0); + + // A two-qubit gate is rejected as a qubit-native instruction (arity). + let cx = name_ptr("CX"); + assert_eq!( + device_set_native_instruction(device, 0, cx.as_ptr(), 0.01, 300.0), + -8 + ); + + // Unknown gate names. + let bogus = name_ptr("nope"); + assert_eq!(device_set_instruction(device, 0, 0, bogus.as_ptr()), -4); + assert_eq!( + device_set_native_instruction(device, 0, bogus.as_ptr(), 0.01, 1.0), + -4 + ); + + // NULL handling. + assert_eq!(device_set_instruction(device, 0, 0, std::ptr::null()), -1); + assert_eq!( + device_set_native_instruction(device, 0, std::ptr::null(), 0.01, 1.0), + -1 + ); + + device_free(device); +} + +#[test] +fn test_layout_result_distances_and_activity() { + let name = name_ptr("dist-line"); + let device = device_line(name.as_ptr(), 3); + assert!(!device.is_null()); + + let graph = physical_layout_graph_from_device(device); + assert!(!graph.is_null()); + + // Two-step pattern: length query, then a full copy of the 3x3 table. + assert_eq!(layout_result_distances(graph, std::ptr::null_mut(), 0), 9); + let mut buf = [0u32; 9]; + assert_eq!(layout_result_distances(graph, buf.as_mut_ptr(), 9), 9); + assert_eq!(buf, [0, 1, 2, 1, 0, 1, 2, 1, 0]); + + // A partial copy only touches the requested prefix. + let mut part = [7u32; 9]; + assert_eq!(layout_result_distances(graph, part.as_mut_ptr(), 3), 9); + assert_eq!(&part[..3], &[0, 1, 2]); + assert_eq!(&part[3..], &[7; 6]); + + assert_eq!( + layout_result_distances(std::ptr::null(), std::ptr::null_mut(), 0), + 0 + ); + physical_layout_graph_free(graph); + + // Activity from interactions cx(0,1) x2 and cx(1,2) x1. + let circuit = circuit_new(3); + assert!(!circuit.is_null()); + assert_eq!(circuit_cx(circuit, 0, 1), 0); + assert_eq!(circuit_cx(circuit, 0, 1), 0); + assert_eq!(circuit_cx(circuit, 1, 2), 0); + + let analysis = analyze_circuit_for_layout(circuit); + assert!(!analysis.is_null()); + assert_eq!( + layout_result_logical_activity(analysis, std::ptr::null_mut(), std::ptr::null_mut(), 0), + 3 + ); + let mut qubits = [0u32; 3]; + let mut act = [0.0f64; 3]; + assert_eq!( + layout_result_logical_activity(analysis, qubits.as_mut_ptr(), act.as_mut_ptr(), 3), + 3 + ); + assert_eq!(qubits, [0, 1, 2]); + let expected = [2.0, 3.0, 1.0]; + for (got, want) in act.iter().zip(expected.iter()) { + assert!((got - want).abs() < 1e-12); + } + circuit_layout_analysis_free(analysis); + + // layout_result_analysis reuses the CCircuitLayoutAnalysis handle type. + let prepared = prepare_sabre_circuit(circuit); + assert!(!prepared.is_null()); + let reused = layout_result_analysis(prepared); + assert!(!reused.is_null()); + assert_eq!(circuit_layout_analysis_num_logical(reused), 3); + circuit_layout_analysis_free(reused); + prepared_sabre_circuit_free(prepared); + assert!(layout_result_analysis(std::ptr::null()).is_null()); + + circuit_free(circuit); + device_free(device); +} + +#[test] +fn test_execution_result_status_flags_and_bitstring() { + let task = name_ptr("task-1"); + let qubits = [0u32, 1, 2]; + let result = execution_result_new(task.as_ptr(), qubits.as_ptr(), 3, 100, std::ptr::null()); + assert!(!result.is_null()); + + // Queued: neither success nor terminal. + assert_eq!(execution_result_is_success(result), 0); + assert_eq!(execution_result_is_terminal(result), 0); + + // Running is still not terminal. + assert_eq!(execution_result_start(result), 0); + assert_eq!(execution_result_is_terminal(result), 0); + + // Complete with counts. + let keys = [name_ptr("000"), name_ptr("111"), name_ptr("101")]; + let key_ptrs = [keys[0].as_ptr(), keys[1].as_ptr(), keys[2].as_ptr()]; + let counts = [50u64, 30, 20]; + assert_eq!( + execution_result_finish(result, key_ptrs.as_ptr(), counts.as_ptr(), 3), + 0 + ); + assert_eq!(execution_result_is_success(result), 1); + assert_eq!(execution_result_is_terminal(result), 1); + + // Bitstring formatting: bit i of the mask is measured qubit i. + for (mask, want) in [ + (0b001u64, "001"), + (0b101, "101"), + (0, "000"), + (u64::MAX, "111"), + ] { + let s = execution_result_to_bitstring(result, mask); + assert_eq!(cstr_to_string(s), want); + cqlib_string_free(s); + } + assert!(execution_result_to_bitstring(std::ptr::null(), 0).is_null()); + + // Failure and cancellation are terminal but not successful. + let boom = name_ptr("boom"); + assert_eq!(execution_result_fail(result, boom.as_ptr(), 7), 0); + assert_eq!(execution_result_is_success(result), 0); + assert_eq!(execution_result_is_terminal(result), 1); + assert_eq!(execution_result_cancel(result), 0); + assert_eq!(execution_result_is_terminal(result), 1); + + assert_eq!(execution_result_is_success(std::ptr::null()), -1); + assert_eq!(execution_result_is_terminal(std::ptr::null()), -1); + + execution_result_free(result); +} + +#[test] +fn test_execution_result_calc_probabilities_two_step() { + let task = name_ptr("task-2"); + let qubits = [0u32, 1]; + let result = execution_result_new(task.as_ptr(), qubits.as_ptr(), 2, 50, std::ptr::null()); + assert!(!result.is_null()); + let keys = [name_ptr("00"), name_ptr("11")]; + let key_ptrs = [keys[0].as_ptr(), keys[1].as_ptr()]; + let counts = [30u64, 20]; + assert_eq!( + execution_result_finish(result, key_ptrs.as_ptr(), counts.as_ptr(), 2), + 0 + ); + + // Two-step pattern with ascending outcome order ("00" then "11"). + assert_eq!(execution_result_calc_probabilities_len(result), 2); + let mut probs = [0.0f64; 2]; + assert_eq!( + execution_result_calc_probabilities(result, probs.as_mut_ptr(), 2), + 2 + ); + assert!((probs[0] - 0.6).abs() < 1e-12); + assert!((probs[1] - 0.4).abs() < 1e-12); + + // A partial copy only touches the requested prefix. + let mut part = [f64::NAN; 2]; + assert_eq!( + execution_result_calc_probabilities(result, part.as_mut_ptr(), 1), + 2 + ); + assert!((part[0] - 0.6).abs() < 1e-12); + assert!(part[1].is_nan()); + + assert_eq!( + execution_result_calc_probabilities_len(std::ptr::null_mut()), + 0 + ); + assert_eq!( + execution_result_calc_probabilities(std::ptr::null_mut(), std::ptr::null_mut(), 0), + 0 + ); + + execution_result_free(result); +} + +#[test] +fn test_noise_model_validity_and_kraus() { + let model = noise_model_new(); + assert!(!model.is_null()); + + let h = name_ptr("H"); + assert_eq!( + noise_model_add_single_qubit(model, h.as_ptr(), 0, NOISE_DEPOLARIZING, 0.1), + 0 + ); + assert_eq!(noise_model_is_valid(model, h.as_ptr(), 0, 0), 1); + + // Kraus output for depolarizing p=0.1: I*sqrt(0.9), X/Y/Z*sqrt(p/3), + // four 2x2 operators written back to back in row-major order. + assert_eq!(noise_model_to_kraus_len(model, h.as_ptr(), 0, 0), 16); + let mut buf = vec![Complex64::new(0.0, 0.0); 16]; + assert_eq!( + noise_model_to_kraus(model, h.as_ptr(), 0, 0, buf.as_mut_ptr(), 16), + 16 + ); + assert!((buf[0].re - 0.9f64.sqrt()).abs() < 1e-12); + assert!(buf[0].im.abs() < 1e-12); + assert!(buf[1].re.abs() < 1e-12); + assert!((buf[3].re - 0.9f64.sqrt()).abs() < 1e-12); + let p_other = (0.1f64 / 3.0).sqrt(); + assert!(buf[4].re.abs() < 1e-12); + assert!((buf[5].re - p_other).abs() < 1e-12); + assert!((buf[6].re - p_other).abs() < 1e-12); + assert!(buf[7].re.abs() < 1e-12); + + // A second channel under a different gate key (X@0, its own index 0). + let x = name_ptr("X"); + assert_eq!( + noise_model_add_single_qubit(model, x.as_ptr(), 0, NOISE_AMPLITUDE_DAMPING, 0.25), + 0 + ); + assert_eq!(noise_model_is_valid(model, x.as_ptr(), 0, 0), 1); + assert_eq!(noise_model_to_kraus_len(model, x.as_ptr(), 0, 0), 8); + let mut buf2 = vec![Complex64::new(0.0, 0.0); 8]; + assert_eq!( + noise_model_to_kraus(model, x.as_ptr(), 0, 0, buf2.as_mut_ptr(), 8), + 8 + ); + assert_eq!(buf2[0].re, 1.0); + assert!((buf2[3].re - 0.75f64.sqrt()).abs() < 1e-12); + assert!(buf2[4].re.abs() < 1e-12 && buf2[4].im.abs() < 1e-12); + assert!((buf2[5].re - 0.5).abs() < 1e-12); + assert!(buf2[7].re.abs() < 1e-12); + + // A partial copy only touches the requested prefix. + let mut part = vec![Complex64::new(f64::NAN, f64::NAN); 8]; + assert_eq!( + noise_model_to_kraus(model, x.as_ptr(), 0, 0, part.as_mut_ptr(), 2), + 8 + ); + assert!((part[0].re - 1.0).abs() < 1e-12); + assert!(part[3].re.is_nan()); + + // Errors: absent key, out-of-bounds index, unknown gate, NULL. + assert_eq!(noise_model_is_valid(model, h.as_ptr(), 1, 0), -8); + assert_eq!(noise_model_to_kraus_len(model, h.as_ptr(), 1, 0), 0); + assert_eq!(noise_model_is_valid(model, h.as_ptr(), 0, 5), -8); + let bogus = name_ptr("nope"); + assert_eq!(noise_model_is_valid(model, bogus.as_ptr(), 0, 0), -4); + assert_eq!(noise_model_to_kraus_len(model, bogus.as_ptr(), 0, 0), 0); + assert_eq!(noise_model_is_valid(std::ptr::null(), h.as_ptr(), 0, 0), -1); + assert_eq!( + noise_model_to_kraus_len(std::ptr::null(), h.as_ptr(), 0, 0), + 0 + ); + + noise_model_free(model); +} diff --git a/crates/binding-c/tests/device_test.rs b/crates/binding-c/tests/device_test.rs new file mode 100644 index 00000000..f28db2df --- /dev/null +++ b/crates/binding-c/tests/device_test.rs @@ -0,0 +1,1258 @@ +//! Integration tests for device module C ABI. + +#![allow(dead_code)] + +use binding_c::circuit::{ + circuit_cx, circuit_free, circuit_h, circuit_index, circuit_new, circuit_t, + value_operation_free, +}; +use binding_c::cqlib_string_free; +use binding_c::device::*; +use std::ffi::{CStr, CString}; +use std::os::raw::c_char; + +fn cstr_to_string(ptr: *mut c_char) -> String { + if ptr.is_null() { + return String::new(); + } + unsafe { CStr::from_ptr(ptr) } + .to_string_lossy() + .into_owned() +} + +fn name_ptr(name: &str) -> CString { + CString::new(name).unwrap() +} + +#[test] +fn test_device_line_and_properties() { + let name = name_ptr("line-2"); + let device = device_line(name.as_ptr(), 2); + assert!(!device.is_null()); + + assert_eq!(device_num_qubits(device), 2); + + let got = device_name(device); + assert_eq!(cstr_to_string(got), "line-2"); + cqlib_string_free(got); + + let topo = device_topology(device); + assert!(!topo.is_null()); + assert_eq!(topology_num_qubits(topo), 2); + assert_eq!(topology_num_couplings(topo), 1); + topology_free(topo); + + device_free(device); +} + +#[test] +fn test_device_topology_variants() { + let name = name_ptr("d"); + + let ring = device_ring(name.as_ptr(), 4); + assert!(!ring.is_null()); + assert_eq!(device_num_qubits(ring), 4); + let topo = device_topology(ring); + assert_eq!(topology_num_couplings(topo), 8); // bidirectional ring + topology_free(topo); + device_free(ring); + + let grid = device_grid(name.as_ptr(), 2, 3); + assert!(!grid.is_null()); + assert_eq!(device_num_qubits(grid), 6); + device_free(grid); + + let star = device_star(name.as_ptr(), 5, 0); + assert!(!star.is_null()); + assert_eq!(device_num_qubits(star), 5); + device_free(star); + + let bidi = device_bidirectional_line(name.as_ptr(), 3); + assert!(!bidi.is_null()); + device_free(bidi); +} + +#[test] +fn test_device_new_null_name() { + assert!(device_line(std::ptr::null(), 2).is_null()); + assert!(device_new(std::ptr::null(), 2).is_null()); +} + +#[test] +fn test_device_free_null() { + device_free(std::ptr::null_mut()); +} + +#[test] +fn test_device_from_edges() { + let name = name_ptr("custom"); + // Path 0-1-2 plus one reverse edge 2->1. + let edges: [u32; 6] = [0, 1, 1, 2, 2, 1]; + let device = device_from_edges(name.as_ptr(), 3, edges.as_ptr(), 3); + assert!(!device.is_null()); + assert_eq!(device_num_qubits(device), 3); + let topo = device_topology(device); + assert_eq!(topology_num_couplings(topo), 3); + topology_free(topo); + device_free(device); +} + +#[test] +fn test_device_with_native_gates_and_validate() { + let name = name_ptr("bidi"); + let device = device_bidirectional_line(name.as_ptr(), 2); + assert!(!device.is_null()); + + // Before setting gates, native gate list is empty -> NULL. + assert!(device_native_gates(device).is_null()); + + let gates = name_ptr("H,CX,RZ"); + assert_eq!(device_with_native_gates(device, gates.as_ptr()), 0); + let got = device_native_gates(device); + let names = cstr_to_string(got); + cqlib_string_free(got); + assert_eq!(names, "H,CX,RZ"); + + // Unknown gate name -> -4. + let bad = name_ptr("H,NOT_A_GATE"); + assert_eq!(device_with_native_gates(device, bad.as_ptr()), -4); + + // A circuit using only native gates validates. + let circuit = circuit_new(2); + assert_eq!(circuit_h(circuit, 0), 0); + assert_eq!(circuit_cx(circuit, 0, 1), 0); + assert_eq!(device_validate_circuit(device, circuit), 0); + circuit_free(circuit); + + device_free(device); +} + +#[test] +fn test_layout_from_pairs_and_get() { + let pairs: [u32; 4] = [0, 2, 1, 0]; // (0->2), (1->0) + let layout = layout_from_pairs(pairs.as_ptr(), 2, 4); + assert!(!layout.is_null()); + assert_eq!(layout_num_logical(layout), 2); + assert_eq!(layout_num_physical(layout), 4); + assert_eq!(layout_get(layout, 0), 2); + assert_eq!(layout_get(layout, 1), 0); + // Unmapped logical -> MAX + assert_eq!(layout_get(layout, 9), u32::MAX); + layout_free(layout); +} + +#[test] +fn test_layout_new_ordered_mapping() { + let logical: [u32; 2] = [0, 1]; + let physical: [u32; 2] = [5, 3]; + let layout = layout_new(logical.as_ptr(), 2, physical.as_ptr(), 2); + assert!(!layout.is_null()); + assert_eq!(layout_get(layout, 0), 5); + assert_eq!(layout_get(layout, 1), 3); + layout_free(layout); +} + +#[test] +fn test_layout_null_and_free() { + layout_free(std::ptr::null_mut()); + assert!(layout_new(std::ptr::null(), 0, std::ptr::null(), 0).is_null()); + assert!(layout_from_pairs(std::ptr::null(), 0, 4).is_null()); + assert_eq!(layout_num_logical(std::ptr::null()), 0); + assert_eq!(layout_num_physical(std::ptr::null()), 0); + assert_eq!(layout_get(std::ptr::null(), 0), u32::MAX); +} + +#[test] +fn test_execution_result_counts_and_probabilities() { + let task = name_ptr("task-1"); + let bits = [name_ptr("00"), name_ptr("11")]; + let bit_ptrs: [*const c_char; 2] = [bits[0].as_ptr(), bits[1].as_ptr()]; + let counts: [u64; 2] = [30, 20]; + let result = + execution_result_from_counts(task.as_ptr(), 2, 50, bit_ptrs.as_ptr(), counts.as_ptr(), 2); + assert!(!result.is_null()); + + assert_eq!(execution_result_shots(result), 50); + assert_eq!(execution_result_num_qubits(result), 2); + + let list = execution_result_counts(result); + assert!(!list.is_null()); + assert_eq!(counts_list_len(list), 2); + let mut total = 0.0; + for i in 0..counts_list_len(list) { + let key = counts_list_get_key(list, i); + let k = cstr_to_string(key); + cqlib_string_free(key); + assert!(k == "00" || k == "11"); + total += counts_list_get_value(list, i); + } + assert!((total - 50.0).abs() < 1e-9); + counts_list_free(list); + + let probs = execution_result_probabilities(result); + assert!(!probs.is_null()); + let mut sum = 0.0; + for i in 0..counts_list_len(probs) { + sum += counts_list_get_value(probs, i); + } + assert!((sum - 1.0).abs() < 1e-9); + counts_list_free(probs); + + execution_result_free(result); +} + +#[test] +fn test_execution_result_null_and_free() { + execution_result_free(std::ptr::null_mut()); + assert!( + execution_result_from_counts( + std::ptr::null(), + 2, + 10, + std::ptr::null(), + std::ptr::null(), + 0 + ) + .is_null() + ); + assert_eq!(execution_result_shots(std::ptr::null()), 0); + assert_eq!(execution_result_num_qubits(std::ptr::null()), 0); + assert!(execution_result_counts(std::ptr::null()).is_null()); + assert!(execution_result_probabilities(std::ptr::null_mut()).is_null()); + counts_list_free(std::ptr::null_mut()); + assert_eq!(counts_list_len(std::ptr::null()), 0); + assert!(counts_list_get_key(std::ptr::null(), 0).is_null()); + assert!(counts_list_get_value(std::ptr::null(), 0).is_nan()); +} + +#[test] +fn test_noise_model_add_errors() { + let model = noise_model_new(); + assert!(!model.is_null()); + + let h = name_ptr("H"); + let cx = name_ptr("CX"); + + // Valid single-qubit depolarizing noise. + assert_eq!( + noise_model_add_single_qubit(model, h.as_ptr(), 0, NOISE_DEPOLARIZING, 0.001), + 0 + ); + + // Unknown gate -> -4. + assert_eq!( + noise_model_add_single_qubit( + model, + name_ptr("BOGUS").as_ptr(), + 0, + NOISE_DEPOLARIZING, + 0.001 + ), + -4 + ); + // Unknown noise tag -> -8. + assert_eq!( + noise_model_add_single_qubit(model, h.as_ptr(), 0, 99, 0.001), + -8 + ); + // Invalid probability -> -8. + assert_eq!( + noise_model_add_single_qubit(model, h.as_ptr(), 0, NOISE_DEPOLARIZING, 1.5), + -8 + ); + + // Valid two-qubit noise. + assert_eq!( + noise_model_add_two_qubit(model, cx.as_ptr(), 0, 1, NOISE_TWO_DEPOLARIZING, 0.01), + 0 + ); + // Same qubit collision -> -8. + assert_eq!( + noise_model_add_two_qubit(model, cx.as_ptr(), 1, 1, NOISE_TWO_DEPOLARIZING, 0.01), + -8 + ); + + // Valid readout error. + assert_eq!(noise_model_add_readout(model, 0, 0.02, 0.01), 0); + // Independent probabilities are valid even when their sum exceeds 1. + assert_eq!(noise_model_add_readout(model, 1, 0.5, 0.7), 0); + // Invalid readout probabilities -> -8. + assert_eq!(noise_model_add_readout(model, 2, 0.5, 1.7), -8); + + noise_model_free(model); + noise_model_free(std::ptr::null_mut()); +} + +// ===== Section 3.1: device attribute queries and updates ===== + +#[test] +fn test_device_defaults_and_attribute_queries() { + let name = name_ptr("dev"); + let device = device_new(name.as_ptr(), 3); + assert!(!device.is_null()); + + // Defaults are unset -> -8 with NaN written to the out parameter. + let mut v = 0.0f64; + assert_eq!(device_default_t1(device, &mut v), -8); + assert!(v.is_nan()); + assert_eq!(device_default_t2(device, &mut v), -8); + assert_eq!(device_default_readout_error(device, &mut v), -8); + assert_eq!(device_default_single_qubit_error(device, &mut v), -8); + assert_eq!(device_default_two_qubit_error(device, &mut v), -8); + + // Set and read back the five device-wide defaults. + assert_eq!(device_set_default_t1(device, 40.0), 0); + assert_eq!(device_set_default_t2(device, 35.0), 0); + assert_eq!(device_set_default_readout_error(device, 0.02), 0); + assert_eq!(device_set_default_single_qubit_error(device, 0.005), 0); + assert_eq!(device_set_default_two_qubit_error(device, 0.01), 0); + assert_eq!(device_default_t1(device, &mut v), 0); + assert_eq!(v, 40.0); + assert_eq!(device_default_t2(device, &mut v), 0); + assert_eq!(v, 35.0); + assert_eq!(device_default_readout_error(device, &mut v), 0); + assert_eq!(v, 0.02); + assert_eq!(device_default_single_qubit_error(device, &mut v), 0); + assert_eq!(v, 0.005); + assert_eq!(device_default_two_qubit_error(device, &mut v), 0); + assert_eq!(v, 0.01); + + // Per-qubit queries fall back to the defaults; registration is not checked. + assert_eq!(device_get_t1(device, 0, &mut v), 0); + assert_eq!(v, 40.0); + assert_eq!(device_get_t2(device, 0, &mut v), 0); + assert_eq!(v, 35.0); + assert_eq!(device_get_readout_error(device, 2, &mut v), 0); + assert_eq!(v, 0.02); + assert_eq!(device_get_t1(device, 9, &mut v), 0); + assert_eq!(v, 40.0); + + // Calibration time is unset -> -8. + let mut ms = 0i64; + assert_eq!(device_calibration_time(device, &mut ms), -8); + + // NULL guards. + assert_eq!(device_get_t1(std::ptr::null(), 0, &mut v), -1); + assert_eq!(device_get_readout_error(std::ptr::null(), 0, &mut v), -1); + assert_eq!(device_set_default_t1(std::ptr::null_mut(), 1.0), -1); + assert_eq!(device_calibration_time(std::ptr::null(), &mut ms), -1); + + device_free(device); +} + +#[test] +fn test_device_invalid_and_usable_qubits() { + let name = name_ptr("dev"); + let device = device_new(name.as_ptr(), 4); + assert!(!device.is_null()); + + assert_eq!(device_invalid_qubits_len(device), 0); + let invalid: [u32; 2] = [1, 3]; + assert_eq!(device_set_invalid_qubits(device, invalid.as_ptr(), 2), 0); + assert_eq!(device_invalid_qubits_len(device), 2); + let mut got = [0u32; 2]; + assert_eq!(device_invalid_qubits(device, got.as_mut_ptr(), 2), 2); + let mut sorted: Vec = got.to_vec(); + sorted.sort_unstable(); + assert_eq!(sorted, vec![1, 3]); + + assert_eq!(device_is_usable_qubit(device, 0), 1); + assert_eq!(device_is_usable_qubit(device, 1), 0); + assert_eq!(device_is_usable_qubit(device, 3), 0); + assert_eq!(device_is_usable_qubit(device, 9), 0); // unregistered + assert_eq!(device_num_usable_qubits(device), 2); + + assert_eq!(device_usable_qubits_len(device), 2); + let mut usable = [0u32; 2]; + assert_eq!(device_usable_qubits(device, usable.as_mut_ptr(), 2), 2); + let mut sorted: Vec = usable.to_vec(); + sorted.sort_unstable(); + assert_eq!(sorted, vec![0, 2]); + + assert_eq!(device_qubits_len(device), 4); + let mut all = [0u32; 4]; + assert_eq!(device_qubits(device, all.as_mut_ptr(), 4), 4); + let mut sorted: Vec = all.to_vec(); + sorted.sort_unstable(); + assert_eq!(sorted, vec![0, 1, 2, 3]); + + // Unregistered qubits are rejected and the existing set is preserved. + let bad: [u32; 1] = [9]; + assert_eq!(device_set_invalid_qubits(device, bad.as_ptr(), 1), -2); + assert_eq!(device_invalid_qubits_len(device), 2); + + // An invalid qubit hides its single-qubit error even when inherited. + let gates = name_ptr("H"); + assert_eq!(device_with_native_gates(device, gates.as_ptr()), 0); + assert_eq!(device_set_default_single_qubit_error(device, 0.005), 0); + let h = name_ptr("H"); + let mut v = 0.0f64; + assert_eq!(device_single_qubit_error(device, h.as_ptr(), 0, &mut v), 0); + assert_eq!(v, 0.005); + assert_eq!(device_single_qubit_error(device, h.as_ptr(), 1, &mut v), -8); + + // Clearing the invalid set restores full usability. + assert_eq!(device_set_invalid_qubits(device, std::ptr::null(), 0), 0); + assert_eq!(device_num_usable_qubits(device), 4); + + // NULL guards. + assert_eq!( + device_set_invalid_qubits(std::ptr::null_mut(), invalid.as_ptr(), 2), + -1 + ); + assert_eq!(device_set_invalid_qubits(device, std::ptr::null(), 2), -1); + assert_eq!(device_is_usable_qubit(std::ptr::null(), 0), -1); + assert_eq!(device_num_usable_qubits(std::ptr::null()), 0); + assert_eq!(device_usable_qubits_len(std::ptr::null()), 0); + assert_eq!(device_invalid_qubits_len(std::ptr::null()), 0); + assert_eq!(device_qubits_len(std::ptr::null()), 0); + + device_free(device); +} + +#[test] +fn test_device_qubit_and_edge_properties() { + let name = name_ptr("line"); + let device = device_line(name.as_ptr(), 3); + assert!(!device.is_null()); + + // No properties recorded yet. + let mut qprop = CQubitProp { + readout_error: 0.0, + t1: 0.0, + t2: 0.0, + prob_meas0_prep1: 0.0, + prob_meas1_prep0: 0.0, + frequency: 0.0, + num_native_instructions: 0, + }; + assert_eq!(device_qubit_properties(device, 0, &mut qprop), -8); + let mut eprop = CEdgeProp { + num_native_instructions: 0, + }; + assert_eq!(device_edge_properties(device, 0, 1, &mut eprop), -8); + + let h = name_ptr("H"); + let cx = name_ptr("CX"); + let mut v = 0.0f64; + // H is not in the device-wide native gates -> unsupported -> -8. + assert_eq!(device_single_qubit_error(device, h.as_ptr(), 0, &mut v), -8); + + // Record qubit-0 properties with one native H instruction. + let gate_names: [*const c_char; 1] = [h.as_ptr()]; + let error_rates: [f64; 1] = [0.001]; + let lengths: [f64; 1] = [50.0]; + let input = CQubitPropInput { + readout_error: 0.01, + t1: 20.0, + t2: 30.0, + prob_meas0_prep1: 0.02, + prob_meas1_prep0: 0.03, + frequency: 5.0, + native_gate_names: gate_names.as_ptr(), + native_error_rates: error_rates.as_ptr(), + native_lengths: lengths.as_ptr(), + num_native_instructions: 1, + }; + assert_eq!(device_add_qubit_properties(device, 0, &input), 0); + + assert_eq!(device_qubit_properties(device, 0, &mut qprop), 0); + assert_eq!(qprop.readout_error, 0.01); + assert_eq!(qprop.t1, 20.0); + assert_eq!(qprop.t2, 30.0); + assert_eq!(qprop.prob_meas0_prep1, 0.02); + assert_eq!(qprop.prob_meas1_prep0, 0.03); + assert_eq!(qprop.frequency, 5.0); + assert_eq!(qprop.num_native_instructions, 1); + + let mut native = CNativeInstruction { + name: std::ptr::null_mut(), + error_rate: 0.0, + length: 0.0, + }; + assert_eq!( + device_qubit_prop_native_instruction(device, 0, 0, &mut native), + 0 + ); + assert_eq!(cstr_to_string(native.name), "H"); + cqlib_string_free(native.name); + assert_eq!(native.error_rate, 0.001); + assert_eq!(native.length, 50.0); + assert_eq!( + device_qubit_prop_native_instruction(device, 0, 1, &mut native), + -8 + ); + + // Per-qubit values now win over the (unset) defaults. + assert_eq!(device_get_t1(device, 0, &mut v), 0); + assert_eq!(v, 20.0); + assert_eq!(device_get_t2(device, 0, &mut v), 0); + assert_eq!(v, 30.0); + assert_eq!(device_get_readout_error(device, 0, &mut v), 0); + assert_eq!(v, 0.01); + + // A non-empty local list is authoritative: H is explicit, X unsupported. + assert_eq!(device_single_qubit_error(device, h.as_ptr(), 0, &mut v), 0); + assert_eq!(v, 0.001); + let x = name_ptr("X"); + assert_eq!(device_single_qubit_error(device, x.as_ptr(), 0, &mut v), -8); + // Two-qubit gates never resolve on single qubits. + assert_eq!( + device_single_qubit_error(device, cx.as_ptr(), 0, &mut v), + -8 + ); + // Unknown gate name -> -4. + assert_eq!( + device_single_qubit_error(device, name_ptr("BOGUS").as_ptr(), 0, &mut v), + -4 + ); + + // Record edge (0 -> 1) properties with one native CX instruction. + let edge_names: [*const c_char; 1] = [cx.as_ptr()]; + let edge_rates: [f64; 1] = [0.02]; + let edge_lengths: [f64; 1] = [100.0]; + let edge_input = CEdgePropInput { + native_gate_names: edge_names.as_ptr(), + native_error_rates: edge_rates.as_ptr(), + native_lengths: edge_lengths.as_ptr(), + num_native_instructions: 1, + }; + assert_eq!(device_add_edge_properties(device, 0, 1, &edge_input), 0); + assert_eq!(device_edge_properties(device, 0, 1, &mut eprop), 0); + assert_eq!(eprop.num_native_instructions, 1); + assert_eq!( + device_edge_prop_native_instruction(device, 0, 1, 0, &mut native), + 0 + ); + assert_eq!(cstr_to_string(native.name), "CX"); + cqlib_string_free(native.name); + assert_eq!(native.error_rate, 0.02); + assert_eq!(native.length, 100.0); + assert_eq!( + device_edge_prop_native_instruction(device, 0, 1, 1, &mut native), + -8 + ); + + assert_eq!(device_two_qubit_error(device, cx.as_ptr(), 0, 1, &mut v), 0); + assert_eq!(v, 0.02); + // The directed line has no reverse coupling 1 -> 0 and no coupling 0 -> 2. + assert_eq!( + device_two_qubit_error(device, cx.as_ptr(), 1, 0, &mut v), + -8 + ); + assert_eq!( + device_two_qubit_error(device, cx.as_ptr(), 0, 2, &mut v), + -8 + ); + // One-qubit gates never resolve on couplings. + assert_eq!(device_two_qubit_error(device, h.as_ptr(), 0, 1, &mut v), -8); + assert_eq!( + device_two_qubit_error(device, name_ptr("BOGUS").as_ptr(), 0, 1, &mut v), + -4 + ); + + // edge_error returns the best calibrated edge instruction error. + assert_eq!(device_edge_error(device, 0, 1, &mut v), 0); + assert_eq!(v, 0.02); + assert_eq!(device_edge_error(device, 1, 0, &mut v), -8); + // 1 -> 2 exists but has no calibration and no default -> -8. + assert_eq!(device_edge_error(device, 1, 2, &mut v), -8); + assert_eq!(device_edge_error(std::ptr::null(), 0, 1, &mut v), -1); + + // Unregistered qubits and unknown couplings are rejected. + assert_eq!(device_add_qubit_properties(device, 9, &input), -2); + assert_eq!(device_add_edge_properties(device, 2, 1, &edge_input), -2); + assert_eq!(device_add_qubit_properties(device, 1, std::ptr::null()), -1); + assert_eq!( + device_add_edge_properties(device, 0, 1, std::ptr::null()), + -1 + ); + + // Native-list validation: wrong arity -> -8, unknown gate -> -4, bad rate -> -8. + let h_names: [*const c_char; 1] = [h.as_ptr()]; + let cx_names: [*const c_char; 1] = [cx.as_ptr()]; + let bogus = name_ptr("BOGUS"); + let bogus_names: [*const c_char; 1] = [bogus.as_ptr()]; + let bad_rate: [f64; 1] = [1.5]; + let bad_edge = CEdgePropInput { + native_gate_names: h_names.as_ptr(), + native_error_rates: edge_rates.as_ptr(), + native_lengths: std::ptr::null(), + num_native_instructions: 1, + }; + assert_eq!(device_add_edge_properties(device, 0, 1, &bad_edge), -8); + let bad_qubit = CQubitPropInput { + readout_error: 0.01, + t1: f64::NAN, + t2: f64::NAN, + prob_meas0_prep1: f64::NAN, + prob_meas1_prep0: f64::NAN, + frequency: f64::NAN, + native_gate_names: cx_names.as_ptr(), + native_error_rates: edge_rates.as_ptr(), + native_lengths: std::ptr::null(), + num_native_instructions: 1, + }; + assert_eq!(device_add_qubit_properties(device, 1, &bad_qubit), -8); + let bogus_input = CQubitPropInput { + readout_error: 0.01, + t1: f64::NAN, + t2: f64::NAN, + prob_meas0_prep1: f64::NAN, + prob_meas1_prep0: f64::NAN, + frequency: f64::NAN, + native_gate_names: bogus_names.as_ptr(), + native_error_rates: edge_rates.as_ptr(), + native_lengths: std::ptr::null(), + num_native_instructions: 1, + }; + assert_eq!(device_add_qubit_properties(device, 1, &bogus_input), -4); + let bad_rate_input = CQubitPropInput { + readout_error: 0.01, + t1: f64::NAN, + t2: f64::NAN, + prob_meas0_prep1: f64::NAN, + prob_meas1_prep0: f64::NAN, + frequency: f64::NAN, + native_gate_names: h_names.as_ptr(), + native_error_rates: bad_rate.as_ptr(), + native_lengths: std::ptr::null(), + num_native_instructions: 1, + }; + assert_eq!(device_add_qubit_properties(device, 1, &bad_rate_input), -8); + + // supports_native_instruction capability queries. + let qargs0: [u32; 1] = [0]; + let qargs1: [u32; 1] = [1]; + let pair01: [u32; 2] = [0, 1]; + let pair10: [u32; 2] = [1, 0]; + assert_eq!( + device_supports_native_instruction(device, h.as_ptr(), qargs0.as_ptr(), 1), + 1 + ); + assert_eq!( + device_supports_native_instruction(device, h.as_ptr(), qargs1.as_ptr(), 1), + 0 + ); + assert_eq!( + device_supports_native_instruction(device, cx.as_ptr(), pair01.as_ptr(), 2), + 1 + ); + assert_eq!( + device_supports_native_instruction(device, cx.as_ptr(), pair10.as_ptr(), 2), + 0 + ); + // Arity mismatch is not a capability. + assert_eq!( + device_supports_native_instruction(device, cx.as_ptr(), qargs0.as_ptr(), 1), + 0 + ); + assert_eq!( + device_supports_native_instruction(device, name_ptr("BOGUS").as_ptr(), qargs0.as_ptr(), 1), + -4 + ); + assert_eq!( + device_supports_native_instruction(device, h.as_ptr(), std::ptr::null(), 1), + -1 + ); + // GPhase carries no qargs; it is not in the device native gates here. + let gphase = name_ptr("GPhase"); + assert_eq!( + device_supports_native_instruction(device, gphase.as_ptr(), std::ptr::null(), 0), + 0 + ); + + device_free(device); +} + +#[test] +fn test_device_line_from_qubits_and_validate_operation() { + let name = name_ptr("line7"); + let qubits: [u32; 3] = [5, 6, 7]; + let device = device_line_from_qubits(name.as_ptr(), qubits.as_ptr(), 3); + assert!(!device.is_null()); + assert_eq!(device_qubits_len(device), 3); + let mut got = [0u32; 3]; + assert_eq!(device_qubits(device, got.as_mut_ptr(), 3), 3); + assert_eq!(got, [5, 6, 7]); + assert_eq!(device_num_usable_qubits(device), 3); + device_free(device); + + // NULL guards. + assert!(device_line_from_qubits(std::ptr::null(), qubits.as_ptr(), 3).is_null()); + assert!(device_line_from_qubits(name.as_ptr(), std::ptr::null(), 3).is_null()); + + // validate_operation consumes a CValueOperation snapshot from circuit_index. + let dev_name = name_ptr("dev"); + let dev = device_line(dev_name.as_ptr(), 2); + let gates = name_ptr("H,CX"); + assert_eq!(device_with_native_gates(dev, gates.as_ptr()), 0); + + let circuit = circuit_new(2); + assert_eq!(circuit_h(circuit, 0), 0); + assert_eq!(circuit_cx(circuit, 0, 1), 0); + let op = circuit_index(circuit, 0); + assert!(!op.is_null()); + assert_eq!(device_validate_operation(dev, op), 0); + assert_eq!(device_validate_value_operation(dev, op), 0); + value_operation_free(op); + + // T is not a native gate -> the device rejects the operation. + assert_eq!(circuit_t(circuit, 0), 0); + let bad_op = circuit_index(circuit, 2); + assert!(!bad_op.is_null()); + assert_eq!(device_validate_operation(dev, bad_op), -3); + assert_eq!(device_validate_value_operation(dev, bad_op), -3); + value_operation_free(bad_op); + assert_eq!(device_validate_operation(dev, std::ptr::null()), -1); + assert_eq!(device_validate_value_operation(dev, std::ptr::null()), -1); + + circuit_free(circuit); + device_free(dev); +} + +// ===== Section 3.2: layout binding and map introspection ===== + +#[test] +fn test_layout_binding_and_maps() { + let pairs: [u32; 4] = [0, 2, 1, 0]; // L0->P2, L1->P0 + let layout = layout_from_pairs(pairs.as_ptr(), 2, 4); + assert!(!layout.is_null()); + + // Reverse lookup distinguishes vacant and unknown physical qubits. + assert_eq!(layout_get_logical(layout, 2), 0); + assert_eq!(layout_get_logical(layout, 0), 1); + assert_eq!(layout_get_logical(layout, 1), u32::MAX); + assert_eq!(layout_get_logical(layout, 3), u32::MAX); + assert_eq!(layout_is_physical_vacant(layout, 1), 1); + assert_eq!(layout_is_physical_vacant(layout, 3), 1); + assert_eq!(layout_is_physical_vacant(layout, 0), 0); + assert_eq!(layout_is_physical_vacant(layout, 2), 0); + assert_eq!(layout_is_physical_vacant(layout, 9), 0); + + // Bind a new logical qubit onto a vacant physical qubit. + assert_eq!(layout_bind(layout, 2, 1), 0); + assert_eq!(layout_get(layout, 2), 1); + assert_eq!(layout_get_logical(layout, 1), 2); + // Either side already mapped -> -8. + assert_eq!(layout_bind(layout, 2, 3), -8); + assert_eq!(layout_bind(layout, 3, 1), -8); + // Physical qubit outside the layout -> -2. + assert_eq!(layout_bind(layout, 3, 9), -2); + // Unbinding an unmapped logical qubit -> -8. + let mut released = 0u32; + assert_eq!(layout_unbind(layout, 9, &mut released), -8); + + // Full map introspection. + assert_eq!(layout_l2p_map_len(layout), 3); + let mut l2p = [0u32; 6]; + assert_eq!(layout_l2p_map(layout, l2p.as_mut_ptr(), 3), 3); + assert_eq!(l2p, [0, 2, 1, 0, 2, 1]); + assert_eq!(layout_p2l_map_len(layout), 3); + let mut p2l = [0u32; 6]; + assert_eq!(layout_p2l_map(layout, p2l.as_mut_ptr(), 3), 3); + assert_eq!(p2l, [0, 1, 1, 2, 2, 0]); + assert_eq!(layout_logical_qubits_len(layout), 3); + let mut logical = [0u32; 3]; + assert_eq!(layout_logical_qubits(layout, logical.as_mut_ptr(), 3), 3); + assert_eq!(logical, [0, 1, 2]); + assert_eq!(layout_physical_qubits_len(layout), 4); + let mut physical = [0u32; 4]; + assert_eq!(layout_physical_qubits(layout, physical.as_mut_ptr(), 4), 4); + assert_eq!(physical, [0, 1, 2, 3]); + assert_eq!(layout_num_vacant_physical(layout), 1); + assert_eq!(layout_vacant_physical_qubits_len(layout), 1); + let mut vacant = [0u32; 1]; + assert_eq!( + layout_vacant_physical_qubits(layout, vacant.as_mut_ptr(), 1), + 1 + ); + assert_eq!(vacant, [3]); + + // Unbind releases the physical qubit. + assert_eq!(layout_unbind(layout, 0, &mut released), 0); + assert_eq!(released, 2); + assert_eq!(layout_get(layout, 0), u32::MAX); + assert_eq!(layout_is_physical_vacant(layout, 2), 1); + assert_eq!(layout_num_vacant_physical(layout), 2); + + // Swapping with a vacant physical qubit moves the logical qubit. + assert_eq!(layout_swap_physical(layout, 1, 3), 0); + assert_eq!(layout_get(layout, 2), 3); + assert_eq!(layout_get_logical(layout, 1), u32::MAX); + assert_eq!(layout_get_logical(layout, 3), 2); + // Unknown physical qubits -> -2. + assert_eq!(layout_swap_physical(layout, 1, 99), -2); + + // NULL guards. + assert_eq!(layout_bind(std::ptr::null_mut(), 0, 0), -1); + assert_eq!(layout_unbind(std::ptr::null_mut(), 0, &mut released), -1); + assert_eq!(layout_get_logical(std::ptr::null(), 0), u32::MAX); + assert_eq!(layout_is_physical_vacant(std::ptr::null(), 0), -1); + assert_eq!(layout_l2p_map_len(std::ptr::null()), 0); + assert_eq!(layout_p2l_map_len(std::ptr::null()), 0); + assert_eq!(layout_logical_qubits_len(std::ptr::null()), 0); + assert_eq!(layout_physical_qubits_len(std::ptr::null()), 0); + assert_eq!(layout_num_vacant_physical(std::ptr::null()), 0); + assert_eq!(layout_vacant_physical_qubits_len(std::ptr::null()), 0); + assert_eq!(layout_swap_physical(std::ptr::null_mut(), 0, 1), -1); + + layout_free(layout); +} + +// ===== Section 3.3: topology graph queries and updates ===== + +#[test] +fn test_topology_graph_queries_and_updates() { + let qubits: [u32; 3] = [0, 1, 2]; + let topo = topology_line(qubits.as_ptr(), 3); + assert!(!topo.is_null()); + assert_eq!(topology_num_qubits(topo), 3); + assert_eq!(topology_num_couplings(topo), 2); + + assert_eq!(topology_contains_qubit(topo, 0), 1); + assert_eq!(topology_contains_qubit(topo, 9), 0); + assert_eq!(topology_contains_qubit(std::ptr::null(), 0), -1); + + // Qubit listing (two-step pattern). + assert_eq!(topology_qubits_len(topo), 3); + let mut got = [0u32; 3]; + assert_eq!(topology_qubits(topo, got.as_mut_ptr(), 3), 3); + let mut sorted: Vec = got.to_vec(); + sorted.sort_unstable(); + assert_eq!(sorted, vec![0, 1, 2]); + + // Directed line: 0 -> 1 -> 2. + let mut neighbors = [0u32; 2]; + assert_eq!(topology_neighbors_undirected_len(topo, 1), 2); + assert_eq!( + topology_neighbors_undirected(topo, 1, neighbors.as_mut_ptr(), 2), + 2 + ); + assert_eq!(neighbors, [0, 2]); + let mut preds = [0u32; 1]; + assert_eq!(topology_predecessors_len(topo, 1), 1); + assert_eq!(topology_predecessors(topo, 1, preds.as_mut_ptr(), 1), 1); + assert_eq!(preds, [0]); + let mut succs = [0u32; 1]; + assert_eq!(topology_successors_len(topo, 1), 1); + assert_eq!(topology_successors(topo, 1, succs.as_mut_ptr(), 1), 1); + assert_eq!(succs, [2]); + assert_eq!(topology_in_degree(topo, 1), 1); + assert_eq!(topology_out_degree(topo, 1), 1); + // Unknown qubits yield empty results. + assert_eq!(topology_neighbors_undirected_len(topo, 9), 0); + assert_eq!(topology_predecessors_len(topo, 9), 0); + assert_eq!(topology_successors_len(topo, 9), 0); + assert_eq!(topology_in_degree(topo, 9), 0); + assert_eq!(topology_out_degree(topo, 9), 0); + + assert_eq!(topology_supports_directed_coupling(topo, 0, 1), 1); + assert_eq!(topology_supports_directed_coupling(topo, 1, 0), 0); + assert_eq!(topology_supports_coupling_either_direction(topo, 1, 0), 1); + assert_eq!(topology_supports_coupling_either_direction(topo, 0, 2), 0); + assert_eq!( + topology_supports_directed_coupling(std::ptr::null(), 0, 1), + -1 + ); + assert_eq!( + topology_supports_coupling_either_direction(std::ptr::null(), 0, 1), + -1 + ); + + // Undirected edge listing collapses direction. + assert_eq!(topology_undirected_edges_len(topo), 2); + let mut undirected = [0u32; 4]; + assert_eq!( + topology_undirected_edges(topo, undirected.as_mut_ptr(), 2), + 2 + ); + assert_eq!(undirected, [0, 1, 1, 2]); + assert_eq!(topology_undirected_edges_len(std::ptr::null()), 0); + + // Add a qubit and a named coupling. + let five: [u32; 1] = [5]; + assert_eq!(topology_add_qubits(topo, five.as_ptr(), 1), 0); + assert_eq!(topology_qubits_len(topo), 4); + assert_eq!(topology_contains_qubit(topo, 5), 1); + // Duplicate qubits -> -8; removal of unknown qubits -> -2. + assert_eq!(topology_add_qubits(topo, five.as_ptr(), 1), -8); + let nine: [u32; 1] = [9]; + assert_eq!(topology_remove_qubits(topo, nine.as_ptr(), 1), -2); + + let edge: [u32; 2] = [1, 5]; + let coupling_name = name_ptr("c15"); + let names: [*const c_char; 1] = [coupling_name.as_ptr()]; + assert_eq!( + topology_add_couplings(topo, edge.as_ptr(), 1, names.as_ptr()), + 0 + ); + assert_eq!(topology_num_couplings(topo), 3); + let got_name = topology_get_coupling_name(topo, 1, 5); + assert_eq!(cstr_to_string(got_name), "c15"); + cqlib_string_free(got_name); + // Existing but unnamed coupling -> empty string; missing coupling -> NULL. + let unnamed = topology_get_coupling_name(topo, 0, 1); + assert_eq!(cstr_to_string(unnamed), ""); + cqlib_string_free(unnamed); + assert!(topology_get_coupling_name(topo, 5, 1).is_null()); + assert!(topology_get_coupling_name(std::ptr::null(), 0, 1).is_null()); + + assert_eq!(topology_supports_directed_coupling(topo, 1, 5), 1); + assert_eq!(topology_supports_coupling_either_direction(topo, 5, 1), 1); + // The new edge extends qubit 1's neighborhood and out-degree. + let mut neighbors2 = [0u32; 3]; + assert_eq!(topology_neighbors_undirected_len(topo, 1), 3); + assert_eq!( + topology_neighbors_undirected(topo, 1, neighbors2.as_mut_ptr(), 3), + 3 + ); + assert_eq!(neighbors2, [0, 2, 5]); + let mut succs2 = [0u32; 2]; + assert_eq!(topology_successors_len(topo, 1), 2); + assert_eq!(topology_successors(topo, 1, succs2.as_mut_ptr(), 2), 2); + assert_eq!(succs2, [2, 5]); + assert_eq!(topology_in_degree(topo, 1), 1); + assert_eq!(topology_out_degree(topo, 1), 2); + + // Coupling updates. + assert_eq!(topology_remove_couplings(topo, edge.as_ptr(), 1), 0); + assert_eq!(topology_supports_directed_coupling(topo, 1, 5), 0); + // Removing a missing coupling -> -2; duplicate add -> -8. + assert_eq!(topology_remove_couplings(topo, edge.as_ptr(), 1), -2); + assert_eq!( + topology_add_couplings(topo, edge.as_ptr(), 1, names.as_ptr()), + 0 + ); + assert_eq!( + topology_add_couplings(topo, edge.as_ptr(), 1, names.as_ptr()), + -8 + ); + // Couplings to unknown qubits -> -2; self couplings -> -8. + let bad_edge: [u32; 2] = [0, 9]; + assert_eq!( + topology_add_couplings(topo, bad_edge.as_ptr(), 1, std::ptr::null()), + -2 + ); + let self_edge: [u32; 2] = [0, 0]; + assert_eq!( + topology_add_couplings(topo, self_edge.as_ptr(), 1, std::ptr::null()), + -8 + ); + // NULL guards. + assert_eq!( + topology_add_qubits(std::ptr::null_mut(), five.as_ptr(), 1), + -1 + ); + assert_eq!( + topology_add_couplings(std::ptr::null_mut(), edge.as_ptr(), 1, std::ptr::null()), + -1 + ); + assert_eq!( + topology_remove_couplings(std::ptr::null_mut(), edge.as_ptr(), 1), + -1 + ); + assert_eq!(topology_add_qubits(topo, std::ptr::null(), 1), -1); + assert_eq!(topology_qubits_len(std::ptr::null()), 0); + assert_eq!(topology_neighbors_undirected_len(std::ptr::null(), 0), 0); + assert_eq!(topology_predecessors_len(std::ptr::null(), 0), 0); + assert_eq!(topology_successors_len(std::ptr::null(), 0), 0); + + // Removing a qubit drops every coupling touching it. + assert_eq!(topology_remove_qubits(topo, five.as_ptr(), 1), 0); + assert_eq!(topology_qubits_len(topo), 3); + assert_eq!(topology_contains_qubit(topo, 5), 0); + assert_eq!(topology_num_couplings(topo), 2); + + topology_free(topo); + assert!(topology_line(std::ptr::null(), 3).is_null()); +} + +// ===== Section 3.4: Pauli channels and noise queries ===== + +#[test] +fn test_noise_model_pauli_and_queries() { + let model = noise_model_new(); + assert!(!model.is_null()); + + let h = name_ptr("H"); + let cx = name_ptr("CX"); + + // Pauli channel add + query. + assert_eq!( + noise_model_add_single_qubit_pauli(model, h.as_ptr(), 0, 0.01, 0.02, 0.03), + 0 + ); + assert_eq!( + noise_model_add_single_qubit_pauli(model, name_ptr("BOGUS").as_ptr(), 0, 0.01, 0.0, 0.0), + -4 + ); + // Probabilities must stay within [0, 1] and sum to at most 1. + assert_eq!( + noise_model_add_single_qubit_pauli(model, h.as_ptr(), 0, 0.5, 0.5, 0.5), + -8 + ); + assert_eq!( + noise_model_add_single_qubit_pauli(std::ptr::null_mut(), h.as_ptr(), 0, 0.01, 0.0, 0.0), + -1 + ); + + assert_eq!( + noise_model_get_single_qubit_errors_len(model, h.as_ptr(), 0), + 1 + ); + let mut channels = [CSingleQubitNoise { + tag: 0, + p: 0.0, + px: 0.0, + py: 0.0, + pz: 0.0, + }]; + assert_eq!( + noise_model_get_single_qubit_errors(model, h.as_ptr(), 0, channels.as_mut_ptr(), 1), + 1 + ); + assert_eq!(channels[0].tag, NOISE_PAULI); + assert_eq!(channels[0].px, 0.01); + assert_eq!(channels[0].py, 0.02); + assert_eq!(channels[0].pz, 0.03); + + // A second channel on the same key. + assert_eq!( + noise_model_add_single_qubit(model, h.as_ptr(), 0, NOISE_DEPOLARIZING, 0.001), + 0 + ); + assert_eq!( + noise_model_get_single_qubit_errors_len(model, h.as_ptr(), 0), + 2 + ); + let mut two = [ + CSingleQubitNoise { + tag: 0, + p: 0.0, + px: 0.0, + py: 0.0, + pz: 0.0, + }, + CSingleQubitNoise { + tag: 0, + p: 0.0, + px: 0.0, + py: 0.0, + pz: 0.0, + }, + ]; + assert_eq!( + noise_model_get_single_qubit_errors(model, h.as_ptr(), 0, two.as_mut_ptr(), 2), + 2 + ); + assert!( + two.iter() + .any(|c| c.tag == NOISE_DEPOLARIZING && (c.p - 0.001).abs() < 1e-12) + ); + assert!(two.iter().any(|c| c.tag == NOISE_PAULI)); + + // Absent keys and unknown gates yield empty lists. + assert_eq!( + noise_model_get_single_qubit_errors_len(model, h.as_ptr(), 1), + 0 + ); + assert_eq!( + noise_model_get_single_qubit_errors_len(model, name_ptr("BOGUS").as_ptr(), 0), + 0 + ); + assert_eq!( + noise_model_get_single_qubit_errors_len(std::ptr::null(), h.as_ptr(), 0), + 0 + ); + + // Two-qubit queries. + assert_eq!( + noise_model_add_two_qubit(model, cx.as_ptr(), 0, 1, NOISE_TWO_DEPOLARIZING, 0.01), + 0 + ); + assert_eq!( + noise_model_get_two_qubit_errors_len(model, cx.as_ptr(), 0, 1), + 1 + ); + let make_single = || CSingleQubitNoise { + tag: 0, + p: 0.0, + px: 0.0, + py: 0.0, + pz: 0.0, + }; + let mut two_q = [CTwoQubitNoise { + tag: 0, + p: 0.0, + q0_noise: make_single(), + q1_noise: make_single(), + op_q0: 0, + op_q1: 0, + }]; + assert_eq!( + noise_model_get_two_qubit_errors(model, cx.as_ptr(), 0, 1, two_q.as_mut_ptr(), 1), + 1 + ); + assert_eq!(two_q[0].tag, NOISE_TWO_DEPOLARIZING); + assert_eq!(two_q[0].p, 0.01); + // Direction, collisions, and unknown gates yield empty lists. + assert_eq!( + noise_model_get_two_qubit_errors_len(model, cx.as_ptr(), 1, 0), + 0 + ); + assert_eq!( + noise_model_get_two_qubit_errors_len(model, cx.as_ptr(), 1, 1), + 0 + ); + assert_eq!( + noise_model_get_two_qubit_errors_len(model, name_ptr("BOGUS").as_ptr(), 0, 1), + 0 + ); + assert_eq!( + noise_model_get_two_qubit_errors_len(std::ptr::null(), cx.as_ptr(), 0, 1), + 0 + ); + + // Readout queries. + assert_eq!(noise_model_add_readout(model, 0, 0.02, 0.03), 0); + let (mut p01, mut p10) = (0.0f64, 0.0f64); + assert_eq!( + noise_model_get_readout_error(model, 0, &mut p01, &mut p10), + 0 + ); + assert_eq!(p01, 0.02); + assert_eq!(p10, 0.03); + assert_eq!( + noise_model_get_readout_error(model, 5, &mut p01, &mut p10), + -8 + ); + assert_eq!( + noise_model_get_readout_error(std::ptr::null(), 0, &mut p01, &mut p10), + -1 + ); + + noise_model_free(model); +} + +// ===== Section 3.5: execution result lifecycle and metadata ===== + +#[test] +fn test_execution_result_lifecycle() { + let task = name_ptr("task-2"); + let qubits: [u32; 2] = [0, 1]; + let result = execution_result_new(task.as_ptr(), qubits.as_ptr(), 2, 100, std::ptr::null()); + assert!(!result.is_null()); + + // Freshly created: queued, no backend, only the creation timestamp exists. + let (mut tag, mut code) = (0u8, 0i32); + assert_eq!(execution_result_status(result, &mut tag, &mut code), 0); + assert_eq!(tag, EXECUTION_STATUS_QUEUED); + assert_eq!(code, 0); + let mut ms = 0i64; + assert_eq!(execution_result_created_at(result, &mut ms), 0); + assert!(ms > 1_600_000_000_000); + assert_eq!(execution_result_started_at(result, &mut ms), -8); + assert_eq!(execution_result_finished_at(result, &mut ms), -8); + assert_eq!(execution_result_qubits_len(result), 2); + let mut got = [0u32; 2]; + assert_eq!(execution_result_qubits(result, got.as_mut_ptr(), 2), 2); + assert_eq!(got, [0, 1]); + assert_eq!(execution_result_shots(result), 100); + assert_eq!(execution_result_num_qubits(result), 2); + let id = execution_result_task_id(result); + assert_eq!(cstr_to_string(id), "task-2"); + cqlib_string_free(id); + assert!(execution_result_backend(result).is_null()); + assert!(execution_result_error_message(result).is_null()); + + // start -> running. + assert_eq!(execution_result_start(result), 0); + assert_eq!(execution_result_status(result, &mut tag, &mut code), 0); + assert_eq!(tag, EXECUTION_STATUS_RUNNING); + assert_eq!(execution_result_started_at(result, &mut ms), 0); + assert!(ms > 1_600_000_000_000); + + // finish -> completed with accumulated counts. + let bits = [name_ptr("00"), name_ptr("11"), name_ptr("00")]; + let bit_ptrs: [*const c_char; 3] = [bits[0].as_ptr(), bits[1].as_ptr(), bits[2].as_ptr()]; + let counts: [u64; 3] = [600, 400, 50]; + assert_eq!( + execution_result_finish(result, bit_ptrs.as_ptr(), counts.as_ptr(), 3), + 0 + ); + assert_eq!(execution_result_status(result, &mut tag, &mut code), 0); + assert_eq!(tag, EXECUTION_STATUS_COMPLETED); + assert_eq!(execution_result_finished_at(result, &mut ms), 0); + let list = execution_result_counts(result); + assert_eq!(counts_list_len(list), 2); + let mut total = 0.0; + for i in 0..counts_list_len(list) { + total += counts_list_get_value(list, i); + } + assert_eq!(total, 1050.0); + counts_list_free(list); + assert!(execution_result_error_message(result).is_null()); + + // Invalid bitstrings are rejected. + let bad_bits = [name_ptr("0x")]; + let bad_ptrs: [*const c_char; 1] = [bad_bits[0].as_ptr()]; + let one: [u64; 1] = [1]; + assert_eq!( + execution_result_finish(result, bad_ptrs.as_ptr(), one.as_ptr(), 1), + -4 + ); + + // fail -> failed with error code and message. + let msg = name_ptr("device offline"); + assert_eq!(execution_result_fail(result, msg.as_ptr(), 42), 0); + assert_eq!(execution_result_status(result, &mut tag, &mut code), 0); + assert_eq!(tag, EXECUTION_STATUS_FAILED); + assert_eq!(code, 42); + let err = execution_result_error_message(result); + assert_eq!(cstr_to_string(err), "device offline"); + cqlib_string_free(err); + assert_eq!(execution_result_fail(result, std::ptr::null(), 1), -1); + + // cancel -> cancelled. + assert_eq!(execution_result_cancel(result), 0); + assert_eq!(execution_result_status(result, &mut tag, &mut code), 0); + assert_eq!(tag, EXECUTION_STATUS_CANCELLED); + + execution_result_free(result); + + // A backend string is echoed back. + let backend = name_ptr("sim-ctx"); + let with_backend = + execution_result_new(task.as_ptr(), qubits.as_ptr(), 2, 10, backend.as_ptr()); + assert!(!with_backend.is_null()); + let got_backend = execution_result_backend(with_backend); + assert_eq!(cstr_to_string(got_backend), "sim-ctx"); + cqlib_string_free(got_backend); + execution_result_free(with_backend); + + // NULL guards. + assert!( + execution_result_new(std::ptr::null(), qubits.as_ptr(), 2, 10, std::ptr::null()).is_null() + ); + assert!( + execution_result_new(task.as_ptr(), std::ptr::null(), 2, 10, std::ptr::null()).is_null() + ); + assert_eq!(execution_result_start(std::ptr::null_mut()), -1); + assert_eq!( + execution_result_finish(std::ptr::null_mut(), std::ptr::null(), std::ptr::null(), 0), + -1 + ); + assert_eq!(execution_result_cancel(std::ptr::null_mut()), -1); + assert_eq!( + execution_result_status(std::ptr::null(), &mut tag, &mut code), + -1 + ); + assert!(execution_result_error_message(std::ptr::null()).is_null()); + assert!(execution_result_task_id(std::ptr::null()).is_null()); + assert!(execution_result_backend(std::ptr::null()).is_null()); + assert_eq!(execution_result_created_at(std::ptr::null(), &mut ms), -1); + assert_eq!(execution_result_started_at(std::ptr::null(), &mut ms), -1); + assert_eq!(execution_result_finished_at(std::ptr::null(), &mut ms), -1); + assert_eq!(execution_result_qubits_len(std::ptr::null()), 0); +} diff --git a/crates/binding-c/tests/error_mitigation_test.rs b/crates/binding-c/tests/error_mitigation_test.rs new file mode 100644 index 00000000..e02562e1 --- /dev/null +++ b/crates/binding-c/tests/error_mitigation_test.rs @@ -0,0 +1,645 @@ +//! Integration tests for error mitigation module C ABI. + +#![allow(dead_code)] + +use binding_c::circuit::{ + circuit_cx, circuit_free, circuit_h, circuit_new, circuit_num_operations, +}; +use binding_c::error_mitigation::*; +use binding_c::qis::{ + hamiltonian_add_term, hamiltonian_free, hamiltonian_new, pauli_string_free, pauli_string_parse, +}; +use std::os::raw::c_char; + +/// Simple C estimator: returns the operation count as the "expectation value" +/// and 0.0 variance. Mirrors what a device/simulator backend would provide. +extern "C" fn counting_estimator( + circuit: *const binding_c::circuit::CCircuit, + _hamiltonian: *const binding_c::qis::CHamiltonian, + _shots: usize, + expectation: *mut f64, + variance: *mut f64, +) { + if circuit.is_null() || expectation.is_null() || variance.is_null() { + return; + } + let ops = circuit_num_operations(circuit); + unsafe { + *expectation = ops as f64; + *variance = 0.0; + } +} + +fn build_test_hamiltonian() -> *mut binding_c::qis::CHamiltonian { + let h = hamiltonian_new(2); + assert!(!h.is_null()); + let pauli = pauli_string_parse(c"ZZ".as_ptr() as *const c_char); + assert!(!pauli.is_null()); + assert_eq!(hamiltonian_add_term(h, pauli, 1.0, 0.0), 0); + pauli_string_free(pauli); + h +} + +#[test] +fn test_zne_mitigation_new_and_free() { + let circuit = circuit_new(2); + assert_eq!(circuit_h(circuit, 0), 0); + assert_eq!(circuit_cx(circuit, 0, 1), 0); + + let levels: [i32; 3] = [0, 1, 2]; + let zne = zne_mitigation_new(circuit, levels.as_ptr(), 3); + assert!(!zne.is_null()); + assert_eq!(zne_mitigation_noise_factor(zne, 0), 1); + assert_eq!(zne_mitigation_noise_factor(zne, 1), 3); + assert_eq!(zne_mitigation_noise_factor(zne, 2), 5); + assert_eq!(zne_mitigation_noise_factor(zne, 9), 0); + + zne_mitigation_free(zne); + zne_mitigation_free(std::ptr::null_mut()); + circuit_free(circuit); +} + +#[test] +fn test_zne_mitigation_fold_circuit() { + let circuit = circuit_new(2); + assert_eq!(circuit_h(circuit, 0), 0); + assert_eq!(circuit_cx(circuit, 0, 1), 0); + + let levels: [i32; 3] = [0, 1, 2]; + let zne = zne_mitigation_new(circuit, levels.as_ptr(), 3); + assert!(!zne.is_null()); + + // Global folding produces one circuit per level; higher levels fold in + // more gate copies. + let list = zne_mitigation_fold_circuit(zne, std::ptr::null()); + assert!(!list.is_null()); + assert_eq!(circuit_list_len(list), 3); + let op0 = circuit_num_operations(circuit_list_get(list, 0)); + let op2 = circuit_num_operations(circuit_list_get(list, 2)); + assert_eq!(op0, 2); + assert!(op2 > op0, "level-2 fold should add operations"); + circuit_list_free(list); + + // Selective folding on H only. + let list = zne_mitigation_fold_circuit(zne, c"H".as_ptr() as *const c_char); + assert!(!list.is_null()); + assert_eq!(circuit_list_len(list), 3); + circuit_list_free(list); + + // Unknown gate name -> NULL. + let list = zne_mitigation_fold_circuit(zne, c"NOT_A_GATE".as_ptr() as *const c_char); + assert!(list.is_null()); + + zne_mitigation_free(zne); + circuit_free(circuit); +} + +#[test] +fn test_error_mitigation_zne_full_pipeline() { + let circuit = circuit_new(2); + assert_eq!(circuit_h(circuit, 0), 0); + assert_eq!(circuit_cx(circuit, 0, 1), 0); + + let levels: [i32; 3] = [0, 1, 2]; + let em = error_mitigation_new(circuit, MITIGATION_ZNE, levels.as_ptr(), 3, 0); + assert!(!em.is_null()); + + let hamiltonian = build_test_hamiltonian(); + assert_eq!( + error_mitigation_run(em, MITIGATION_ZNE, hamiltonian, 0, 0, counting_estimator), + 0 + ); + + let mut expectation = f64::NAN; + let mut variance = f64::NAN; + assert_eq!( + error_mitigation_get_mitigated( + em, + PROCESS_ZNE_POLYNOMIAL, + 1, + &mut expectation, + &mut variance + ), + 0 + ); + assert!( + expectation.is_finite(), + "mitigated expectation should be finite" + ); + + hamiltonian_free(hamiltonian); + error_mitigation_free(em); + circuit_free(circuit); +} + +#[test] +fn test_error_mitigation_run_requires_run_before_mitigate() { + let circuit = circuit_new(1); + let levels: [i32; 2] = [0, 1]; + let em = error_mitigation_new(circuit, MITIGATION_ZNE, levels.as_ptr(), 2, 0); + assert!(!em.is_null()); + + let mut expectation = f64::NAN; + let mut variance = f64::NAN; + // Calling get_mitigated before run must fail (-7). + assert_eq!( + error_mitigation_get_mitigated( + em, + PROCESS_ZNE_POLYNOMIAL, + 0, + &mut expectation, + &mut variance + ), + -7 + ); + + error_mitigation_free(em); + circuit_free(circuit); +} + +#[test] +fn test_error_mitigation_invalid_params() { + let circuit = circuit_new(1); + + // ZNE without fold levels -> NULL. + assert!(error_mitigation_new(circuit, MITIGATION_ZNE, std::ptr::null(), 0, 0).is_null()); + // VD with zero copies -> NULL. + assert!( + error_mitigation_new( + circuit, + MITIGATION_VIRTUAL_DISTILLATION, + std::ptr::null(), + 0, + 0 + ) + .is_null() + ); + // Unknown method -> NULL. + assert!(error_mitigation_new(circuit, 42, std::ptr::null(), 0, 0).is_null()); + + circuit_free(circuit); +} + +#[test] +fn test_error_mitigation_null_and_free() { + error_mitigation_free(std::ptr::null_mut()); + assert!( + error_mitigation_new(std::ptr::null(), MITIGATION_ZNE, std::ptr::null(), 0, 0).is_null() + ); +} + +#[test] +fn test_virtual_distillation_new_and_build() { + let circuit = circuit_new(1); + assert_eq!(circuit_h(circuit, 0), 0); + + let vd = virtual_distillation_new(circuit, 2); + assert!(!vd.is_null()); + + let copy_swap = virtual_distillation_build_circuit(vd); + assert!(!copy_swap.is_null()); + assert!( + circuit_num_operations(copy_swap) > 0, + "copy-swap circuit should contain operations" + ); + circuit_free(copy_swap); + + virtual_distillation_free(vd); + virtual_distillation_free(std::ptr::null_mut()); + + // Zero copies -> NULL. + assert!(virtual_distillation_new(circuit, 0).is_null()); + circuit_free(circuit); +} + +// ===== Fine-grained ZNE API (checklist section 5) ===== + +#[test] +fn test_zne_circuit_levels_and_noise_factors_roundtrip() { + let circuit = circuit_new(2); + assert_eq!(circuit_h(circuit, 0), 0); + assert_eq!(circuit_cx(circuit, 0, 1), 0); + + let levels: [i32; 3] = [0, 1, 2]; + let zne = zne_mitigation_new(circuit, levels.as_ptr(), 3); + assert!(!zne.is_null()); + + // zne_circuit returns the original (unfolded) circuit. + let original = zne_circuit(zne); + assert!(!original.is_null()); + assert_eq!(circuit_num_operations(original), 2); + circuit_free(original); + + // Fold levels roundtrip (two-step). + assert_eq!(zne_fold_levels_len(zne), 3); + let mut out_levels = [0i32; 3]; + assert_eq!(zne_fold_levels(zne, out_levels.as_mut_ptr(), 3), 0); + assert_eq!(out_levels, [0, 1, 2]); + assert_eq!(zne_fold_levels(zne, out_levels.as_mut_ptr(), 2), -8); + + // Noise factors roundtrip (two-step). + assert_eq!(zne_noise_factors_len(zne), 3); + let mut factors = [0i32; 3]; + assert_eq!(zne_noise_factors(zne, factors.as_mut_ptr(), 3), 0); + assert_eq!(factors, [1, 3, 5]); + assert_eq!(zne_noise_factors(zne, factors.as_mut_ptr(), 4), -8); + + // NULL handling. + assert!(zne_circuit(std::ptr::null()).is_null()); + assert_eq!(zne_fold_levels_len(std::ptr::null()), 0); + assert_eq!(zne_noise_factors_len(std::ptr::null()), 0); + assert_eq!( + zne_fold_levels(std::ptr::null(), out_levels.as_mut_ptr(), 3), + -1 + ); + + zne_mitigation_free(zne); + circuit_free(circuit); +} + +#[test] +fn test_zne_fold_circuits_two_step() { + let circuit = circuit_new(2); + assert_eq!(circuit_h(circuit, 0), 0); + assert_eq!(circuit_cx(circuit, 0, 1), 0); + + let levels: [i32; 3] = [0, 1, 2]; + let zne = zne_mitigation_new(circuit, levels.as_ptr(), 3); + assert!(!zne.is_null()); + + // Two-step folding: one owned circuit per fold level. + assert_eq!(zne_fold_circuits_len(zne, std::ptr::null()), 3); + let mut folded: Vec<*mut binding_c::circuit::CCircuit> = vec![std::ptr::null_mut(); 3]; + assert_eq!( + zne_fold_circuits(zne, std::ptr::null(), folded.as_mut_ptr(), 3), + 0 + ); + + // Global folding scales the circuit to (2 * level + 1) copies: the + // 2-operation Bell circuit becomes 2 / 6 / 10 operations. + let ops: Vec = folded.iter().map(|c| circuit_num_operations(*c)).collect(); + assert_eq!(ops, vec![2, 6, 10]); + for circuit in folded { + circuit_free(circuit); + } + + // Selective folding on H only keeps CX untouched: level 1 yields + // H H H CX (4 ops) and level 2 yields 5 H's + CX (6 ops). + assert_eq!(zne_fold_circuits_len(zne, c"H".as_ptr()), 3); + let mut folded: Vec<*mut binding_c::circuit::CCircuit> = vec![std::ptr::null_mut(); 3]; + assert_eq!( + zne_fold_circuits(zne, c"H".as_ptr(), folded.as_mut_ptr(), 3), + 0 + ); + assert_eq!(circuit_num_operations(folded[1]), 4); + assert_eq!(circuit_num_operations(folded[2]), 6); + for circuit in folded { + circuit_free(circuit); + } + + // Buffer length mismatch and unknown gate names must fail. + let mut sink: *mut binding_c::circuit::CCircuit = std::ptr::null_mut(); + assert_eq!(zne_fold_circuits(zne, std::ptr::null(), &mut sink, 2), -8); + assert_eq!( + zne_fold_circuits(zne, c"NOT_A_GATE".as_ptr(), &mut sink, 3), + -4 + ); + assert_eq!(zne_fold_circuits_len(zne, c"NOT_A_GATE".as_ptr()), 0); + + zne_mitigation_free(zne); + circuit_free(circuit); +} + +#[test] +fn test_zne_extrapolate_known_values() { + let circuit = circuit_new(2); + assert_eq!(circuit_h(circuit, 0), 0); + assert_eq!(circuit_cx(circuit, 0, 1), 0); + + let levels: [i32; 3] = [0, 1, 2]; + let zne = zne_mitigation_new(circuit, levels.as_ptr(), 3); + assert!(!zne.is_null()); + + // Linear data y = 2x + 1 over noise factors x = [1, 3, 5]: the degree-1 + // fit extrapolates exactly to y(0) = 1. + let linear = [3.0, 7.0, 11.0]; + let mut value = f64::NAN; + assert_eq!( + zne_poly_extrapolate(zne, linear.as_ptr(), 3, 1, &mut value), + 0 + ); + assert!((value - 1.0).abs() < 1e-9); + + // The unified entry point with the method tag must agree. + assert_eq!( + zne_extrapolate( + zne, + linear.as_ptr(), + 3, + ZNE_EXTRAPOLATE_POLYNOMIAL, + 1, + &mut value + ), + 0 + ); + assert!((value - 1.0).abs() < 1e-9); + + // Degenerate degree-0 fit collapses to the mean of the data. + assert_eq!( + zne_poly_extrapolate(zne, linear.as_ptr(), 3, 0, &mut value), + 0 + ); + assert!((value - 7.0).abs() < 1e-9); + + // Exponential decay y = exp(-x) extrapolates to A = 1 at x = 0. + let decay = [(-1.0f64).exp(), (-3.0f64).exp(), (-5.0f64).exp()]; + assert_eq!(zne_exp_extrapolate(zne, decay.as_ptr(), 3, &mut value), 0); + assert!((value - 1.0).abs() < 1e-9); + assert_eq!( + zne_extrapolate( + zne, + decay.as_ptr(), + 3, + ZNE_EXTRAPOLATE_EXPONENTIAL, + 0, + &mut value + ), + 0 + ); + assert!((value - 1.0).abs() < 1e-9); + + // Invalid inputs must fail. + assert_ne!( + zne_poly_extrapolate(zne, linear.as_ptr(), 3, 3, &mut value), + 0 + ); + assert_ne!( + zne_poly_extrapolate(zne, linear.as_ptr(), 2, 1, &mut value), + 0 + ); + assert_ne!( + zne_exp_extrapolate(zne, [0.0, 1.0, 2.0].as_ptr(), 3, &mut value), + 0 + ); + assert_ne!( + zne_extrapolate(zne, linear.as_ptr(), 3, 42, 1, &mut value), + 0 + ); + + zne_mitigation_free(zne); + circuit_free(circuit); +} + +#[test] +fn test_zne_run_em_sequence() { + let circuit = circuit_new(2); + assert_eq!(circuit_h(circuit, 0), 0); + assert_eq!(circuit_cx(circuit, 0, 1), 0); + + let levels: [i32; 3] = [0, 1, 2]; + let zne = zne_mitigation_new(circuit, levels.as_ptr(), 3); + assert!(!zne.is_null()); + + let hamiltonian = build_test_hamiltonian(); + assert_eq!(zne_run_em_sequence_len(zne), 3); + assert_eq!(zne_run_em_sequence_with_shots_len(zne), 3); + + // The counting estimator reports the folded circuit lengths. + let mut sequence = [f64::NAN; 3]; + assert_eq!( + zne_run_em_sequence( + zne, + std::ptr::null(), + hamiltonian, + counting_estimator, + sequence.as_mut_ptr(), + 3 + ), + 0 + ); + assert_eq!(sequence, [2.0, 6.0, 10.0]); + + // The with-shots variant forwards the shot count to the estimator. + let mut sequence = [f64::NAN; 3]; + assert_eq!( + zne_run_em_sequence_with_shots( + zne, + c"H".as_ptr(), + hamiltonian, + 128, + counting_estimator, + sequence.as_mut_ptr(), + 3 + ), + 0 + ); + assert_eq!(sequence, [2.0, 4.0, 6.0]); + + // Buffer mismatch, Hamiltonian width mismatch, and unknown gate names + // must fail. + let mut sink = [0.0f64; 2]; + assert_eq!( + zne_run_em_sequence( + zne, + std::ptr::null(), + hamiltonian, + counting_estimator, + sink.as_mut_ptr(), + 2 + ), + -8 + ); + let wide = hamiltonian_new(3); + assert!(!wide.is_null()); + assert_eq!( + zne_run_em_sequence( + zne, + std::ptr::null(), + wide, + counting_estimator, + sequence.as_mut_ptr(), + 3 + ), + -8 + ); + hamiltonian_free(wide); + assert_eq!( + zne_run_em_sequence( + zne, + c"NOT_A_GATE".as_ptr(), + hamiltonian, + counting_estimator, + sequence.as_mut_ptr(), + 3 + ), + -4 + ); + assert_eq!( + zne_run_em_sequence( + std::ptr::null(), + std::ptr::null(), + hamiltonian, + counting_estimator, + sequence.as_mut_ptr(), + 3 + ), + -1 + ); + + hamiltonian_free(hamiltonian); + zne_mitigation_free(zne); + circuit_free(circuit); +} + +// ===== Fine-grained VirtualDistillation API (checklist section 5) ===== + +/// Static estimator: numerator invocations (Hamiltonian present) report +/// (4.0, 0.1); denominator invocations (Hamiltonian NULL) report (2.0, 0.05). +extern "C" fn static_vd_estimator( + _circuit: *const binding_c::circuit::CCircuit, + hamiltonian: *const binding_c::qis::CHamiltonian, + _shots: usize, + expectation: *mut f64, + variance: *mut f64, +) { + if expectation.is_null() || variance.is_null() { + return; + } + unsafe { + if hamiltonian.is_null() { + *expectation = 2.0; + *variance = 0.05; + } else { + *expectation = 4.0; + *variance = 0.1; + } + } +} + +#[test] +fn test_virtual_distillation_copies_roundtrip() { + let circuit = circuit_new(1); + assert_eq!(circuit_h(circuit, 0), 0); + + let vd = virtual_distillation_new(circuit, 2); + assert!(!vd.is_null()); + assert_eq!(virtual_distillation_copies(vd), 2); + + assert_eq!(virtual_distillation_set_copies(vd, 3), 0); + assert_eq!(virtual_distillation_copies(vd), 3); + + // Copies below 2 are rejected. + assert_eq!(virtual_distillation_set_copies(vd, 1), -8); + assert_eq!(virtual_distillation_set_copies(vd, 0), -8); + assert_eq!(virtual_distillation_copies(vd), 3); + + // NULL handling. + assert_eq!(virtual_distillation_copies(std::ptr::null()), 0); + assert_eq!(virtual_distillation_set_copies(std::ptr::null_mut(), 3), -1); + + virtual_distillation_free(vd); + circuit_free(circuit); +} + +#[test] +fn test_virtual_distillation_run_vd_sampling() { + let circuit = circuit_new(1); + assert_eq!(circuit_h(circuit, 0), 0); + + let vd = virtual_distillation_new(circuit, 2); + assert!(!vd.is_null()); + + let hamiltonian = hamiltonian_new(1); + assert!(!hamiltonian.is_null()); + let pauli = pauli_string_parse(c"Z".as_ptr() as *const c_char); + assert!(!pauli.is_null()); + assert_eq!(hamiltonian_add_term(hamiltonian, pauli, 1.0, 0.0), 0); + pauli_string_free(pauli); + + // Numerator: the estimator sees the copy-swap circuit and the expanded + // Hamiltonian; denominator: no Hamiltonian. + let mut num_mean = f64::NAN; + let mut num_var = f64::NAN; + assert_eq!( + virtual_distillation_run_numerator_circuit( + vd, + hamiltonian, + 100, + static_vd_estimator, + &mut num_mean, + &mut num_var + ), + 0 + ); + assert_eq!(num_mean, 4.0); + assert_eq!(num_var, 0.1); + + let mut den_mean = f64::NAN; + let mut den_var = f64::NAN; + assert_eq!( + virtual_distillation_run_denominator_circuit( + vd, + 100, + static_vd_estimator, + &mut den_mean, + &mut den_var + ), + 0 + ); + assert_eq!(den_mean, 2.0); + assert_eq!(den_var, 0.05); + + // run_vd normalizes the numerator by the denominator: 4 / 2 = 2 with + // var = 0.1 / 2^2 + 4^2 * 0.05 / 2^4 = 0.075. + let mut mean = f64::NAN; + let mut variance = f64::NAN; + assert_eq!( + virtual_distillation_run_vd( + vd, + hamiltonian, + 100, + 100, + static_vd_estimator, + &mut mean, + &mut variance + ), + 0 + ); + assert!((mean - 2.0).abs() < 1e-12); + assert!((variance - 0.075).abs() < 1e-12); + + // Hamiltonian width mismatch must fail. + let wide = hamiltonian_new(2); + assert!(!wide.is_null()); + assert_eq!( + virtual_distillation_run_vd( + vd, + wide, + 100, + 100, + static_vd_estimator, + &mut mean, + &mut variance + ), + -8 + ); + hamiltonian_free(wide); + + // NULL handling. + assert_eq!( + virtual_distillation_run_vd( + std::ptr::null(), + hamiltonian, + 100, + 100, + static_vd_estimator, + &mut mean, + &mut variance + ), + -1 + ); + + hamiltonian_free(hamiltonian); + virtual_distillation_free(vd); + circuit_free(circuit); +} diff --git a/crates/binding-c/tests/ffi_test.rs b/crates/binding-c/tests/ffi_test.rs deleted file mode 100644 index 10f478fc..00000000 --- a/crates/binding-c/tests/ffi_test.rs +++ /dev/null @@ -1,104 +0,0 @@ -// This code is part of Cqlib. -// -// (C) Copyright China Telecom Quantum Group 2026 -// -// This code is licensed under the Apache License, Version 2.0. You may -// obtain a copy of this license in the LICENSE.txt file in the root directory -// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -// -// Any modifications or derivative works of this code must retain this -// copyright notice, and modified files need to carry a notice indicating -// that they have been altered from the originals. - -use binding_c::circuit::{ - circuit_assign_params, circuit_cx, circuit_cz, circuit_free, circuit_h, circuit_measure, - circuit_new, circuit_num_operations, circuit_num_parameters, circuit_num_qubits, circuit_reset, - circuit_rx, circuit_rx_param, circuit_ry, circuit_ry_param, circuit_rz, circuit_rz_param, - circuit_validate, circuit_x, circuit_y, circuit_z, param_evaluate, param_free, param_parse, -}; -use std::ffi::CString; - -#[test] -fn circuit_lifecycle_and_basic_gates() { - let circuit = circuit_new(2); - assert!(!circuit.is_null()); - assert_eq!(circuit_num_qubits(circuit), 2); - assert_eq!(circuit_num_operations(circuit), 0); - - assert_eq!(circuit_h(circuit, 0), 0); - assert_eq!(circuit_x(circuit, 1), 0); - assert_eq!(circuit_y(circuit, 0), 0); - assert_eq!(circuit_z(circuit, 1), 0); - assert_eq!(circuit_rx(circuit, 0, 0.25), 0); - assert_eq!(circuit_ry(circuit, 1, 0.5), 0); - assert_eq!(circuit_rz(circuit, 0, 0.75), 0); - assert_eq!(circuit_cx(circuit, 0, 1), 0); - assert_eq!(circuit_cz(circuit, 1, 0), 0); - assert_eq!(circuit_measure(circuit, 0), 0); - assert_eq!(circuit_reset(circuit, 1), 0); - - assert_eq!(circuit_num_operations(circuit), 11); - assert_eq!(circuit_validate(circuit), 0); - circuit_free(circuit); -} - -#[test] -fn circuit_error_codes_are_stable() { - assert_eq!(circuit_num_qubits(std::ptr::null()), 0); - assert_eq!(circuit_num_operations(std::ptr::null()), 0); - assert_eq!(circuit_num_parameters(std::ptr::null()), 0); - assert_eq!(circuit_validate(std::ptr::null()), -1); - assert_eq!(circuit_h(std::ptr::null_mut(), 0), -1); - circuit_free(std::ptr::null_mut()); - - let circuit = circuit_new(1); - assert_eq!(circuit_h(circuit, 1), -2); - assert_eq!(circuit_cx(circuit, 0, 1), -2); - assert_eq!(circuit_rx(circuit, 0, f64::NAN), -3); - circuit_free(circuit); -} - -#[test] -fn symbolic_parameters_can_be_evaluated_and_assigned() { - let theta = CString::new("theta").unwrap(); - let phi = CString::new("phi").unwrap(); - let theta_ptr = param_parse(theta.as_ptr()); - let phi_ptr = param_parse(phi.as_ptr()); - assert!(!theta_ptr.is_null()); - assert!(!phi_ptr.is_null()); - - let bindings = CString::new("theta:0.5,phi:1.25").unwrap(); - assert!((param_evaluate(theta_ptr, bindings.as_ptr()) - 0.5).abs() < 1e-12); - assert!((param_evaluate(phi_ptr, bindings.as_ptr()) - 1.25).abs() < 1e-12); - - let circuit = circuit_new(2); - assert_eq!(circuit_rx_param(circuit, 0, theta_ptr), 0); - assert_eq!(circuit_ry_param(circuit, 1, phi_ptr), 0); - assert_eq!(circuit_rz_param(circuit, 0, theta_ptr), 0); - assert_eq!(circuit_cx(circuit, 0, 1), 0); - assert_eq!(circuit_num_operations(circuit), 4); - assert_eq!(circuit_num_parameters(circuit), 2); - - let assigned = circuit_assign_params(circuit, bindings.as_ptr()); - assert!(!assigned.is_null()); - assert_eq!(circuit_num_operations(assigned), 4); - assert_eq!(circuit_num_parameters(assigned), 0); - assert_eq!(circuit_validate(assigned), 0); - - circuit_free(assigned); - circuit_free(circuit); - param_free(theta_ptr); - param_free(phi_ptr); -} - -#[test] -fn invalid_parameters_return_null_or_error() { - assert!(param_parse(std::ptr::null()).is_null()); - assert_eq!(param_evaluate(std::ptr::null(), std::ptr::null()), 0.0); - - let circuit = circuit_new(1); - assert_eq!(circuit_rx_param(circuit, 0, std::ptr::null()), -1); - assert!(circuit_assign_params(std::ptr::null(), std::ptr::null()).is_null()); - circuit_free(circuit); - param_free(std::ptr::null_mut()); -} diff --git a/crates/binding-c/tests/ir_test.rs b/crates/binding-c/tests/ir_test.rs new file mode 100644 index 00000000..82767540 --- /dev/null +++ b/crates/binding-c/tests/ir_test.rs @@ -0,0 +1,148 @@ +//! Integration tests for IR module C ABI. + +#![allow(dead_code)] + +use binding_c::circuit::{circuit_free, circuit_num_qubits}; +use binding_c::cqlib_string_free; +use binding_c::ir::{qasm2_dump, qasm2_dumps, qasm2_loads, qasm3_dumps, qcis_dumps, qcis_loads}; +use std::ffi::{CStr, CString}; +use std::os::raw::c_char; + +const QASM2_SAMPLE: &str = r#" +OPENQASM 2.0; +include "qelib1.inc"; +qreg q[2]; +h q[0]; +cx q[0], q[1]; +"#; + +const QCIS_SAMPLE: &str = "H Q0\nCZ Q0 Q1\nM Q0 Q1"; + +fn cstr_to_string(ptr: *mut c_char) -> String { + if ptr.is_null() { + return String::new(); + } + unsafe { CStr::from_ptr(ptr) } + .to_string_lossy() + .into_owned() +} + +#[test] +fn test_qasm2_loads_and_dumps() { + let source = CString::new(QASM2_SAMPLE).unwrap(); + let circuit = qasm2_loads(source.as_ptr()); + assert!(!circuit.is_null(), "qasm2_loads should succeed"); + + let num_qubits = circuit_num_qubits(circuit); + assert_eq!(num_qubits, 2); + + let dump_ptr = qasm2_dumps(circuit); + assert!(!dump_ptr.is_null(), "qasm2_dumps should succeed"); + let dumped = cstr_to_string(dump_ptr); + cqlib_string_free(dump_ptr); + assert!(dumped.contains("h")); + assert!(dumped.contains("cx")); + + circuit_free(circuit); +} + +#[test] +fn test_qasm2_loads_null() { + let circuit = qasm2_loads(std::ptr::null()); + assert!(circuit.is_null(), "NULL source should return NULL"); +} + +#[test] +fn test_qasm2_loads_invalid() { + let source = CString::new("not valid qasm").unwrap(); + let circuit = qasm2_loads(source.as_ptr()); + assert!(circuit.is_null(), "invalid source should return NULL"); +} + +#[test] +fn test_qasm2_dumps_null() { + let dump_ptr = qasm2_dumps(std::ptr::null()); + assert!(dump_ptr.is_null(), "NULL circuit should return NULL"); +} + +#[test] +fn test_qasm3_dumps() { + let source = CString::new(QASM2_SAMPLE).unwrap(); + let circuit = qasm2_loads(source.as_ptr()); + assert!(!circuit.is_null()); + + let dump_ptr = qasm3_dumps(circuit); + assert!(!dump_ptr.is_null(), "qasm3_dumps should succeed"); + cqlib_string_free(dump_ptr); + + circuit_free(circuit); +} + +#[test] +fn test_qcis_loads_and_dumps() { + let source = CString::new(QCIS_SAMPLE).unwrap(); + let circuit = qcis_loads(source.as_ptr()); + assert!(!circuit.is_null(), "qcis_loads should succeed"); + + let num_qubits = circuit_num_qubits(circuit); + assert_eq!(num_qubits, 2); + + let dump_ptr = qcis_dumps(circuit); + assert!(!dump_ptr.is_null(), "qcis_dumps should succeed"); + let dumped = cstr_to_string(dump_ptr); + cqlib_string_free(dump_ptr); + assert!(dumped.to_lowercase().contains("h")); + + circuit_free(circuit); +} + +#[test] +fn test_qcis_loads_null() { + let circuit = qcis_loads(std::ptr::null()); + assert!(circuit.is_null()); +} + +#[test] +fn test_qcis_loads_invalid() { + let source = CString::new("@@@INVALID_GARBAGE@@@").unwrap(); + let circuit = qcis_loads(source.as_ptr()); + assert!(circuit.is_null(), "invalid QCIS should return NULL"); +} + +#[test] +fn test_qasm2_dump_to_file() { + let source = CString::new(QASM2_SAMPLE).unwrap(); + let circuit = qasm2_loads(source.as_ptr()); + assert!(!circuit.is_null()); + + let tmp = std::env::temp_dir().join("cqlib_test_qasm2_dump.qasm"); + let path = CString::new(tmp.to_str().unwrap()).unwrap(); + let ret = qasm2_dump(circuit, path.as_ptr()); + assert_eq!(ret, 0, "qasm2_dump should return 0 on success"); + + let content = std::fs::read_to_string(&tmp); + assert!(content.is_ok()); + assert!(content.unwrap().contains("h")); + let _ = std::fs::remove_file(&tmp); + + circuit_free(circuit); +} + +#[test] +fn test_qasm2_dump_null_circuit() { + let path = CString::new("target/dummy.qasm").unwrap(); + let ret = qasm2_dump(std::ptr::null(), path.as_ptr()); + assert_eq!(ret, -1, "NULL circuit should return NullPtr error"); +} + +#[test] +fn test_qasm2_dump_null_path() { + let source = CString::new(QASM2_SAMPLE).unwrap(); + let circuit = qasm2_loads(source.as_ptr()); + assert!(!circuit.is_null()); + + let ret = qasm2_dump(circuit, std::ptr::null()); + assert_eq!(ret, -1, "NULL path should return NullPtr error"); + + circuit_free(circuit); +} diff --git a/crates/binding-c/tests/misc_missing_test.rs b/crates/binding-c/tests/misc_missing_test.rs new file mode 100644 index 00000000..7bda9de6 --- /dev/null +++ b/crates/binding-c/tests/misc_missing_test.rs @@ -0,0 +1,528 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! Integration tests for the remaining classical-expression, Hamiltonian, +//! and state-inspection C ABI entries. + +use binding_c::circuit::{ + CClassicalValueInfo, CQLIB_CLASSICAL_TYPE_BIT, CQLIB_CLASSICAL_TYPE_BIT_VEC, + CQLIB_CLASSICAL_TYPE_BOOL, CQLIB_CLASSICAL_TYPE_UINT, circuit_free, circuit_measure_into, + circuit_new, circuit_var, classical_expr_bit_literal, classical_expr_bool_literal, + classical_expr_free, classical_expr_is_bit_false, classical_expr_is_bit_true, + classical_expr_is_bool_false, classical_expr_is_bool_true, classical_expr_remap_classical_ids, + classical_expr_ty, classical_expr_values, classical_expr_values_len, classical_expr_var, + classical_expr_vars, classical_expr_vars_len, classical_expr_xor, classical_type_one_literal, + classical_type_zero_literal, classical_var_free, classical_var_id, +}; +use binding_c::qis::{ + CProbMeasurement, hamiltonian_add_term, hamiltonian_expectation_probs, hamiltonian_free, + hamiltonian_new, hamiltonian_variance_statevector, pauli_string_free, pauli_string_parse, + statevector_apply_cx, statevector_apply_h, statevector_apply_x, statevector_free, + statevector_new, statevector_num_qubits, +}; +use binding_c::visualization::{ + local_bloch_vectors, local_bloch_vectors_len, state_to_density_matrix, + state_to_density_matrix_len, +}; +use num_complex::Complex64; +use std::ffi::CString; +use std::os::raw::c_char; + +const EPS: f64 = 1e-10; + +fn assert_close(actual: f64, expected: f64) { + assert!( + (actual - expected).abs() < EPS, + "expected {expected}, got {actual}" + ); +} + +/// Checks the four literal predicates against the expected mutually exclusive +/// pattern `(bit_true, bit_false, bool_true, bool_false)`. +fn assert_predicates( + expr: *const binding_c::circuit::CClassicalExpr, + expected: (i32, i32, i32, i32), +) { + assert_eq!(classical_expr_is_bit_true(expr), expected.0); + assert_eq!(classical_expr_is_bit_false(expr), expected.1); + assert_eq!(classical_expr_is_bool_true(expr), expected.2); + assert_eq!(classical_expr_is_bool_false(expr), expected.3); +} + +#[test] +fn classical_expr_literal_predicates_are_mutually_exclusive() { + let bit_true = classical_expr_bit_literal(true); + let bit_false = classical_expr_bit_literal(false); + let bool_true = classical_expr_bool_literal(true); + let bool_false = classical_expr_bool_literal(false); + + assert_predicates(bit_true, (1, 0, 0, 0)); + assert_predicates(bit_false, (0, 1, 0, 0)); + assert_predicates(bool_true, (0, 0, 1, 0)); + assert_predicates(bool_false, (0, 0, 0, 1)); + + // Non-literal expressions satisfy none of the predicates. + let circuit = circuit_new(1); + let var = circuit_var(circuit, CQLIB_CLASSICAL_TYPE_BIT, 0); + let var_expr = classical_expr_var(var); + assert_predicates(var_expr, (0, 0, 0, 0)); + + // NULL input maps to -1. + assert_eq!(classical_expr_is_bit_true(std::ptr::null()), -1); + assert_eq!(classical_expr_is_bit_false(std::ptr::null()), -1); + assert_eq!(classical_expr_is_bool_true(std::ptr::null()), -1); + assert_eq!(classical_expr_is_bool_false(std::ptr::null()), -1); + + classical_expr_free(var_expr); + classical_var_free(var); + circuit_free(circuit); + classical_expr_free(bit_true); + classical_expr_free(bit_false); + classical_expr_free(bool_true); + classical_expr_free(bool_false); +} + +#[test] +fn classical_type_zero_and_one_literals() { + // Bit: zero/false and one/true, verified via the literal predicates. + let bit_zero = classical_type_zero_literal(CQLIB_CLASSICAL_TYPE_BIT, 0); + let bit_one = classical_type_one_literal(CQLIB_CLASSICAL_TYPE_BIT, 0); + assert_eq!(classical_expr_is_bit_false(bit_zero), 1); + assert_eq!(classical_expr_is_bit_true(bit_one), 1); + + // Bool: same pattern on the Bool predicates. + let bool_zero = classical_type_zero_literal(CQLIB_CLASSICAL_TYPE_BOOL, 0); + let bool_one = classical_type_one_literal(CQLIB_CLASSICAL_TYPE_BOOL, 0); + assert_eq!(classical_expr_is_bool_false(bool_zero), 1); + assert_eq!(classical_expr_is_bool_true(bool_one), 1); + + // Width-bearing types produce the matching literal kind and type. + let uint_one = classical_type_one_literal(CQLIB_CLASSICAL_TYPE_UINT, 8); + let (mut tag, mut width) = (u32::MAX, u32::MAX); + assert_eq!(classical_expr_ty(uint_one, &mut tag, &mut width), 0); + assert_eq!(tag, CQLIB_CLASSICAL_TYPE_UINT); + assert_eq!(width, 8); + + let bit_vec_zero = classical_type_zero_literal(CQLIB_CLASSICAL_TYPE_BIT_VEC, 4); + assert_eq!(classical_expr_ty(bit_vec_zero, &mut tag, &mut width), 0); + assert_eq!(tag, CQLIB_CLASSICAL_TYPE_BIT_VEC); + assert_eq!(width, 4); + + // Invalid parameters return NULL. + assert!(classical_type_zero_literal(99, 0).is_null()); + assert!(classical_type_zero_literal(CQLIB_CLASSICAL_TYPE_UINT, 0).is_null()); + assert!(classical_type_zero_literal(CQLIB_CLASSICAL_TYPE_BIT_VEC, 129).is_null()); + assert!(classical_type_one_literal(CQLIB_CLASSICAL_TYPE_UINT, 129).is_null()); + + classical_expr_free(bit_zero); + classical_expr_free(bit_one); + classical_expr_free(bool_zero); + classical_expr_free(bool_one); + classical_expr_free(uint_one); + classical_expr_free(bit_vec_zero); +} + +#[test] +fn classical_expr_remap_classical_ids_rewrites_vars() { + let circuit = circuit_new(2); + let var0 = circuit_var(circuit, CQLIB_CLASSICAL_TYPE_BIT, 0); + let var1 = circuit_var(circuit, CQLIB_CLASSICAL_TYPE_BIT, 0); + let expr0 = classical_expr_var(var0); + let expr1 = classical_expr_var(var1); + let expr = classical_expr_xor(expr0, expr1); + assert_eq!(classical_expr_vars_len(expr), 2); + + let old = [0u32, 1u32]; + let new = [5u32, 6u32]; + let remapped = classical_expr_remap_classical_ids( + expr, + old.as_ptr(), + new.as_ptr(), + old.len(), + std::ptr::null(), + std::ptr::null(), + 0, + ); + assert!(!remapped.is_null()); + assert_eq!(classical_expr_vars_len(remapped), 2); + + let mut handles = [std::ptr::null_mut(); 2]; + assert_eq!(classical_expr_vars(remapped, handles.as_mut_ptr(), 2), 0); + let mut ids = [0u32; 2]; + for (i, handle) in handles.iter().enumerate() { + ids[i] = classical_var_id(*handle); + classical_var_free(*handle); + } + ids.sort_unstable(); + assert_eq!(ids, [5, 6]); + + // Every referenced variable must be covered by the mapping. + let missing = classical_expr_remap_classical_ids( + expr, + old.as_ptr(), + new.as_ptr(), + 1, + std::ptr::null(), + std::ptr::null(), + 0, + ); + assert!(missing.is_null()); + + // NULL arrays for a non-zero length fail. + assert!( + classical_expr_remap_classical_ids( + expr, + std::ptr::null(), + new.as_ptr(), + 2, + std::ptr::null(), + std::ptr::null(), + 0 + ) + .is_null() + ); + // NULL expression fails. + assert!( + classical_expr_remap_classical_ids( + std::ptr::null(), + old.as_ptr(), + new.as_ptr(), + 2, + std::ptr::null(), + std::ptr::null(), + 0 + ) + .is_null() + ); + + classical_expr_free(remapped); + classical_expr_free(expr); + classical_expr_free(expr0); + classical_expr_free(expr1); + classical_var_free(var0); + classical_var_free(var1); + circuit_free(circuit); +} + +#[test] +fn classical_expr_remap_classical_ids_rewrites_measurement_values() { + let circuit = circuit_new(1); + let target = circuit_var(circuit, CQLIB_CLASSICAL_TYPE_BIT, 0); + let measurement = circuit_measure_into(circuit, 0, target); + assert!(!measurement.is_null()); + assert_eq!(classical_expr_values_len(measurement), 1); + + let mut infos = [CClassicalValueInfo { + index: u32::MAX, + tag: u32::MAX, + width: u32::MAX, + }]; + assert_eq!(classical_expr_values(measurement, infos.as_mut_ptr(), 1), 0); + let old_index = infos[0].index; + + let old = [old_index]; + let new = [7u32]; + let remapped = classical_expr_remap_classical_ids( + measurement, + std::ptr::null(), + std::ptr::null(), + 0, + old.as_ptr(), + new.as_ptr(), + old.len(), + ); + assert!(!remapped.is_null()); + + let mut remapped_infos = [CClassicalValueInfo { + index: u32::MAX, + tag: u32::MAX, + width: u32::MAX, + }]; + assert_eq!( + classical_expr_values(remapped, remapped_infos.as_mut_ptr(), 1), + 0 + ); + assert_eq!(remapped_infos[0].index, 7); + assert_eq!(remapped_infos[0].tag, infos[0].tag); + assert_eq!(remapped_infos[0].width, infos[0].width); + + // Missing value mapping fails. + let missing = classical_expr_remap_classical_ids( + measurement, + std::ptr::null(), + std::ptr::null(), + 0, + [old_index + 100].as_ptr(), + new.as_ptr(), + 1, + ); + assert!(missing.is_null()); + + classical_expr_free(remapped); + classical_expr_free(measurement); + classical_var_free(target); + circuit_free(circuit); +} + +/// Builds a 1-qubit Hamiltonian `H = Z` and returns both handles. The Pauli +/// string stays owned by the caller (`add_term` clones it). +fn z_hamiltonian() -> ( + *mut binding_c::qis::CHamiltonian, + *mut binding_c::qis::CPauliString, +) { + let source = CString::new("Z").unwrap(); + let pauli = pauli_string_parse(source.as_ptr()); + assert!(!pauli.is_null()); + let hamiltonian = hamiltonian_new(1); + assert!(!hamiltonian.is_null()); + assert_eq!(hamiltonian_add_term(hamiltonian, pauli, 1.0, 0.0), 0); + (hamiltonian, pauli) +} + +#[test] +fn hamiltonian_expectation_probs_basic() { + let (hamiltonian, pauli) = z_hamiltonian(); + + let states = [CString::new("0").unwrap(), CString::new("1").unwrap()]; + let state_ptrs = [ + states[0].as_ptr() as *const c_char, + states[1].as_ptr() as *const c_char, + ]; + + // Deterministic outcome |0> is the +1 eigenstate of Z. + let probs = [1.0f64, 0.0]; + let entry = CProbMeasurement { + basis: pauli, + states: state_ptrs.as_ptr(), + probs: probs.as_ptr(), + len: 2, + }; + let mut out = f64::NAN; + assert_eq!( + hamiltonian_expectation_probs(hamiltonian, &entry, 1, &mut out), + 0 + ); + assert_close(out, 1.0); + + // Balanced distribution averages to zero. + let balanced = [0.5f64, 0.5]; + let entry = CProbMeasurement { + basis: pauli, + states: state_ptrs.as_ptr(), + probs: balanced.as_ptr(), + len: 2, + }; + let mut out = f64::NAN; + assert_eq!( + hamiltonian_expectation_probs(hamiltonian, &entry, 1, &mut out), + 0 + ); + assert_close(out, 0.0); + + pauli_string_free(pauli); + hamiltonian_free(hamiltonian); +} + +#[test] +fn hamiltonian_expectation_probs_error_paths() { + let (hamiltonian, pauli) = z_hamiltonian(); + let states = [CString::new("0").unwrap(), CString::new("1").unwrap()]; + let state_ptrs = [ + states[0].as_ptr() as *const c_char, + states[1].as_ptr() as *const c_char, + ]; + let probs = [1.0f64, 0.0]; + let entry = CProbMeasurement { + basis: pauli, + states: state_ptrs.as_ptr(), + probs: probs.as_ptr(), + len: 2, + }; + let mut out = f64::NAN; + + // NULL handling. + assert_eq!( + hamiltonian_expectation_probs(std::ptr::null(), &entry, 1, &mut out), + -1 + ); + assert_eq!( + hamiltonian_expectation_probs(hamiltonian, &entry, 1, std::ptr::null_mut()), + -1 + ); + assert_eq!( + hamiltonian_expectation_probs(hamiltonian, std::ptr::null(), 1, &mut out), + -1 + ); + let null_basis = CProbMeasurement { + basis: std::ptr::null(), + states: state_ptrs.as_ptr(), + probs: probs.as_ptr(), + len: 2, + }; + assert_eq!( + hamiltonian_expectation_probs(hamiltonian, &null_basis, 1, &mut out), + -1 + ); + + // Empty measurement list: no compatible basis for the Z term. + assert_eq!( + hamiltonian_expectation_probs(hamiltonian, std::ptr::null(), 0, &mut out), + -7 + ); + + // Invalid UTF-8 state string. + let invalid = CString::new(&[0xFFu8][..]).unwrap(); + let bad_states = [ + invalid.as_ptr() as *const c_char, + states[1].as_ptr() as *const c_char, + ]; + let bad_entry = CProbMeasurement { + basis: pauli, + states: bad_states.as_ptr(), + probs: probs.as_ptr(), + len: 2, + }; + assert_eq!( + hamiltonian_expectation_probs(hamiltonian, &bad_entry, 1, &mut out), + -4 + ); + + pauli_string_free(pauli); + hamiltonian_free(hamiltonian); +} + +#[test] +fn hamiltonian_variance_statevector_basic() { + let (hamiltonian, pauli) = z_hamiltonian(); + + // |0> is an eigenstate of Z: zero variance. + let sv = statevector_new(1); + let mut out = f64::NAN; + assert_eq!( + hamiltonian_variance_statevector(hamiltonian, sv, &mut out), + 0 + ); + assert_close(out, 0.0); + + // |+> spreads over both Z eigenstates: Var(Z) = 1. + assert_eq!(statevector_apply_h(sv, 0), 0); + assert_eq!( + hamiltonian_variance_statevector(hamiltonian, sv, &mut out), + 0 + ); + assert_close(out, 1.0); + + // Qubit-count mismatch. + let big = statevector_new(2); + assert_eq!( + hamiltonian_variance_statevector(hamiltonian, big, &mut out), + -8 + ); + statevector_free(big); + + // NULL handling. + assert_eq!( + hamiltonian_variance_statevector(std::ptr::null(), sv, &mut out), + -1 + ); + assert_eq!( + hamiltonian_variance_statevector(hamiltonian, std::ptr::null(), &mut out), + -1 + ); + assert_eq!( + hamiltonian_variance_statevector(hamiltonian, sv, std::ptr::null_mut()), + -1 + ); + + statevector_free(sv); + pauli_string_free(pauli); + hamiltonian_free(hamiltonian); +} + +#[test] +fn local_bloch_vectors_statevector_layout() { + // |01>: qubit 0 flipped to |1>, qubit 1 remains |0>. + let sv = statevector_new(2); + assert_eq!(statevector_apply_x(sv, 0), 0); + + let len = local_bloch_vectors_len(sv); + assert_eq!(len, 3 * statevector_num_qubits(sv)); + let mut out = vec![f64::NAN; len]; + assert_eq!(local_bloch_vectors(sv, out.as_mut_ptr(), len), 0); + // Flat layout [x0, y0, z0, x1, y1, z1]. + assert_close(out[0], 0.0); + assert_close(out[1], 0.0); + assert_close(out[2], -1.0); + assert_close(out[3], 0.0); + assert_close(out[4], 0.0); + assert_close(out[5], 1.0); + + // Bell state: every local Bloch vector vanishes. + assert_eq!(statevector_apply_h(sv, 0), 0); + assert_eq!(statevector_apply_cx(sv, 0, 1), 0); + let mut out = vec![f64::NAN; len]; + assert_eq!(local_bloch_vectors(sv, out.as_mut_ptr(), len), 0); + for value in &out { + assert_close(*value, 0.0); + } + + // Error paths. + assert_eq!(local_bloch_vectors_len(std::ptr::null()), 0); + assert_eq!( + local_bloch_vectors(std::ptr::null(), out.as_mut_ptr(), len), + -1 + ); + assert_eq!(local_bloch_vectors(sv, std::ptr::null_mut(), len), -1); + assert_eq!(local_bloch_vectors(sv, out.as_mut_ptr(), len + 1), -8); + + statevector_free(sv); +} + +#[test] +fn state_to_density_matrix_row_major() { + // |+> = (|0> + |1>) / sqrt(2): rho is the all-0.5 matrix. + let sv = statevector_new(1); + assert_eq!(statevector_apply_h(sv, 0), 0); + + let len = state_to_density_matrix_len(sv); + assert_eq!(len, 4); + let mut out = vec![Complex64::new(f64::NAN, f64::NAN); len]; + assert_eq!(state_to_density_matrix(sv, out.as_mut_ptr(), len), 0); + for element in &out { + assert_close(element.re, 0.5); + assert_close(element.im, 0.0); + } + + // |0>: rho = [[1, 0], [0, 0]] in row-major order. + let zero = statevector_new(1); + let mut rho = vec![Complex64::new(f64::NAN, f64::NAN); 4]; + assert_eq!(state_to_density_matrix(zero, rho.as_mut_ptr(), 4), 0); + assert_close(rho[0].re, 1.0); + assert_close(rho[1].re, 0.0); + assert_close(rho[2].re, 0.0); + assert_close(rho[3].re, 0.0); + for element in &rho { + assert_close(element.im, 0.0); + } + + // Error paths. + assert_eq!(state_to_density_matrix_len(std::ptr::null()), 0); + assert_eq!( + state_to_density_matrix(std::ptr::null(), rho.as_mut_ptr(), 4), + -1 + ); + assert_eq!(state_to_density_matrix(zero, std::ptr::null_mut(), 4), -1); + assert_eq!(state_to_density_matrix(zero, rho.as_mut_ptr(), 5), -8); + + statevector_free(zero); + statevector_free(sv); +} diff --git a/crates/binding-c/tests/parameter_test.rs b/crates/binding-c/tests/parameter_test.rs new file mode 100644 index 00000000..7afb1e84 --- /dev/null +++ b/crates/binding-c/tests/parameter_test.rs @@ -0,0 +1,281 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +use binding_c::circuit::*; +use binding_c::cqlib_string_free; +use std::ffi::CString; + +fn cstr(text: &str) -> CString { + CString::new(text).unwrap() +} + +fn eval(ptr: *const CParameter, bindings: &str) -> f64 { + param_evaluate(ptr, cstr(bindings).as_ptr()) +} + +#[test] +fn param_construction_and_constants() { + let theta = param_symbol(cstr("theta").as_ptr()); + assert!(!theta.is_null()); + assert_eq!(param_is_constant(theta), 0); + + let name = param_as_symbol(theta); + assert!(!name.is_null()); + assert_eq!( + unsafe { std::ffi::CStr::from_ptr(name) }, + cstr("theta").as_c_str() + ); + cqlib_string_free(name); + + let pi = param_pi(); + assert!((eval(pi, "") - std::f64::consts::PI).abs() < 1e-12); + assert_eq!(param_is_constant(pi), 1); + + let e = param_e(); + assert!((eval(e, "") - std::f64::consts::E).abs() < 1e-12); + + let numeric = param_from_double(0.25); + assert!((eval(numeric, "") - 0.25).abs() < 1e-12); + assert_eq!(param_is_constant(numeric), 1); + + assert!(param_from_double(f64::NAN).is_null()); + assert!(param_from_double(f64::INFINITY).is_null()); + assert!(param_symbol(std::ptr::null()).is_null()); + + param_free(theta); + param_free(pi); + param_free(e); + param_free(numeric); +} + +#[test] +fn param_arithmetic_operations() { + let theta = param_symbol(cstr("theta").as_ptr()); + let phi = param_symbol(cstr("phi").as_ptr()); + + let sum = param_add(theta, phi); + assert!((eval(sum, "theta:0.5,phi:1.5") - 2.0).abs() < 1e-12); + + let diff = param_sub(theta, phi); + assert!((eval(diff, "theta:0.5,phi:1.5") + 1.0).abs() < 1e-12); + + let prod = param_mul(theta, phi); + assert!((eval(prod, "theta:0.5,phi:1.5") - 0.75).abs() < 1e-12); + + let quot = param_div(theta, phi); + assert!((eval(quot, "theta:0.75,phi:1.5") - 0.5).abs() < 1e-12); + + let neg = param_neg(theta); + assert!((eval(neg, "theta:0.5") + 0.5).abs() < 1e-12); + + assert!(param_add(std::ptr::null(), theta).is_null()); + assert!(param_sub(theta, std::ptr::null()).is_null()); + assert!(param_neg(std::ptr::null()).is_null()); + + param_free(theta); + param_free(phi); + param_free(sum); + param_free(diff); + param_free(prod); + param_free(quot); + param_free(neg); +} + +#[test] +fn param_math_functions() { + let x = param_from_double(2.0); + + let cases: Vec<(*mut CParameter, f64)> = vec![ + (param_abs(param_neg(x)), 2.0), + (param_sqrt(x), std::f64::consts::SQRT_2), + (param_exp(x), (2.0_f64).exp()), + (param_ln(param_from_double(1.0)), 0.0), + ( + param_log(param_from_double(8.0), param_from_double(2.0)), + 3.0, + ), + (param_sin(param_from_double(0.5)), (0.5_f64).sin()), + (param_cos(param_from_double(0.5)), (0.5_f64).cos()), + (param_tan(param_from_double(0.5)), (0.5_f64).tan()), + (param_asin(param_from_double(0.5)), (0.5_f64).asin()), + (param_acos(param_from_double(0.5)), (0.5_f64).acos()), + (param_atan(param_from_double(0.5)), (0.5_f64).atan()), + (param_sinh(param_from_double(0.5)), (0.5_f64).sinh()), + (param_cosh(param_from_double(0.5)), (0.5_f64).cosh()), + (param_tanh(param_from_double(0.5)), (0.5_f64).tanh()), + (param_floor(param_from_double(1.75)), 1.0), + (param_ceil(param_from_double(1.25)), 2.0), + (param_round(param_from_double(1.75)), 2.0), + (param_pow(x, param_from_double(3.0)), 8.0), + ]; + for (ptr, expected) in &cases { + assert!((eval(*ptr, "") - expected).abs() < 1e-12); + param_free(*ptr); + } + + assert!(param_sin(std::ptr::null()).is_null()); + assert!(param_log(x, std::ptr::null()).is_null()); + assert!(param_pow(std::ptr::null(), x).is_null()); + param_free(x); +} + +#[test] +fn param_symbol_queries() { + let theta = param_symbol(cstr("theta").as_ptr()); + let expr = param_parse(cstr("phi + theta * 2").as_ptr()); + + assert_eq!(param_symbols_len(expr), 2); + let mut names: Vec<*mut std::os::raw::c_char> = vec![std::ptr::null_mut(); 2]; + assert_eq!(param_symbols(expr, names.as_mut_ptr(), 2), 0); + assert_eq!( + unsafe { std::ffi::CStr::from_ptr(names[0]) }, + cstr("phi").as_c_str() + ); + assert_eq!( + unsafe { std::ffi::CStr::from_ptr(names[1]) }, + cstr("theta").as_c_str() + ); + for name in names { + cqlib_string_free(name); + } + + assert_eq!(param_symbols(expr, std::ptr::null_mut(), 1), -8); + assert_eq!(param_symbols(std::ptr::null(), std::ptr::null_mut(), 0), -1); + assert_eq!(param_symbols_len(std::ptr::null()), 0); + assert!(param_as_symbol(expr).is_null()); + assert!(param_as_symbol(std::ptr::null()).is_null()); + + let single = param_as_symbol(theta); + assert!(!single.is_null()); + cqlib_string_free(single); + + let constant = param_from_double(1.0); + assert_eq!(param_symbols(constant, std::ptr::null_mut(), 0), 0); + + param_free(theta); + param_free(expr); + param_free(constant); +} + +#[test] +fn param_state_checks() { + let zero = param_from_double(0.0); + let one = param_from_double(1.0); + let theta = param_symbol(cstr("theta").as_ptr()); + + assert_eq!(param_is_exact_zero(zero), 1); + assert_eq!(param_is_exact_zero(one), 0); + assert_eq!(param_is_exact_zero(theta), 0); + assert_eq!(param_is_zero(zero), 1); + assert_eq!(param_is_one(one), 1); + assert_eq!(param_is_one(zero), 0); + assert_eq!(param_is_zero(theta), 0); + + assert_eq!(param_is_constant(std::ptr::null()), -1); + assert_eq!(param_is_exact_zero(std::ptr::null()), -1); + assert_eq!(param_is_zero(std::ptr::null()), -1); + assert_eq!(param_is_one(std::ptr::null()), -1); + + param_free(zero); + param_free(one); + param_free(theta); +} + +#[test] +fn param_simplify_canonicalize_and_substitute() { + let theta = param_symbol(cstr("theta").as_ptr()); + let expr = param_add(theta, param_from_double(0.0)); + + let simplified = param_simplify(expr); + assert!(!simplified.is_null()); + assert_eq!(param_provably_equal(simplified, theta, 1e-12), 1); + + let canonical = param_canonicalized(param_parse(cstr("pi / 2").as_ptr())); + assert!(!canonical.is_null()); + assert_eq!(param_is_constant(canonical), 1); + assert!((eval(canonical, "") - std::f64::consts::FRAC_PI_2).abs() < 1e-12); + + let replaced = param_replace( + param_parse(cstr("x + 2.0").as_ptr()), + cstr("x").as_ptr(), + param_from_double(1.0), + ); + assert!((eval(replaced, "") - 3.0).abs() < 1e-12); + + let name_x = cstr("x"); + let name_y = cstr("y"); + let names = [name_x.as_ptr(), name_y.as_ptr()]; + let values: [*const CParameter; 2] = [param_from_double(2.0), param_from_double(3.0)]; + let sum = param_substitute_many( + param_parse(cstr("x + y").as_ptr()), + names.as_ptr(), + values.as_ptr(), + 2, + ); + assert!((eval(sum, "") - 5.0).abs() < 1e-12); + + assert!(param_substitute_many(std::ptr::null(), names.as_ptr(), values.as_ptr(), 2).is_null()); + assert!(param_replace(std::ptr::null(), cstr("x").as_ptr(), values[0]).is_null()); + assert!(param_simplify(std::ptr::null()).is_null()); + assert!(param_canonicalized(std::ptr::null()).is_null()); + + for ptr in [simplified, canonical, replaced, sum] { + param_free(ptr); + } + param_free(expr); + param_free(theta); + for value in values { + param_free(value as *mut CParameter); + } +} + +#[test] +fn param_symbolic_derivative() { + let theta = param_symbol(cstr("theta").as_ptr()); + let expr = param_mul(theta, theta); + + let deriv = param_derivative(expr, cstr("theta").as_ptr()); + assert!(!deriv.is_null()); + assert!((eval(deriv, "theta:3.0") - 6.0).abs() < 1e-12); + + assert!(param_derivative(std::ptr::null(), cstr("theta").as_ptr()).is_null()); + assert!(param_derivative(expr, std::ptr::null()).is_null()); + + param_free(deriv); + param_free(expr); + param_free(theta); +} + +#[test] +fn param_provable_equivalence() { + let half = param_from_double(0.5); + let shifted = param_from_double(0.5 + 2.0 * std::f64::consts::PI); + let two_pi = param_from_double(2.0 * std::f64::consts::PI); + + assert_eq!(param_provably_equal(half, half, 1e-12), 1); + assert_eq!(param_provably_equal(half, param_from_double(0.6), 1e-12), 0); + assert_eq!(param_provably_equal_modulo(half, shifted, two_pi, 1e-12), 1); + assert_eq!( + param_provably_equal_modulo(half, param_from_double(0.6), two_pi, 1e-12), + 0 + ); + + assert_eq!(param_provably_equal(std::ptr::null(), half, 1e-12), -1); + assert_eq!( + param_provably_equal_modulo(std::ptr::null(), half, two_pi, 1e-12), + -1 + ); + + param_free(half); + param_free(shifted); + param_free(two_pi); +} diff --git a/crates/binding-c/tests/parity_circuit_compile3_test.rs b/crates/binding-c/tests/parity_circuit_compile3_test.rs new file mode 100644 index 00000000..37263799 --- /dev/null +++ b/crates/binding-c/tests/parity_circuit_compile3_test.rs @@ -0,0 +1,539 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of the License in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! Rust FFI parity tests for the third circuit/compile binding batch: +//! `unitary_gate_matrix_for_params`, the by-value +//! `rewrite_config_with_target_instructions` setter, and the +//! `LAYOUT_OBJECTIVE_FIDELITY_REQUIRED` tag. + +use binding_c::circuit::{ + circuit_cx, circuit_free, circuit_new, circuit_num_operations, param_free, param_parse, + symbolic_complex_free, symbolic_complex_new, symbolic_matrix_free, symbolic_matrix_new, + unitary_gate_matrix_for_params, unitary_gate_matrix_for_params_len, +}; +use binding_c::compile::{ + LAYOUT_OBJECTIVE_FIDELITY_REQUIRED, REWRITE_MODE_LOWERING, analyze_circuit_for_layout, + circuit_layout_analysis_free, knowledge_rewrite_result_changed, + knowledge_rewrite_result_circuit, knowledge_rewrite_result_free, layout_result_free, + physical_layout_graph_free, physical_layout_graph_from_device, + physical_layout_graph_has_fidelity_data, rewrite_circuit, rewrite_config_default, + rewrite_config_lowering, rewrite_config_with_target_instructions, score_layout, + transform_knowledge_rewrite, trivial_layout, trivial_layout_prepared, +}; +use binding_c::device::{ + device_free, device_from_edges, device_set_default_readout_error, layout_free, layout_new, +}; +use num_complex::Complex64; +use std::ffi::CString; +use std::os::raw::c_char; + +fn cstr(s: &str) -> CString { + CString::new(s).unwrap() +} + +/// Builds a `CSymbolicComplex` from real/imaginary expression strings. The +/// returned handle (and the parsed parameter handles) are owned by the +/// caller and freed after `symbolic_matrix_new` clones them. +fn sym_complex( + re: &str, + im: &str, +) -> ( + *const binding_c::circuit::CSymbolicComplex, + *mut binding_c::circuit::CParameter, + *mut binding_c::circuit::CParameter, +) { + let re_param = param_parse(cstr(re).as_ptr()); + assert!(!re_param.is_null()); + let im_param = param_parse(cstr(im).as_ptr()); + assert!(!im_param.is_null()); + let complex = symbolic_complex_new(re_param, im_param); + assert!(!complex.is_null()); + (complex, re_param, im_param) +} + +fn free_sym_complex( + parts: ( + *const binding_c::circuit::CSymbolicComplex, + *mut binding_c::circuit::CParameter, + *mut binding_c::circuit::CParameter, + ), +) { + symbolic_complex_free(parts.0 as *mut binding_c::circuit::CSymbolicComplex); + param_free(parts.1); + param_free(parts.2); +} + +fn close(actual: Complex64, expected: Complex64) -> bool { + (actual - expected).norm() < 1e-9 +} + +#[test] +fn unitary_gate_matrix_for_params_evaluates_symbolic_matrix() { + // U(theta) = cos(theta) * I + i sin(theta) * X, a unitary matrix whose + // expressions need no unary-minus parsing. + let e00 = sym_complex("cos(theta)", "0"); + let e01 = sym_complex("0", "sin(theta)"); + let e10 = sym_complex("0", "sin(theta)"); + let e11 = sym_complex("cos(theta)", "0"); + let elements = [e00.0, e01.0, e10.0, e11.0]; + let matrix = symbolic_matrix_new(2, 2, elements.as_ptr(), 4); + assert!(!matrix.is_null()); + assert_eq!(unitary_gate_matrix_for_params_len(matrix), 4); + + let theta = cstr("theta"); + let names = [theta.as_ptr()]; + + // theta = 0 -> identity. + let mut buffer = [Complex64::new(0.0, 0.0); 4]; + assert_eq!( + unitary_gate_matrix_for_params( + matrix, + names.as_ptr(), + 1, + [0.0f64].as_ptr(), + 1, + buffer.as_mut_ptr(), + 4 + ), + 0 + ); + assert!(close(buffer[0], Complex64::new(1.0, 0.0))); + assert!(close(buffer[1], Complex64::new(0.0, 0.0))); + assert!(close(buffer[2], Complex64::new(0.0, 0.0))); + assert!(close(buffer[3], Complex64::new(1.0, 0.0))); + + // theta = pi/2 -> iX. + assert_eq!( + unitary_gate_matrix_for_params( + matrix, + names.as_ptr(), + 1, + [std::f64::consts::FRAC_PI_2].as_ptr(), + 1, + buffer.as_mut_ptr(), + 4 + ), + 0 + ); + assert!(close(buffer[0], Complex64::new(0.0, 0.0))); + assert!(close(buffer[1], Complex64::new(0.0, 1.0))); + assert!(close(buffer[2], Complex64::new(0.0, 1.0))); + assert!(close(buffer[3], Complex64::new(0.0, 0.0))); + + symbolic_matrix_free(matrix); + for element in [e00, e01, e10, e11] { + free_sym_complex(element); + } +} + +#[test] +fn unitary_gate_matrix_for_params_accepts_parameterless_constant_matrix() { + // The constant Pauli-X matrix with zero formal parameters. + let e00 = sym_complex("0", "0"); + let e01 = sym_complex("1", "0"); + let e10 = sym_complex("1", "0"); + let e11 = sym_complex("0", "0"); + let elements = [e00.0, e01.0, e10.0, e11.0]; + let matrix = symbolic_matrix_new(2, 2, elements.as_ptr(), 4); + assert!(!matrix.is_null()); + + let mut buffer = [Complex64::new(9.0, 9.0); 4]; + assert_eq!( + unitary_gate_matrix_for_params( + matrix, + std::ptr::null(), + 0, + std::ptr::null(), + 0, + buffer.as_mut_ptr(), + 4 + ), + 0 + ); + assert!(close(buffer[0], Complex64::new(0.0, 0.0))); + assert!(close(buffer[1], Complex64::new(1.0, 0.0))); + assert!(close(buffer[2], Complex64::new(1.0, 0.0))); + assert!(close(buffer[3], Complex64::new(0.0, 0.0))); + + symbolic_matrix_free(matrix); + for element in [e00, e01, e10, e11] { + free_sym_complex(element); + } +} + +#[test] +fn unitary_gate_matrix_for_params_reports_errors() { + let e00 = sym_complex("cos(theta)", "0"); + let e01 = sym_complex("0", "sin(theta)"); + let e10 = sym_complex("0", "sin(theta)"); + let e11 = sym_complex("cos(theta)", "0"); + let elements = [e00.0, e01.0, e10.0, e11.0]; + let matrix = symbolic_matrix_new(2, 2, elements.as_ptr(), 4); + assert!(!matrix.is_null()); + + let theta = cstr("theta"); + let names = [theta.as_ptr()]; + let mut buffer = [Complex64::new(0.0, 0.0); 4]; + + // NULL inputs. + assert_eq!( + unitary_gate_matrix_for_params( + std::ptr::null(), + names.as_ptr(), + 1, + [0.0].as_ptr(), + 1, + buffer.as_mut_ptr(), + 4 + ), + -1 + ); + assert_eq!( + unitary_gate_matrix_for_params( + matrix, + names.as_ptr(), + 1, + [0.0].as_ptr(), + 1, + std::ptr::null_mut(), + 4 + ), + -1 + ); + assert_eq!( + unitary_gate_matrix_for_params( + matrix, + std::ptr::null(), + 1, + [0.0].as_ptr(), + 1, + buffer.as_mut_ptr(), + 4 + ), + -1 + ); + assert_eq!( + unitary_gate_matrix_for_params( + matrix, + names.as_ptr(), + 1, + std::ptr::null(), + 1, + buffer.as_mut_ptr(), + 4 + ), + -1 + ); + + // A NULL name entry and invalid UTF-8 names are rejected. + let null_names: [*const c_char; 1] = [std::ptr::null()]; + assert_eq!( + unitary_gate_matrix_for_params( + matrix, + null_names.as_ptr(), + 1, + [0.0].as_ptr(), + 1, + buffer.as_mut_ptr(), + 4 + ), + -1 + ); + let invalid = unsafe { CString::from_vec_unchecked(vec![0xFF, 0xFE]) }; + let bad_names = [invalid.as_ptr()]; + assert_eq!( + unitary_gate_matrix_for_params( + matrix, + bad_names.as_ptr(), + 1, + [0.0].as_ptr(), + 1, + buffer.as_mut_ptr(), + 4 + ), + -4 + ); + + // Parameter-count mismatch and non-finite values map to -3. + assert_eq!( + unitary_gate_matrix_for_params( + matrix, + names.as_ptr(), + 1, + std::ptr::null(), + 0, + buffer.as_mut_ptr(), + 4 + ), + -3 + ); + assert_eq!( + unitary_gate_matrix_for_params( + matrix, + names.as_ptr(), + 1, + [f64::NAN].as_ptr(), + 1, + buffer.as_mut_ptr(), + 4 + ), + -3 + ); + + // A too-small buffer maps to -8. + assert_eq!( + unitary_gate_matrix_for_params( + matrix, + names.as_ptr(), + 1, + [0.0].as_ptr(), + 1, + buffer.as_mut_ptr(), + 3 + ), + -8 + ); + + symbolic_matrix_free(matrix); + for element in [e00, e01, e10, e11] { + free_sym_complex(element); + } + + // A singular constant matrix fails the core unitarity check (-3). + let z00 = sym_complex("1", "0"); + let z01 = sym_complex("0", "0"); + let z10 = sym_complex("0", "0"); + let z11 = sym_complex("0", "0"); + let zero_elements = [z00.0, z01.0, z10.0, z11.0]; + let singular = symbolic_matrix_new(2, 2, zero_elements.as_ptr(), 4); + assert!(!singular.is_null()); + assert_eq!( + unitary_gate_matrix_for_params( + singular, + std::ptr::null(), + 0, + std::ptr::null(), + 0, + buffer.as_mut_ptr(), + 4 + ), + -3 + ); + symbolic_matrix_free(singular); + for element in [z00, z01, z10, z11] { + free_sym_complex(element); + } + + // NULL/non-square length queries return 0. + assert_eq!(unitary_gate_matrix_for_params_len(std::ptr::null()), 0); +} + +#[test] +fn rewrite_config_target_instructions_setter_validates_inputs() { + let mut config = rewrite_config_default(); + assert_eq!(config.mode, 0); + assert!(config.target_gate_names.is_null()); + assert_eq!(config.target_gate_names_len, 0); + + // NULL config, empty basis and NULL arrays with a non-zero length. + assert_eq!( + rewrite_config_with_target_instructions(std::ptr::null_mut(), std::ptr::null(), 2), + -1 + ); + assert_eq!( + rewrite_config_with_target_instructions(&mut config, std::ptr::null(), 0), + -8 + ); + assert_eq!( + rewrite_config_with_target_instructions(&mut config, std::ptr::null(), 2), + -1 + ); + + // Unknown gate name -> -4 and the field stays unset. + let bad = cstr("NOPE"); + let bad_ptrs = [bad.as_ptr()]; + assert_eq!( + rewrite_config_with_target_instructions(&mut config, bad_ptrs.as_ptr(), 1), + -4 + ); + assert!(config.target_gate_names.is_null()); + + // A NULL name entry -> -1. + let null_entry: [*const c_char; 1] = [std::ptr::null()]; + assert_eq!( + rewrite_config_with_target_instructions(&mut config, null_entry.as_ptr(), 1), + -1 + ); + + // Invalid UTF-8 -> -4. + let invalid = unsafe { CString::from_vec_unchecked(vec![0xFF, 0xFE]) }; + let invalid_ptrs = [invalid.as_ptr()]; + assert_eq!( + rewrite_config_with_target_instructions(&mut config, invalid_ptrs.as_ptr(), 1), + -4 + ); + + // Valid names are stored on the by-value config. + let h = cstr("H"); + let cz = cstr("CZ"); + let gates = [h.as_ptr(), cz.as_ptr()]; + assert_eq!( + rewrite_config_with_target_instructions(&mut config, gates.as_ptr(), 2), + 0 + ); + assert_eq!(config.target_gate_names_len, 2); +} + +#[test] +fn rewrite_config_target_instructions_drives_knowledge_lowering() { + let circuit = circuit_new(2); + assert_eq!(circuit_cx(circuit, 0, 1), 0); + + // CX lowers to H-CZ-H against the {H, CZ} target basis. + let h = cstr("H"); + let cz = cstr("CZ"); + let gates = [h.as_ptr(), cz.as_ptr()]; + let mut config = rewrite_config_lowering(); + assert_eq!(config.mode, REWRITE_MODE_LOWERING); + assert_eq!( + rewrite_config_with_target_instructions(&mut config, gates.as_ptr(), 2), + 0 + ); + let result = rewrite_circuit(circuit, config); + assert!(!result.is_null()); + assert_eq!(knowledge_rewrite_result_changed(result), 1); + let rewritten = knowledge_rewrite_result_circuit(result); + assert!(!rewritten.is_null()); + assert_eq!(circuit_num_operations(rewritten), 3); + circuit_free(rewritten); + knowledge_rewrite_result_free(result); + + // The standalone transform wrapper consumes the same by-value config. + let mut config2 = rewrite_config_lowering(); + assert_eq!( + rewrite_config_with_target_instructions(&mut config2, gates.as_ptr(), 2), + 0 + ); + let wrapped = transform_knowledge_rewrite(circuit, config2); + assert!(!wrapped.is_null()); + assert_eq!(circuit_num_operations(wrapped), 3); + circuit_free(wrapped); + + // A basis that cannot represent CX fails final-target validation. + let x = cstr("X"); + let x_only = [x.as_ptr()]; + let mut impossible = rewrite_config_lowering(); + assert_eq!( + rewrite_config_with_target_instructions(&mut impossible, x_only.as_ptr(), 1), + 0 + ); + assert!(rewrite_circuit(circuit, impossible).is_null()); + + circuit_free(circuit); +} + +fn line3_device() -> *mut binding_c::device::CDevice { + let edges: [u32; 4] = [0, 1, 1, 2]; + device_from_edges(cstr("line-3").as_ptr(), 3, edges.as_ptr(), 2) +} + +#[test] +fn layout_objective_fidelity_required_tag_value() { + // The C tag follows the core constructor ordering: 3. + assert_eq!(LAYOUT_OBJECTIVE_FIDELITY_REQUIRED, 3); +} + +#[test] +fn layout_objective_fidelity_required_without_calibration_errors() { + let device = line3_device(); + let circuit = circuit_new(2); + assert_eq!(circuit_cx(circuit, 0, 1), 0); + + let graph = physical_layout_graph_from_device(device); + assert!(!graph.is_null()); + assert_eq!(physical_layout_graph_has_fidelity_data(graph), 0); + + // Pointer-returning entries surface the missing calibration as NULL. + assert!(trivial_layout(circuit, device, LAYOUT_OBJECTIVE_FIDELITY_REQUIRED).is_null()); + let first_analysis = analyze_circuit_for_layout(circuit); + assert!( + trivial_layout_prepared(first_analysis, graph, LAYOUT_OBJECTIVE_FIDELITY_REQUIRED) + .is_null() + ); + circuit_layout_analysis_free(first_analysis); + + // The scoring entry reports the invalid configuration as -8. + let analysis = analyze_circuit_for_layout(circuit); + let qubits = [0u32, 1u32]; + let layout = layout_new(qubits.as_ptr(), 2, qubits.as_ptr(), 2); + let mut score: binding_c::compile::CLayoutScore = unsafe { std::mem::zeroed() }; + assert_eq!( + score_layout( + LAYOUT_OBJECTIVE_FIDELITY_REQUIRED, + analysis, + graph, + layout, + &mut score + ), + -8 + ); + + layout_free(layout); + circuit_layout_analysis_free(analysis); + physical_layout_graph_free(graph); + circuit_free(circuit); + device_free(device); +} + +#[test] +fn layout_objective_fidelity_required_with_calibration_succeeds() { + let device = line3_device(); + assert_eq!(device_set_default_readout_error(device, 0.01), 0); + + let circuit = circuit_new(2); + assert_eq!(circuit_cx(circuit, 0, 1), 0); + + let graph = physical_layout_graph_from_device(device); + assert!(!graph.is_null()); + assert_eq!(physical_layout_graph_has_fidelity_data(graph), 1); + + let result = trivial_layout(circuit, device, LAYOUT_OBJECTIVE_FIDELITY_REQUIRED); + assert!(!result.is_null()); + layout_result_free(result); + + let analysis = analyze_circuit_for_layout(circuit); + let prepared = trivial_layout_prepared(analysis, graph, LAYOUT_OBJECTIVE_FIDELITY_REQUIRED); + assert!(!prepared.is_null()); + layout_result_free(prepared); + + // The resolved objective carries fidelity terms into scoring. + let qubits = [0u32, 1u32]; + let layout = layout_new(qubits.as_ptr(), 2, qubits.as_ptr(), 2); + let mut score: binding_c::compile::CLayoutScore = unsafe { std::mem::zeroed() }; + assert_eq!( + score_layout( + LAYOUT_OBJECTIVE_FIDELITY_REQUIRED, + analysis, + graph, + layout, + &mut score + ), + 0 + ); + assert_eq!(score.used_fidelity, 1); + + layout_free(layout); + circuit_layout_analysis_free(analysis); + physical_layout_graph_free(graph); + circuit_free(circuit); + device_free(device); +} diff --git a/crates/binding-c/tests/parity_circuit_qis_test.rs b/crates/binding-c/tests/parity_circuit_qis_test.rs new file mode 100644 index 00000000..def33899 --- /dev/null +++ b/crates/binding-c/tests/parity_circuit_qis_test.rs @@ -0,0 +1,301 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! Rust FFI parity tests for the circuit/QIS binding additions: +//! `circuit_dag_add_parameter`, `pauli_string_expectation`, +//! `density_matrix_zeros`, `density_matrix_validate_physical`, and +//! `statevector_entanglement_entropy_pure`. + +use binding_c::circuit::{ + circuit_dag_add_parameter, circuit_dag_free, circuit_dag_from_circuit, + circuit_dag_parameters_len, circuit_free, circuit_new, circuit_rx_param, param_free, + param_parse, +}; +use binding_c::qis::{ + PAULI_X, PAULI_Z, density_matrix_free, density_matrix_maximally_mixed, density_matrix_new, + density_matrix_num_qubits, density_matrix_trace, density_matrix_validate_physical, + density_matrix_zeros, pauli_string_expectation, pauli_string_free, pauli_string_new, + pauli_string_set_pauli, statevector_apply_cx, statevector_apply_h, + statevector_entanglement_entropy_pure, statevector_free, statevector_new, +}; +use std::ffi::CString; + +fn cstr(s: &str) -> CString { + CString::new(s).unwrap() +} + +#[test] +fn dag_add_parameter_interns_and_reports_insertion() { + let theta = cstr("theta"); + let theta_ptr = param_parse(theta.as_ptr()); + assert!(!theta_ptr.is_null()); + + // A DAG built from a circuit already interns "theta" through the RX. + let circuit = circuit_new(1); + assert_eq!(circuit_rx_param(circuit, 0, theta_ptr), 0); + let dag = circuit_dag_from_circuit(circuit); + assert!(!dag.is_null()); + assert_eq!(circuit_dag_parameters_len(dag), 1); + + // Re-adding the resident parameter reports "not inserted" with the + // stable table index. + let mut index = 99usize; + let mut inserted = -1i32; + assert_eq!( + circuit_dag_add_parameter(dag, theta_ptr, &mut index, &mut inserted), + 0 + ); + assert_eq!((index, inserted), (0, 0)); + assert_eq!(circuit_dag_parameters_len(dag), 1); + + // A fresh parameter is inserted at the next table index. + let phi = cstr("phi"); + let phi_ptr = param_parse(phi.as_ptr()); + assert!(!phi_ptr.is_null()); + assert_eq!( + circuit_dag_add_parameter(dag, phi_ptr, &mut index, &mut inserted), + 0 + ); + assert_eq!((index, inserted), (1, 1)); + assert_eq!(circuit_dag_parameters_len(dag), 2); + + // Adding it a second time keeps the index and reports "not inserted". + assert_eq!( + circuit_dag_add_parameter(dag, phi_ptr, &mut index, &mut inserted), + 0 + ); + assert_eq!((index, inserted), (1, 0)); + assert_eq!(circuit_dag_parameters_len(dag), 2); + + // Output pointers may be omitted. + assert_eq!( + circuit_dag_add_parameter(dag, phi_ptr, std::ptr::null_mut(), std::ptr::null_mut()), + 0 + ); + + // NULL handles and NULL parameters are rejected. + assert_eq!( + circuit_dag_add_parameter(std::ptr::null_mut(), phi_ptr, &mut index, &mut inserted), + -1 + ); + assert_eq!( + circuit_dag_add_parameter(dag, std::ptr::null(), &mut index, &mut inserted), + -1 + ); + + circuit_dag_free(dag); + circuit_free(circuit); + param_free(theta_ptr); + param_free(phi_ptr); +} + +#[test] +fn pauli_string_expectation_diagonal_and_offdiagonal() { + let ps = pauli_string_new(2); + assert!(!ps.is_null()); + assert_eq!(pauli_string_set_pauli(ps, 0, PAULI_Z), 0); + + // Z0 on |00> gives +1; on the little-endian "01" (qubit 0 = 1) gives -1. + let zero0 = cstr("00"); + let one0 = cstr("01"); + let bitstrings_1 = [zero0.as_ptr()]; + let probs_1 = [1.0f64]; + let mut out = 99.0f64; + assert_eq!( + pauli_string_expectation(ps, bitstrings_1.as_ptr(), probs_1.as_ptr(), 1, &mut out), + 0 + ); + assert_eq!(out, 1.0); + + let bitstrings_2 = [one0.as_ptr()]; + assert_eq!( + pauli_string_expectation(ps, bitstrings_2.as_ptr(), probs_1.as_ptr(), 1, &mut out), + 0 + ); + assert_eq!(out, -1.0); + + // An even mixture of the two eigenstates averages to zero. + let bitstrings_mixed = [zero0.as_ptr(), one0.as_ptr()]; + let probs_mixed = [0.5f64, 0.5]; + assert_eq!( + pauli_string_expectation( + ps, + bitstrings_mixed.as_ptr(), + probs_mixed.as_ptr(), + 2, + &mut out + ), + 0 + ); + assert!(out.abs() < 1e-12); + + // Core rejections on a diagonal string: wrong length and invalid + // characters map through the QisError table (both fall to the generic + // -7 bucket). Off-diagonal strings never reach validation. + let bad_len = cstr("000"); + let bitstrings_len = [bad_len.as_ptr()]; + assert_eq!( + pauli_string_expectation(ps, bitstrings_len.as_ptr(), probs_1.as_ptr(), 1, &mut out), + -7 + ); + let bad_char = cstr("0a"); + let bitstrings_char = [bad_char.as_ptr()]; + assert_eq!( + pauli_string_expectation(ps, bitstrings_char.as_ptr(), probs_1.as_ptr(), 1, &mut out), + -7 + ); + + // Off-diagonal strings (X or Y support) vanish on basis distributions. + assert_eq!(pauli_string_set_pauli(ps, 0, PAULI_X), 0); + assert_eq!( + pauli_string_expectation(ps, bitstrings_1.as_ptr(), probs_1.as_ptr(), 1, &mut out), + 0 + ); + assert_eq!(out, 0.0); + + // NULL and invalid-parameter paths. + assert_eq!( + pauli_string_expectation( + std::ptr::null(), + bitstrings_1.as_ptr(), + probs_1.as_ptr(), + 1, + &mut out + ), + -1 + ); + assert_eq!( + pauli_string_expectation( + ps, + bitstrings_1.as_ptr(), + probs_1.as_ptr(), + 1, + std::ptr::null_mut() + ), + -1 + ); + assert_eq!( + pauli_string_expectation(ps, std::ptr::null(), probs_1.as_ptr(), 1, &mut out), + -1 + ); + assert_eq!( + pauli_string_expectation(ps, bitstrings_1.as_ptr(), std::ptr::null(), 1, &mut out), + -1 + ); + let bitstrings_null = [std::ptr::null()]; + assert_eq!( + pauli_string_expectation(ps, bitstrings_null.as_ptr(), probs_1.as_ptr(), 1, &mut out), + -8 + ); + + // An empty distribution is valid and yields zero. + assert_eq!( + pauli_string_expectation(ps, std::ptr::null(), std::ptr::null(), 0, &mut out), + 0 + ); + assert_eq!(out, 0.0); + + pauli_string_free(ps); +} + +#[test] +fn density_matrix_zeros_is_an_unphysical_accumulator() { + let dm = density_matrix_zeros(2); + assert!(!dm.is_null()); + assert_eq!(density_matrix_num_qubits(dm), 2); + + // Every element is zero, so the trace vanishes. + let mut trace_re = 9.0f64; + let mut trace_im = 9.0f64; + assert_eq!(density_matrix_trace(dm, &mut trace_re, &mut trace_im), 0); + assert_eq!((trace_re, trace_im), (0.0, 0.0)); + + // Zero trace fails the unit-trace check (NotNormalized -> -8). + assert_eq!(density_matrix_validate_physical(dm, 1e-10), -8); + + density_matrix_free(dm); + + // Physical states pass: the pure |0> state and I/2. + let pure = density_matrix_new(1); + assert!(!pure.is_null()); + assert_eq!(density_matrix_validate_physical(pure, 1e-10), 0); + density_matrix_free(pure); + + let mixed = density_matrix_maximally_mixed(1); + assert!(!mixed.is_null()); + assert_eq!(density_matrix_validate_physical(mixed, 1e-10), 0); + // NULL handle and non-finite tolerance are rejected; an oversized + // qubit count fails the constructor instead of panicking. + assert_eq!( + density_matrix_validate_physical(std::ptr::null(), 1e-10), + -1 + ); + assert_eq!(density_matrix_validate_physical(mixed, f64::NAN), -8); + density_matrix_free(mixed); + assert!(density_matrix_zeros(100).is_null()); +} + +#[test] +fn statevector_entanglement_entropy_pure_bell_state() { + let sv = statevector_new(2); + assert!(!sv.is_null()); + assert_eq!(statevector_apply_h(sv, 0), 0); + assert_eq!(statevector_apply_cx(sv, 0, 1), 0); + + // A Bell state has one bit (log2) of entanglement across any split. + let subsys = [0u32]; + let mut out = 99.0f64; + assert_eq!( + statevector_entanglement_entropy_pure(sv, subsys.as_ptr(), 1, &mut out), + 0 + ); + assert!((out - 1.0).abs() < 1e-9); + + // A product state carries no entanglement. + let sv_product = statevector_new(2); + assert!(!sv_product.is_null()); + assert_eq!( + statevector_entanglement_entropy_pure(sv_product, subsys.as_ptr(), 1, &mut out), + 0 + ); + assert!(out.abs() < 1e-9); + statevector_free(sv_product); + + // Core rejections: a full or empty subsystem is invalid (-7), an + // out-of-bounds index reports -2. + let both = [0u32, 1]; + assert_eq!( + statevector_entanglement_entropy_pure(sv, both.as_ptr(), 2, &mut out), + -7 + ); + assert_eq!( + statevector_entanglement_entropy_pure(sv, std::ptr::null(), 0, &mut out), + -7 + ); + let bad = [5u32]; + assert_eq!( + statevector_entanglement_entropy_pure(sv, bad.as_ptr(), 1, &mut out), + -2 + ); + + // NULL handle, NULL output, and a NULL subsystem buffer with data. + assert_eq!( + statevector_entanglement_entropy_pure(std::ptr::null(), subsys.as_ptr(), 1, &mut out), + -1 + ); + assert_eq!( + statevector_entanglement_entropy_pure(sv, subsys.as_ptr(), 1, std::ptr::null_mut()), + -1 + ); + + statevector_free(sv); +} diff --git a/crates/binding-c/tests/parity_compile_em_test.rs b/crates/binding-c/tests/parity_compile_em_test.rs new file mode 100644 index 00000000..efcc7873 --- /dev/null +++ b/crates/binding-c/tests/parity_compile_em_test.rs @@ -0,0 +1,289 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! Parity tests for the compile / error-mitigation C ABI additions: +//! `PauliEvolutionAnsatz` evolution info, `VirtualDistillation` copy-swap +//! circuit building, knowledge-rule symbol queries, and compile step skip +//! reasons. + +use binding_c::circuit::{ + CPauliEvolutionInfo, TROTTER_FIRST_ORDER, TROTTER_MODE_NONE, circuit_cx, circuit_free, + circuit_h, circuit_new, circuit_num_operations, circuit_num_qubits, + pauli_evolution_ansatz_evolution_info, pauli_evolution_ansatz_free, pauli_evolution_ansatz_new, +}; +use binding_c::compile::{ + CKnowledgeRule, COMPILE_MODE_NORMAL, COMPILE_TARGET_LOGICAL, CompileConfigC, compile, + compile_result_free, compile_result_num_steps, compile_result_step_name, + compile_result_step_reason, knowledge_rule_condition_symbols, + knowledge_rule_condition_symbols_len, knowledge_rule_free, knowledge_rule_from_dsl, + knowledge_rule_operation_symbols, knowledge_rule_operation_symbols_len, + knowledge_rule_target_symbols, knowledge_rule_target_symbols_len, +}; +use binding_c::cqlib_string_free; +use binding_c::error_mitigation::{ + virtual_distillation_build_copy_swap_circuit, virtual_distillation_free, + virtual_distillation_new, +}; +use binding_c::qis::{ + CHamiltonian, hamiltonian_add_term, hamiltonian_free, hamiltonian_new, pauli_string_free, + pauli_string_parse, +}; +use std::ffi::{CStr, CString}; +use std::os::raw::c_char; + +const MERGE_DSL: &str = "rule merge_rz {\n match {\n RZ(a) 0\n RZ(b) 0\n }\n rewrite {\n RZ(a + b) 0\n }\n}"; +const CANCEL_H_DSL: &str = + "rule cancel_h {\n match {\n H 0\n H 0\n }\n rewrite {}\n}"; +const CONDITION_DSL: &str = "rule cancel_rz_inverse {\n match { RZ(a) 0, RZ(b) 0 }\n require { a + b == 0 mod 4*π }\n rewrite {}\n}"; + +fn cstr(s: &str) -> CString { + CString::new(s).unwrap() +} + +fn read_string(ptr: *mut c_char) -> String { + assert!(!ptr.is_null()); + let text = unsafe { CStr::from_ptr(ptr) } + .to_string_lossy() + .into_owned(); + cqlib_string_free(ptr); + text +} + +fn make_rule(dsl: &str) -> *mut CKnowledgeRule { + let source = cstr(dsl); + let mut rule: *mut CKnowledgeRule = std::ptr::null_mut(); + assert_eq!(knowledge_rule_from_dsl(source.as_ptr(), &mut rule), 0); + assert!(!rule.is_null()); + rule +} + +/// Builds a real-coefficient Hamiltonian from `(pauli text, coefficient)` +/// terms. +fn hamiltonian_with_terms(width: usize, terms: &[(&str, f64)]) -> *mut CHamiltonian { + let h = hamiltonian_new(width); + assert!(!h.is_null()); + for (text, coeff) in terms { + let pauli = pauli_string_parse(cstr(text).as_ptr()); + assert!(!pauli.is_null()); + assert_eq!(hamiltonian_add_term(h, pauli, *coeff, 0.0), 0); + pauli_string_free(pauli); + } + h +} + +// ===== PauliEvolutionAnsatz::evolution_info ===== + +#[test] +fn test_evolution_info_commuting_exact() { + // ZZ and ZI mutually commute: Auto takes the exact single-pass path. + let h = hamiltonian_with_terms(2, &[("ZZ", 0.5), ("ZI", 0.3)]); + let ansatz = pauli_evolution_ansatz_new(h); + assert!(!ansatz.is_null()); + + let mut info: CPauliEvolutionInfo = unsafe { std::mem::zeroed() }; + assert_eq!(pauli_evolution_ansatz_evolution_info(ansatz, &mut info), 0); + assert_eq!(info.is_exact, 1); + assert_eq!(info.all_terms_commute, 1); + assert_eq!(info.steps, 1); + assert_eq!(info.num_terms, 2); + assert_eq!(info.trotter_mode, TROTTER_MODE_NONE); + assert_eq!(info.trotter_seed, 0); + + // NULL guards. + let mut sink: CPauliEvolutionInfo = unsafe { std::mem::zeroed() }; + assert_eq!( + pauli_evolution_ansatz_evolution_info(std::ptr::null(), &mut sink), + -1 + ); + assert_eq!( + pauli_evolution_ansatz_evolution_info(ansatz, std::ptr::null_mut()), + -1 + ); + + pauli_evolution_ansatz_free(ansatz); + hamiltonian_free(h); +} + +#[test] +fn test_evolution_info_non_commuting_trotter() { + // X and Z do not commute: Auto falls back to a first-order Trotter pass. + let h = hamiltonian_with_terms(1, &[("X", 1.0), ("Z", 1.0)]); + let ansatz = pauli_evolution_ansatz_new(h); + assert!(!ansatz.is_null()); + + let mut info: CPauliEvolutionInfo = unsafe { std::mem::zeroed() }; + assert_eq!(pauli_evolution_ansatz_evolution_info(ansatz, &mut info), 0); + assert_eq!(info.is_exact, 0); + assert_eq!(info.all_terms_commute, 0); + assert_eq!(info.steps, 1); + assert_eq!(info.num_terms, 2); + assert_eq!(info.trotter_mode, TROTTER_FIRST_ORDER); + assert_eq!(info.trotter_seed, 0); + + pauli_evolution_ansatz_free(ansatz); + hamiltonian_free(h); +} + +// ===== VirtualDistillation::build_copy_swap_circuit ===== + +#[test] +fn test_virtual_distillation_build_copy_swap_circuit() { + let circuit = circuit_new(1); + assert_eq!(circuit_h(circuit, 0), 0); + + let vd = virtual_distillation_new(circuit, 2); + assert!(!vd.is_null()); + + let copy_swap = virtual_distillation_build_copy_swap_circuit(vd); + assert!(!copy_swap.is_null()); + // The copy-swap width is copies (2) times the base width (1). + assert_eq!(circuit_num_qubits(copy_swap), 2); + assert!( + circuit_num_operations(copy_swap) > 0, + "copy-swap circuit should contain operations" + ); + circuit_free(copy_swap); + + // NULL input yields NULL. + assert!(virtual_distillation_build_copy_swap_circuit(std::ptr::null()).is_null()); + + virtual_distillation_free(vd); + circuit_free(circuit); +} + +// ===== KnowledgeRule symbol queries ===== + +#[test] +fn test_knowledge_rule_symbol_families() { + let merge = make_rule(MERGE_DSL); + let cancel = make_rule(CANCEL_H_DSL); + let condition = make_rule(CONDITION_DSL); + + // Match-block symbols: both merge branches bind a and b (sorted). + assert_eq!(knowledge_rule_operation_symbols_len(merge), 2); + let mut ops: [*mut c_char; 2] = [std::ptr::null_mut(); 2]; + assert_eq!( + knowledge_rule_operation_symbols(merge, ops.as_mut_ptr(), 2), + 0 + ); + assert_eq!(read_string(ops[0]), "a"); + assert_eq!(read_string(ops[1]), "b"); + + // Rewrite-target symbols: the merge target references a and b; the + // cancel target is empty. + assert_eq!(knowledge_rule_target_symbols_len(merge), 2); + let mut targets: [*mut c_char; 2] = [std::ptr::null_mut(); 2]; + assert_eq!( + knowledge_rule_target_symbols(merge, targets.as_mut_ptr(), 2), + 0 + ); + assert_eq!(read_string(targets[0]), "a"); + assert_eq!(read_string(targets[1]), "b"); + assert_eq!(knowledge_rule_target_symbols_len(cancel), 0); + assert_eq!( + knowledge_rule_target_symbols(cancel, std::ptr::null_mut(), 0), + 0 + ); + + // Condition symbols: only the condition rule carries a require block. + assert_eq!(knowledge_rule_condition_symbols_len(condition), 2); + let mut conds: [*mut c_char; 2] = [std::ptr::null_mut(); 2]; + assert_eq!( + knowledge_rule_condition_symbols(condition, conds.as_mut_ptr(), 2), + 0 + ); + assert_eq!(read_string(conds[0]), "a"); + assert_eq!(read_string(conds[1]), "b"); + assert_eq!(knowledge_rule_condition_symbols_len(merge), 0); + assert_eq!(knowledge_rule_condition_symbols_len(cancel), 0); + + // A wrong buffer length is rejected before any string is written. + let mut short: [*mut c_char; 1] = [std::ptr::null_mut()]; + assert_eq!( + knowledge_rule_operation_symbols(merge, short.as_mut_ptr(), 1), + -8 + ); + assert!(short[0].is_null()); + // A NULL buffer with a positive length is rejected. + assert_eq!( + knowledge_rule_operation_symbols(merge, std::ptr::null_mut(), 2), + -1 + ); + + // NULL guards. + assert_eq!(knowledge_rule_operation_symbols_len(std::ptr::null()), 0); + assert_eq!(knowledge_rule_target_symbols_len(std::ptr::null()), 0); + assert_eq!(knowledge_rule_condition_symbols_len(std::ptr::null()), 0); + assert_eq!( + knowledge_rule_operation_symbols(std::ptr::null(), std::ptr::null_mut(), 0), + -1 + ); + assert_eq!( + knowledge_rule_target_symbols(std::ptr::null(), std::ptr::null_mut(), 0), + -1 + ); + assert_eq!( + knowledge_rule_condition_symbols(std::ptr::null(), std::ptr::null_mut(), 0), + -1 + ); + + knowledge_rule_free(condition); + knowledge_rule_free(cancel); + knowledge_rule_free(merge); +} + +// ===== Compile step skip reasons ===== + +#[test] +fn test_compile_result_step_reason() { + let circuit = circuit_new(2); + assert_eq!(circuit_h(circuit, 0), 0); + assert_eq!(circuit_cx(circuit, 0, 1), 0); + + let config = CompileConfigC { + mode: COMPILE_MODE_NORMAL, + target: COMPILE_TARGET_LOGICAL, + allow_dirty_ancilla: 0, + allow_clean_ancilla: 0, + _reserved: [0; 4], + }; + let result = compile(circuit, config); + assert!(!result.is_null(), "compile should succeed"); + + let n = compile_result_num_steps(result); + assert!(n > 0); + + // Every step has a name; steps with a reason report non-empty text. A + // logical-target compile skips the target-basis steps with documented + // reasons, so at least one reason must be present. + let mut reasons = 0; + for i in 0..n { + let name_ptr = compile_result_step_name(result, i); + assert!(!name_ptr.is_null(), "step {i} should have a name"); + cqlib_string_free(name_ptr); + + let reason_ptr = compile_result_step_reason(result, i); + if !reason_ptr.is_null() { + let reason = read_string(reason_ptr); + assert!(!reason.is_empty(), "step {i} reason should be non-empty"); + reasons += 1; + } + } + assert!(reasons > 0, "expected at least one step with a skip reason"); + + // Out-of-bounds indices and NULL input yield NULL. + assert!(compile_result_step_reason(result, n + 10).is_null()); + assert!(compile_result_step_reason(std::ptr::null(), 0).is_null()); + + compile_result_free(result); + circuit_free(circuit); +} diff --git a/crates/binding-c/tests/parity_qis3_test.rs b/crates/binding-c/tests/parity_qis3_test.rs new file mode 100644 index 00000000..44568d78 --- /dev/null +++ b/crates/binding-c/tests/parity_qis3_test.rs @@ -0,0 +1,220 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! Rust FFI parity tests for the third QIS binding batch: +//! `statevector_data_mut`, `density_matrix_is_positive_semidefinite`, and +//! `pauli_mul_with_phase`. + +use binding_c::qis::{ + PAULI_I, PAULI_X, PAULI_Y, PAULI_Z, density_matrix_free, density_matrix_from_state, + density_matrix_is_positive_semidefinite, density_matrix_maximally_mixed, density_matrix_new, + density_matrix_zeros, pauli_mul_with_phase, statevector_apply_z, statevector_data, + statevector_data_len, statevector_data_mut, statevector_free, statevector_new, +}; +use cqlib_core::qis::{Pauli, Phase}; +use num_complex::Complex64; +use std::ptr; + +fn read_amplitudes(ptr: *const binding_c::qis::CStatevector) -> Vec { + let len = statevector_data_len(ptr); + let mut buffer = vec![Complex64::new(0.0, 0.0); len]; + assert_eq!(statevector_data(ptr, buffer.as_mut_ptr(), len), 0); + buffer +} + +#[test] +fn statevector_data_mut_overwrites_amplitudes() { + let sv = statevector_new(1); + assert_eq!(statevector_data_len(sv), 2); + + // Overwrite |0> with |1>. + let one = [Complex64::new(0.0, 0.0), Complex64::new(1.0, 0.0)]; + assert_eq!(statevector_data_mut(sv, one.as_ptr(), 2), 0); + let amps = read_amplitudes(sv); + assert_eq!(amps[0], Complex64::new(0.0, 0.0)); + assert_eq!(amps[1], Complex64::new(1.0, 0.0)); + + // The simulator keeps using the written amplitudes: Z|1> = -|1>. + assert_eq!(statevector_apply_z(sv, 0), 0); + let amps = read_amplitudes(sv); + assert!(amps[0].norm() < 1e-12); + assert_eq!(amps[1], Complex64::new(-1.0, 0.0)); + + // A normalized superposition can also be written directly. + let inv = std::f64::consts::FRAC_1_SQRT_2; + let plus = [Complex64::new(inv, 0.0), Complex64::new(inv, 0.0)]; + assert_eq!(statevector_data_mut(sv, plus.as_ptr(), 2), 0); + let amps = read_amplitudes(sv); + assert!((amps[0].re - inv).abs() < 1e-12); + assert!((amps[1].re - inv).abs() < 1e-12); + + // A length mismatch is rejected and leaves the state untouched. + let short = [Complex64::new(9.0, 0.0)]; + assert_eq!(statevector_data_mut(sv, short.as_ptr(), 1), -8); + let amps = read_amplitudes(sv); + assert!((amps[0].re - inv).abs() < 1e-12); + + // NULL pointers are rejected. + assert_eq!(statevector_data_mut(ptr::null_mut(), plus.as_ptr(), 2), -1); + assert_eq!(statevector_data_mut(sv, ptr::null(), 2), -1); + + statevector_free(sv); +} + +#[test] +fn density_matrix_psd_reports_physical_states() { + // Ground state |0><0| is positive semidefinite. + let dm = density_matrix_new(1); + let mut positive = false; + assert_eq!( + density_matrix_is_positive_semidefinite(dm, 1e-10, &mut positive), + 0 + ); + assert!(positive); + density_matrix_free(dm); + + // Maximally mixed state I/4 is positive semidefinite. + let mixed = density_matrix_maximally_mixed(2); + assert_eq!( + density_matrix_is_positive_semidefinite(mixed, 1e-10, &mut positive), + 0 + ); + assert!(positive); + density_matrix_free(mixed); + + // Pure |+><+| state is positive semidefinite. + let inv = std::f64::consts::FRAC_1_SQRT_2; + let plus = [Complex64::new(inv, 0.0), Complex64::new(inv, 0.0)]; + let pure = density_matrix_from_state(1, plus.as_ptr(), 2); + assert!(!pure.is_null()); + assert_eq!( + density_matrix_is_positive_semidefinite(pure, 1e-10, &mut positive), + 0 + ); + assert!(positive); + density_matrix_free(pure); + + // The zero matrix has zero eigenvalues and also passes. + let zero = density_matrix_zeros(1); + assert_eq!( + density_matrix_is_positive_semidefinite(zero, 1e-10, &mut positive), + 0 + ); + assert!(positive); + density_matrix_free(zero); + + // Error paths. + assert_eq!( + density_matrix_is_positive_semidefinite(ptr::null(), 1e-10, &mut positive), + -1 + ); + let valid = density_matrix_new(1); + assert_eq!( + density_matrix_is_positive_semidefinite(valid, 1e-10, ptr::null_mut()), + -1 + ); + assert_eq!( + density_matrix_is_positive_semidefinite(valid, f64::NAN, &mut positive), + -8 + ); + density_matrix_free(valid); +} + +#[test] +fn pauli_mul_with_phase_matches_core() { + // X * Z = -iY. + let mut tag = 0xFFu8; + let mut phase = Complex64::new(9.0, 9.0); + assert_eq!( + pauli_mul_with_phase(PAULI_X, PAULI_Z, &mut tag, &mut phase), + 0 + ); + assert_eq!(tag, PAULI_Y); + assert_eq!(phase, Complex64::new(0.0, -1.0)); + + // Z * X = iY (anticommutation reverses the sign). + assert_eq!( + pauli_mul_with_phase(PAULI_Z, PAULI_X, &mut tag, &mut phase), + 0 + ); + assert_eq!(tag, PAULI_Y); + assert_eq!(phase, Complex64::new(0.0, 1.0)); + + // X * X = I with phase +1; X * Y = iZ. + assert_eq!( + pauli_mul_with_phase(PAULI_X, PAULI_X, &mut tag, &mut phase), + 0 + ); + assert_eq!(tag, PAULI_I); + assert_eq!(phase, Complex64::new(1.0, 0.0)); + assert_eq!( + pauli_mul_with_phase(PAULI_X, PAULI_Y, &mut tag, &mut phase), + 0 + ); + assert_eq!(tag, PAULI_Z); + assert_eq!(phase, Complex64::new(0.0, 1.0)); + + // Identity leaves the other operator untouched with phase +1. + assert_eq!( + pauli_mul_with_phase(PAULI_I, PAULI_Y, &mut tag, &mut phase), + 0 + ); + assert_eq!(tag, PAULI_Y); + assert_eq!(phase, Complex64::new(1.0, 0.0)); + assert_eq!( + pauli_mul_with_phase(PAULI_Y, PAULI_I, &mut tag, &mut phase), + 0 + ); + assert_eq!(tag, PAULI_Y); + assert_eq!(phase, Complex64::new(1.0, 0.0)); + + // Full 4x4 parity check against the core implementation. + let tags = [PAULI_I, PAULI_X, PAULI_Y, PAULI_Z]; + let core = [Pauli::I, Pauli::X, Pauli::Y, Pauli::Z]; + for (l_tag, l_core) in tags.iter().zip(core.iter()) { + for (r_tag, r_core) in tags.iter().zip(core.iter()) { + let (expected_pauli, expected_phase) = l_core.mul_with_phase(*r_core); + assert_eq!( + pauli_mul_with_phase(*l_tag, *r_tag, &mut tag, &mut phase), + 0 + ); + let expected_tag = match expected_pauli { + Pauli::I => PAULI_I, + Pauli::X => PAULI_X, + Pauli::Y => PAULI_Y, + Pauli::Z => PAULI_Z, + }; + assert_eq!(tag, expected_tag); + assert_eq!(phase, expected_phase.to_complex()); + // Sanity: core phases are always a fourth root of unity. + assert!(matches!( + expected_phase, + Phase::Plus | Phase::I | Phase::Minus | Phase::MinusI + )); + } + } + + // Invalid tags are rejected, and NULL output pointers return -1. + assert_eq!(pauli_mul_with_phase(9, PAULI_X, &mut tag, &mut phase), -8); + assert_eq!( + pauli_mul_with_phase(PAULI_X, 0xFF, &mut tag, &mut phase), + -8 + ); + assert_eq!( + pauli_mul_with_phase(PAULI_X, PAULI_Z, ptr::null_mut(), &mut phase), + -1 + ); + assert_eq!( + pauli_mul_with_phase(PAULI_X, PAULI_Z, &mut tag, ptr::null_mut()), + -1 + ); +} diff --git a/crates/binding-c/tests/qis_measurement_test.rs b/crates/binding-c/tests/qis_measurement_test.rs new file mode 100644 index 00000000..05646b2b --- /dev/null +++ b/crates/binding-c/tests/qis_measurement_test.rs @@ -0,0 +1,508 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! Integration tests for the QIS simulator supplement and Measurement +//! C ABI (checklist 2.5 + 2.6). + +use binding_c::circuit::{ + CQLIB_CLASSICAL_EXPR_VALUE, CQLIB_CLASSICAL_TYPE_BIT_VEC, CQLIB_CLASSICAL_TYPE_UINT, + circuit_cx, circuit_free, circuit_h, circuit_measure_bits, circuit_new, classical_expr_free, + classical_expr_kind, +}; +use binding_c::cqlib_string_free; +use binding_c::qis::*; +use num_complex::Complex64; +use std::ffi::{CStr, CString}; +use std::os::raw::c_char; +use std::ptr; + +/// Reads a heap-allocated C string and frees it with `cqlib_string_free`. +fn cstr_to_string(p: *mut c_char) -> String { + assert!(!p.is_null()); + let s = unsafe { CStr::from_ptr(p) }.to_string_lossy().into_owned(); + cqlib_string_free(p); + s +} + +fn gate(name: &str) -> CString { + CString::new(name).unwrap() +} + +fn read_statevector(sv: *const CStatevector) -> Vec { + let len = statevector_data_len(sv); + let mut buffer = vec![Complex64::new(0.0, 0.0); len]; + assert_eq!(statevector_data(sv, buffer.as_mut_ptr(), len), 0); + buffer +} + +fn read_density_matrix(dm: *const CDensityMatrix) -> Vec { + let len = density_matrix_data_len(dm); + let mut buffer = vec![Complex64::new(0.0, 0.0); len]; + assert_eq!(density_matrix_data(dm, buffer.as_mut_ptr(), len), 0); + buffer +} + +#[test] +fn test_statevector_data_and_from_state() { + let sv = statevector_new(2); + assert_eq!(statevector_apply_h(sv, 0), 0); + assert_eq!(statevector_apply_cx(sv, 0, 1), 0); + + // Bell state: amplitudes at |00> (index 0) and |11> (index 3). + assert_eq!(statevector_data_len(sv), 4); + let amps = read_statevector(sv); + let inv = std::f64::consts::FRAC_1_SQRT_2; + assert!((amps[0].re - inv).abs() < 1e-12 && amps[0].im.abs() < 1e-12); + assert!(amps[1].norm() < 1e-12 && amps[2].norm() < 1e-12); + assert!((amps[3].re - inv).abs() < 1e-12 && amps[3].im.abs() < 1e-12); + + // Error paths for the two-step data access. + let mut out = vec![Complex64::new(0.0, 0.0); 4]; + assert_eq!(statevector_data(sv, out.as_mut_ptr(), 3), -8); + assert_eq!(statevector_data(sv, ptr::null_mut(), 4), -1); + assert_eq!(statevector_data(ptr::null(), out.as_mut_ptr(), 4), -1); + assert_eq!(statevector_data_len(ptr::null()), 0); + statevector_free(sv); + + // Reconstruct the Bell state via from_state and compare. + let bell = vec![ + Complex64::new(inv, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(inv, 0.0), + ]; + let rebuilt = statevector_from_state(2, bell.as_ptr(), bell.len()); + assert!(!rebuilt.is_null()); + assert_eq!(read_statevector(rebuilt), bell); + statevector_free(rebuilt); + + // from_state error paths: wrong dimension, NULL input, not normalized. + assert!(statevector_from_state(2, bell.as_ptr(), 3).is_null()); + assert!(statevector_from_state(2, ptr::null(), 4).is_null()); + let unnormalized = [Complex64::new(1.0, 0.0), Complex64::new(1.0, 0.0)]; + assert!(statevector_from_state(1, unnormalized.as_ptr(), 2).is_null()); +} + +#[test] +fn test_statevector_apply_standard_gate() { + // "H" by name must match the dedicated apply_h entry point. + let via_helper = statevector_new(1); + assert_eq!(statevector_apply_h(via_helper, 0), 0); + let via_name = statevector_new(1); + let h = gate("H"); + let targets = [0u32]; + assert_eq!( + statevector_apply_standard_gate(via_name, h.as_ptr(), targets.as_ptr(), 1, ptr::null(), 0), + 0 + ); + assert_eq!(read_statevector(via_name), read_statevector(via_helper)); + statevector_free(via_helper); + statevector_free(via_name); + + // RX(pi) maps |0> to -i|1>. + let sv = statevector_new(1); + let rx = gate("RX"); + let params = [std::f64::consts::PI]; + assert_eq!( + statevector_apply_standard_gate(sv, rx.as_ptr(), targets.as_ptr(), 1, params.as_ptr(), 1), + 0 + ); + let amps = read_statevector(sv); + assert!(amps[0].norm() < 1e-12); + assert!((amps[1].re - 0.0).abs() < 1e-12 && (amps[1].im + 1.0).abs() < 1e-12); + statevector_free(sv); + + // Error paths. + let sv = statevector_new(1); + let unknown = gate("NOPE"); + assert_eq!( + statevector_apply_standard_gate(sv, unknown.as_ptr(), targets.as_ptr(), 1, ptr::null(), 0), + -8 + ); + assert_eq!( + statevector_apply_standard_gate(sv, ptr::null(), targets.as_ptr(), 1, ptr::null(), 0), + -1 + ); + let oob = [5u32]; + assert_eq!( + statevector_apply_standard_gate(sv, h.as_ptr(), oob.as_ptr(), 1, ptr::null(), 0), + -2 + ); + assert_eq!( + statevector_apply_standard_gate(sv, rx.as_ptr(), targets.as_ptr(), 1, ptr::null(), 1), + -1 + ); + let nan = [f64::NAN]; + assert_eq!( + statevector_apply_standard_gate(sv, rx.as_ptr(), targets.as_ptr(), 1, nan.as_ptr(), 1), + -8 + ); + assert_eq!( + statevector_apply_standard_gate( + ptr::null_mut(), + h.as_ptr(), + targets.as_ptr(), + 1, + ptr::null(), + 0 + ), + -1 + ); + statevector_free(sv); +} + +#[test] +fn test_statevector_apply_pauli_rotation() { + // exp(-i * pi / 2 * X) maps |0> to -i|1>. + let pauli = pauli_string_new(1); + assert!(!pauli.is_null()); + assert_eq!(pauli_string_set_pauli(pauli, 0, PAULI_X), 0); + + let sv = statevector_new(1); + assert_eq!( + statevector_apply_pauli_rotation(sv, pauli, std::f64::consts::PI), + 0 + ); + let amps = read_statevector(sv); + assert!(amps[0].norm() < 1e-12); + assert!((amps[1].im + 1.0).abs() < 1e-12 && amps[1].re.abs() < 1e-12); + + // Error paths. + assert_eq!(statevector_apply_pauli_rotation(sv, pauli, f64::NAN), -8); + assert_eq!(statevector_apply_pauli_rotation(sv, ptr::null(), 0.0), -1); + assert_eq!( + statevector_apply_pauli_rotation(ptr::null_mut(), pauli, 0.0), + -1 + ); + + statevector_free(sv); + pauli_string_free(pauli); +} + +#[test] +fn test_statevector_apply_unitary_gate() { + // X matrix [[0, 1], [1, 0]] maps |0> to |1>. + let matrix = [ + Complex64::new(0.0, 0.0), + Complex64::new(1.0, 0.0), + Complex64::new(1.0, 0.0), + Complex64::new(0.0, 0.0), + ]; + let targets = [0u32]; + let sv = statevector_new(1); + assert_eq!( + statevector_apply_unitary_gate(sv, targets.as_ptr(), 1, matrix.as_ptr(), 2), + 0 + ); + let amps = read_statevector(sv); + assert!(amps[0].norm() < 1e-12); + assert!((amps[1].re - 1.0).abs() < 1e-12 && amps[1].im.abs() < 1e-12); + + // Error paths. + assert_eq!( + statevector_apply_unitary_gate(sv, targets.as_ptr(), 1, ptr::null(), 2), + -1 + ); + assert_eq!( + statevector_apply_unitary_gate(sv, ptr::null(), 1, matrix.as_ptr(), 2), + -1 + ); + assert_eq!( + statevector_apply_unitary_gate(ptr::null_mut(), targets.as_ptr(), 1, matrix.as_ptr(), 2), + -1 + ); + // dim inconsistent with the qubit count is rejected by the core. + assert!(statevector_apply_unitary_gate(sv, targets.as_ptr(), 1, matrix.as_ptr(), 4) < 0); + statevector_free(sv); +} + +#[test] +fn test_density_matrix_supplement() { + let inv = std::f64::consts::FRAC_1_SQRT_2; + + // rho = |+><+| has all entries 0.5. + let plus = [Complex64::new(inv, 0.0), Complex64::new(inv, 0.0)]; + let dm = density_matrix_from_state(1, plus.as_ptr(), plus.len()); + assert!(!dm.is_null()); + assert_eq!(density_matrix_data_len(dm), 4); + let data = read_density_matrix(dm); + for entry in &data { + assert!((entry.re - 0.5).abs() < 1e-12 && entry.im.abs() < 1e-12); + } + + // Trace of a density matrix is 1. + let (mut re, mut im) = (0.0f64, 0.0f64); + assert_eq!(density_matrix_trace(dm, &mut re, &mut im), 0); + assert!((re - 1.0).abs() < 1e-12 && im.abs() < 1e-12); + + // |+><+| is Hermitian. + let mut hermitian = false; + assert_eq!(density_matrix_is_hermitian(dm, 1e-10, &mut hermitian), 0); + assert!(hermitian); + assert_eq!( + density_matrix_is_hermitian(dm, f64::NAN, &mut hermitian), + -8 + ); + + // Error paths. + assert_eq!(density_matrix_trace(dm, ptr::null_mut(), &mut im), -1); + let mut out = vec![Complex64::new(0.0, 0.0); 4]; + assert_eq!(density_matrix_data(dm, out.as_mut_ptr(), 3), -8); + assert_eq!(density_matrix_data(ptr::null(), out.as_mut_ptr(), 4), -1); + assert!(density_matrix_from_state(1, plus.as_ptr(), 3).is_null()); + density_matrix_free(dm); + + // Maximally mixed state of 2 qubits: trace 1, diagonal 0.25. + let mm = density_matrix_maximally_mixed(2); + assert!(!mm.is_null()); + assert_eq!(density_matrix_data_len(mm), 16); + let data = read_density_matrix(mm); + for i in 0..4 { + assert!((data[i * 4 + i].re - 0.25).abs() < 1e-12); + } + assert_eq!(density_matrix_trace(mm, &mut re, &mut im), 0); + assert!((re - 1.0).abs() < 1e-12 && im.abs() < 1e-12); + density_matrix_free(mm); + + // "X" by name flips |0><0| to |1><1|. + let ket0 = [Complex64::new(1.0, 0.0), Complex64::new(0.0, 0.0)]; + let dm = density_matrix_from_state(1, ket0.as_ptr(), ket0.len()); + let x = gate("X"); + let targets = [0u32]; + assert_eq!( + density_matrix_apply_standard_gate(dm, x.as_ptr(), targets.as_ptr(), 1, ptr::null(), 0), + 0 + ); + let data = read_density_matrix(dm); + assert!(data[0].norm() < 1e-12); + assert!((data[3].re - 1.0).abs() < 1e-12); + assert_eq!( + density_matrix_apply_standard_gate( + dm, + gate("NOPE").as_ptr(), + targets.as_ptr(), + 1, + ptr::null(), + 0 + ), + -8 + ); + density_matrix_free(dm); +} + +#[test] +fn test_stabilizer_supplement() { + // Fresh 1-qubit state: destabilizer +X, stabilizer +Z, stim export "+Z\n". + let s = stabilizer_new(1); + assert!(!s.is_null()); + assert_eq!(stabilizer_get_destabilizers_len(s), 1); + assert_eq!(cstr_to_string(stabilizer_get_destabilizer(s, 0)), "+X"); + assert!(stabilizer_get_destabilizer(s, 5).is_null()); + assert_eq!(cstr_to_string(stabilizer_to_stim_format(s)), "+Z\n"); + assert_eq!(stabilizer_get_destabilizers_len(ptr::null()), 0); + assert!(stabilizer_get_destabilizer(ptr::null(), 0).is_null()); + assert!(stabilizer_to_stim_format(ptr::null()).is_null()); + stabilizer_free(s); + + // Bell state probabilities. + let bell = stabilizer_new(2); + assert_eq!(stabilizer_apply_h(bell, 0), 0); + assert_eq!(stabilizer_apply_cx(bell, 0, 1), 0); + let mut probability = 0.0f64; + assert_eq!( + stabilizer_probability_of(bell, [false, false].as_ptr(), 2, &mut probability), + 0 + ); + assert!((probability - 0.5).abs() < 1e-12); + assert_eq!( + stabilizer_probability_of(bell, [true, true].as_ptr(), 2, &mut probability), + 0 + ); + assert!((probability - 0.5).abs() < 1e-12); + assert_eq!( + stabilizer_probability_of(bell, [false, true].as_ptr(), 2, &mut probability), + 0 + ); + assert!(probability < 1e-12); + // Length mismatch maps to -8 (QubitMismatch). + assert_eq!( + stabilizer_probability_of(bell, [false].as_ptr(), 1, &mut probability), + -8 + ); + assert_eq!( + stabilizer_probability_of(ptr::null(), [false, false].as_ptr(), 2, &mut probability), + -1 + ); + stabilizer_free(bell); + + // Standard gate by name: H works, non-Clifford T is rejected with -7. + let s = stabilizer_new(1); + let h = gate("H"); + let targets = [0u32]; + assert_eq!( + stabilizer_apply_standard_gate(s, h.as_ptr(), targets.as_ptr(), 1, ptr::null(), 0), + 0 + ); + assert_eq!(cstr_to_string(stabilizer_get_stabilizer(s, 0)), "+X"); + let t = gate("T"); + assert_eq!( + stabilizer_apply_standard_gate(s, t.as_ptr(), targets.as_ptr(), 1, ptr::null(), 0), + -7 + ); + assert_eq!( + stabilizer_apply_standard_gate( + s, + gate("NOPE").as_ptr(), + targets.as_ptr(), + 1, + ptr::null(), + 0 + ), + -8 + ); + stabilizer_free(s); +} + +#[test] +fn test_stabilizer_run_circuit_collapses_measurements() { + // Bell circuit with a terminal measure_bits: run_circuit collapses the + // state (unlike from_circuit, which ignores the measurement). + let circuit = circuit_new(2); + assert_eq!(circuit_h(circuit, 0), 0); + assert_eq!(circuit_cx(circuit, 0, 1), 0); + let qubits = [0u32, 1]; + let measured = circuit_measure_bits(circuit, qubits.as_ptr(), 2); + assert!(!measured.is_null()); + classical_expr_free(measured); + + let state = stabilizer_run_circuit(circuit); + assert!(!state.is_null()); + circuit_free(circuit); + + // The collapsed state is either |00> or |11>, never a superposition. + let mut p00 = 0.0f64; + let mut p11 = 0.0f64; + let mut p01 = 0.0f64; + assert_eq!( + stabilizer_probability_of(state, [false, false].as_ptr(), 2, &mut p00), + 0 + ); + assert_eq!( + stabilizer_probability_of(state, [true, true].as_ptr(), 2, &mut p11), + 0 + ); + assert_eq!( + stabilizer_probability_of(state, [false, true].as_ptr(), 2, &mut p01), + 0 + ); + assert!((p00 + p11 - 1.0).abs() < 1e-12); + assert!(p01 < 1e-12); + stabilizer_free(state); + + assert!(stabilizer_run_circuit(ptr::null()).is_null()); +} + +#[test] +fn test_measurement_lifecycle() { + let qubits = [2u32, 0]; + let m = measurement_new(0, CQLIB_CLASSICAL_TYPE_BIT_VEC, 2, qubits.as_ptr(), 2); + assert!(!m.is_null()); + + // Getters: qubits keep the given (result bit) order. + assert_eq!(measurement_qubits_len(m), 2); + assert_eq!(measurement_width(m), 2); + let mut buffer = [99u32; 2]; + assert_eq!(measurement_qubits(m, buffer.as_mut_ptr(), 2), 0); + assert_eq!(buffer, [2, 0]); + assert_eq!(measurement_qubits(m, buffer.as_mut_ptr(), 1), -8); + assert_eq!(measurement_qubits(m, ptr::null_mut(), 2), -1); + + let (mut tag, mut width) = (99u32, 99u32); + assert_eq!(measurement_ty(m, &mut tag, &mut width), 0); + assert_eq!(tag, CQLIB_CLASSICAL_TYPE_BIT_VEC); + assert_eq!(width, 2); + + // Value / expression chain. + let value = measurement_value(m); + assert!(!value.is_null()); + assert_eq!(classical_value_index(value), 0); + assert_eq!(classical_value_ty(value, &mut tag, &mut width), 0); + assert_eq!(tag, CQLIB_CLASSICAL_TYPE_BIT_VEC); + assert_eq!(width, 2); + let value_expr = classical_value_expr(value); + assert!(!value_expr.is_null()); + let mut kind = 99u32; + assert_eq!(classical_expr_kind(value_expr, &mut kind), 0); + assert_eq!(kind, CQLIB_CLASSICAL_EXPR_VALUE); + classical_expr_free(value_expr); + classical_value_free(value); + + let expr = measurement_expr(m); + assert!(!expr.is_null()); + assert_eq!(classical_expr_kind(expr, &mut kind), 0); + assert_eq!(kind, CQLIB_CLASSICAL_EXPR_VALUE); + classical_expr_free(expr); + + // Qubit-order validation against a register size. + assert_eq!(measurement_check_qubits(m, 3), 0); + assert_eq!(measurement_check_qubits(m, 2), -2); + assert_eq!(measurement_check_qubits(ptr::null(), 3), -1); + + // Projection: register "101" (MSB left) has q2=1 and q0=1, and the + // measurement reads qubits [2, 0], so both result bits are 1 -> "11". + let bitstring = CString::new("101").unwrap(); + assert_eq!( + cstr_to_string(measurement_project(m, bitstring.as_ptr())), + "11" + ); + assert_eq!(cstr_to_string(measurement_project_basis(m, 0b101)), "11"); + // Only q0 is set in "001": result bit 0 (qubit 2) is 0, bit 1 (qubit 0) is 1. + let bitstring = CString::new("001").unwrap(); + assert_eq!( + cstr_to_string(measurement_project(m, bitstring.as_ptr())), + "10" + ); + assert!(measurement_project(m, ptr::null()).is_null()); + let bad = CString::new("10a").unwrap(); + assert!(measurement_project(m, bad.as_ptr()).is_null()); + + measurement_free(m); + + // Constructor error paths. + assert!(measurement_new(0, CQLIB_CLASSICAL_TYPE_BIT_VEC, 2, ptr::null(), 2).is_null()); + assert!(measurement_new(0, CQLIB_CLASSICAL_TYPE_BIT_VEC, 2, qubits.as_ptr(), 0).is_null()); + assert!(measurement_new(0, 99, 2, qubits.as_ptr(), 2).is_null()); + assert!(measurement_new(0, CQLIB_CLASSICAL_TYPE_UINT, 0, qubits.as_ptr(), 2).is_null()); +} + +#[test] +fn test_measurement_and_value_null_handling() { + assert!(measurement_value(ptr::null()).is_null()); + assert!(measurement_expr(ptr::null()).is_null()); + assert_eq!(measurement_qubits_len(ptr::null()), 0); + assert_eq!(measurement_width(ptr::null()), 0); + assert_eq!( + measurement_ty(ptr::null(), ptr::null_mut(), ptr::null_mut()), + -1 + ); + assert!(measurement_project(ptr::null(), ptr::null()).is_null()); + assert!(measurement_project_basis(ptr::null(), 0).is_null()); + + classical_value_free(ptr::null_mut()); + measurement_free(ptr::null_mut()); + assert_eq!(classical_value_index(ptr::null()), u32::MAX); + assert_eq!( + classical_value_ty(ptr::null(), ptr::null_mut(), ptr::null_mut()), + -1 + ); + assert!(classical_value_expr(ptr::null()).is_null()); +} diff --git a/crates/binding-c/tests/qis_metrics_test.rs b/crates/binding-c/tests/qis_metrics_test.rs new file mode 100644 index 00000000..bfcb4735 --- /dev/null +++ b/crates/binding-c/tests/qis_metrics_test.rs @@ -0,0 +1,732 @@ +//! Integration tests for QIS entropy / metrics / evolution C ABI +//! (gap checklist sections 2.1 - 2.4). + +#![allow(dead_code)] + +use binding_c::circuit::{circuit_free, circuit_num_qubits}; +use binding_c::qis::*; +use std::ffi::CString; + +/// Layout-compatible mirror of the `Complex64` struct injected by build.rs. +#[repr(C)] +#[derive(Clone, Copy)] +struct CComplex { + re: f64, + im: f64, +} + +fn bell_dm() -> *mut CDensityMatrix { + let dm = density_matrix_new(2); + assert!(!dm.is_null()); + assert_eq!(density_matrix_apply_h(dm, 0), 0); + assert_eq!(density_matrix_apply_cx(dm, 0, 1), 0); + dm +} + +fn bell_sv() -> *mut CStatevector { + let sv = statevector_new(2); + assert!(!sv.is_null()); + assert_eq!(statevector_apply_h(sv, 0), 0); + assert_eq!(statevector_apply_cx(sv, 0, 1), 0); + sv +} + +/// One-qubit maximally mixed state I/2 (partial trace of the Bell state). +fn maximally_mixed_1q() -> *mut CDensityMatrix { + let bell = bell_dm(); + let keep = [0u32]; + let reduced = density_matrix_partial_trace(bell, keep.as_ptr(), 1); + assert!(!reduced.is_null()); + density_matrix_free(bell); + reduced +} + +fn hamiltonian_with(pauli: &str, coeff_re: f64) -> *mut CHamiltonian { + let p = pauli_string_parse(CString::new(pauli).unwrap().as_ptr()); + assert!(!p.is_null()); + let h = hamiltonian_new(pauli.len()); + assert!(!h.is_null()); + assert_eq!(hamiltonian_add_term(h, p, coeff_re, 0.0), 0); + pauli_string_free(p); + h +} + +// --------------------------------------------------------------------------- +// 2.1 Entropy and entanglement measures +// --------------------------------------------------------------------------- + +#[test] +fn test_linear_entropy() { + let mut out = -1.0f64; + + // Pure Bell state: linear entropy ~ 0. + let bell = bell_dm(); + assert_eq!(density_matrix_linear_entropy(bell, &mut out), 0); + assert!(out.abs() < 1e-10); + + // Maximally mixed 1-qubit state I/2: 1 - Tr(rho^2) = 0.5. + let mixed = maximally_mixed_1q(); + assert_eq!(density_matrix_linear_entropy(mixed, &mut out), 0); + assert!((out - 0.5).abs() < 1e-10); + + assert_eq!( + density_matrix_linear_entropy(std::ptr::null(), &mut out), + -1 + ); + assert_eq!( + density_matrix_linear_entropy(bell, std::ptr::null_mut()), + -1 + ); + + density_matrix_free(mixed); + density_matrix_free(bell); +} + +#[test] +fn test_renyi_entropy() { + let mut out = -1.0f64; + + // Pure state: all Renyi entropies vanish. + let bell = bell_dm(); + assert_eq!(density_matrix_renyi_entropy(bell, 2.0, &mut out), 0); + assert!(out.abs() < 1e-10); + + // Collision entropy of I/2: -log2(1/2) = 1. + let mixed = maximally_mixed_1q(); + assert_eq!(density_matrix_renyi_entropy(mixed, 2.0, &mut out), 0); + assert!((out - 1.0).abs() < 1e-10); + + // Alpha equal to 1 falls back to the Von Neumann entropy (1 bit). + assert_eq!(density_matrix_renyi_entropy(mixed, 1.0, &mut out), 0); + assert!((out - 1.0).abs() < 1e-10); + + // Non-positive alpha is rejected. + assert_eq!(density_matrix_renyi_entropy(mixed, 0.0, &mut out), -8); + + density_matrix_free(mixed); + density_matrix_free(bell); +} + +#[test] +fn test_entanglement_entropy() { + let mut out = -1.0f64; + + // Bell state: maximally entangled, S(rho_A) = 1 bit. + let bell = bell_sv(); + let subsys = [0u32]; + assert_eq!( + statevector_entanglement_entropy(bell, subsys.as_ptr(), 1, &mut out), + 0 + ); + assert!((out - 1.0).abs() < 1e-10); + + // Product state: no entanglement. + let product = statevector_new(2); + statevector_apply_h(product, 0); + assert_eq!( + statevector_entanglement_entropy(product, subsys.as_ptr(), 1, &mut out), + 0 + ); + assert!(out.abs() < 1e-10); + + // Invalid subsystems: empty, full system, out-of-bounds. + assert_eq!( + statevector_entanglement_entropy(bell, std::ptr::null(), 0, &mut out), + -7 + ); + let full = [0u32, 1]; + assert_eq!( + statevector_entanglement_entropy(bell, full.as_ptr(), 2, &mut out), + -7 + ); + let oob = [5u32]; + assert_eq!( + statevector_entanglement_entropy(bell, oob.as_ptr(), 1, &mut out), + -2 + ); + // NULL qubit array with positive length. + assert_eq!( + statevector_entanglement_entropy(bell, std::ptr::null(), 1, &mut out), + -1 + ); + + statevector_free(product); + statevector_free(bell); +} + +#[test] +fn test_negativity() { + let mut out = -1.0f64; + + // Bell state: negativity = 0.5. + let bell = bell_dm(); + let subsys = [0u32]; + assert_eq!( + density_matrix_negativity(bell, subsys.as_ptr(), 1, &mut out), + 0 + ); + assert!((out - 0.5).abs() < 1e-10); + + // Separable pure state: negativity = 0. + let sep = density_matrix_new(2); + assert_eq!( + density_matrix_negativity(sep, subsys.as_ptr(), 1, &mut out), + 0 + ); + assert!(out.abs() < 1e-10); + + assert_eq!( + density_matrix_negativity(bell, std::ptr::null(), 1, &mut out), + -1 + ); + + density_matrix_free(sep); + density_matrix_free(bell); +} + +#[test] +fn test_concurrence() { + let mut out = -1.0f64; + + // Bell state: concurrence = 1. + let bell = bell_dm(); + assert_eq!(density_matrix_concurrence(bell, &mut out), 0); + assert!((out - 1.0).abs() < 1e-10); + + // Separable pure state: concurrence = 0. + let sep = density_matrix_new(2); + assert_eq!(density_matrix_concurrence(sep, &mut out), 0); + assert!(out.abs() < 1e-10); + + // Only 2-qubit states are supported. + let three = density_matrix_new(3); + assert_eq!(density_matrix_concurrence(three, &mut out), -7); + + density_matrix_free(three); + density_matrix_free(sep); + density_matrix_free(bell); +} + +#[test] +fn test_entanglement_of_formation() { + let mut out = -1.0f64; + + // Bell state: E_F = 1 bit. + let bell = bell_dm(); + assert_eq!(density_matrix_entanglement_of_formation(bell, &mut out), 0); + assert!((out - 1.0).abs() < 1e-10); + + // Separable pure state: E_F = 0. + let sep = density_matrix_new(2); + assert_eq!(density_matrix_entanglement_of_formation(sep, &mut out), 0); + assert!(out.abs() < 1e-10); + + assert_eq!( + density_matrix_entanglement_of_formation(std::ptr::null(), &mut out), + -1 + ); + + density_matrix_free(sep); + density_matrix_free(bell); +} + +// --------------------------------------------------------------------------- +// 2.2 Metrics +// --------------------------------------------------------------------------- + +#[test] +fn test_statevector_purity() { + let mut out = 0.0f64; + + let sv = statevector_new(2); + assert_eq!(statevector_purity(sv, &mut out), 0); + assert!((out - 1.0).abs() < 1e-10); + + let bell = bell_sv(); + assert_eq!(statevector_purity(bell, &mut out), 0); + assert!((out - 1.0).abs() < 1e-10); + + assert_eq!(statevector_purity(std::ptr::null(), &mut out), -1); + + statevector_free(bell); + statevector_free(sv); +} + +#[test] +fn test_density_matrix_purity() { + let mut out = 0.0f64; + + let bell = bell_dm(); + assert_eq!(density_matrix_purity(bell, &mut out), 0); + assert!((out - 1.0).abs() < 1e-10); + + let mixed = maximally_mixed_1q(); + assert_eq!(density_matrix_purity(mixed, &mut out), 0); + assert!((out - 0.5).abs() < 1e-10); + + density_matrix_free(mixed); + density_matrix_free(bell); +} + +#[test] +fn test_statevector_fidelity_and_trace_distance() { + let mut out = -1.0f64; + + let bell = bell_sv(); + let same = bell_sv(); + assert_eq!(statevector_fidelity(bell, same, &mut out), 0); + assert!((out - 1.0).abs() < 1e-10); + assert_eq!(statevector_trace_distance(bell, same, &mut out), 0); + assert!(out.abs() < 1e-10); + + // Orthogonal pure states: fidelity 0, trace distance 1. + let zero = statevector_new(1); + let one = statevector_new(1); + statevector_apply_x(one, 0); + assert_eq!(statevector_fidelity(zero, one, &mut out), 0); + assert!(out.abs() < 1e-10); + assert_eq!(statevector_trace_distance(zero, one, &mut out), 0); + assert!((out - 1.0).abs() < 1e-10); + + // Qubit-count mismatch. + assert_eq!(statevector_fidelity(bell, zero, &mut out), -8); + + assert_eq!(statevector_fidelity(std::ptr::null(), zero, &mut out), -1); + assert_eq!( + statevector_trace_distance(zero, std::ptr::null(), &mut out), + -1 + ); + + statevector_free(one); + statevector_free(zero); + statevector_free(same); + statevector_free(bell); +} + +#[test] +fn test_statevector_fidelity_pure_mixed() { + let mut out = -1.0f64; + + // = 1. + let bell = bell_sv(); + let bell_dm_ptr = bell_dm(); + assert_eq!( + statevector_fidelity_pure_mixed(bell, bell_dm_ptr, &mut out), + 0 + ); + assert!((out - 1.0).abs() < 1e-10); + + // <0| I/2 |0> = 0.5. + let zero = statevector_new(1); + let mixed = maximally_mixed_1q(); + assert_eq!(statevector_fidelity_pure_mixed(zero, mixed, &mut out), 0); + assert!((out - 0.5).abs() < 1e-10); + + // Qubit-count mismatch. + assert_eq!(statevector_fidelity_pure_mixed(bell, mixed, &mut out), -8); + + density_matrix_free(mixed); + density_matrix_free(bell_dm_ptr); + statevector_free(zero); + statevector_free(bell); +} + +#[test] +fn test_density_matrix_entropy() { + let mut out = -1.0f64; + + // Von Neumann entropy of I/2 is 1 bit. + let mixed = maximally_mixed_1q(); + assert_eq!(density_matrix_entropy(mixed, &mut out), 0); + assert!((out - 1.0).abs() < 1e-10); + + // Pure state: entropy 0. + let bell = bell_dm(); + assert_eq!(density_matrix_entropy(bell, &mut out), 0); + assert!(out.abs() < 1e-10); + + density_matrix_free(bell); + density_matrix_free(mixed); +} + +#[test] +fn test_density_matrix_trace_distance_and_fidelity() { + let mut out = -1.0f64; + + let bell = bell_dm(); + let same = bell_dm(); + + assert_eq!(density_matrix_trace_distance(bell, same, &mut out), 0); + assert!(out.abs() < 1e-10); + assert_eq!(density_matrix_fidelity(bell, same, &mut out), 0); + assert!((out - 1.0).abs() < 1e-10); + + // Orthogonal pure states (Bell vs |01>): distance 1, fidelity 0. + let sep = density_matrix_new(2); + assert_eq!(density_matrix_apply_x(sep, 0), 0); + assert_eq!(density_matrix_trace_distance(bell, sep, &mut out), 0); + assert!((out - 1.0).abs() < 1e-10); + assert_eq!(density_matrix_fidelity(bell, sep, &mut out), 0); + assert!(out.abs() < 1e-10); + + // Qubit-count mismatch. + let one_qubit = density_matrix_new(1); + assert_eq!(density_matrix_trace_distance(bell, one_qubit, &mut out), -8); + assert_eq!(density_matrix_fidelity(bell, one_qubit, &mut out), -8); + + density_matrix_free(one_qubit); + density_matrix_free(sep); + density_matrix_free(same); + density_matrix_free(bell); +} + +#[test] +fn test_partial_transpose() { + // Partial transpose of the Bell state on qubit 0 has eigenvalues + // (1/2, 1/2, 1/2, -1/2), so its purity is still 1.0. + let bell = bell_dm(); + let qubits = [0u32]; + let pt = density_matrix_partial_transpose(bell, qubits.as_ptr(), 1); + assert!(!pt.is_null()); + assert_eq!(density_matrix_num_qubits(pt), 2); + + let mut out = 0.0f64; + assert_eq!(density_matrix_purity(pt, &mut out), 0); + assert!((out - 1.0).abs() < 1e-10); + + // NULL qubit array with positive length is rejected. + assert!(density_matrix_partial_transpose(bell, std::ptr::null(), 1).is_null()); + // Out-of-bounds qubit is rejected. + let oob = [7u32]; + assert!(density_matrix_partial_transpose(bell, oob.as_ptr(), 1).is_null()); + + density_matrix_free(pt); + density_matrix_free(bell); +} + +#[test] +fn test_logarithmic_negativity() { + let mut out = -1.0f64; + + // Bell state: log2(||rho^T_A||_1) = log2(2) = 1. + let bell = bell_dm(); + let subsys = [0u32]; + assert_eq!( + density_matrix_logarithmic_negativity(bell, subsys.as_ptr(), 1, &mut out), + 0 + ); + assert!((out - 1.0).abs() < 1e-10); + + // Separable state: 0. + let sep = density_matrix_new(2); + assert_eq!( + density_matrix_logarithmic_negativity(sep, subsys.as_ptr(), 1, &mut out), + 0 + ); + assert!(out.abs() < 1e-10); + + density_matrix_free(sep); + density_matrix_free(bell); +} + +// --------------------------------------------------------------------------- +// 2.3 Evolution circuits +// --------------------------------------------------------------------------- + +#[test] +fn test_trotter_circuit() { + // H = ZZ on 2 qubits. + let h = hamiltonian_with("ZZ", 1.0); + + let circuit = hamiltonian_to_trotter_circuit(h, 1.0, 3, TROTTER_MODE_FIRST_ORDER); + assert!(!circuit.is_null()); + assert_eq!(circuit_num_qubits(circuit), 2); + circuit_free(circuit); + + let circuit = hamiltonian_to_trotter_circuit(h, 1.0, 3, TROTTER_MODE_SECOND_ORDER); + assert!(!circuit.is_null()); + assert_eq!(circuit_num_qubits(circuit), 2); + circuit_free(circuit); + + // Invalid mode tag, zero steps, and NULL handle all yield NULL. + assert!(hamiltonian_to_trotter_circuit(h, 1.0, 3, 99).is_null()); + assert!(hamiltonian_to_trotter_circuit(h, 1.0, 0, TROTTER_MODE_FIRST_ORDER).is_null()); + assert!( + hamiltonian_to_trotter_circuit(std::ptr::null(), 1.0, 3, TROTTER_MODE_FIRST_ORDER) + .is_null() + ); + + hamiltonian_free(h); +} + +#[test] +fn test_evolution_circuit() { + // Commuting Hamiltonian: exact single-pass decomposition. + let h = hamiltonian_with("ZZ", 0.5); + let circuit = hamiltonian_to_evolution_circuit(h, 1.0, 1, TROTTER_MODE_FIRST_ORDER); + assert!(!circuit.is_null()); + assert_eq!(circuit_num_qubits(circuit), 2); + circuit_free(circuit); + hamiltonian_free(h); + + // Non-commuting Hamiltonian: falls back to Trotter. + let h2 = hamiltonian_new(1); + let px = pauli_string_parse(CString::new("X").unwrap().as_ptr()); + let pz = pauli_string_parse(CString::new("Z").unwrap().as_ptr()); + assert_eq!(hamiltonian_add_term(h2, px, 1.0, 0.0), 0); + assert_eq!(hamiltonian_add_term(h2, pz, 1.0, 0.0), 0); + pauli_string_free(px); + pauli_string_free(pz); + + let circuit = hamiltonian_to_evolution_circuit(h2, 0.5, 10, TROTTER_MODE_SECOND_ORDER); + assert!(!circuit.is_null()); + assert_eq!(circuit_num_qubits(circuit), 1); + circuit_free(circuit); + hamiltonian_free(h2); + + // Empty Hamiltonian: NULL. + let empty = hamiltonian_new(2); + assert!(hamiltonian_to_evolution_circuit(empty, 1.0, 5, TROTTER_MODE_FIRST_ORDER).is_null()); + hamiltonian_free(empty); +} + +// --------------------------------------------------------------------------- +// 2.4 Hamiltonian supplements +// --------------------------------------------------------------------------- + +#[test] +fn test_hamiltonian_scale() { + // 0.5 * ZZ scaled by 2.0 must equal ZZ. + let h = hamiltonian_with("ZZ", 0.5); + assert_eq!(hamiltonian_scale(h, 2.0, 0.0), 0); + + let len = hamiltonian_to_matrix_len(h); + assert_eq!(len, 4); + let mut buffer = vec![CComplex { re: 0.0, im: 0.0 }; len * len]; + assert_eq!( + hamiltonian_to_matrix(h, buffer.as_mut_ptr() as *mut _, buffer.len()), + 0 + ); + + // diag(1, -1, -1, 1) for ZZ. + for i in 0..4 { + for j in 0..4 { + let expected = if i == j { + [1.0, -1.0, -1.0, 1.0][i] + } else { + 0.0 + }; + assert!((buffer[i * 4 + j].re - expected).abs() < 1e-10); + assert!(buffer[i * 4 + j].im.abs() < 1e-10); + } + } + + assert_eq!(hamiltonian_scale(std::ptr::null_mut(), 2.0, 0.0), -1); + hamiltonian_free(h); +} + +#[test] +fn test_hamiltonian_to_matrix_errors() { + let h = hamiltonian_with("Z", 1.0); + assert_eq!(hamiltonian_to_matrix_len(h), 2); + + let mut buffer = vec![CComplex { re: 0.0, im: 0.0 }; 4]; + // Wrong length. + assert_eq!( + hamiltonian_to_matrix(h, buffer.as_mut_ptr() as *mut _, 3), + -8 + ); + // NULL buffer. + assert_eq!(hamiltonian_to_matrix(h, std::ptr::null_mut(), 4), -1); + // NULL handle. + assert_eq!(hamiltonian_to_matrix_len(std::ptr::null()), 0); + + // Correct read: Z = diag(1, -1). + assert_eq!( + hamiltonian_to_matrix(h, buffer.as_mut_ptr() as *mut _, 4), + 0 + ); + assert!((buffer[0].re - 1.0).abs() < 1e-10); + assert!((buffer[3].re + 1.0).abs() < 1e-10); + + hamiltonian_free(h); +} + +#[test] +fn test_hamiltonian_all_terms_commute() { + // ZZ and ZI commute. + let h = hamiltonian_new(2); + let zz = pauli_string_parse(CString::new("ZZ").unwrap().as_ptr()); + let zi = pauli_string_parse(CString::new("ZI").unwrap().as_ptr()); + assert_eq!(hamiltonian_add_term(h, zz, 1.0, 0.0), 0); + assert_eq!(hamiltonian_add_term(h, zi, 0.5, 0.0), 0); + assert_eq!(hamiltonian_all_terms_commute(h), 1); + pauli_string_free(zz); + pauli_string_free(zi); + hamiltonian_free(h); + + // X and Z anticommute. + let h2 = hamiltonian_new(1); + let px = pauli_string_parse(CString::new("X").unwrap().as_ptr()); + let pz = pauli_string_parse(CString::new("Z").unwrap().as_ptr()); + assert_eq!(hamiltonian_add_term(h2, px, 1.0, 0.0), 0); + assert_eq!(hamiltonian_add_term(h2, pz, 1.0, 0.0), 0); + assert_eq!(hamiltonian_all_terms_commute(h2), 0); + pauli_string_free(px); + pauli_string_free(pz); + hamiltonian_free(h2); + + assert_eq!(hamiltonian_all_terms_commute(std::ptr::null()), -1); +} + +// --------------------------------------------------------------------------- +// 2.4 PauliString supplements +// --------------------------------------------------------------------------- + +#[test] +fn test_pauli_string_commutes_with() { + // X⊗Z and Z⊗X commute. + let a = pauli_string_parse(CString::new("XZ").unwrap().as_ptr()); + let b = pauli_string_parse(CString::new("ZX").unwrap().as_ptr()); + assert_eq!(pauli_string_commutes_with(a, b), 1); + assert_eq!(pauli_string_commutes_with(b, a), 1); + + // X and Z anticommute. + let x = pauli_string_parse(CString::new("XI").unwrap().as_ptr()); + let z = pauli_string_parse(CString::new("ZI").unwrap().as_ptr()); + assert_eq!(pauli_string_commutes_with(x, z), 0); + + // Qubit-count mismatch and NULL handles. + let single = pauli_string_parse(CString::new("X").unwrap().as_ptr()); + assert_eq!(pauli_string_commutes_with(a, single), -8); + assert_eq!(pauli_string_commutes_with(std::ptr::null(), a), -1); + + pauli_string_free(single); + pauli_string_free(z); + pauli_string_free(x); + pauli_string_free(b); + pauli_string_free(a); +} + +#[test] +fn test_pauli_string_masks_and_y_phase() { + // "XY": qubit 1 = X, qubit 0 = Y. + let ps = pauli_string_parse(CString::new("XY").unwrap().as_ptr()); + assert!(!ps.is_null()); + assert_eq!(pauli_string_x_mask(ps), 0b11); + assert_eq!(pauli_string_z_mask(ps), 0b01); + + let mut phase = CComplex { re: 0.0, im: 0.0 }; + assert_eq!( + pauli_string_y_phase(ps, &mut phase as *mut CComplex as *mut _), + 0 + ); + assert!((phase.re - 0.0).abs() < 1e-10); + assert!((phase.im - 1.0).abs() < 1e-10); + pauli_string_free(ps); + + // "YY": two Y operators give phase i^2 = -1. + let ps = pauli_string_parse(CString::new("YY").unwrap().as_ptr()); + assert_eq!( + pauli_string_y_phase(ps, &mut phase as *mut CComplex as *mut _), + 0 + ); + assert!((phase.re + 1.0).abs() < 1e-10); + assert!(phase.im.abs() < 1e-10); + assert_eq!(pauli_string_x_mask(ps), 0b11); + assert_eq!(pauli_string_z_mask(ps), 0b11); + pauli_string_free(ps); + + // No Y operators: phase = 1. + let ps = pauli_string_parse(CString::new("XZ").unwrap().as_ptr()); + assert_eq!( + pauli_string_y_phase(ps, &mut phase as *mut CComplex as *mut _), + 0 + ); + assert!((phase.re - 1.0).abs() < 1e-10); + pauli_string_free(ps); + + // NULL handling. + assert_eq!(pauli_string_x_mask(std::ptr::null()), 0); + assert_eq!(pauli_string_z_mask(std::ptr::null()), 0); + assert_eq!( + pauli_string_y_phase(std::ptr::null(), &mut phase as *mut CComplex as *mut _), + -1 + ); +} + +#[test] +fn test_pauli_string_support() { + // "XZI": qubit 2 = X, qubit 1 = Z, qubit 0 = I -> support [1, 2]. + let ps = pauli_string_parse(CString::new("XZI").unwrap().as_ptr()); + assert_eq!(pauli_string_support_len(ps), 2); + + let mut buffer = [0u32; 2]; + assert_eq!(pauli_string_support(ps, buffer.as_mut_ptr(), 2), 0); + assert_eq!(buffer, [1, 2]); + + // Wrong length is rejected. + assert_eq!(pauli_string_support(ps, buffer.as_mut_ptr(), 3), -8); + assert_eq!(pauli_string_support(ps, std::ptr::null_mut(), 2), -1); + + pauli_string_free(ps); + + // Identity string has empty support. + let ps = pauli_string_parse(CString::new("III").unwrap().as_ptr()); + assert_eq!(pauli_string_support_len(ps), 0); + assert_eq!(pauli_string_support(ps, buffer.as_mut_ptr(), 0), 0); + pauli_string_free(ps); + + assert_eq!(pauli_string_support_len(std::ptr::null()), 0); +} + +#[test] +fn test_pauli_string_try_get_set_pauli() { + // "XZI": qubit 2 = X, qubit 1 = Z, qubit 0 = I. + let ps = pauli_string_parse(CString::new("XZI").unwrap().as_ptr()); + let mut tag: u8 = 0xFF; + assert_eq!(pauli_string_try_get_pauli(ps, 0, &mut tag), 0); + assert_eq!(tag, PAULI_I); + assert_eq!(pauli_string_try_get_pauli(ps, 1, &mut tag), 0); + assert_eq!(tag, PAULI_Z); + assert_eq!(pauli_string_try_get_pauli(ps, 2, &mut tag), 0); + assert_eq!(tag, PAULI_X); + + // Out of bounds: -2 (IndexOutOfBounds). + assert_eq!(pauli_string_try_get_pauli(ps, 3, &mut tag), -2); + assert_eq!( + pauli_string_try_get_pauli(std::ptr::null(), 0, &mut tag), + -1 + ); + pauli_string_free(ps); + + // try_set_pauli: roundtrip and error paths. + let ps = pauli_string_new(2); + assert!(!ps.is_null()); + assert_eq!(pauli_string_try_set_pauli(ps, 0, PAULI_Y), 0); + assert_eq!(pauli_string_try_get_pauli(ps, 0, &mut tag), 0); + assert_eq!(tag, PAULI_Y); + assert_eq!(pauli_string_try_set_pauli(ps, 5, PAULI_X), -2); + assert_eq!(pauli_string_try_set_pauli(ps, 0, 200), -8); + assert_eq!( + pauli_string_try_set_pauli(std::ptr::null_mut(), 0, PAULI_X), + -1 + ); + pauli_string_free(ps); +} + +#[test] +fn test_pauli_tag_roundtrip() { + // Every Pauli tag survives a set/get roundtrip through the symplectic + // representation. + let tags = [PAULI_I, PAULI_X, PAULI_Y, PAULI_Z]; + for (idx, &expected) in tags.iter().enumerate() { + let ps = pauli_string_new(4); + assert!(!ps.is_null()); + assert_eq!(pauli_string_try_set_pauli(ps, idx, expected), 0); + let mut tag: u8 = 0xFF; + assert_eq!(pauli_string_try_get_pauli(ps, idx, &mut tag), 0); + assert_eq!(tag, expected); + pauli_string_free(ps); + } +} diff --git a/crates/binding-c/tests/qis_noise_test.rs b/crates/binding-c/tests/qis_noise_test.rs new file mode 100644 index 00000000..989e236e --- /dev/null +++ b/crates/binding-c/tests/qis_noise_test.rs @@ -0,0 +1,606 @@ +//! Integration tests for the DensityMatrixNoise C ABI. + +#![allow(dead_code)] + +use binding_c::circuit::{circuit_cx, circuit_free, circuit_h, circuit_new, circuit_x}; +use binding_c::cqlib_string_free; +use binding_c::device::{ + NOISE_BIT_FLIP, noise_model_add_readout, noise_model_add_single_qubit, noise_model_free, + noise_model_new, +}; +use binding_c::qis::*; +use num_complex::Complex64; +use std::ffi::{CStr, CString}; +use std::os::raw::c_char; +use std::ptr; + +fn cstr_to_string(ptr: *mut c_char) -> String { + if ptr.is_null() { + return String::new(); + } + unsafe { CStr::from_ptr(ptr) } + .to_string_lossy() + .into_owned() +} + +/// Build a noise model with a bit-flip channel of probability `p` on X@q0. +fn bit_flip_noise_model(p: f64) -> *mut binding_c::device::CNoiseModel { + let nm = noise_model_new(); + assert!(!nm.is_null()); + let gate = CString::new("X").unwrap(); + assert_eq!( + noise_model_add_single_qubit(nm, gate.as_ptr(), 0, NOISE_BIT_FLIP, p), + 0 + ); + nm +} + +fn read_probs(sim: *const CDensityMatrixNoise, len: usize) -> Vec { + let mut probs = vec![0.0f64; len]; + assert_eq!( + density_matrix_noise_probabilities(sim, probs.as_mut_ptr(), len), + 0 + ); + probs +} + +#[test] +fn test_new_free_and_null_handling() { + let sim = density_matrix_noise_new(2, ptr::null()); + assert!(!sim.is_null()); + assert_eq!(density_matrix_noise_num_qubits(sim), 2); + density_matrix_noise_free(sim); + + density_matrix_noise_free(ptr::null_mut()); + assert_eq!(density_matrix_noise_num_qubits(ptr::null()), 0); + assert_eq!(density_matrix_noise_probabilities_len(ptr::null()), 0); + assert_eq!(density_matrix_noise_apply_h(ptr::null_mut(), 0), -1); + assert_eq!(density_matrix_noise_measure(ptr::null_mut(), 0), -1); + assert!(density_matrix_noise_measure_all(ptr::null_mut()).is_null()); + assert!(density_matrix_noise_sample_shots(ptr::null(), 4).is_null()); +} + +#[test] +fn test_new_with_noise_model_handle() { + let nm = noise_model_new(); + let sim = density_matrix_noise_new(1, nm); + assert!(!sim.is_null()); + assert_eq!(density_matrix_noise_num_qubits(sim), 1); + density_matrix_noise_free(sim); + // The handle stays owned by the caller and remains usable. + let sim2 = density_matrix_noise_new(1, nm); + assert!(!sim2.is_null()); + density_matrix_noise_free(sim2); + noise_model_free(nm); +} + +#[test] +fn test_apply_h_probabilities_normalized() { + let sim = density_matrix_noise_new(1, ptr::null()); + assert_eq!(density_matrix_noise_apply_h(sim, 0), 0); + + assert_eq!(density_matrix_noise_probabilities_len(sim), 2); + let probs = read_probs(sim, 2); + assert!((probs[0] - 0.5).abs() < 1e-10); + assert!((probs[1] - 0.5).abs() < 1e-10); + assert!((probs.iter().sum::() - 1.0).abs() < 1e-10); + + density_matrix_noise_free(sim); +} + +#[test] +fn test_gate_noise_shifts_probabilities() { + // Mirrors core `test_bit_flip_noise`: X followed by 10% bit-flip noise + // on qubit 0 leaves P(|1>) = 0.9 and P(|0>) = 0.1. + let nm = bit_flip_noise_model(0.1); + let sim = density_matrix_noise_new(1, nm); + + assert_eq!(density_matrix_noise_apply_x(sim, 0), 0); + let probs = read_probs(sim, 2); + assert!((probs[1] - 0.9).abs() < 1e-6, "P(|1>) was {}", probs[1]); + assert!((probs[0] - 0.1).abs() < 1e-6, "P(|0>) was {}", probs[0]); + + density_matrix_noise_free(sim); + noise_model_free(nm); +} + +#[test] +fn test_from_circuit_with_noise() { + let nm = bit_flip_noise_model(0.1); + + let circuit = circuit_new(1); + assert_eq!(circuit_x(circuit, 0), 0); + + let sim = density_matrix_noise_from_circuit(circuit, nm); + assert!(!sim.is_null()); + assert_eq!(density_matrix_noise_num_qubits(sim), 1); + + let probs = read_probs(sim, 2); + assert!((probs[1] - 0.9).abs() < 1e-6); + assert!((probs[0] - 0.1).abs() < 1e-6); + + density_matrix_noise_free(sim); + circuit_free(circuit); + noise_model_free(nm); +} + +#[test] +fn test_from_circuit_null_args() { + assert!(density_matrix_noise_from_circuit(ptr::null(), ptr::null()).is_null()); +} + +#[test] +fn test_apply_circuit_bell_state() { + let circuit = circuit_new(2); + assert_eq!(circuit_h(circuit, 0), 0); + assert_eq!(circuit_cx(circuit, 0, 1), 0); + + let sim = density_matrix_noise_new(2, ptr::null()); + assert_eq!(density_matrix_noise_apply_circuit(sim, circuit), 0); + + let probs = read_probs(sim, 4); + assert!((probs[0] - 0.5).abs() < 1e-10); + assert!(probs[1] < 1e-10); + assert!(probs[2] < 1e-10); + assert!((probs[3] - 0.5).abs() < 1e-10); + + // NULL arguments are rejected. + assert_eq!(density_matrix_noise_apply_circuit(sim, ptr::null()), -1); + + density_matrix_noise_free(sim); + circuit_free(circuit); +} + +#[test] +fn test_qubit_out_of_bounds() { + let sim = density_matrix_noise_new(1, ptr::null()); + assert_eq!(density_matrix_noise_apply_h(sim, 5), -2); + assert_eq!(density_matrix_noise_apply_cx(sim, 0, 5), -2); + assert_eq!(density_matrix_noise_apply_ccx(sim, 0, 0, 5), -2); + assert_eq!(density_matrix_noise_reset(sim, 5), -2); + assert_eq!(density_matrix_noise_measure(sim, 5), -2); + density_matrix_noise_free(sim); +} + +#[test] +fn test_readout_error_difference() { + // Mirrors core `test_readout_error`: |0> with P(1|0) = 0.1 readout error. + let nm = noise_model_new(); + assert_eq!(noise_model_add_readout(nm, 0, 0.0, 0.1), 0); + + let sim = density_matrix_noise_new(1, nm); + + // Ideal distribution is unaffected by readout configuration. + let ideal = read_probs(sim, 2); + assert!((ideal[0] - 1.0).abs() < 1e-10); + assert!(ideal[1] < 1e-10); + + // With readout: P(|0>) = 0.9, P(|1>) = 0.1. + assert_eq!(density_matrix_noise_probabilities_with_readout_len(sim), 2); + let qs = [0u32]; + let mut probs = vec![0.0f64; 2]; + assert_eq!( + density_matrix_noise_probabilities_with_readout(sim, probs.as_mut_ptr(), 2, qs.as_ptr(), 1), + 0 + ); + assert!((probs[0] - 0.9).abs() < 1e-6, "P(|0>) was {}", probs[0]); + assert!((probs[1] - 0.1).abs() < 1e-6, "P(|1>) was {}", probs[1]); + + // Empty qubit selection behaves like the ideal distribution. + assert_eq!( + density_matrix_noise_probabilities_with_readout(sim, probs.as_mut_ptr(), 2, ptr::null(), 0), + 0 + ); + assert!((probs[0] - 1.0).abs() < 1e-10); + + // Out-of-bounds readout qubit is rejected. + let bad = [99u32]; + assert_eq!( + density_matrix_noise_probabilities_with_readout( + sim, + probs.as_mut_ptr(), + 2, + bad.as_ptr(), + 1 + ), + -2 + ); + + // Wrong buffer length is rejected. + assert_eq!( + density_matrix_noise_probabilities_with_readout(sim, probs.as_mut_ptr(), 4, qs.as_ptr(), 1), + -8 + ); + + density_matrix_noise_free(sim); + noise_model_free(nm); +} + +#[test] +fn test_multi_qubit_readout() { + // Mirrors core `test_multi_qubit_readout_error`. + let nm = noise_model_new(); + assert_eq!(noise_model_add_readout(nm, 0, 0.1, 0.2), 0); + assert_eq!(noise_model_add_readout(nm, 1, 0.3, 0.4), 0); + + let sim = density_matrix_noise_new(2, nm); + assert_eq!(density_matrix_noise_apply_x(sim, 0), 0); + + let qs = [0u32, 1u32]; + let mut probs = vec![0.0f64; 4]; + assert_eq!( + density_matrix_noise_probabilities_with_readout(sim, probs.as_mut_ptr(), 4, qs.as_ptr(), 2), + 0 + ); + assert!((probs[0] - 0.06).abs() < 1e-6, "P(|00>) was {}", probs[0]); + assert!((probs[1] - 0.54).abs() < 1e-6, "P(|01>) was {}", probs[1]); + assert!((probs[2] - 0.04).abs() < 1e-6, "P(|10>) was {}", probs[2]); + assert!((probs[3] - 0.36).abs() < 1e-6, "P(|11>) was {}", probs[3]); + + density_matrix_noise_free(sim); + noise_model_free(nm); +} + +#[test] +fn test_probabilities_len_mismatch() { + let sim = density_matrix_noise_new(1, ptr::null()); + let mut probs = vec![0.0f64; 4]; + assert_eq!( + density_matrix_noise_probabilities(sim, probs.as_mut_ptr(), 4), + -8 + ); + density_matrix_noise_free(sim); +} + +#[test] +fn test_measure_all_bell_state() { + let sim = density_matrix_noise_new(2, ptr::null()); + assert_eq!(density_matrix_noise_apply_h(sim, 0), 0); + assert_eq!(density_matrix_noise_apply_cx(sim, 0, 1), 0); + + let bitstring = density_matrix_noise_measure_all(sim); + let s = cstr_to_string(bitstring); + cqlib_string_free(bitstring); + assert!(s == "00" || s == "11", "outcome was {}", s); + + density_matrix_noise_free(sim); +} + +#[test] +fn test_measure_and_reset() { + let sim = density_matrix_noise_new(1, ptr::null()); + assert_eq!(density_matrix_noise_apply_x(sim, 0), 0); + assert_eq!(density_matrix_noise_measure(sim, 0), 1); + + assert_eq!(density_matrix_noise_reset(sim, 0), 0); + let probs = read_probs(sim, 2); + assert!((probs[0] - 1.0).abs() < 1e-10); + assert!(probs[1] < 1e-10); + + density_matrix_noise_free(sim); +} + +#[test] +fn test_sample_shots_bell_state() { + let sim = density_matrix_noise_new(2, ptr::null()); + assert_eq!(density_matrix_noise_apply_h(sim, 0), 0); + assert_eq!(density_matrix_noise_apply_cx(sim, 0, 1), 0); + + let list = density_matrix_noise_sample_shots(sim, 64); + assert!(!list.is_null()); + assert_eq!(outcome_list_len(list), 64); + + for i in 0..outcome_list_len(list) { + let ptr = outcome_list_get(list, i); + let s = cstr_to_string(ptr); + cqlib_string_free(ptr); + assert!(s == "00" || s == "11", "outcome was {}", s); + } + + outcome_list_free(list); + density_matrix_noise_free(sim); +} + +#[test] +fn test_expectation_value() { + // = -1 on |1>. + let sim = density_matrix_noise_new(1, ptr::null()); + assert_eq!(density_matrix_noise_apply_x(sim, 0), 0); + + let z = CString::new("Z").unwrap(); + let pauli = pauli_string_parse(z.as_ptr()); + assert!(!pauli.is_null()); + let ham = hamiltonian_from_pauli(pauli); + assert!(!ham.is_null()); + + let mut value = 0.0f64; + assert_eq!(density_matrix_noise_expectation(sim, ham, &mut value), 0); + assert!((value + 1.0).abs() < 1e-10, " was {}", value); + + // NULL arguments are rejected. + assert_eq!( + density_matrix_noise_expectation(sim, ptr::null(), &mut value), + -1 + ); + + hamiltonian_free(ham); + // `hamiltonian_from_pauli` consumed the `pauli` handle; do not free it. + density_matrix_noise_free(sim); +} + +#[test] +fn test_apply_standard_gate_noise() { + let sim = density_matrix_noise_new(1, ptr::null()); + + // "H" via the generic entry point. + let gate = CString::new("H").unwrap(); + let qs = [0u32]; + assert_eq!( + density_matrix_noise_apply_standard_gate_noise( + sim, + gate.as_ptr(), + qs.as_ptr(), + 1, + ptr::null(), + 0 + ), + 0 + ); + let probs = read_probs(sim, 2); + assert!((probs[0] - 0.5).abs() < 1e-10); + assert!((probs[1] - 0.5).abs() < 1e-10); + + // Parameterised gate "RY"(pi/2) maps |+> ... just check it succeeds and + // stays normalized; then "RZ" with a parameter. + let ry = CString::new("RY").unwrap(); + let params = [std::f64::consts::FRAC_PI_2]; + assert_eq!( + density_matrix_noise_apply_standard_gate_noise( + sim, + ry.as_ptr(), + qs.as_ptr(), + 1, + params.as_ptr(), + 1 + ), + 0 + ); + let probs = read_probs(sim, 2); + assert!((probs.iter().sum::() - 1.0).abs() < 1e-10); + + // Unknown gate name -> -8. + let bogus = CString::new("NOT_A_GATE").unwrap(); + assert_eq!( + density_matrix_noise_apply_standard_gate_noise( + sim, + bogus.as_ptr(), + qs.as_ptr(), + 1, + ptr::null(), + 0 + ), + -8 + ); + + // Wrong arity -> -8. + let cx = CString::new("CX").unwrap(); + assert_eq!( + density_matrix_noise_apply_standard_gate_noise( + sim, + cx.as_ptr(), + qs.as_ptr(), + 1, + ptr::null(), + 0 + ), + -8 + ); + + // Wrong parameter count -> -8. + let rx = CString::new("RX").unwrap(); + assert_eq!( + density_matrix_noise_apply_standard_gate_noise( + sim, + rx.as_ptr(), + qs.as_ptr(), + 1, + ptr::null(), + 0 + ), + -8 + ); + + // Out-of-bounds qubit -> -2. + let oob = [5u32]; + assert_eq!( + density_matrix_noise_apply_standard_gate_noise( + sim, + gate.as_ptr(), + oob.as_ptr(), + 1, + ptr::null(), + 0 + ), + -2 + ); + + // NULL handle / NULL name -> -1. + assert_eq!( + density_matrix_noise_apply_standard_gate_noise( + ptr::null_mut(), + gate.as_ptr(), + qs.as_ptr(), + 1, + ptr::null(), + 0 + ), + -1 + ); + assert_eq!( + density_matrix_noise_apply_standard_gate_noise( + sim, + ptr::null(), + qs.as_ptr(), + 1, + ptr::null(), + 0 + ), + -1 + ); + + density_matrix_noise_free(sim); +} + +#[test] +fn test_apply_standard_gate_noise_applies_noise() { + // The generic entry point must inject gate noise like the dedicated one. + let nm = bit_flip_noise_model(0.1); + let sim = density_matrix_noise_new(1, nm); + + let gate = CString::new("X").unwrap(); + let qs = [0u32]; + assert_eq!( + density_matrix_noise_apply_standard_gate_noise( + sim, + gate.as_ptr(), + qs.as_ptr(), + 1, + ptr::null(), + 0 + ), + 0 + ); + let probs = read_probs(sim, 2); + assert!((probs[1] - 0.9).abs() < 1e-6, "P(|1>) was {}", probs[1]); + assert!((probs[0] - 0.1).abs() < 1e-6, "P(|0>) was {}", probs[0]); + + density_matrix_noise_free(sim); + noise_model_free(nm); +} + +#[test] +fn test_apply_unitary_gate() { + let sim = density_matrix_noise_new(1, ptr::null()); + + // Pauli-X as a 2x2 row-major Complex64 matrix. + let matrix = [ + Complex64::new(0.0, 0.0), + Complex64::new(1.0, 0.0), + Complex64::new(1.0, 0.0), + Complex64::new(0.0, 0.0), + ]; + let qs = [0u32]; + assert_eq!( + density_matrix_noise_apply_unitary_gate(sim, qs.as_ptr(), 1, matrix.as_ptr(), 2), + 0 + ); + let probs = read_probs(sim, 2); + assert!((probs[1] - 1.0).abs() < 1e-10); + assert!(probs[0] < 1e-10); + + // Dimension inconsistent with qubit count -> -8. + let two_qs = [0u32, 0]; + assert_eq!( + density_matrix_noise_apply_unitary_gate(sim, two_qs.as_ptr(), 2, matrix.as_ptr(), 2), + -8 + ); + + // Out-of-bounds qubit -> -2. + let oob = [5u32]; + assert_eq!( + density_matrix_noise_apply_unitary_gate(sim, oob.as_ptr(), 1, matrix.as_ptr(), 2), + -2 + ); + + // NULL arguments -> -1. + assert_eq!( + density_matrix_noise_apply_unitary_gate( + ptr::null_mut(), + qs.as_ptr(), + 1, + matrix.as_ptr(), + 2 + ), + -1 + ); + assert_eq!( + density_matrix_noise_apply_unitary_gate(sim, qs.as_ptr(), 1, ptr::null(), 2), + -1 + ); + + density_matrix_noise_free(sim); +} + +#[test] +fn test_parameterised_gates_and_gphase() { + let sim = density_matrix_noise_new(1, ptr::null()); + + assert_eq!( + density_matrix_noise_apply_rx(sim, 0, std::f64::consts::PI), + 0 + ); + assert_eq!(density_matrix_noise_apply_u(sim, 0, 0.1, 0.2, 0.3), 0); + assert_eq!(density_matrix_noise_apply_rxy(sim, 0, 0.1, 0.2), 0); + assert_eq!(density_matrix_noise_apply_xy2p(sim, 0, 0.4), 0); + assert_eq!(density_matrix_noise_apply_xy2m(sim, 0, 0.4), 0); + assert_eq!(density_matrix_noise_apply_x2p(sim, 0), 0); + assert_eq!(density_matrix_noise_apply_y2m(sim, 0), 0); + assert_eq!(density_matrix_noise_apply_gphase(sim, 0.7), 0); + + let probs = read_probs(sim, 2); + assert!((probs.iter().sum::() - 1.0).abs() < 1e-10); + + // Non-finite parameter -> -8. + assert_eq!(density_matrix_noise_apply_ry(sim, 0, f64::NAN), -8); + assert_eq!(density_matrix_noise_apply_gphase(sim, f64::NAN), -8); + + density_matrix_noise_free(sim); +} + +#[test] +fn test_two_qubit_gates() { + let sim = density_matrix_noise_new(2, ptr::null()); + assert_eq!(density_matrix_noise_apply_h(sim, 0), 0); + + assert_eq!(density_matrix_noise_apply_cz(sim, 0, 1), 0); + assert_eq!(density_matrix_noise_apply_swap(sim, 0, 1), 0); + assert_eq!(density_matrix_noise_apply_rxx(sim, 0, 1, 0.3), 0); + assert_eq!(density_matrix_noise_apply_ryy(sim, 0, 1, 0.3), 0); + assert_eq!(density_matrix_noise_apply_rzz(sim, 0, 1, 0.3), 0); + assert_eq!(density_matrix_noise_apply_rzx(sim, 0, 1, 0.3), 0); + assert_eq!(density_matrix_noise_apply_crx(sim, 0, 1, 0.3), 0); + assert_eq!(density_matrix_noise_apply_cry(sim, 0, 1, 0.3), 0); + assert_eq!(density_matrix_noise_apply_crz(sim, 0, 1, 0.3), 0); + assert_eq!(density_matrix_noise_apply_fsim(sim, 0, 1, 0.3, 0.2), 0); + assert_eq!(density_matrix_noise_apply_cy(sim, 0, 1), 0); + + let probs = read_probs(sim, 4); + assert!((probs.iter().sum::() - 1.0).abs() < 1e-10); + + density_matrix_noise_free(sim); +} + +#[test] +fn test_ccx_gate() { + // Mirrors core `test_ccx`. + let sim = density_matrix_noise_new(3, ptr::null()); + assert_eq!(density_matrix_noise_apply_x(sim, 0), 0); + assert_eq!(density_matrix_noise_apply_x(sim, 1), 0); + assert_eq!(density_matrix_noise_apply_ccx(sim, 0, 1, 2), 0); + + let qs = [2u32]; + let mut probs = vec![0.0f64; 8]; + assert_eq!( + density_matrix_noise_probabilities_with_readout(sim, probs.as_mut_ptr(), 8, qs.as_ptr(), 1), + 0 + ); + let p1: f64 = probs + .iter() + .enumerate() + .filter(|(s, _)| (s >> 2) & 1 == 1) + .map(|(_, p)| p) + .sum(); + assert!((p1 - 1.0).abs() < 1e-6); + + density_matrix_noise_free(sim); +} diff --git a/crates/binding-c/tests/qis_test.rs b/crates/binding-c/tests/qis_test.rs new file mode 100644 index 00000000..05e2174d --- /dev/null +++ b/crates/binding-c/tests/qis_test.rs @@ -0,0 +1,429 @@ +//! Integration tests for QIS module C ABI. + +#![allow(dead_code)] + +use binding_c::circuit::{circuit_cx, circuit_free, circuit_h, circuit_new, circuit_rz}; +use binding_c::cqlib_string_free; +use binding_c::qis::*; +use std::ffi::{CStr, CString}; +use std::os::raw::c_char; + +fn cstr_to_string(ptr: *mut c_char) -> String { + if ptr.is_null() { + return String::new(); + } + unsafe { CStr::from_ptr(ptr) } + .to_string_lossy() + .into_owned() +} + +fn build_bell_circuit() -> *mut binding_c::circuit::CCircuit { + let circuit = circuit_new(2); + assert!(!circuit.is_null()); + assert_eq!(circuit_h(circuit, 0), 0); + assert_eq!(circuit_cx(circuit, 0, 1), 0); + circuit +} + +#[test] +fn test_statevector_new_and_free() { + let sv = statevector_new(2); + assert!(!sv.is_null()); + assert_eq!(statevector_num_qubits(sv), 2); + statevector_free(sv); +} + +#[test] +fn test_statevector_null_handling() { + statevector_free(std::ptr::null_mut()); + assert_eq!(statevector_num_qubits(std::ptr::null()), 0); + assert_eq!(statevector_apply_h(std::ptr::null_mut(), 0), -1); + assert_eq!(statevector_measure(std::ptr::null_mut(), 0), -1); +} + +#[test] +fn test_statevector_apply_h_probabilities() { + let sv = statevector_new(1); + assert_eq!(statevector_apply_h(sv, 0), 0); + + let len = statevector_probabilities_len(sv); + assert_eq!(len, 2); + + let mut probs = vec![0.0f64; len]; + let ret = statevector_probabilities(sv, probs.as_mut_ptr(), len); + assert_eq!(ret, 0); + assert!((probs[0] - 0.5).abs() < 1e-10); + assert!((probs[1] - 0.5).abs() < 1e-10); + + statevector_free(sv); +} + +#[test] +fn test_statevector_qubit_out_of_bounds() { + let sv = statevector_new(1); + assert_eq!(statevector_apply_h(sv, 5), -2); + assert_eq!(statevector_apply_cx(sv, 0, 5), -2); + statevector_free(sv); +} + +#[test] +fn test_statevector_apply_circuit_bell_state() { + let circuit = build_bell_circuit(); + let sv = statevector_from_circuit(circuit); + assert!(!sv.is_null()); + assert_eq!(statevector_num_qubits(sv), 2); + + let len = statevector_probabilities_len(sv); + let mut probs = vec![0.0f64; len]; + statevector_probabilities(sv, probs.as_mut_ptr(), len); + assert!((probs[0] - 0.5).abs() < 1e-10); + assert!((probs[1] < 1e-10)); + assert!((probs[2] < 1e-10)); + assert!((probs[3] - 0.5).abs() < 1e-10); + + statevector_free(sv); + circuit_free(circuit); +} + +#[test] +fn test_statevector_measure_all() { + let sv = statevector_new(2); + statevector_apply_h(sv, 0); + statevector_apply_cx(sv, 0, 1); + + let bitstring = statevector_measure_all(sv); + let s = cstr_to_string(bitstring); + cqlib_string_free(bitstring); + assert!(s == "00" || s == "11"); + + statevector_free(sv); +} + +#[test] +fn test_statevector_sample_shots() { + let sv = statevector_new(2); + statevector_apply_h(sv, 0); + statevector_apply_cx(sv, 0, 1); + + let list = statevector_sample_shots(sv, 50); + assert!(!list.is_null()); + assert_eq!(outcome_list_len(list), 50); + + for i in 0..outcome_list_len(list) { + let ptr = outcome_list_get(list, i); + let s = cstr_to_string(ptr); + cqlib_string_free(ptr); + assert!(s == "00" || s == "11"); + } + + outcome_list_free(list); + statevector_free(sv); +} + +#[test] +fn test_statevector_reset() { + let sv = statevector_new(1); + statevector_apply_x(sv, 0); + assert_eq!(statevector_reset(sv, 0), 0); + + let mut probs = vec![0.0f64; 2]; + statevector_probabilities(sv, probs.as_mut_ptr(), 2); + assert!((probs[0] - 1.0).abs() < 1e-10); + statevector_free(sv); +} + +#[test] +fn test_density_matrix_new_and_gates() { + let dm = density_matrix_new(2); + assert!(!dm.is_null()); + assert_eq!(density_matrix_num_qubits(dm), 2); + + assert_eq!(density_matrix_apply_h(dm, 0), 0); + assert_eq!(density_matrix_apply_cx(dm, 0, 1), 0); + + let len = density_matrix_probabilities_len(dm); + let mut probs = vec![0.0f64; len]; + density_matrix_probabilities(dm, probs.as_mut_ptr(), len); + assert!((probs[0] - 0.5).abs() < 1e-10); + assert!((probs[3] - 0.5).abs() < 1e-10); + + density_matrix_free(dm); +} + +#[test] +fn test_density_matrix_partial_trace() { + let dm = density_matrix_new(2); + density_matrix_apply_h(dm, 0); + density_matrix_apply_cx(dm, 0, 1); + + let keep = [0u32]; + let reduced = density_matrix_partial_trace(dm, keep.as_ptr(), 1); + assert!(!reduced.is_null()); + assert_eq!(density_matrix_num_qubits(reduced), 1); + + density_matrix_free(reduced); + density_matrix_free(dm); +} + +#[test] +fn test_stabilizer_new_and_clifford() { + let ss = stabilizer_new(2); + assert!(!ss.is_null()); + assert_eq!(stabilizer_num_qubits(ss), 2); + + assert_eq!(stabilizer_apply_h(ss, 0), 0); + assert_eq!(stabilizer_apply_cx(ss, 0, 1), 0); + + let len = stabilizer_probabilities_len(ss); + let mut probs = vec![0.0f64; len]; + let ret = stabilizer_probabilities(ss, probs.as_mut_ptr(), len); + assert_eq!(ret, 0); + assert!((probs[0] - 0.5).abs() < 1e-10); + assert!((probs[3] - 0.5).abs() < 1e-10); + + stabilizer_free(ss); +} + +#[test] +fn test_stabilizer_new_zero_qubits() { + let ss = stabilizer_new(0); + assert!(ss.is_null(), "stabilizer_new(0) should return NULL"); +} + +#[test] +fn test_stabilizer_measure_all_bell() { + let ss = stabilizer_new(2); + stabilizer_apply_h(ss, 0); + stabilizer_apply_cx(ss, 0, 1); + + let bitstring = stabilizer_measure_all(ss); + let s = cstr_to_string(bitstring); + cqlib_string_free(bitstring); + assert!(s == "00" || s == "11"); + + stabilizer_free(ss); +} + +#[test] +fn test_stabilizer_pauli_expectation() { + let ss = stabilizer_new(1); + stabilizer_apply_h(ss, 0); + + let pauli = pauli_string_parse(CString::new("X").unwrap().as_ptr()); + assert!(!pauli.is_null()); + + let mut value = 0i32; + let ret = stabilizer_pauli_expectation(ss, pauli, &mut value); + assert_eq!(ret, 0); + assert_eq!(value, 1); + + pauli_string_free(pauli); + stabilizer_free(ss); +} + +#[test] +fn test_stabilizer_get_stabilizers() { + let ss = stabilizer_new(2); + let n = stabilizer_get_stabilizers_len(ss); + assert_eq!(n, 2); + + let label = stabilizer_get_stabilizer(ss, 0); + let s = cstr_to_string(label); + cqlib_string_free(label); + assert!(s.contains('Z')); + + stabilizer_free(ss); +} + +#[test] +fn test_pauli_string_new_and_parse() { + let ps = pauli_string_parse(CString::new("XYZ").unwrap().as_ptr()); + assert!(!ps.is_null()); + assert_eq!(pauli_string_num_qubits(ps), 3); + + let label = pauli_string_to_string(ps); + let s = cstr_to_string(label); + cqlib_string_free(label); + assert!(s.contains('X')); + + pauli_string_free(ps); +} + +#[test] +fn test_pauli_string_set_get() { + let ps = pauli_string_new(2); + assert!(!ps.is_null()); + + assert_eq!(pauli_string_set_pauli(ps, 0, PAULI_X), 0); + assert_eq!(pauli_string_set_pauli(ps, 1, PAULI_Z), 0); + assert_eq!(pauli_string_get_pauli(ps, 0), PAULI_X); + assert_eq!(pauli_string_get_pauli(ps, 1), PAULI_Z); + + pauli_string_free(ps); +} + +#[test] +fn test_pauli_string_get_out_of_bounds() { + let ps = pauli_string_new(1); + assert_eq!(pauli_string_get_pauli(ps, 10), 0xFF); + pauli_string_free(ps); +} + +#[test] +fn test_hamiltonian_new_and_add_term() { + let h = hamiltonian_new(2); + assert!(!h.is_null()); + assert_eq!(hamiltonian_num_qubits(h), 2); + assert_eq!(hamiltonian_num_terms(h), 0); + + let pauli = pauli_string_parse(CString::new("ZZ").unwrap().as_ptr()); + assert!(!pauli.is_null()); + + assert_eq!(hamiltonian_add_term(h, pauli, 0.5, 0.0), 0); + assert_eq!(hamiltonian_num_terms(h), 1); + + pauli_string_free(pauli); + hamiltonian_free(h); +} + +#[test] +fn test_hamiltonian_from_pauli() { + let pauli = pauli_string_parse(CString::new("Z").unwrap().as_ptr()); + assert!(!pauli.is_null()); + + let h = hamiltonian_from_pauli(pauli); + assert!(!h.is_null()); + assert_eq!(hamiltonian_num_qubits(h), 1); + assert_eq!(hamiltonian_num_terms(h), 1); + + hamiltonian_free(h); +} + +#[test] +fn test_statevector_expectation() { + let sv = statevector_new(1); + statevector_apply_h(sv, 0); + + let pauli = pauli_string_parse(CString::new("X").unwrap().as_ptr()); + let h = hamiltonian_from_pauli(pauli); + + let mut value = 0.0f64; + let ret = statevector_expectation(sv, h, &mut value); + assert_eq!(ret, 0); + assert!((value - 1.0).abs() < 1e-10); + + hamiltonian_free(h); + statevector_free(sv); +} + +#[test] +fn test_statevector_y_expectation_from_circuit() { + for sign in [-1.0, 1.0] { + let circuit = circuit_new(1); + assert!(!circuit.is_null()); + assert_eq!(circuit_h(circuit, 0), 0); + assert_eq!( + circuit_rz(circuit, 0, sign * std::f64::consts::FRAC_PI_2), + 0 + ); + let sv = statevector_from_circuit(circuit); + assert!(!sv.is_null()); + let pauli = pauli_string_parse(c"Y".as_ptr()); + assert!(!pauli.is_null()); + let h = hamiltonian_from_pauli(pauli); // Consumes the Pauli handle. + assert!(!h.is_null()); + let mut value = f64::NAN; + assert_eq!(statevector_expectation(sv, h, &mut value), 0); + assert!((value - sign).abs() < 1e-10); + hamiltonian_free(h); + statevector_free(sv); + circuit_free(circuit); + } +} + +#[test] +fn test_statevector_pauli_phases_and_y_parity() { + for n in 1..=3 { + let sv = statevector_new(n); + assert!(!sv.is_null()); + let mut single = Vec::new(); + for q in 0..n { + let theta = 0.7 + 0.2 * q as f64; + let phi = -0.3 + 0.4 * q as f64; + assert_eq!(statevector_apply_ry(sv, q as u32, theta), 0); + assert_eq!(statevector_apply_rz(sv, q as u32, phi), 0); + // Independent Bloch components of RZ(phi) RY(theta)|0>. + single.push([ + 1.0, + theta.sin() * phi.cos(), + theta.sin() * phi.sin(), + theta.cos(), + ]); + } + for encoded in 0..4usize.pow(n as u32) { + let mut label = vec![b'I'; n]; + let mut expected = -0.37; + for q in 0..n { + let op = (encoded >> (2 * q)) & 3; + label[n - 1 - q] = b"IXYZ"[op]; + expected *= single[q][op]; + } + assert!(expected.abs() > 1e-10); + let label = String::from_utf8(label).unwrap(); + // Absorb each Pauli phase into the coefficient: H = -0.37 P. + for (prefix, re, im) in [ + ("", -0.37, 0.0), + ("-", 0.37, 0.0), + ("i", 0.0, 0.37), + ("-i", 0.0, -0.37), + ] { + let source = CString::new(format!("{prefix}{label}")).unwrap(); + let pauli = pauli_string_parse(source.as_ptr()); + assert!(!pauli.is_null()); + let h = hamiltonian_new(n); + assert!(!h.is_null()); + assert_eq!(hamiltonian_add_term(h, pauli, re, im), 0); + pauli_string_free(pauli); // add_term clones the Pauli handle. + let mut value = f64::NAN; + assert_eq!(statevector_expectation(sv, h, &mut value), 0); + assert!((value - expected).abs() < 1e-10, "{prefix}{label}"); + assert_eq!(hamiltonian_simplify(h), 0); + assert_eq!(statevector_expectation(sv, h, &mut value), 0); + assert!((value - expected).abs() < 1e-10, "{prefix}{label}"); + hamiltonian_free(h); + } + } + statevector_free(sv); + } +} + +#[test] +fn test_statevector_mixed_hamiltonian_expectation() { + let sv = statevector_new(1); + assert!(!sv.is_null()); + assert_eq!(statevector_apply_h(sv, 0), 0); + assert_eq!(statevector_apply_rz(sv, 0, std::f64::consts::FRAC_PI_4), 0); + let h = hamiltonian_new(1); + assert!(!h.is_null()); + for (label, re, im) in [ + (c"I", 0.3, 0.0), + (c"X", 0.5, 0.0), + (c"-iY", 0.0, 0.75), + (c"Y", -0.25, 0.0), + ] { + let pauli = pauli_string_parse(label.as_ptr()); + assert!(!pauli.is_null()); + assert_eq!(hamiltonian_add_term(h, pauli, re, im), 0); + pauli_string_free(pauli); + } + // H = 0.3 I + 0.5 X + 0.5 Y; = = 1/sqrt(2). + let expected = 0.3 + std::f64::consts::FRAC_1_SQRT_2; + let mut value = f64::NAN; + assert_eq!(statevector_expectation(sv, h, &mut value), 0); + assert!((value - expected).abs() < 1e-10); + assert_eq!(hamiltonian_simplify(h), 0); + assert_eq!(statevector_expectation(sv, h, &mut value), 0); + assert!((value - expected).abs() < 1e-10); + hamiltonian_free(h); + statevector_free(sv); +} diff --git a/crates/binding-c/tests/qubit_test.rs b/crates/binding-c/tests/qubit_test.rs new file mode 100644 index 00000000..952bf608 --- /dev/null +++ b/crates/binding-c/tests/qubit_test.rs @@ -0,0 +1,98 @@ +//! Integration tests for the qubit identifier C ABI. + +use binding_c::circuit::qubit::{ + CQubit, qubit_compare, qubit_equal, qubit_id, qubit_index, qubit_new, qubit_to_string, + qubit_try_from_i64, qubit_try_from_u64, +}; +use binding_c::cqlib_string_free; +use binding_c::device::qubit::{ + logical_qubit_compare, logical_qubit_equal, logical_qubit_from_qubit, logical_qubit_id, + logical_qubit_new, logical_qubit_qubit, logical_qubit_to_string, physical_qubit_compare, + physical_qubit_equal, physical_qubit_from_qubit, physical_qubit_id, physical_qubit_new, + physical_qubit_qubit, physical_qubit_to_string, +}; +use std::ffi::CStr; + +#[test] +fn test_qubit_basics() { + let q = qubit_new(12); + assert_eq!(q.id, 12); + assert_eq!(qubit_id(q), 12); + assert_eq!(qubit_index(q), 12usize); + + assert_eq!(qubit_equal(q, qubit_new(12)), 1); + assert_eq!(qubit_equal(q, qubit_new(11)), 0); + + assert_eq!(qubit_compare(qubit_new(0), qubit_new(1)), -1); + assert_eq!(qubit_compare(qubit_new(7), qubit_new(7)), 0); + assert_eq!(qubit_compare(qubit_new(2), qubit_new(1)), 1); + + let s = qubit_to_string(q); + assert!(!s.is_null()); + let text = unsafe { CStr::from_ptr(s) }; + assert_eq!(text.to_bytes(), b"Q12"); + cqlib_string_free(s); +} + +#[test] +fn test_qubit_checked_conversions() { + let mut out = CQubit { id: 0 }; + + assert_eq!(qubit_try_from_i64(-1, &mut out), -8); + assert_eq!(qubit_try_from_i64(i64::from(u32::MAX) + 1, &mut out), -8); + assert_eq!(qubit_try_from_i64(3, &mut out), 0); + assert_eq!(out.id, 3); + + assert_eq!(qubit_try_from_u64(u64::from(u32::MAX) + 1, &mut out), -8); + assert_eq!(qubit_try_from_u64(20, &mut out), 0); + assert_eq!(out.id, 20); + + assert_eq!(qubit_try_from_i64(0, std::ptr::null_mut()), -1); + assert_eq!(qubit_try_from_u64(0, std::ptr::null_mut()), -1); +} + +#[test] +fn test_logical_qubit_roundtrip() { + let wire = qubit_new(3); + let logical = logical_qubit_from_qubit(wire); + assert_eq!(logical.id, 3); + assert_eq!(logical_qubit_id(logical), 3); + + let back = logical_qubit_qubit(logical); + assert_eq!(back.id, 3); + + assert_eq!(logical_qubit_equal(logical, logical_qubit_new(3)), 1); + assert_eq!(logical_qubit_equal(logical, logical_qubit_new(2)), 0); + assert_eq!( + logical_qubit_compare(logical_qubit_new(2), logical_qubit_new(0)), + 1 + ); + + let s = logical_qubit_to_string(logical); + let text = unsafe { CStr::from_ptr(s) }; + assert_eq!(text.to_bytes(), b"L3"); + cqlib_string_free(s); +} + +#[test] +fn test_physical_qubit_roundtrip() { + let wire = qubit_new(11); + let physical = physical_qubit_from_qubit(wire); + assert_eq!(physical.id, 11); + assert_eq!(physical_qubit_id(physical), 11); + + let back = physical_qubit_qubit(physical); + assert_eq!(back.id, 11); + + assert_eq!(physical_qubit_equal(physical, physical_qubit_new(11)), 1); + assert_eq!(physical_qubit_equal(physical, physical_qubit_new(10)), 0); + assert_eq!( + physical_qubit_compare(physical_qubit_new(0), physical_qubit_new(2)), + -1 + ); + + let s = physical_qubit_to_string(physical); + let text = unsafe { CStr::from_ptr(s) }; + assert_eq!(text.to_bytes(), b"P11"); + cqlib_string_free(s); +} diff --git a/crates/binding-c/tests/symbolic_matrix_test.rs b/crates/binding-c/tests/symbolic_matrix_test.rs new file mode 100644 index 00000000..b1dbf241 --- /dev/null +++ b/crates/binding-c/tests/symbolic_matrix_test.rs @@ -0,0 +1,814 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! Rust FFI tests for symbolic complex values, element-level matrix +//! construction and the in-place `apply_*` gate family. + +use binding_c::circuit::symbolic_matrix_element; +use binding_c::circuit::{ + CParameter, CSymbolicComplex, CSymbolicMatrix, circuit_cx, circuit_free, circuit_h, + circuit_new, circuit_to_symbolic_matrix, param_add, param_evaluate, param_free, + param_from_double, param_sub, param_symbol, standard_gate_symbolic_matrix, + symbolic_complex_evaluate, symbolic_complex_exp_i, symbolic_complex_free, + symbolic_complex_from_complex, symbolic_complex_from_real, symbolic_complex_i, + symbolic_complex_im, symbolic_complex_is_one_exact, symbolic_complex_is_zero_exact, + symbolic_complex_new, symbolic_complex_one, symbolic_complex_re, symbolic_complex_replace, + symbolic_complex_simplifies_to_zero, symbolic_complex_simplify, symbolic_complex_zero, + symbolic_eye, symbolic_matrices_equivalent, symbolic_matrix_apply_diagonal_gate, + symbolic_matrix_apply_diagonal_gate_num, symbolic_matrix_apply_gate, + symbolic_matrix_apply_gate_num, symbolic_matrix_apply_general_gate, + symbolic_matrix_apply_general_gate_num, symbolic_matrix_apply_permutation_gate, + symbolic_matrix_apply_permutation_gate_num, symbolic_matrix_apply_single_qubit_gate, + symbolic_matrix_apply_single_qubit_gate_num, symbolic_matrix_apply_standard_gate, + symbolic_matrix_apply_two_qubit_gate, symbolic_matrix_apply_two_qubit_gate_num, + symbolic_matrix_evaluate, symbolic_matrix_evaluate_len, symbolic_matrix_free, + symbolic_matrix_new, symbolic_matrix_rows, +}; +use num_complex::Complex64; +use std::ffi::CString; + +fn cstr(s: &str) -> CString { + CString::new(s).unwrap() +} + +fn eval_re(param: *const CParameter, bindings: &str) -> f64 { + param_evaluate(param, cstr(bindings).as_ptr()) +} + +fn eval_complex(ptr: *const CSymbolicComplex, bindings: &str) -> Result { + let bindings = cstr(bindings); + let mut out = Complex64::new(f64::NAN, f64::NAN); + let code = symbolic_complex_evaluate(ptr, bindings.as_ptr(), &mut out); + if code == 0 { Ok(out) } else { Err(code) } +} + +/// Evaluates a fully numeric matrix with no bindings. +fn evaluated(matrix: *const CSymbolicMatrix) -> Vec { + let len = symbolic_matrix_evaluate_len(matrix); + let mut buffer = vec![Complex64::new(0.0, 0.0); len]; + let code = + symbolic_matrix_evaluate(matrix, std::ptr::null(), buffer.as_mut_ptr(), buffer.len()); + assert_eq!(code, 0); + buffer +} + +fn assert_close(a: &[Complex64], b: &[Complex64]) { + assert_eq!(a.len(), b.len()); + for (x, y) in a.iter().zip(b.iter()) { + assert!((x.re - y.re).abs() < 1e-9, "{x:?} != {y:?}"); + assert!((x.im - y.im).abs() < 1e-9, "{x:?} != {y:?}"); + } +} + +const X_NUM: [Complex64; 4] = [ + Complex64::new(0.0, 0.0), + Complex64::new(1.0, 0.0), + Complex64::new(1.0, 0.0), + Complex64::new(0.0, 0.0), +]; + +#[test] +fn symbolic_complex_construction() { + // NULL tolerance and invalid-input handling. + assert!(symbolic_complex_new(std::ptr::null(), std::ptr::null()).is_null()); + assert!(symbolic_complex_from_real(std::ptr::null()).is_null()); + assert!(symbolic_complex_exp_i(std::ptr::null()).is_null()); + assert!(symbolic_complex_from_complex(f64::NAN, 0.0).is_null()); + assert!(symbolic_complex_re(std::ptr::null()).is_null()); + assert!(symbolic_complex_im(std::ptr::null()).is_null()); + assert_eq!(symbolic_complex_is_zero_exact(std::ptr::null()), -1); + assert_eq!(symbolic_complex_is_one_exact(std::ptr::null()), -1); + assert_eq!(symbolic_complex_simplifies_to_zero(std::ptr::null()), -1); + assert!(symbolic_complex_simplify(std::ptr::null()).is_null()); + symbolic_complex_free(std::ptr::null_mut()); + + // zero / one / i constants. + let zero = symbolic_complex_zero(); + assert_eq!(symbolic_complex_is_zero_exact(zero), 1); + assert_eq!(symbolic_complex_simplifies_to_zero(zero), 1); + assert_eq!(eval_complex(zero, "").unwrap(), Complex64::new(0.0, 0.0)); + + let one = symbolic_complex_one(); + assert_eq!(symbolic_complex_is_one_exact(one), 1); + assert_eq!(symbolic_complex_is_zero_exact(one), 0); + assert_eq!(eval_complex(one, "").unwrap(), Complex64::new(1.0, 0.0)); + + let i = symbolic_complex_i(); + assert_eq!(symbolic_complex_is_zero_exact(i), 0); + assert_eq!(eval_complex(i, "").unwrap(), Complex64::new(0.0, 1.0)); + assert_eq!(symbolic_complex_is_one_exact(i), 0); + + // from_complex / from_real. + let raw = symbolic_complex_from_complex(0.5, -0.25); + assert_eq!(eval_complex(raw, "").unwrap(), Complex64::new(0.5, -0.25)); + + let theta = param_symbol(cstr("theta").as_ptr()); + let real = symbolic_complex_from_real(theta); + assert_eq!( + eval_complex(real, "theta:0.75").unwrap(), + Complex64::new(0.75, 0.0) + ); + + // new(re, im) and the re/im accessors. + let two = param_from_double(2.0); + let value = symbolic_complex_new(theta, two); + assert!(!value.is_null()); + let re = symbolic_complex_re(value); + let im = symbolic_complex_im(value); + assert!((eval_re(re, "theta:0.5") - 0.5).abs() < 1e-12); + assert!((eval_re(im, "") - 2.0).abs() < 1e-12); + assert_eq!( + eval_complex(value, "theta:0.5").unwrap(), + Complex64::new(0.5, 2.0) + ); + + // exp_i(theta) evaluates to cos(theta) + i*sin(theta). + let phase = symbolic_complex_exp_i(theta); + let at_zero = eval_complex(phase, "theta:0").unwrap(); + assert!((at_zero.re - 1.0).abs() < 1e-12); + assert!(at_zero.im.abs() < 1e-12); + let at_pi = eval_complex(phase, "theta:3.141592653589793").unwrap(); + assert!((at_pi.re + 1.0).abs() < 1e-9); + assert!(at_pi.im.abs() < 1e-9); + + param_free(re); + param_free(im); + symbolic_complex_free(zero); + symbolic_complex_free(one); + symbolic_complex_free(i); + symbolic_complex_free(raw); + symbolic_complex_free(real); + symbolic_complex_free(value); + symbolic_complex_free(phase); + param_free(theta); + param_free(two); +} + +#[test] +fn symbolic_complex_evaluate_simplify_replace() { + let theta = param_symbol(cstr("theta").as_ptr()); + // (x - theta, 0): after replacing x by theta both parts simplify to + // exact zero. + let x = param_symbol(cstr("x").as_ptr()); + let diff = param_sub(x, theta); + let zero_param = param_from_double(0.0); + let value = symbolic_complex_new(diff, zero_param); + + // Evaluation needs bound symbols: an unbound one yields -3 and a + // malformed bindings string yields -4. + let mut out = Complex64::new(0.0, 0.0); + assert_eq!( + symbolic_complex_evaluate(value, cstr("theta:1").as_ptr(), &mut out), + -3 + ); + assert_eq!(eval_complex(value, "theta:1"), Err(-3)); + assert_eq!(eval_complex(value, "bogus;"), Err(-4)); + + // Replace x by theta, then both parts simplify cleanly. + let replaced = symbolic_complex_replace(value, cstr("x").as_ptr(), theta); + assert!(!replaced.is_null()); + assert_eq!(symbolic_complex_simplifies_to_zero(replaced), 1); + + // symbolic_matrix-style simplification through the complex API. + let simplified = symbolic_complex_simplify(replaced); + assert!(!simplified.is_null()); + assert_eq!(symbolic_complex_is_zero_exact(simplified), 1); + assert_eq!( + eval_complex(simplified, "").unwrap(), + Complex64::new(0.0, 0.0) + ); + + // A replacement handle takes precedence over simplification errors. + assert!(symbolic_complex_replace(std::ptr::null(), cstr("x").as_ptr(), theta).is_null()); + assert!(symbolic_complex_replace(value, std::ptr::null(), theta).is_null()); + assert!(symbolic_complex_replace(value, cstr("x").as_ptr(), std::ptr::null()).is_null()); + + // simplify failure path: exp_i with a constant beyond [-1, 1] is fine, + // so exercise the Ok branch instead through nested arithmetic. + let sum = param_add(theta, theta); + let composed = symbolic_complex_from_real(sum); + assert!(!symbolic_complex_simplify(composed).is_null()); + + symbolic_complex_free(simplified); + symbolic_complex_free(replaced); + symbolic_complex_free(value); + symbolic_complex_free(composed); + param_free(diff); + param_free(sum); + param_free(zero_param); + param_free(x); + param_free(theta); +} + +#[test] +fn symbolic_matrix_new_and_element() { + // eye(2): build an element snapshot, then rebuild a matrix from it. + let eye = symbolic_eye(2); + let e00 = symbolic_matrix_element(eye, 0, 0); + let e01 = symbolic_matrix_element(eye, 0, 1); + let e10 = symbolic_matrix_element(eye, 1, 0); + let e11 = symbolic_matrix_element(eye, 1, 1); + assert!(!e00.is_null() && !e01.is_null() && !e10.is_null() && !e11.is_null()); + assert_eq!(symbolic_complex_is_one_exact(e00), 1); + assert_eq!(symbolic_complex_is_zero_exact(e01), 1); + + let elements: [*const CSymbolicComplex; 4] = [e00, e01, e10, e11]; + let rebuilt = symbolic_matrix_new(2, 2, elements.as_ptr(), 4); + assert!(!rebuilt.is_null()); + assert_eq!(symbolic_matrix_rows(rebuilt), 2); + assert_eq!(symbolic_matrices_equivalent(eye, rebuilt), 1); + + // Out-of-bounds element access returns NULL. + assert!(symbolic_matrix_element(eye, 2, 0).is_null()); + assert!(symbolic_matrix_element(eye, 0, 2).is_null()); + assert!(symbolic_matrix_element(std::ptr::null(), 0, 0).is_null()); + + // Constructors reject shape/length mismatches and NULL handles. + assert!(symbolic_matrix_new(2, 2, elements.as_ptr(), 3).is_null()); + assert!(symbolic_matrix_new(0, 2, elements.as_ptr(), 0).is_null()); + assert!(symbolic_matrix_new(2, 0, elements.as_ptr(), 0).is_null()); + assert!(symbolic_matrix_new(2, 2, std::ptr::null(), 4).is_null()); + let bad: [*const CSymbolicComplex; 4] = [e00, std::ptr::null(), e10, e11]; + assert!(symbolic_matrix_new(2, 2, bad.as_ptr(), 4).is_null()); + + for handle in elements { + symbolic_complex_free(handle as *mut CSymbolicComplex); + } + symbolic_matrix_free(eye); + symbolic_matrix_free(rebuilt); +} + +#[test] +fn symbolic_matrix_apply_standard_gate_matches_circuit() { + // Reference: a 2-qubit circuit H(0); CX(1, 0). + let circuit = circuit_new(2); + assert_eq!(circuit_h(circuit, 0), 0); + assert_eq!(circuit_cx(circuit, 0, 1), 0); + let reference = circuit_to_symbolic_matrix(circuit, std::ptr::null(), 0); + assert!(!reference.is_null()); + + // Same unitary assembled in place from the identity matrix. + let matrix = symbolic_eye(4); + let bits: [usize; 1] = [0]; + assert_eq!( + symbolic_matrix_apply_standard_gate( + matrix, + cstr("H").as_ptr(), + bits.as_ptr(), + 1, + std::ptr::null(), + 0, + ), + 0 + ); + // `apply_gate_to_matrix` expects the target bits in the system's + // Little-Endian order, and `circuit_to_symbolic_matrix` reverses the + // circuit's [control, target] pair before applying, so + // CX(control=0, target=1) maps to bits [1, 0]. + let two_bits: [usize; 2] = [1, 0]; + assert_eq!( + symbolic_matrix_apply_standard_gate( + matrix, + cstr("CX").as_ptr(), + two_bits.as_ptr(), + 2, + std::ptr::null(), + 0, + ), + 0 + ); + assert_eq!(symbolic_matrices_equivalent(reference, matrix), 1); + + // Error paths: NULL handles, unknown gate, bad bit arrays. + assert_eq!( + symbolic_matrix_apply_standard_gate( + std::ptr::null_mut(), + cstr("H").as_ptr(), + bits.as_ptr(), + 1, + std::ptr::null(), + 0, + ), + -1 + ); + assert_eq!( + symbolic_matrix_apply_standard_gate( + matrix, + cstr("NOPE").as_ptr(), + bits.as_ptr(), + 1, + std::ptr::null(), + 0 + ), + -8 + ); + assert_eq!( + symbolic_matrix_apply_standard_gate( + matrix, + cstr("H").as_ptr(), + std::ptr::null(), + 1, + std::ptr::null(), + 0 + ), + -8 + ); + let out_of_range: [usize; 1] = [9]; + assert_eq!( + symbolic_matrix_apply_standard_gate( + matrix, + cstr("H").as_ptr(), + out_of_range.as_ptr(), + 1, + std::ptr::null(), + 0 + ), + -8 + ); + let duplicated: [usize; 2] = [1, 1]; + assert_eq!( + symbolic_matrix_apply_standard_gate( + matrix, + cstr("CX").as_ptr(), + duplicated.as_ptr(), + 2, + std::ptr::null(), + 0 + ), + -8 + ); + + // RX with one parameter works and NULL parameter handles are rejected. + let rebuilt = symbolic_eye(4); + let params: [*const CParameter; 1] = [param_from_double(0.5)]; + assert_eq!( + symbolic_matrix_apply_standard_gate( + rebuilt, + cstr("RX").as_ptr(), + bits.as_ptr(), + 1, + params.as_ptr(), + 1, + ), + 0 + ); + let null_params: [*const CParameter; 1] = [std::ptr::null()]; + assert_eq!( + symbolic_matrix_apply_standard_gate( + rebuilt, + cstr("RX").as_ptr(), + bits.as_ptr(), + 1, + null_params.as_ptr(), + 1 + ), + -1 + ); + + param_free(params[0] as *mut CParameter); + symbolic_matrix_free(matrix); + symbolic_matrix_free(rebuilt); + symbolic_matrix_free(reference); + circuit_free(circuit); +} + +#[test] +fn symbolic_matrix_apply_gate_and_single_qubit() { + let x_gate = standard_gate_symbolic_matrix(cstr("X").as_ptr(), std::ptr::null(), 0); + assert!(!x_gate.is_null()); + + // symbolic apply_gate mirrors the numeric X application. + let symbolic = symbolic_eye(4); + let bit: [usize; 1] = [1]; + assert_eq!( + symbolic_matrix_apply_gate(std::ptr::null_mut(), x_gate, bit.as_ptr(), 1), + -1 + ); + assert_eq!( + symbolic_matrix_apply_gate(symbolic, std::ptr::null(), bit.as_ptr(), 1), + -1 + ); + assert_eq!( + symbolic_matrix_apply_gate(symbolic, x_gate, std::ptr::null(), 1), + -8 + ); + assert_eq!( + symbolic_matrix_apply_gate(symbolic, x_gate, bit.as_ptr(), 1), + 0 + ); + + let numeric = symbolic_eye(4); + assert_eq!( + symbolic_matrix_apply_gate_num(numeric, X_NUM.as_ptr(), 2, bit.as_ptr(), 1), + 0 + ); + // gate_dim must equal 1 << bits_len. + assert_eq!( + symbolic_matrix_apply_gate_num(numeric, X_NUM.as_ptr(), 4, bit.as_ptr(), 1), + -8 + ); + // The gate dimension power-of-two overflow guard cannot be reached on + // 64-bit targets, but a dim/len mismatch is checkable. + assert_eq!( + symbolic_matrix_apply_gate_num(std::ptr::null_mut(), X_NUM.as_ptr(), 2, bit.as_ptr(), 1), + -1 + ); + + // The two application paths agree numerically. + assert_close(&evaluated(symbolic), &evaluated(numeric)); + // Applying X on bit 1 swaps matrix rows r <-> r ^ 2, so the identity + // becomes the row-permutation matrix with values[4*r + c] = 1 iff + // c == r ^ 2. + let values = evaluated(numeric); + let idx = |row: usize, col: usize| 4 * row + col; + assert_eq!(values[idx(0b00, 0b10)], Complex64::new(1.0, 0.0)); + assert_eq!(values[idx(0b10, 0b00)], Complex64::new(1.0, 0.0)); + assert_eq!(values[idx(0b01, 0b11)], Complex64::new(1.0, 0.0)); + assert_eq!(values[idx(0b11, 0b01)], Complex64::new(1.0, 0.0)); + assert_eq!(values[idx(0b00, 0b00)], Complex64::new(0.0, 0.0)); + assert_eq!(values[idx(0b01, 0b01)], Complex64::new(0.0, 0.0)); + + // single-qubit variants. A non-2x2 gate payload is rejected. + let swapped = symbolic_eye(2); + let cx_gate = standard_gate_symbolic_matrix(cstr("CX").as_ptr(), std::ptr::null(), 0); + assert_eq!( + symbolic_matrix_apply_single_qubit_gate(swapped, cx_gate, 0), + -8 // gate must be 2x2 + ); + symbolic_matrix_free(cx_gate); + assert_eq!( + symbolic_matrix_apply_single_qubit_gate(swapped, x_gate, 0), + 0 + ); + assert_eq!( + symbolic_matrix_apply_single_qubit_gate(swapped, x_gate, 5), + -8 // bit out of range + ); + + let swapped_num = symbolic_eye(2); + assert_eq!( + symbolic_matrix_apply_single_qubit_gate_num(swapped_num, X_NUM.as_ptr(), 0), + 0 + ); + assert_eq!( + symbolic_matrix_apply_single_qubit_gate_num(swapped_num, X_NUM.as_ptr(), 2), + -8 + ); + assert_close(&evaluated(swapped), &evaluated(swapped_num)); + + symbolic_matrix_free(swapped_num); + symbolic_matrix_free(swapped); + symbolic_matrix_free(numeric); + symbolic_matrix_free(symbolic); + symbolic_matrix_free(x_gate); +} + +#[test] +fn symbolic_matrix_apply_two_qubit_and_general() { + let cx_gate = standard_gate_symbolic_matrix(cstr("CX").as_ptr(), std::ptr::null(), 0); + assert!(!cx_gate.is_null()); + + // A 2-qubit symbolic gate must be 4x4 (CX is) and the bits distinct. + let symbolic = symbolic_eye(4); + assert_eq!( + symbolic_matrix_apply_two_qubit_gate(std::ptr::null_mut(), cx_gate, 0, 1), + -1 + ); + assert_eq!( + symbolic_matrix_apply_two_qubit_gate(symbolic, std::ptr::null(), 0, 1), + -1 + ); + assert_eq!( + symbolic_matrix_apply_two_qubit_gate(symbolic, cx_gate, 0, 0), + -8 + ); + assert_eq!( + symbolic_matrix_apply_two_qubit_gate(symbolic, cx_gate, 0, 7), + -8 + ); + assert_eq!( + symbolic_matrix_apply_two_qubit_gate(symbolic, cx_gate, 0, 1), + 0 + ); + + // The numeric payload is read back from the standard CX gate so both + // paths use the exact same matrix. + let cx_num = evaluated(cx_gate); + + let numeric = symbolic_eye(4); + // NULL gate data. + assert_eq!( + symbolic_matrix_apply_two_qubit_gate_num(numeric, std::ptr::null(), 0, 1), + -1 + ); + assert_eq!( + symbolic_matrix_apply_two_qubit_gate_num(numeric, cx_num.as_ptr(), 1, 1), + -8 + ); + assert_eq!( + symbolic_matrix_apply_two_qubit_gate_num(std::ptr::null_mut(), cx_num.as_ptr(), 0, 1), + -1 + ); + assert_eq!( + symbolic_matrix_apply_two_qubit_gate_num(numeric, cx_num.as_ptr(), 0, 1), + 0 + ); + assert_close(&evaluated(symbolic), &evaluated(numeric)); + + // Cross-check the two-qubit path against apply_standard_gate on a + // freshly built identity: both produce the same CX permutation. + let via_standard = symbolic_eye(4); + let std_bits: [usize; 2] = [0, 1]; + assert_eq!( + symbolic_matrix_apply_standard_gate( + via_standard, + cstr("CX").as_ptr(), + std_bits.as_ptr(), + 2, + std::ptr::null(), + 0, + ), + 0 + ); + assert_close(&evaluated(via_standard), &evaluated(symbolic)); + + // general-gate variants with a 2-bit target. + let general = symbolic_eye(4); + let bits: [usize; 2] = [0, 1]; + assert_eq!( + symbolic_matrix_apply_general_gate(general, cx_gate, bits.as_ptr(), 2), + 0 + ); + assert_close(&evaluated(general), &evaluated(symbolic)); + + // Shape mismatch on the general variant: a 2x2 gate with 2 bits. + let x_gate = standard_gate_symbolic_matrix(cstr("X").as_ptr(), std::ptr::null(), 0); + let bad_shape = symbolic_eye(4); + assert_eq!( + symbolic_matrix_apply_general_gate(bad_shape, x_gate, bits.as_ptr(), 2), + -8 + ); + + // General numeric application equals the symbolic one. + let general_num = symbolic_eye(4); + assert_eq!( + symbolic_matrix_apply_general_gate_num(general_num, cx_num.as_ptr(), bits.as_ptr(), 2), + 0 + ); + assert_close(&evaluated(general), &evaluated(general_num)); + assert_eq!( + symbolic_matrix_apply_general_gate_num( + std::ptr::null_mut(), + cx_num.as_ptr(), + bits.as_ptr(), + 2 + ), + -1 + ); + + symbolic_matrix_free(general_num); + symbolic_matrix_free(bad_shape); + symbolic_matrix_free(x_gate); + symbolic_matrix_free(via_standard); + symbolic_matrix_free(general); + symbolic_matrix_free(numeric); + symbolic_matrix_free(symbolic); + symbolic_matrix_free(cx_gate); +} + +#[test] +fn symbolic_matrix_apply_permutation_and_diagonal() { + // X on bit 0 equals the permutation (i -> (1 - i, one)). + let one_a = symbolic_complex_one(); + let one_b = symbolic_complex_one(); + let symbolic = symbolic_eye(2); + let bit: [usize; 1] = [0]; + let indices: [usize; 2] = [1, 0]; + let values: [*const CSymbolicComplex; 2] = [one_a, one_b]; + assert_eq!( + symbolic_matrix_apply_permutation_gate( + std::ptr::null_mut(), + indices.as_ptr(), + values.as_ptr(), + 2, + bit.as_ptr(), + 1, + ), + -1 + ); + assert_eq!( + symbolic_matrix_apply_permutation_gate( + symbolic, + std::ptr::null(), + values.as_ptr(), + 2, + bit.as_ptr(), + 1, + ), + -8 + ); + // len must equal 1 << bits_len. + assert_eq!( + symbolic_matrix_apply_permutation_gate( + symbolic, + indices.as_ptr(), + values.as_ptr(), + 3, + bit.as_ptr(), + 1, + ), + -8 + ); + // Source index beyond the gate dimension. + let bad_index: [usize; 2] = [5, 0]; + assert_eq!( + symbolic_matrix_apply_permutation_gate( + symbolic, + bad_index.as_ptr(), + values.as_ptr(), + 2, + bit.as_ptr(), + 1, + ), + -1 + ); + // NULL value handle. + let null_values: [*const CSymbolicComplex; 2] = [one_a, std::ptr::null()]; + assert_eq!( + symbolic_matrix_apply_permutation_gate( + symbolic, + indices.as_ptr(), + null_values.as_ptr(), + 2, + bit.as_ptr(), + 1, + ), + -1 + ); + assert_eq!( + symbolic_matrix_apply_permutation_gate( + symbolic, + indices.as_ptr(), + values.as_ptr(), + 2, + bit.as_ptr(), + 1, + ), + 0 + ); + // The permutation swaps rows 0 <-> 1 of the identity, i.e. it is X. + let permuted = evaluated(symbolic); + assert_eq!(permuted[0], Complex64::new(0.0, 0.0)); + assert_eq!(permuted[1], Complex64::new(1.0, 0.0)); + assert_eq!(permuted[2], Complex64::new(1.0, 0.0)); + assert_eq!(permuted[3], Complex64::new(0.0, 0.0)); + + // Numeric permutation with factors (1, -1): output row 0 comes from + // input row 1 unchanged and output row 1 is the negated input row 0. + let perm_num = symbolic_eye(2); + let factors: [Complex64; 2] = [Complex64::new(1.0, 0.0), Complex64::new(-1.0, 0.0)]; + assert_eq!( + symbolic_matrix_apply_permutation_gate_num( + perm_num, + indices.as_ptr(), + factors.as_ptr(), + 2, + bit.as_ptr(), + 1, + ), + 0 + ); + assert_eq!( + symbolic_matrix_apply_permutation_gate_num( + std::ptr::null_mut(), + indices.as_ptr(), + factors.as_ptr(), + 2, + bit.as_ptr(), + 1, + ), + -1 + ); + assert_eq!( + symbolic_matrix_apply_permutation_gate_num( + perm_num, + bad_index.as_ptr(), + factors.as_ptr(), + 2, + bit.as_ptr(), + 1, + ), + -8 + ); + let flipped = evaluated(perm_num); + assert_eq!(flipped[0], Complex64::new(0.0, 0.0)); + assert_eq!(flipped[1], Complex64::new(1.0, 0.0)); + assert_eq!(flipped[2], Complex64::new(-1.0, 0.0)); + assert_eq!(flipped[3], Complex64::new(0.0, 0.0)); + + // Symbolic diagonal: diag(1, i) rotated as the S gate on bit 0. + let i_value = symbolic_complex_i(); + let diag_one = symbolic_complex_one(); + let diagonal = symbolic_eye(2); + let diag_values: [*const CSymbolicComplex; 2] = [diag_one, i_value]; + assert_eq!( + symbolic_matrix_apply_diagonal_gate( + std::ptr::null_mut(), + diag_values.as_ptr(), + 2, + bit.as_ptr(), + 1, + ), + -1 + ); + // len/dim mismatch. + assert_eq!( + symbolic_matrix_apply_diagonal_gate(diagonal, diag_values.as_ptr(), 3, bit.as_ptr(), 1,), + -8 + ); + assert_eq!( + symbolic_matrix_apply_diagonal_gate(diagonal, std::ptr::null(), 2, bit.as_ptr(), 1), + -8 + ); + assert_eq!( + symbolic_matrix_apply_diagonal_gate(diagonal, diag_values.as_ptr(), 2, bit.as_ptr(), 1), + 0 + ); + let rotated = evaluated(diagonal); + assert_eq!(rotated[0], Complex64::new(1.0, 0.0)); + assert_eq!(rotated[3], Complex64::new(0.0, 1.0)); + + // Numeric diagonal with the same payload. + let diagonal_num = symbolic_eye(2); + let raw_diag: [Complex64; 2] = [Complex64::new(1.0, 0.0), Complex64::new(0.0, 1.0)]; + assert_eq!( + symbolic_matrix_apply_diagonal_gate_num( + diagonal_num, + raw_diag.as_ptr(), + 2, + bit.as_ptr(), + 1, + ), + 0 + ); + assert_eq!( + symbolic_matrix_apply_diagonal_gate_num( + std::ptr::null_mut(), + raw_diag.as_ptr(), + 2, + bit.as_ptr(), + 1, + ), + -1 + ); + assert_eq!( + symbolic_matrix_apply_diagonal_gate_num( + diagonal_num, + raw_diag.as_ptr(), + 5, + bit.as_ptr(), + 1, + ), + -8 + ); + assert_close(&rotated, &evaluated(diagonal_num)); + + symbolic_matrix_free(diagonal_num); + symbolic_matrix_free(diagonal); + symbolic_matrix_free(perm_num); + symbolic_matrix_free(symbolic); + symbolic_complex_free(i_value); + symbolic_complex_free(diag_one); + symbolic_complex_free(one_a); + symbolic_complex_free(one_b); +} + +#[test] +fn symbolic_matrix_columns_of_identity_unchanged() { + // Sanity: applying gates never changes the matrix shape. + let matrix = symbolic_eye(4); + let bits: [usize; 2] = [0, 1]; + assert_eq!( + symbolic_matrix_apply_standard_gate( + matrix, + cstr("CX").as_ptr(), + bits.as_ptr(), + 2, + std::ptr::null(), + 0, + ), + 0 + ); + assert_eq!(symbolic_matrix_rows(matrix), 4); + assert_eq!(symbolic_matrix_evaluate_len(matrix), 16); + symbolic_matrix_free(matrix); +} diff --git a/crates/binding-c/tests/test_ansatz_feature_map.c b/crates/binding-c/tests/test_ansatz_feature_map.c new file mode 100644 index 00000000..d9994ea6 --- /dev/null +++ b/crates/binding-c/tests/test_ansatz_feature_map.c @@ -0,0 +1,436 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating that +// they have been altered from the originals. + +#include +#include +#include +#include + +#include "cqlib_c.h" + +/* Builds a Hamiltonian from a single Pauli word (coefficient 1.0). + * `hamiltonian_from_pauli` takes ownership of the pauli handle, so the + * caller must free only the returned Hamiltonian, never the pauli + * handle once this helper returns. */ +static struct CHamiltonian* pauli_hamiltonian(const char* text) { + struct CPauliString* pauli = pauli_string_parse(text); + if (pauli == NULL) { + return NULL; + } + return hamiltonian_from_pauli(pauli); +} + +static void test_z_feature_map(void) { + struct CZFeatureMap* map = z_feature_map_new(3); + assert(map != NULL); + assert(z_feature_map_num_qubits(map) == 3); + + /* Defaults: reps = 2, one feature parameter per qubit. */ + assert(z_feature_map_num_parameters(map) == 3); + assert(z_feature_map_validate(map) == 0); + + /* reps = 0 drops all parameters; restoring it brings them back. */ + assert(z_feature_map_reps(map, 0) == 0); + assert(z_feature_map_num_parameters(map) == 0); + assert(z_feature_map_reps(map, 2) == 0); + assert(z_feature_map_num_parameters(map) == 3); + + /* Each layer is a Hadamard plus one RZ per qubit: 2 * (3 + 3) = 12 ops + * over the 3 unique feature parameters. */ + struct CCircuit* circuit = z_feature_map_build_circuit(map, "z"); + assert(circuit != NULL); + assert(circuit_num_qubits(circuit) == 3); + assert(circuit_num_parameters(circuit) == 3); + assert(circuit_num_operations(circuit) == 2 * (3 + 3)); + circuit_free(circuit); + + /* NULL and non-UTF-8 prefixes are rejected. */ + assert(z_feature_map_build_circuit(map, NULL) == NULL); + const char invalid_utf8[2] = {(char)0xFF, '\0'}; + assert(z_feature_map_build_circuit(map, invalid_utf8) == NULL); + z_feature_map_free(map); +} + +static void test_zz_feature_map(void) { + struct CZZFeatureMap* map = zz_feature_map_new(3); + assert(map != NULL); + assert(zz_feature_map_num_qubits(map) == 3); + + /* Defaults: reps = 2, full entanglement; one feature parameter per + * qubit regardless of the interaction layer count. */ + assert(zz_feature_map_num_parameters(map) == 3); + assert(zz_feature_map_validate(map) == 0); + + /* Topologies are accepted; unknown tags and malformed pair lists are + * rejected with distinct error codes. */ + assert(zz_feature_map_entanglement(map, ENTANGLEMENT_LINEAR, NULL, 0) == 0); + assert(zz_feature_map_entanglement(map, ENTANGLEMENT_CIRCULAR, NULL, 0) == 0); + const uint32_t pairs[4] = {0, 1, 1, 2}; + assert(zz_feature_map_entanglement(map, ENTANGLEMENT_CUSTOM, pairs, 4) == 0); + assert(zz_feature_map_validate(map) == 0); + assert(zz_feature_map_entanglement(map, 99, NULL, 0) == -8); + assert(zz_feature_map_entanglement(map, ENTANGLEMENT_CUSTOM, pairs, 3) == -8); + assert(zz_feature_map_entanglement(map, ENTANGLEMENT_CUSTOM, NULL, 2) == -1); + + /* reps = 0 drops all parameters. */ + assert(zz_feature_map_reps(map, 0) == 0); + assert(zz_feature_map_num_parameters(map) == 0); + assert(zz_feature_map_reps(map, 1) == 0); + + /* Restore full entanglement and build. The ZZ interactions lower to + * Pauli evolution gates, so only the H + RZ floor is pinned. Each + * gate interns its whole angle expression, so the circuit exposes + * 3 RZ expressions + 3 evolution expressions (pi is nested inside + * them, never interned on its own). */ + assert(zz_feature_map_entanglement(map, ENTANGLEMENT_FULL, NULL, 0) == 0); + struct CCircuit* circuit = zz_feature_map_build_circuit(map, "x"); + assert(circuit != NULL); + assert(circuit_num_qubits(circuit) == 3); + assert(circuit_num_parameters(circuit) == 6); + assert(circuit_num_operations(circuit) >= 3 + 3); + circuit_free(circuit); + zz_feature_map_free(map); +} + +static void test_iqp_feature_map(void) { + struct CIQPFeatureMap* map = iqp_feature_map_new(2); + assert(map != NULL); + assert(iqp_feature_map_num_qubits(map) == 2); + assert(iqp_feature_map_num_parameters(map) == 2); + assert(iqp_feature_map_validate(map) == 0); + + assert(iqp_feature_map_reps(map, 1) == 0); + assert(iqp_feature_map_num_parameters(map) == 2); + + /* A custom pair list constrains the two-qubit diagonal interactions. + * The built circuit interns one angle expression per gate: 2 RZ + 1 + * evolution expression. */ + const uint32_t pairs[2] = {0, 1}; + assert(iqp_feature_map_entanglement(map, ENTANGLEMENT_CUSTOM, pairs, 2) == 0); + struct CCircuit* circuit = iqp_feature_map_build_circuit(map, "x"); + assert(circuit != NULL); + assert(circuit_num_qubits(circuit) == 2); + assert(circuit_num_parameters(circuit) == 3); + circuit_free(circuit); + iqp_feature_map_free(map); +} + +static void test_pauli_feature_map(void) { + struct CPauliFeatureMap* map = pauli_feature_map_new(3); + assert(map != NULL); + assert(pauli_feature_map_num_qubits(map) == 3); + + /* Defaults: reps = 2, paulis ["Z", "ZZ"], prefix "x". */ + assert(pauli_feature_map_num_parameters(map) == 3); + assert(pauli_feature_map_validate(map) == 0); + + /* Parallel text/label arrays configure the Pauli templates. */ + const char* texts[2] = {"X", "XX"}; + const char* labels[2] = {"x", "xx"}; + assert(pauli_feature_map_paulis(map, texts, labels, 2) == 0); + assert(pauli_feature_map_validate(map) == 0); + + /* Unparsable Pauli words, NULL entries and invalid UTF-8 are rejected + * without clobbering the current configuration. */ + const char* bad_texts[1] = {"QZ"}; + const char* bad_labels[1] = {"qz"}; + assert(pauli_feature_map_paulis(map, bad_texts, bad_labels, 1) == -8); + const char* null_texts[1] = {NULL}; + assert(pauli_feature_map_paulis(map, null_texts, labels, 1) == -1); + const char invalid_utf8[2] = {(char)0xFF, '\0'}; + const char* utf8_labels[1] = {invalid_utf8}; + assert(pauli_feature_map_paulis(map, texts, utf8_labels, 1) == -4); + assert(pauli_feature_map_validate(map) == 0); + + /* The parameter prefix is a fallback for empty build prefixes. */ + assert(pauli_feature_map_parameter_prefix(map, "f") == 0); + assert(pauli_feature_map_parameter_prefix(map, invalid_utf8) == -4); + assert(pauli_feature_map_parameter_prefix(map, NULL) == -1); + + /* An empty prefix falls back to the configured parameter prefix. + * Angle expressions are interned whole: 3 single-qubit + 3 pair + * expressions for the ["X", "XX"] templates on 3 qubits. */ + struct CCircuit* circuit = pauli_feature_map_build_circuit(map, ""); + assert(circuit != NULL); + assert(circuit_num_qubits(circuit) == 3); + assert(circuit_num_parameters(circuit) == 6); + circuit_free(circuit); + assert(pauli_feature_map_build_circuit(map, NULL) == NULL); + pauli_feature_map_free(map); +} + +static void test_feature_map_facades(void) { + /* zz_feature_map_build mirrors the new + setters chain. */ + struct CZZFeatureMap* zz = zz_feature_map_build(3, 1, ENTANGLEMENT_CIRCULAR, NULL, 0); + assert(zz != NULL); + assert(zz_feature_map_num_qubits(zz) == 3); + assert(zz_feature_map_num_parameters(zz) == 3); + assert(zz_feature_map_validate(zz) == 0); + zz_feature_map_free(zz); + + const uint32_t pairs[2] = {0, 2}; + struct CZZFeatureMap* custom = zz_feature_map_build(3, 1, ENTANGLEMENT_CUSTOM, pairs, 2); + assert(custom != NULL); + assert(zz_feature_map_validate(custom) == 0); + zz_feature_map_free(custom); + assert(zz_feature_map_build(3, 1, 99, NULL, 0) == NULL); + + /* pauli_feature_map_build takes explicit Pauli templates. The ZZ + * angles add the symbolic pi constant to the 3 features. */ + const char* texts[2] = {"Z", "ZZ"}; + const char* labels[2] = {"z", "zz"}; + struct CPauliFeatureMap* pf = + pauli_feature_map_build(3, 1, texts, labels, 2, ENTANGLEMENT_FULL, NULL, 0); + assert(pf != NULL); + assert(pauli_feature_map_num_qubits(pf) == 3); + assert(pauli_feature_map_num_parameters(pf) == 3); + assert(pauli_feature_map_validate(pf) == 0); + pauli_feature_map_free(pf); + /* Unparsable Pauli words and unknown topologies fail construction. */ + const char* bad_texts[1] = {"QZ"}; + const char* bad_labels[1] = {"qz"}; + assert(pauli_feature_map_build(3, 1, bad_texts, bad_labels, 1, ENTANGLEMENT_FULL, NULL, 0) == + NULL); + assert(pauli_feature_map_build(3, 1, texts, labels, 2, 99, NULL, 0) == NULL); +} + +static void test_ansatz_facades(void) { + /* EfficientSU2: [RY, RZ] rotations, CX entanglement, full topology. + * 3 rotation layers * 3 qubits * 2 gates + 2 entanglement layers * 3 + * CX = 24 operations over (2 + 1) * 3 * 2 = 18 parameters. */ + struct CTwoLocal* su2 = efficient_su2(3, 2, ENTANGLEMENT_FULL, NULL, 0); + assert(su2 != NULL); + assert(two_local_num_qubits(su2) == 3); + assert(two_local_num_parameters(su2) == 18); + assert(two_local_validate(su2) == 0); + struct CCircuit* circuit = two_local_build_circuit(su2, "p"); + assert(circuit != NULL); + assert(circuit_num_qubits(circuit) == 3); + assert(circuit_num_operations(circuit) == 3 * 3 * 2 + 2 * 3); + assert(circuit_num_parameters(circuit) == 18); + circuit_free(circuit); + two_local_free(su2); + + /* RealAmplitudes: [RY] rotations, linear topology gives 2 CX per + * entanglement layer: 9 rotations + 4 CX = 13 operations. */ + struct CTwoLocal* ra = real_amplitudes(3, 2, ENTANGLEMENT_LINEAR, NULL, 0); + assert(ra != NULL); + assert(two_local_num_parameters(ra) == 9); + struct CCircuit* ra_circuit = two_local_build_circuit(ra, "t"); + assert(ra_circuit != NULL); + assert(circuit_num_qubits(ra_circuit) == 3); + assert(circuit_num_operations(ra_circuit) == 13); + assert(circuit_num_parameters(ra_circuit) == 9); + circuit_free(ra_circuit); + two_local_free(ra); + + /* Invalid topologies are rejected at construction time. */ + assert(efficient_su2(3, 2, 99, NULL, 0) == NULL); + const uint32_t pairs[3] = {0, 1, 1}; + assert(real_amplitudes(3, 2, ENTANGLEMENT_CUSTOM, pairs, 3) == NULL); +} + +static void test_entanglement_helpers(void) { + uint32_t out[12]; + + /* Linear pairs on 4 qubits: (0,1), (1,2), (2,3). */ + uintptr_t len = entanglement_generate_pairs_len(ENTANGLEMENT_LINEAR, NULL, 0, 4); + assert(len == 6); + assert(entanglement_generate_pairs(ENTANGLEMENT_LINEAR, NULL, 0, 4, out, len) == 0); + assert(out[0] == 0 && out[1] == 1 && out[2] == 1 && out[3] == 2); + assert(out[4] == 2 && out[5] == 3); + + /* Full pairs run the upper triangle: (0,1), (0,2), (1,2). */ + len = entanglement_generate_pairs_len(ENTANGLEMENT_FULL, NULL, 0, 3); + assert(len == 6); + assert(entanglement_generate_pairs(ENTANGLEMENT_FULL, NULL, 0, 3, out, len) == 0); + assert(out[0] == 0 && out[1] == 1 && out[2] == 0 && out[3] == 2); + assert(out[4] == 1 && out[5] == 2); + + /* Circular wraps the ring: (0,1), (1,2), (2,3), (3,0). */ + len = entanglement_generate_pairs_len(ENTANGLEMENT_CIRCULAR, NULL, 0, 4); + assert(len == 8); + assert(entanglement_generate_pairs(ENTANGLEMENT_CIRCULAR, NULL, 0, 4, out, len) == 0); + assert(out[6] == 3 && out[7] == 0); + + /* Custom pairs are echoed verbatim. */ + const uint32_t custom[4] = {2, 0, 1, 2}; + len = entanglement_generate_pairs_len(ENTANGLEMENT_CUSTOM, custom, 4, 3); + assert(len == 4); + uint32_t echo[4]; + assert(entanglement_generate_pairs(ENTANGLEMENT_CUSTOM, custom, 4, 3, echo, 4) == 0); + assert(echo[0] == 2 && echo[1] == 0 && echo[2] == 1 && echo[3] == 2); + + /* Unknown topologies report length 0; a mismatched buffer length is + * rejected with -8. */ + assert(entanglement_generate_pairs_len(99, NULL, 0, 3) == 0); + assert(entanglement_generate_pairs(ENTANGLEMENT_FULL, NULL, 0, 3, out, 4) == -8); + + /* Full triples of 4 qubits: (0,1,2), (0,1,3), (0,2,3), (1,2,3). */ + len = entanglement_generate_k_tuples_len(ENTANGLEMENT_FULL, NULL, 0, 3, 4); + assert(len == 12); + assert(entanglement_generate_k_tuples(ENTANGLEMENT_FULL, NULL, 0, 3, 4, out, len) == 0); + assert(out[0] == 0 && out[1] == 1 && out[2] == 2); + assert(out[9] == 1 && out[10] == 2 && out[11] == 3); + + /* Linear triples slide a window: (0,1,2), (1,2,3). */ + len = entanglement_generate_k_tuples_len(ENTANGLEMENT_LINEAR, NULL, 0, 3, 4); + assert(len == 6); + assert(entanglement_generate_k_tuples(ENTANGLEMENT_LINEAR, NULL, 0, 3, 4, out, len) == 0); + assert(out[0] == 0 && out[1] == 1 && out[2] == 2); + assert(out[3] == 1 && out[4] == 2 && out[5] == 3); + + /* Degenerate k values and mismatched lengths are rejected. */ + assert(entanglement_generate_k_tuples_len(ENTANGLEMENT_FULL, NULL, 0, 0, 4) == 0); + assert(entanglement_generate_k_tuples_len(ENTANGLEMENT_FULL, NULL, 0, 5, 4) == 0); + assert(entanglement_generate_k_tuples(ENTANGLEMENT_FULL, NULL, 0, 0, 4, NULL, 0) == 0); + assert(entanglement_generate_k_tuples(ENTANGLEMENT_FULL, NULL, 0, 3, 4, out, 6) == -8); +} + +static void test_strongly_entangling_layers(void) { + struct CStronglyEntanglingLayers* layers = strongly_entangling_layers_new(3); + assert(layers != NULL); + assert(strongly_entangling_layers_num_qubits(layers) == 3); + + /* Defaults: reps = 1 with a 3-parameter U gate per qubit. */ + assert(strongly_entangling_layers_num_parameters(layers) == 9); + assert(strongly_entangling_layers_validate(layers) == 0); + assert(strongly_entangling_layers_reps(layers, 2) == 0); + assert(strongly_entangling_layers_num_parameters(layers) == 18); + + /* Gate selection tolerates CX/CY/CZ and rejects everything else. */ + assert(strongly_entangling_layers_entanglement_gate(layers, "CZ") == 0); + assert(strongly_entangling_layers_entanglement_gate(layers, "NOT_A_GATE") == -8); + const char invalid_utf8[2] = {(char)0xFF, '\0'}; + assert(strongly_entangling_layers_entanglement_gate(layers, invalid_utf8) == -4); + assert(strongly_entangling_layers_entanglement_gate(layers, NULL) == -1); + + /* Ranges cycle across layers; an empty list fails validation until + * replaced, and a range must stay below num_qubits. */ + uintptr_t ranges[2] = {2, 1}; + assert(strongly_entangling_layers_ranges(layers, ranges, 2) == 0); + assert(strongly_entangling_layers_validate(layers) == 0); + assert(strongly_entangling_layers_ranges(layers, NULL, 0) == 0); + assert(strongly_entangling_layers_validate(layers) == -3); + uintptr_t wide[1] = {3}; + assert(strongly_entangling_layers_ranges(layers, wide, 1) == 0); + assert(strongly_entangling_layers_validate(layers) == -3); + uintptr_t in_range[1] = {2}; + assert(strongly_entangling_layers_ranges(layers, in_range, 1) == 0); + assert(strongly_entangling_layers_validate(layers) == 0); + + /* Two layers: 3 U gates + 3 entanglers per layer = 12 operations + * over 18 parameters. */ + struct CCircuit* circuit = strongly_entangling_layers_build_circuit(layers, "u"); + assert(circuit != NULL); + assert(circuit_num_qubits(circuit) == 3); + assert(circuit_num_parameters(circuit) == 18); + assert(circuit_num_operations(circuit) == 2 * (3 + 3)); + circuit_free(circuit); + strongly_entangling_layers_free(layers); +} + +static void test_pauli_evolution_ansatz(void) { + struct CHamiltonian* cost = pauli_hamiltonian("ZZ"); + assert(cost != NULL); + struct CPauliEvolutionAnsatz* ansatz = pauli_evolution_ansatz_new(cost); + assert(ansatz != NULL); + + /* The time-parameter symbol can be overridden and reset with an + * empty name; invalid UTF-8 and NULL handles are rejected. */ + assert(pauli_evolution_ansatz_with_time_param_name(ansatz, "t") == 0); + assert(pauli_evolution_ansatz_with_time_param_name(ansatz, "") == 0); + const char invalid_utf8[2] = {(char)0xFF, '\0'}; + assert(pauli_evolution_ansatz_with_time_param_name(ansatz, invalid_utf8) == -4); + assert(pauli_evolution_ansatz_with_time_param_name(ansatz, NULL) == -1); + pauli_evolution_ansatz_free(ansatz); + + /* NULL and empty operators are rejected at construction time. */ + assert(pauli_evolution_ansatz_new(NULL) == NULL); + struct CHamiltonian* empty = hamiltonian_new(2); + assert(empty != NULL); + assert(pauli_evolution_ansatz_new(empty) == NULL); + hamiltonian_free(empty); + + /* The constructor clones its input, so the Hamiltonian stays owned + * by this test. Freeing NULL is a no-op. */ + hamiltonian_free(cost); + pauli_evolution_ansatz_free(NULL); +} + +static void test_feature_map_null_handles(void) { + /* Z / ZZ / IQP / Pauli feature maps. */ + assert(z_feature_map_reps(NULL, 1) == -1); + assert(z_feature_map_validate(NULL) == -1); + assert(z_feature_map_num_parameters(NULL) == 0); + assert(z_feature_map_num_qubits(NULL) == 0); + assert(z_feature_map_build_circuit(NULL, "z") == NULL); + assert(zz_feature_map_reps(NULL, 1) == -1); + assert(zz_feature_map_entanglement(NULL, ENTANGLEMENT_LINEAR, NULL, 0) == -1); + assert(zz_feature_map_validate(NULL) == -1); + assert(zz_feature_map_num_parameters(NULL) == 0); + assert(zz_feature_map_num_qubits(NULL) == 0); + assert(zz_feature_map_build_circuit(NULL, "x") == NULL); + assert(iqp_feature_map_reps(NULL, 1) == -1); + assert(iqp_feature_map_entanglement(NULL, ENTANGLEMENT_FULL, NULL, 0) == -1); + assert(iqp_feature_map_validate(NULL) == -1); + assert(iqp_feature_map_num_parameters(NULL) == 0); + assert(iqp_feature_map_num_qubits(NULL) == 0); + assert(iqp_feature_map_build_circuit(NULL, "x") == NULL); + assert(pauli_feature_map_reps(NULL, 1) == -1); + assert(pauli_feature_map_paulis(NULL, NULL, NULL, 0) == -1); + assert(pauli_feature_map_entanglement(NULL, ENTANGLEMENT_FULL, NULL, 0) == -1); + assert(pauli_feature_map_parameter_prefix(NULL, "x") == -1); + assert(pauli_feature_map_validate(NULL) == -1); + assert(pauli_feature_map_num_parameters(NULL) == 0); + assert(pauli_feature_map_num_qubits(NULL) == 0); + assert(pauli_feature_map_build_circuit(NULL, "x") == NULL); + + /* Entanglement helpers. */ + assert(entanglement_generate_pairs_len(ENTANGLEMENT_FULL, NULL, 0, 3) > 0); + assert(entanglement_generate_pairs(ENTANGLEMENT_FULL, NULL, 0, 3, NULL, 6) == -1); + + /* StronglyEntanglingLayers. */ + assert(strongly_entangling_layers_reps(NULL, 1) == -1); + assert(strongly_entangling_layers_entanglement_gate(NULL, "CX") == -1); + assert(strongly_entangling_layers_ranges(NULL, NULL, 0) == -1); + assert(strongly_entangling_layers_validate(NULL) == -1); + assert(strongly_entangling_layers_num_parameters(NULL) == 0); + assert(strongly_entangling_layers_num_qubits(NULL) == 0); + assert(strongly_entangling_layers_build_circuit(NULL, "u") == NULL); + + /* PauliEvolutionAnsatz. */ + assert(pauli_evolution_ansatz_with_time_param_name(NULL, "t") == -1); + + /* Freeing NULL is a no-op everywhere. */ + z_feature_map_free(NULL); + zz_feature_map_free(NULL); + iqp_feature_map_free(NULL); + pauli_feature_map_free(NULL); + strongly_entangling_layers_free(NULL); + pauli_evolution_ansatz_free(NULL); +} + +int main(void) { + test_z_feature_map(); + test_zz_feature_map(); + test_iqp_feature_map(); + test_pauli_feature_map(); + test_feature_map_facades(); + test_ansatz_facades(); + test_entanglement_helpers(); + test_strongly_entangling_layers(); + test_pauli_evolution_ansatz(); + test_feature_map_null_handles(); + printf("binding-c ansatz feature map tests passed\n"); + return 0; +} diff --git a/crates/binding-c/tests/test_circuit.c b/crates/binding-c/tests/test_circuit.c index f14d4fe5..f1f33944 100644 --- a/crates/binding-c/tests/test_circuit.c +++ b/crates/binding-c/tests/test_circuit.c @@ -13,11 +13,12 @@ #include #include #include +#include #include "cqlib_c.h" static void test_basic_circuit(void) { - CircuitWrapper* circuit = circuit_new(2); + CCircuit* circuit = circuit_new(2); assert(circuit != NULL); assert(circuit_num_qubits(circuit) == 2); assert(circuit_num_operations(circuit) == 0); @@ -47,15 +48,15 @@ static void test_errors(void) { assert(circuit_h(NULL, 0) == -1); circuit_free(NULL); - CircuitWrapper* circuit = circuit_new(1); + CCircuit* circuit = circuit_new(1); assert(circuit_h(circuit, 1) == -2); assert(circuit_cx(circuit, 0, 1) == -2); circuit_free(circuit); } static void test_symbolic_parameters(void) { - ParameterWrapper* theta = param_parse("theta"); - ParameterWrapper* phi = param_parse("phi"); + CParameter* theta = param_parse("theta"); + CParameter* phi = param_parse("phi"); assert(theta != NULL); assert(phi != NULL); @@ -63,7 +64,7 @@ static void test_symbolic_parameters(void) { assert(fabs(param_evaluate(theta, bindings) - 0.5) < 1e-12); assert(fabs(param_evaluate(phi, bindings) - 1.25) < 1e-12); - CircuitWrapper* circuit = circuit_new(2); + CCircuit* circuit = circuit_new(2); assert(circuit_rx_param(circuit, 0, theta) == 0); assert(circuit_ry_param(circuit, 1, phi) == 0); assert(circuit_rz_param(circuit, 0, theta) == 0); @@ -71,7 +72,7 @@ static void test_symbolic_parameters(void) { assert(circuit_num_operations(circuit) == 4); assert(circuit_num_parameters(circuit) == 2); - CircuitWrapper* assigned = circuit_assign_params(circuit, bindings); + CCircuit* assigned = circuit_assign_params(circuit, bindings); assert(assigned != NULL); assert(circuit_num_operations(assigned) == 4); assert(circuit_num_parameters(assigned) == 0); @@ -83,10 +84,92 @@ static void test_symbolic_parameters(void) { param_free(phi); } +static void test_extended_gates(void) { + CCircuit* circuit = circuit_new(3); + assert(circuit != NULL); + + /* Section 2.1.1: single-qubit no-param gates */ + assert(circuit_i(circuit, 0) == 0); + assert(circuit_s(circuit, 1) == 0); + assert(circuit_sdg(circuit, 2) == 0); + assert(circuit_t(circuit, 0) == 0); + assert(circuit_tdg(circuit, 1) == 0); + assert(circuit_x2p(circuit, 2) == 0); + assert(circuit_x2m(circuit, 0) == 0); + assert(circuit_y2p(circuit, 1) == 0); + assert(circuit_y2m(circuit, 2) == 0); + + /* Section 2.1.2: single-qubit param gates (numeric) */ + assert(circuit_phase(circuit, 0, 0.5) == 0); + assert(circuit_u(circuit, 1, 0.1, 0.2, 0.3) == 0); + assert(circuit_xy(circuit, 2, 0.4) == 0); + assert(circuit_xy2p(circuit, 0, 0.5) == 0); + assert(circuit_xy2m(circuit, 1, 0.6) == 0); + assert(circuit_rxy(circuit, 2, 0.7, 0.8) == 0); + + /* Section 2.1.3: two-qubit no-param gates */ + assert(circuit_cy(circuit, 0, 1) == 0); + assert(circuit_swap(circuit, 1, 2) == 0); + + /* Section 2.1.4: two-qubit param gates (numeric) */ + assert(circuit_rxx(circuit, 0, 1, 0.5) == 0); + assert(circuit_ryy(circuit, 1, 2, 0.5) == 0); + assert(circuit_rzz(circuit, 0, 2, 0.5) == 0); + assert(circuit_rzx(circuit, 1, 0, 0.5) == 0); + assert(circuit_crx(circuit, 0, 1, 0.5) == 0); + assert(circuit_cry(circuit, 1, 2, 0.5) == 0); + assert(circuit_crz(circuit, 0, 2, 0.5) == 0); + assert(circuit_fsim(circuit, 1, 0, 0.3, 0.7) == 0); + + /* Section 2.1.5: three-qubit gate */ + assert(circuit_ccx(circuit, 0, 1, 2) == 0); + + /* Section 2.1.6: barrier */ + uint32_t barrier_qubits[] = {0, 1}; + assert(circuit_barrier(circuit, barrier_qubits, 2) == 0); + assert(circuit_barrier(circuit, NULL, 0) == 0); + + /* Section 2.1.7: properties */ + assert(circuit_width(circuit) == 3); + assert(circuit_qubits_len(circuit) == 3); + uint32_t qbuf[3]; + assert(circuit_qubits(circuit, qbuf, 3) == 3); + assert(qbuf[0] == 0 && qbuf[1] == 1 && qbuf[2] == 2); + assert(circuit_depth(circuit, false) > 0); + assert(circuit_remove_operation(circuit, 0) == 0); + + /* NaN rejection */ + assert(circuit_u(circuit, 0, NAN, 0.0, 0.0) == -3); + assert(circuit_i(circuit, 10) == -2); + + /* Inverse and decompose */ + CCircuit* inv = circuit_inverse(circuit); + assert(inv != NULL); + circuit_free(inv); + + CCircuit* dec = circuit_decompose(circuit); + assert(dec != NULL); + circuit_free(dec); + + /* to_matrix */ + size_t mlen = circuit_to_matrix_len(circuit, NULL, 0); + assert(mlen == 64); /* 2^3 * 2^3 = 64 */ + double* mbuf = (double*)malloc(mlen * 2 * sizeof(double)); + assert(circuit_to_matrix(circuit, NULL, 0, mbuf, mlen * 2) == mlen); + free(mbuf); + + /* global phase */ + assert(circuit_set_global_phase(circuit, 0.5) == 0); + assert(circuit_set_global_phase(circuit, NAN) == -3); + + circuit_free(circuit); +} + int main(void) { test_basic_circuit(); test_errors(); test_symbolic_parameters(); + test_extended_gates(); printf("binding-c circuit tests passed\n"); return 0; } diff --git a/crates/binding-c/tests/test_circuit_advanced.c b/crates/binding-c/tests/test_circuit_advanced.c new file mode 100644 index 00000000..cf56f692 --- /dev/null +++ b/crates/binding-c/tests/test_circuit_advanced.c @@ -0,0 +1,507 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +#include +#include +#include +#include +#include + +#include "cqlib_c.h" + +/* Large enough for a 3-qubit unitary (64 complex = 128 doubles). */ +#define MAX_MATRIX_DOUBLES 512 + +static uintptr_t matrix_of(const CCircuit* circuit, double* buf) { + uintptr_t len = circuit_to_matrix_len(circuit, NULL, 0); + assert(len > 0); + assert(len * 2 <= MAX_MATRIX_DOUBLES); + assert(circuit_to_matrix(circuit, NULL, 0, buf, len * 2) == len); + return len; +} + +static void assert_matrices_equal(const double* a, const double* b, uintptr_t len) { + for (uintptr_t i = 0; i < len; i++) { + assert(fabs(a[i] - b[i]) < 1e-12); + } +} + +static void test_parameter_metadata(void) { + CParameter* theta = param_parse("theta"); + assert(theta != NULL); + + CCircuit* circuit = circuit_new(1); + assert(circuit != NULL); + + /* Interning registers the parameter and its symbols but does not mark it used. */ + assert(circuit_add_parameter(circuit, theta) == 0); + assert(circuit_parameters_len(circuit) == 1); + assert(circuit_num_parameters(circuit) == 1); + assert(circuit_symbols_len(circuit) == 1); + assert(circuit_used_symbols_len(circuit) == 0); + assert(circuit_uses_symbol(circuit, "theta") == 0); + + /* Referencing it through a gate marks the symbol as used. */ + assert(circuit_rx_param(circuit, 0, theta) == 0); + assert(circuit_uses_symbol(circuit, "theta") == 1); + assert(circuit_used_symbols_len(circuit) == 1); + + /* Two-step symbol lists; each string is freed by the caller. */ + char* names[1] = {NULL}; + assert(circuit_symbols(circuit, names, 1) == 1); + assert(strcmp(names[0], "theta") == 0); + cqlib_string_free(names[0]); + assert(circuit_used_symbols(circuit, names, 1) == 1); + assert(strcmp(names[0], "theta") == 0); + cqlib_string_free(names[0]); + + /* Two-step parameter list: cloned handles the caller frees. */ + CParameter* params[1] = {NULL}; + assert(circuit_parameters(circuit, params, 1) == 1); + assert(fabs(param_evaluate(params[0], "theta:2") - 2.0) < 1e-12); + param_free(params[0]); + + circuit_free(circuit); + param_free(theta); +} + +static void test_map_resolve_parameter_value(void) { + CCircuit* circuit = circuit_new(1); + CParameter* theta = param_parse("theta"); + CParameter* phi = param_parse("phi * 2"); + assert(circuit_rx_param(circuit, 0, theta) == 0); + + /* Mapping a symbolic expression yields a parameter-table index. */ + CCircuitParam mapped = {255, 0, 0.0}; + assert(circuit_map_param(circuit, phi, &mapped) == 0); + assert(mapped.tag == CIRCUIT_PARAM_TAG_INDEX); + assert(mapped.index == 1); + + /* Mapping a constant yields a fixed value. */ + CParameter* half = param_parse("0.5"); + CCircuitParam fixed = {255, 0, 0.0}; + assert(circuit_map_param(circuit, half, &fixed) == 0); + assert(fixed.tag == CIRCUIT_PARAM_TAG_FIXED); + assert(fabs(fixed.value - 0.5) < 1e-12); + + /* Resolve an index back into its expression and evaluate it. */ + CParameter* resolved = circuit_resolve_parameter(circuit, &mapped); + assert(resolved != NULL); + assert(fabs(param_evaluate(resolved, "theta:0.25,phi:1.5") - 3.0) < 1e-12); + param_free(resolved); + + /* Parameter values: index -> symbolic handle, fixed -> number. */ + CParameterValue value = {255, 0.0, NULL}; + assert(circuit_parameter_value(circuit, &mapped, &value) == 0); + assert(value.tag == PARAMETER_VALUE_TAG_PARAM); + assert(value.param != NULL); + assert(fabs(param_evaluate(value.param, "theta:0.25,phi:1.5") - 3.0) < 1e-12); + param_free(value.param); + + assert(circuit_parameter_value(circuit, &fixed, &value) == 0); + assert(value.tag == PARAMETER_VALUE_TAG_FIXED); + assert(fabs(value.value - 0.5) < 1e-12); + + /* Invalid index -> -3, invalid tag -> -8. */ + CCircuitParam bad_index = {CIRCUIT_PARAM_TAG_INDEX, 99, 0.0}; + assert(circuit_parameter_value(circuit, &bad_index, &value) == -3); + assert(circuit_resolve_parameter(circuit, &bad_index) == NULL); + CCircuitParam bad_tag = {7, 0, 1.0}; + assert(circuit_parameter_value(circuit, &bad_tag, &value) == -8); + + circuit_free(circuit); + param_free(half); + param_free(phi); + param_free(theta); +} + +static void test_global_phase(void) { + CCircuit* circuit = circuit_new(1); + CParameterValue value = {255, 0.0, NULL}; + + assert(circuit_set_global_phase(circuit, 0.25) == 0); + assert(circuit_global_phase(circuit, &value) == 0); + assert(value.tag == PARAMETER_VALUE_TAG_FIXED); + assert(fabs(value.value - 0.25) < 1e-12); + + /* A symbolic phase is reported as a parameter handle. */ + CParameter* alpha = param_parse("alpha"); + assert(circuit_set_global_phase_param(circuit, alpha) == 0); + assert(circuit_global_phase(circuit, &value) == 0); + assert(value.tag == PARAMETER_VALUE_TAG_PARAM); + assert(fabs(param_evaluate(value.param, "alpha:1.5") - 1.5) < 1e-12); + param_free(value.param); + + assert(circuit_global_phase(NULL, &value) == -1); + circuit_free(circuit); + param_free(alpha); +} + +static void test_id_and_from_qubits(void) { + CCircuit* circuit = circuit_new(2); + assert(circuit_id(circuit, 0) == 0); + assert(circuit_id(circuit, 1) == 0); + assert(circuit_num_operations(circuit) == 2); + assert(circuit_id(circuit, 5) == -2); + assert(circuit_id(NULL, 0) == -1); + assert(circuit_validate(circuit) == 0); + circuit_free(circuit); + + /* Explicit sparse qubit ids. */ + const uint32_t ids[3] = {0, 2, 5}; + CCircuit* sparse = circuit_from_qubits(ids, 3); + assert(sparse != NULL); + assert(circuit_num_qubits(sparse) == 3); + uint32_t out[3]; + assert(circuit_qubits(sparse, out, 3) == 3); + assert(out[0] == 0 && out[1] == 2 && out[2] == 5); + circuit_free(sparse); + + /* Duplicate ids are rejected; an empty list is allowed. */ + const uint32_t dup[2] = {1, 1}; + assert(circuit_from_qubits(dup, 2) == NULL); + assert(circuit_from_qubits(NULL, 1) == NULL); + CCircuit* empty = circuit_from_qubits(NULL, 0); + assert(empty != NULL); + circuit_free(empty); +} + +static void test_unitary(void) { + /* Pauli-X as a custom unitary matches the standard X gate matrix. */ + const Complex64 x_matrix[4] = { + {0.0, 0.0}, + {1.0, 0.0}, + {1.0, 0.0}, + {0.0, 0.0}, + }; + const uint32_t qs[1] = {0}; + + CCircuit* circuit = circuit_new(2); + assert(circuit_unitary(circuit, "my_x", 1, x_matrix, qs, 1) == 0); + + CCircuit* reference = circuit_new(2); + assert(circuit_x(reference, 0) == 0); + + double a[MAX_MATRIX_DOUBLES], b[MAX_MATRIX_DOUBLES]; + uintptr_t len_a = matrix_of(circuit, a); + uintptr_t len_b = matrix_of(reference, b); + assert(len_a == len_b); + assert_matrices_equal(a, b, 2 * len_a); + + /* Out-of-bounds qubit -> -2; too many qubits -> -8; NULL circuit -> -1. */ + const uint32_t oob[1] = {5}; + assert(circuit_unitary(circuit, "my_x", 1, x_matrix, oob, 1) == -2); + assert(circuit_unitary(circuit, "my_x", 21, x_matrix, qs, 1) == -8); + assert(circuit_unitary(NULL, "my_x", 1, x_matrix, qs, 1) == -1); + + circuit_free(reference); + circuit_free(circuit); +} + +static void test_unitary_with_params(void) { + /* diag(1, e^{i*theta}) as a symbolic matrix; bind theta = 0.5. */ + const char* re[4] = {"1", "0", "0", "cos(theta)"}; + const char* im[4] = {"0", "0", "0", "sin(theta)"}; + const char* names[1] = {"theta"}; + const uint32_t qs[1] = {0}; + + CParameter* arg = param_parse("0.5"); + const CParameter* args[1] = {arg}; + + CCircuit* circuit = circuit_new(1); + assert(circuit_unitary_with_params(circuit, "phase_like", 1, re, im, names, 1, qs, 1, args, + 1) == 0); + + /* The constant argument is interned as an index; fold it before reading + * the numeric matrix. */ + CCircuit* resolved = circuit_assign_params(circuit, NULL); + assert(resolved != NULL); + double m[MAX_MATRIX_DOUBLES]; + assert(matrix_of(resolved, m) == 4); + assert(fabs(m[0] - 1.0) < 1e-12); /* [0][0].re */ + assert(fabs(m[1]) < 1e-12); /* [0][0].im */ + assert(fabs(m[(1 * 2 + 1) * 2] - cos(0.5)) < 1e-12); /* [1][1].re */ + assert(fabs(m[(1 * 2 + 1) * 2 + 1] - sin(0.5)) < 1e-12); /* [1][1].im */ + + /* A NULL imaginary-part array is treated as all zeros: with theta = 0 + * the gate becomes the identity. */ + CParameter* zero = param_parse("0"); + const CParameter* zero_args[1] = {zero}; + CCircuit* plain = circuit_new(1); + assert(circuit_unitary_with_params(plain, "phase_like", 1, re, NULL, names, 1, qs, 1, zero_args, + 1) == 0); + CCircuit* resolved_plain = circuit_assign_params(plain, NULL); + assert(resolved_plain != NULL); + double p[MAX_MATRIX_DOUBLES]; + assert(matrix_of(resolved_plain, p) == 4); + assert(fabs(p[(1 * 2 + 1) * 2] - 1.0) < 1e-12); /* cos(0) = 1 */ + assert(fabs(p[(1 * 2 + 1) * 2 + 1]) < 1e-12); + + /* Wrong parameter count -> -3. */ + assert(circuit_unitary_with_params(circuit, "phase_like", 1, re, im, names, 1, qs, 1, NULL, + 0) == -3); + + circuit_free(resolved_plain); + circuit_free(plain); + circuit_free(resolved); + circuit_free(circuit); + param_free(zero); + param_free(arg); +} + +static void test_to_gate_circuit_gate(void) { + /* Bell circuit frozen into a composite gate and re-applied. */ + CCircuit* inner = circuit_new(2); + assert(circuit_h(inner, 0) == 0); + assert(circuit_cx(inner, 0, 1) == 0); + + CCircuitGate* gate = circuit_to_gate(inner, "bell"); + assert(gate != NULL); + char* name = circuit_gate_name(gate); + assert(name != NULL && strcmp(name, "bell") == 0); + cqlib_string_free(name); + assert(circuit_gate_num_qubits(gate) == 2); + assert(circuit_gate_num_params(gate) == 0); + assert(circuit_gate_signature_params_len(gate) == 0); + + const uint32_t qs[2] = {0, 1}; + CCircuit* outer = circuit_new(2); + assert(circuit_circuit_gate(outer, gate, qs, 2, NULL, 0) == 0); + + /* Applying the composite gate reproduces the original circuit. */ + double a[MAX_MATRIX_DOUBLES], b[MAX_MATRIX_DOUBLES]; + uintptr_t len_a = matrix_of(outer, a); + uintptr_t len_b = matrix_of(inner, b); + assert(len_a == len_b); + assert_matrices_equal(a, b, 2 * len_a); + + /* Qubit arity mismatch -> -3; out-of-bounds qubit -> -2. */ + const uint32_t single[1] = {0}; + assert(circuit_circuit_gate(outer, gate, single, 1, NULL, 0) == -3); + const uint32_t oob[2] = {0, 9}; + assert(circuit_circuit_gate(outer, gate, oob, 2, NULL, 0) == -2); + + circuit_free(outer); + circuit_gate_free(gate); + circuit_free(inner); +} + +static void test_to_gate_parametrized(void) { + /* RX(theta) template applied with theta = 0.5. */ + CParameter* theta = param_parse("theta"); + CCircuit* template = circuit_new(1); + assert(circuit_rx_param(template, 0, theta) == 0); + + CCircuitGate* gate = circuit_to_gate(template, "rx_theta"); + assert(gate != NULL); + assert(circuit_gate_num_params(gate) == 1); + assert(circuit_gate_signature_params_len(gate) == 1); + char* names[1] = {NULL}; + assert(circuit_gate_signature_params(gate, names, 1) == 1); + assert(strcmp(names[0], "theta") == 0); + cqlib_string_free(names[0]); + + CParameter* arg = param_parse("0.5"); + const CParameter* args[1] = {arg}; + const uint32_t qs[1] = {0}; + CCircuit* outer = circuit_new(1); + assert(circuit_circuit_gate(outer, gate, qs, 1, args, 1) == 0); + + /* Fold the constant argument, then compare against RX(0.5). */ + CCircuit* resolved = circuit_assign_params(outer, NULL); + assert(resolved != NULL); + CCircuit* reference = circuit_new(1); + assert(circuit_rx(reference, 0, 0.5) == 0); + + double a[MAX_MATRIX_DOUBLES], b[MAX_MATRIX_DOUBLES]; + uintptr_t len_a = matrix_of(resolved, a); + uintptr_t len_b = matrix_of(reference, b); + assert(len_a == len_b); + assert_matrices_equal(a, b, 2 * len_a); + + /* Wrong parameter count -> -3. */ + assert(circuit_circuit_gate(outer, gate, qs, 1, NULL, 0) == -3); + + circuit_free(reference); + circuit_free(resolved); + circuit_free(outer); + param_free(arg); + circuit_gate_free(gate); + circuit_free(template); + param_free(theta); +} + +static void test_multi_control(void) { + CCircuit* circuit = circuit_new(3); + const uint32_t controls[2] = {0, 1}; + const uint32_t targets[1] = {2}; + + /* Double-controlled X is CCX. */ + assert(circuit_multi_control(circuit, "X", controls, 2, targets, 1, NULL, 0) == 0); + + CCircuit* reference = circuit_new(3); + assert(circuit_ccx(reference, 0, 1, 2) == 0); + + double a[MAX_MATRIX_DOUBLES], b[MAX_MATRIX_DOUBLES]; + uintptr_t len_a = matrix_of(circuit, a); + uintptr_t len_b = matrix_of(reference, b); + assert(len_a == len_b); + assert_matrices_equal(a, b, 2 * len_a); + + /* Unknown gate name -> -8; missing target -> -3; OOB control -> -2. */ + assert(circuit_multi_control(circuit, "NOT_A_GATE", controls, 2, targets, 1, NULL, 0) == -8); + assert(circuit_multi_control(circuit, "H", controls, 2, NULL, 0, NULL, 0) == -3); + const uint32_t oob[2] = {0, 9}; + assert(circuit_multi_control(circuit, "X", oob, 2, targets, 1, NULL, 0) == -2); + + circuit_free(reference); + circuit_free(circuit); +} + +static void test_delay_index(void) { + CCircuit* circuit = circuit_new(2); + assert(circuit_delay(circuit, 1, 100.0) == 0); + assert(circuit_delay(circuit, 5, 1.0) == -2); /* OOB qubit */ + assert(circuit_delay(circuit, 0, NAN) == -3); /* non-finite duration */ + assert(circuit_delay(NULL, 0, 1.0) == -1); + + CParameter* dt = param_parse("dt"); + assert(circuit_delay_param(circuit, 0, dt) == 0); + assert(circuit_delay_param(circuit, 0, NULL) == -1); + assert(circuit_num_operations(circuit) == 2); + assert(circuit_num_parameters(circuit) == 1); + + /* Snapshot of the numeric delay. */ + CValueOperation* op = circuit_index(circuit, 0); + assert(op != NULL); + char* name = value_operation_name(op); + assert(name != NULL && strcmp(name, "delay") == 0); + cqlib_string_free(name); + char* type = value_operation_instruction_type(op); + assert(type != NULL && strcmp(type, "delay") == 0); + cqlib_string_free(type); + assert(value_operation_label(op) == NULL); + assert(value_operation_num_qubits(op) == 1); + uint32_t qubits[1]; + assert(value_operation_qubits(op, qubits, 1) == 1); + assert(qubits[0] == 1); + assert(value_operation_num_params(op) == 1); + + CParameterValue value = {255, 0.0, NULL}; + assert(value_operation_param(op, 0, &value) == 0); + assert(value.tag == PARAMETER_VALUE_TAG_FIXED); + assert(fabs(value.value - 100.0) < 1e-12); + assert(value_operation_param(op, 1, &value) == -3); + value_operation_free(op); + + /* Snapshot of the symbolic delay. */ + CValueOperation* symbolic = circuit_index(circuit, 1); + assert(symbolic != NULL); + assert(value_operation_param(symbolic, 0, &value) == 0); + assert(value.tag == PARAMETER_VALUE_TAG_PARAM); + assert(fabs(param_evaluate(value.param, "dt:7") - 7.0) < 1e-12); + param_free(value.param); + value_operation_free(symbolic); + + assert(circuit_index(circuit, 99) == NULL); + assert(circuit_index(NULL, 0) == NULL); + + circuit_free(circuit); + param_free(dt); +} + +static void test_remove_operations(void) { + CCircuit* circuit = circuit_new(1); + assert(circuit_h(circuit, 0) == 0); + assert(circuit_x(circuit, 0) == 0); + assert(circuit_phase(circuit, 0, 0.5) == 0); + + /* Indices refer to pre-deletion positions: drop H and phase, keep X. */ + const uintptr_t indices[2] = {0, 2}; + assert(circuit_remove_operations(circuit, indices, 2) == 0); + assert(circuit_num_operations(circuit) == 1); + + CValueOperation* op = circuit_index(circuit, 0); + assert(op != NULL); + char* name = value_operation_name(op); + assert(name != NULL && strcmp(name, "X") == 0); + cqlib_string_free(name); + value_operation_free(op); + + /* Empty index list is a successful no-op. */ + assert(circuit_remove_operations(circuit, NULL, 0) == 0); + assert(circuit_num_operations(circuit) == 1); + + /* Out-of-bounds index -> -3; NULL circuit -> -1. */ + const uintptr_t bad[2] = {0, 9}; + assert(circuit_remove_operations(circuit, bad, 2) == -3); + assert(circuit_remove_operations(NULL, NULL, 0) == -1); + + circuit_free(circuit); +} + +static void test_null_checks(void) { + assert(circuit_add_parameter(NULL, NULL) == -1); + assert(circuit_parameters_len(NULL) == 0); + assert(circuit_parameters(NULL, NULL, 0) == 0); + assert(circuit_symbols_len(NULL) == 0); + assert(circuit_symbols(NULL, NULL, 0) == 0); + assert(circuit_used_symbols_len(NULL) == 0); + assert(circuit_used_symbols(NULL, NULL, 0) == 0); + assert(circuit_uses_symbol(NULL, NULL) == -1); + assert(circuit_global_phase(NULL, NULL) == -1); + assert(circuit_map_param(NULL, NULL, NULL) == -1); + assert(circuit_resolve_parameter(NULL, NULL) == NULL); + assert(circuit_parameter_value(NULL, NULL, NULL) == -1); + assert(circuit_unitary(NULL, NULL, 0, NULL, NULL, 0) == -1); + assert(circuit_unitary_with_params(NULL, NULL, 0, NULL, NULL, NULL, 0, NULL, 0, NULL, 0) == -1); + assert(circuit_to_gate(NULL, NULL) == NULL); + assert(circuit_circuit_gate(NULL, NULL, NULL, 0, NULL, 0) == -1); + assert(circuit_multi_control(NULL, NULL, NULL, 0, NULL, 0, NULL, 0) == -1); + assert(circuit_delay(NULL, 0, 1.0) == -1); + assert(circuit_delay_param(NULL, 0, NULL) == -1); + assert(circuit_index(NULL, 0) == NULL); + assert(circuit_remove_operations(NULL, NULL, 0) == -1); + + /* Handle accessors tolerate NULL. */ + assert(circuit_gate_name(NULL) == NULL); + assert(circuit_gate_num_qubits(NULL) == 0); + assert(circuit_gate_num_params(NULL) == 0); + assert(circuit_gate_signature_params_len(NULL) == 0); + assert(circuit_gate_signature_params(NULL, NULL, 0) == 0); + assert(value_operation_name(NULL) == NULL); + assert(value_operation_instruction_type(NULL) == NULL); + assert(value_operation_label(NULL) == NULL); + assert(value_operation_num_qubits(NULL) == 0); + assert(value_operation_qubits(NULL, NULL, 0) == 0); + assert(value_operation_num_params(NULL) == 0); + assert(value_operation_param(NULL, 0, NULL) == -1); + circuit_gate_free(NULL); + value_operation_free(NULL); +} + +int main(void) { + test_parameter_metadata(); + test_map_resolve_parameter_value(); + test_global_phase(); + test_id_and_from_qubits(); + test_unitary(); + test_unitary_with_params(); + test_to_gate_circuit_gate(); + test_to_gate_parametrized(); + test_multi_control(); + test_delay_index(); + test_remove_operations(); + test_null_checks(); + printf("binding-c circuit advanced tests passed\n"); + return 0; +} diff --git a/crates/binding-c/tests/test_circuit_ansatz.c b/crates/binding-c/tests/test_circuit_ansatz.c new file mode 100644 index 00000000..d63da36f --- /dev/null +++ b/crates/binding-c/tests/test_circuit_ansatz.c @@ -0,0 +1,425 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +#include +#include +#include + +#include "cqlib_c.h" + +static int g_failures = 0; + +#define CHECK(cond, name) \ + do { \ + if (cond) { \ + printf("ok: %s\n", (name)); \ + } else { \ + printf("FAIL: %s\n", (name)); \ + g_failures++; \ + } \ + } while (0) + +/* Builds a Hamiltonian from a single Pauli word (coefficient 1.0). + * `hamiltonian_from_pauli` takes ownership of the pauli handle, so the + * caller must free only the returned Hamiltonian, never the pauli + * handle once this helper returns. */ +static struct CHamiltonian* pauli_hamiltonian(const char* text) { + struct CPauliString* pauli = pauli_string_parse(text); + if (pauli == NULL) { + return NULL; + } + return hamiltonian_from_pauli(pauli); +} + +static void test_two_local_structure(void) { + struct CTwoLocal* ansatz = two_local_new(3); + CHECK(ansatz != NULL, "two_local_new"); + CHECK(two_local_num_qubits(ansatz) == 3, "two_local_num_qubits"); + CHECK(two_local_validate(ansatz) == 0, "two_local_validate default"); + + /* Defaults: reps = 3, RY rotations, final rotation layer kept => + * (3 + 1) layers * 3 qubits * 1 gate = 12 parameters. */ + CHECK(two_local_num_parameters(ansatz) == 12, "num_parameters reps=3"); + + /* reps = 2 => (2 + 1) * 3 * 1 = 9. */ + CHECK(two_local_reps(ansatz, 2) == 0, "two_local_reps"); + CHECK(two_local_num_parameters(ansatz) == 9, "num_parameters reps=2"); + + /* Skipping the final rotation layer leaves exactly `reps` layers. */ + CHECK(two_local_skip_final_rotation_layer(ansatz, true) == 0, + "two_local_skip_final_rotation_layer"); + CHECK(two_local_num_parameters(ansatz) == 2 * 3, "num_parameters skip final"); + + /* The entangler choice never changes the parameter count. */ + CHECK(two_local_entanglement_gate(ansatz, "CZ") == 0, "two_local_entanglement_gate CZ"); + CHECK(two_local_num_parameters(ansatz) == 6, "CZ keeps parameter count"); + CHECK(two_local_validate(ansatz) == 0, "two_local_validate CZ"); + + /* Two rotation gates per qubit: 2 layers * 3 qubits * 2 gates = 12. */ + const char* names[2] = {"RY", "RZ"}; + CHECK(two_local_rotation_gates(ansatz, names, 2) == 0, "two_local_rotation_gates RY RZ"); + CHECK(two_local_num_parameters(ansatz) == 12, "dual rotation gates double parameters"); + + /* Topologies are accepted and orthogonal to the parameter count. */ + CHECK(two_local_entanglement(ansatz, ENTANGLEMENT_CIRCULAR, NULL, 0) == 0, + "entanglement circular"); + CHECK(two_local_entanglement(ansatz, ENTANGLEMENT_FULL, NULL, 0) == 0, "entanglement full"); + CHECK(two_local_entanglement(ansatz, ENTANGLEMENT_LINEAR, NULL, 0) == 0, "entanglement linear"); + const uint32_t pairs[4] = {1, 2, 0, 2}; + CHECK(two_local_entanglement(ansatz, ENTANGLEMENT_CUSTOM, pairs, 4) == 0, + "entanglement custom"); + CHECK(two_local_num_parameters(ansatz) == 12, "topologies keep parameter count"); + CHECK(two_local_validate(ansatz) == 0, "two_local_validate final"); + + two_local_free(ansatz); +} + +static void test_two_local_invalid_inputs(void) { + /* Gate-name parsing: unknown names, case and invalid UTF-8. */ + struct CTwoLocal* ansatz = two_local_new(3); + CHECK(ansatz != NULL, "invalid-input fixture"); + CHECK(two_local_entanglement_gate(ansatz, "NOT_A_GATE") == -8, + "entanglement_gate unknown name"); + CHECK(two_local_entanglement_gate(ansatz, "cx") == -8, "entanglement_gate lowercase name"); + const char invalid_utf8[3] = {(char)0xFF, (char)0xFF, '\0'}; + CHECK(two_local_entanglement_gate(ansatz, invalid_utf8) == -4, + "entanglement_gate invalid utf-8"); + CHECK(two_local_entanglement_gate(ansatz, NULL) == -1, "entanglement_gate null name"); + + /* A known single-qubit gate parses fine, but as an entangler it + * fails validate and build (the gate must be CX, CY or CZ). */ + CHECK(two_local_entanglement_gate(ansatz, "H") == 0, "entanglement_gate H accepted"); + CHECK(two_local_validate(ansatz) == -3, "H entangler validate"); + CHECK(two_local_build_circuit(ansatz, "t") == NULL, "H entangler build fails"); + two_local_free(ansatz); + + /* Zero-qubit templates are created but never valid. */ + struct CTwoLocal* zero = two_local_new(0); + CHECK(zero != NULL, "two_local_new(0)"); + CHECK(two_local_num_qubits(zero) == 0, "zero num_qubits"); + CHECK(two_local_validate(zero) == -3, "zero validate"); + CHECK(two_local_build_circuit(zero, "t") == NULL, "zero build fails"); + two_local_free(zero); + + /* reps = 0 is legal: exactly one rotation layer remains. */ + struct CTwoLocal* sparse = two_local_new(3); + CHECK(two_local_reps(sparse, 0) == 0, "reps zero"); + CHECK(two_local_num_parameters(sparse) == 3, "reps zero params"); + CHECK(two_local_validate(sparse) == 0, "reps zero valid"); + /* Skipping the final layer removes those last parameters. */ + CHECK(two_local_skip_final_rotation_layer(sparse, true) == 0, "reps zero skip final"); + CHECK(two_local_num_parameters(sparse) == 0, "reps zero skip params"); + two_local_free(sparse); + + /* rotation_gates argument validation. */ + struct CTwoLocal* setter = two_local_new(2); + CHECK(setter != NULL, "setter fixture"); + CHECK(two_local_rotation_gates(setter, NULL, 1) == -1, "rotation_gates null buffer"); + const char* null_names[1] = {NULL}; + CHECK(two_local_rotation_gates(setter, null_names, 1) == -1, "rotation_gates null entry"); + /* SWAP parses as a standard gate but is rejected by validate as a + * rotation gate (must be RX, RY or RZ). */ + const char* swap_names[1] = {"SWAP"}; + CHECK(two_local_rotation_gates(setter, swap_names, 1) == 0, "rotation_gates SWAP accepted"); + CHECK(two_local_validate(setter) == -3, "SWAP rotation validate"); + two_local_free(setter); + + /* Topology argument validation. */ + struct CTwoLocal* topo = two_local_new(3); + CHECK(two_local_entanglement(topo, 99, NULL, 0) == -8, "entanglement unknown tag"); + const uint32_t pairs[2] = {0, 1}; + CHECK(two_local_entanglement(topo, ENTANGLEMENT_CUSTOM, pairs, 3) == -8, + "entanglement odd pairs_len"); + CHECK(two_local_entanglement(topo, ENTANGLEMENT_CUSTOM, NULL, 2) == -1, + "entanglement null pairs"); + /* A self-loop pair is stored but fails validate. */ + const uint32_t self_loop[2] = {1, 1}; + CHECK(two_local_entanglement(topo, ENTANGLEMENT_CUSTOM, self_loop, 2) == 0, + "entanglement self-loop stored"); + CHECK(two_local_validate(topo) == -3, "self-loop validate"); + two_local_free(topo); + + /* build_circuit prefix validation. */ + struct CTwoLocal* builder = two_local_new(2); + CHECK(two_local_build_circuit(builder, NULL) == NULL, "build null prefix"); + CHECK(two_local_build_circuit(builder, invalid_utf8) == NULL, "build invalid utf-8 prefix"); + two_local_free(builder); +} + +static void test_basic_entangler_layers_structure(void) { + struct CBasicEntanglerLayers* layers = basic_entangler_layers_new(3); + CHECK(layers != NULL, "basic_entangler_layers_new"); + CHECK(basic_entangler_layers_num_qubits(layers) == 3, "basic_entangler_layers_num_qubits"); + + /* Defaults: reps = 1, one rotation per qubit per layer => 3 params + * (parameter count = reps * num_qubits). */ + CHECK(basic_entangler_layers_num_parameters(layers) == 3, "num_parameters reps=1"); + + /* reps = 2 => 2 * 3 = 6. */ + CHECK(basic_entangler_layers_reps(layers, 2) == 0, "basic_entangler_layers_reps"); + CHECK(basic_entangler_layers_num_parameters(layers) == 6, "num_parameters reps=2"); + + /* Gate swaps keep the one-parameter-per-qubit-per-layer count. */ + CHECK(basic_entangler_layers_rotation_gate(layers, "RY") == 0, "rotation_gate RY"); + CHECK(basic_entangler_layers_entanglement_gate(layers, "CZ") == 0, "entanglement_gate CZ"); + CHECK(basic_entangler_layers_num_parameters(layers) == 6, "gate swaps keep parameter count"); + + /* Bad gate names: unknown, non-UTF-8 and NULL handles. */ + CHECK(basic_entangler_layers_rotation_gate(layers, "NOT_A_GATE") == -8, + "rotation_gate unknown name"); + const char invalid_utf8[2] = {(char)0xFF, '\0'}; + CHECK(basic_entangler_layers_entanglement_gate(layers, invalid_utf8) == -4, + "entanglement_gate invalid utf-8"); + CHECK(basic_entangler_layers_rotation_gate(NULL, "RY") == -1, "rotation_gate null handle"); + CHECK(basic_entangler_layers_entanglement_gate(NULL, "CZ") == -1, + "entanglement_gate null handle"); + /* The failed setters leave the template configured. */ + CHECK(basic_entangler_layers_num_parameters(layers) == 6, + "failed setters keep parameter count"); + + basic_entangler_layers_free(layers); +} + +static void test_qaoa_ansatz_structure(void) { + /* Cost Hamiltonian: a single ZZ term on 2 qubits. */ + struct CHamiltonian* cost = pauli_hamiltonian("ZZ"); + CHECK(cost != NULL, "qaoa cost fixture"); + struct CQAOAAnsatz* ansatz = qaoa_ansatz_new(cost); + CHECK(ansatz != NULL, "qaoa_ansatz_new"); + + /* Qubit count is inferred from the cost operator; default reps = 1 + * gives one gamma and one beta angle. */ + CHECK(qaoa_ansatz_num_qubits(ansatz) == 2, "qaoa_ansatz_num_qubits"); + CHECK(qaoa_ansatz_num_parameters(ansatz) == 2, "qaoa_ansatz_num_parameters"); + CHECK(qaoa_ansatz_validate(ansatz) == 0, "qaoa_ansatz_validate"); + + /* reps = 2 doubles the parameter count. */ + CHECK(qaoa_ansatz_reps(ansatz, 2) == 0, "qaoa_ansatz_reps"); + CHECK(qaoa_ansatz_num_parameters(ansatz) == 4, "num_parameters reps=2"); + + /* A same-width mixer override succeeds and stays valid. The mixer is + * cloned, so the handle stays owned by this test (freed below). */ + struct CHamiltonian* mixer = pauli_hamiltonian("XX"); + CHECK(mixer != NULL, "qaoa mixer fixture"); + CHECK(qaoa_ansatz_mixer(ansatz, mixer) == 0, "qaoa_ansatz_mixer XX"); + CHECK(qaoa_ansatz_validate(ansatz) == 0, "XX mixer valid"); + CHECK(qaoa_ansatz_num_parameters(ansatz) == 4, "XX mixer parameter count"); + + /* Mixers on a different qubit count are rejected. */ + struct CHamiltonian* wide = pauli_hamiltonian("XXX"); + CHECK(wide != NULL, "qaoa wide mixer fixture"); + CHECK(qaoa_ansatz_mixer(ansatz, wide) == -3, "qaoa_ansatz_mixer XXX"); + CHECK(qaoa_ansatz_mixer(ansatz, NULL) == -1, "qaoa_ansatz_mixer null"); + + /* Initial-state circuits must match the qubit count. The circuit is + * cloned, so each one is freed here as usual. */ + struct CCircuit* init = circuit_new(2); + CHECK(init != NULL, "qaoa init fixture"); + CHECK(qaoa_ansatz_initial_state(ansatz, init) == 0, "qaoa_ansatz_initial_state"); + circuit_free(init); + struct CCircuit* big = circuit_new(3); + CHECK(big != NULL, "qaoa big init fixture"); + CHECK(qaoa_ansatz_initial_state(ansatz, big) == -3, "initial_state qubit mismatch"); + circuit_free(big); + CHECK(qaoa_ansatz_initial_state(ansatz, NULL) == -1, "initial_state null"); + + /* Evolution strategy selection. ZZ and XX are single terms, so the + * exact decomposition is valid; Trotter accepts any Hermitian + * input. */ + CHECK(qaoa_ansatz_evolution_strategy(ansatz, EVOLUTION_STRATEGY_AUTO, TROTTER_FIRST_ORDER, 1, + 0) == 0, + "evolution strategy auto"); + CHECK(qaoa_ansatz_evolution_strategy(ansatz, EVOLUTION_STRATEGY_EXACT, TROTTER_FIRST_ORDER, 0, + 0) == 0, + "evolution strategy exact"); + CHECK(qaoa_ansatz_validate(ansatz) == 0, "exact valid"); + CHECK(qaoa_ansatz_evolution_strategy(ansatz, EVOLUTION_STRATEGY_TROTTER, TROTTER_SECOND_ORDER, + 2, 7) == 0, + "evolution strategy trotter second order"); + CHECK(qaoa_ansatz_validate(ansatz) == 0, "trotter second order valid"); + CHECK(qaoa_ansatz_evolution_strategy(ansatz, EVOLUTION_STRATEGY_TROTTER, TROTTER_RANDOMIZED, 2, + 42) == 0, + "evolution strategy trotter randomized"); + CHECK(qaoa_ansatz_validate(ansatz) == 0, "trotter randomized valid"); + + /* Unknown strategy or Trotter-mode tags are rejected. */ + CHECK(qaoa_ansatz_evolution_strategy(ansatz, EVOLUTION_STRATEGY_TROTTER, 99, 1, 0) == -8, + "evolution strategy unknown trotter mode"); + CHECK(qaoa_ansatz_evolution_strategy(ansatz, 99, TROTTER_FIRST_ORDER, 1, 0) == -8, + "evolution strategy unknown tag"); + + /* Zero-step strategies are stored but fail validation until + * restored. */ + CHECK(qaoa_ansatz_evolution_strategy(ansatz, EVOLUTION_STRATEGY_AUTO, TROTTER_FIRST_ORDER, 0, + 0) == 0, + "evolution strategy auto zero steps"); + CHECK(qaoa_ansatz_validate(ansatz) == -3, "auto zero steps validate fails"); + CHECK(qaoa_ansatz_evolution_strategy(ansatz, EVOLUTION_STRATEGY_AUTO, TROTTER_FIRST_ORDER, 1, + 0) == 0, + "evolution strategy auto restored"); + CHECK(qaoa_ansatz_validate(ansatz) == 0, "auto restored valid"); + + qaoa_ansatz_free(ansatz); + /* `qaoa_ansatz_new` / `qaoa_ansatz_mixer` clone their inputs, so + * every Hamiltonian handed to the ansatz stays owned by this test + * and is freed here. */ + hamiltonian_free(mixer); + hamiltonian_free(wide); + hamiltonian_free(cost); + + /* The constructor rejects NULL, empty and identity-only cost + * operators. */ + CHECK(qaoa_ansatz_new(NULL) == NULL, "qaoa_ansatz_new null"); + struct CHamiltonian* empty = hamiltonian_new(2); + CHECK(empty != NULL, "empty hamiltonian fixture"); + CHECK(qaoa_ansatz_new(empty) == NULL, "qaoa_ansatz_new empty terms"); + hamiltonian_free(empty); + struct CHamiltonian* identity = pauli_hamiltonian("II"); + CHECK(identity != NULL, "identity hamiltonian fixture"); + CHECK(qaoa_ansatz_new(identity) == NULL, "qaoa_ansatz_new identity"); + hamiltonian_free(identity); +} + +static void test_ansatz_null_handles(void) { + /* TwoLocal: every exported entry point tolerates a NULL handle. */ + CHECK(two_local_reps(NULL, 1) == -1, "two_local_reps(null)"); + CHECK(two_local_rotation_gates(NULL, NULL, 0) == -1, "two_local_rotation_gates(null)"); + CHECK(two_local_entanglement_gate(NULL, NULL) == -1, "two_local_entanglement_gate(null)"); + CHECK(two_local_entanglement(NULL, ENTANGLEMENT_LINEAR, NULL, 0) == -1, + "two_local_entanglement(null)"); + CHECK(two_local_skip_final_rotation_layer(NULL, true) == -1, + "two_local_skip_final_rotation_layer(null)"); + CHECK(two_local_validate(NULL) == -1, "two_local_validate(null)"); + CHECK(two_local_num_parameters(NULL) == 0, "two_local_num_parameters(null)"); + CHECK(two_local_num_qubits(NULL) == 0, "two_local_num_qubits(null)"); + CHECK(two_local_build_circuit(NULL, "t") == NULL, "two_local_build_circuit(null)"); + + /* BasicEntanglerLayers. */ + CHECK(basic_entangler_layers_reps(NULL, 1) == -1, "basic_entangler_layers_reps(null)"); + CHECK(basic_entangler_layers_rotation_gate(NULL, NULL) == -1, + "basic_entangler_layers_rotation_gate(null)"); + CHECK(basic_entangler_layers_entanglement_gate(NULL, NULL) == -1, + "basic_entangler_layers_entanglement_gate(null)"); + CHECK(basic_entangler_layers_num_parameters(NULL) == 0, + "basic_entangler_layers_num_parameters(null)"); + CHECK(basic_entangler_layers_num_qubits(NULL) == 0, "basic_entangler_layers_num_qubits(null)"); + CHECK(basic_entangler_layers_build_circuit(NULL, "t") == NULL, + "basic_entangler_layers_build_circuit(null)"); + + /* QAOAAnsatz. */ + CHECK(qaoa_ansatz_new(NULL) == NULL, "qaoa_ansatz_new(null)"); + CHECK(qaoa_ansatz_reps(NULL, 1) == -1, "qaoa_ansatz_reps(null)"); + CHECK(qaoa_ansatz_mixer(NULL, NULL) == -1, "qaoa_ansatz_mixer(null)"); + CHECK(qaoa_ansatz_initial_state(NULL, NULL) == -1, "qaoa_ansatz_initial_state(null)"); + CHECK(qaoa_ansatz_evolution_strategy(NULL, EVOLUTION_STRATEGY_EXACT, TROTTER_FIRST_ORDER, 1, + 0) == -1, + "qaoa_ansatz_evolution_strategy(null)"); + CHECK(qaoa_ansatz_validate(NULL) == -1, "qaoa_ansatz_validate(null)"); + CHECK(qaoa_ansatz_num_parameters(NULL) == 0, "qaoa_ansatz_num_parameters(null)"); + CHECK(qaoa_ansatz_num_qubits(NULL) == 0, "qaoa_ansatz_num_qubits(null)"); + CHECK(qaoa_ansatz_build_circuit(NULL, "p") == NULL, "qaoa_ansatz_build_circuit(null)"); + + /* Freeing NULL is a no-op everywhere. */ + two_local_free(NULL); + basic_entangler_layers_free(NULL); + qaoa_ansatz_free(NULL); + hamiltonian_free(NULL); + CHECK(1, "ansatz free(null)"); +} + +static void test_ansatz_build_circuit_smoke(void) { + /* TwoLocal: reps = 2, RY, CX, linear. Three rotation layers of 3 RY + * plus two entanglement layers of 2 CX => 13 operations, 9 params. */ + struct CTwoLocal* ansatz = two_local_new(3); + CHECK(ansatz != NULL, "smoke TwoLocal fixture"); + CHECK(two_local_reps(ansatz, 2) == 0, "smoke TwoLocal reps"); + struct CCircuit* circuit = two_local_build_circuit(ansatz, "t"); + CHECK(circuit != NULL, "two_local_build_circuit"); + CHECK(circuit_num_qubits(circuit) == 3, "TwoLocal built qubits"); + CHECK(circuit_num_operations(circuit) == 13, "TwoLocal built ops"); + CHECK(circuit_num_parameters(circuit) == 9, "TwoLocal built params"); + circuit_free(circuit); + + /* Skipping the final rotation layer drops the last 3 RY gates. */ + CHECK(two_local_skip_final_rotation_layer(ansatz, true) == 0, "smoke TwoLocal skip final"); + struct CCircuit* skipped = two_local_build_circuit(ansatz, "t"); + CHECK(skipped != NULL, "TwoLocal skipped build"); + CHECK(circuit_num_operations(skipped) == 2 * 3 + 2 * 2, "TwoLocal skipped ops"); + CHECK(circuit_num_parameters(skipped) == 6, "TwoLocal skipped params"); + circuit_free(skipped); + + /* Full topology: C(3,2) = 3 CX per entanglement layer => 15 ops. */ + CHECK(two_local_skip_final_rotation_layer(ansatz, false) == 0, "smoke TwoLocal restore final"); + CHECK(two_local_entanglement(ansatz, ENTANGLEMENT_FULL, NULL, 0) == 0, + "smoke TwoLocal full topology"); + struct CCircuit* full = two_local_build_circuit(ansatz, "t"); + CHECK(full != NULL, "TwoLocal full build"); + CHECK(circuit_num_operations(full) == 9 + 2 * 3, "TwoLocal full ops"); + circuit_free(full); + two_local_free(ansatz); + + /* BasicEntanglerLayers: reps = 2 on a 3-qubit ring => per layer + * 3 rotations + 3 entanglers => 12 operations, 6 parameters. */ + struct CBasicEntanglerLayers* layers = basic_entangler_layers_new(3); + CHECK(layers != NULL, "smoke layers fixture"); + CHECK(basic_entangler_layers_reps(layers, 2) == 0, "smoke layers reps"); + struct CCircuit* ring = basic_entangler_layers_build_circuit(layers, "t"); + CHECK(ring != NULL, "basic_entangler_layers_build_circuit"); + CHECK(circuit_num_qubits(ring) == 3, "layers built qubits"); + CHECK(circuit_num_operations(ring) == 12, "layers built ops"); + CHECK(circuit_num_parameters(ring) == 6, "layers built params"); + CHECK(basic_entangler_layers_build_circuit(layers, NULL) == NULL, "layers build null prefix"); + circuit_free(ring); + basic_entangler_layers_free(layers); + + /* A single qubit has no entanglement: only the rotations remain. */ + struct CBasicEntanglerLayers* single = basic_entangler_layers_new(1); + CHECK(single != NULL, "smoke single-qubit fixture"); + CHECK(basic_entangler_layers_reps(single, 2) == 0, "smoke single-qubit reps"); + struct CCircuit* solo = basic_entangler_layers_build_circuit(single, "t"); + CHECK(solo != NULL, "single-qubit build"); + CHECK(circuit_num_operations(solo) == 2, "single-qubit ops"); + CHECK(circuit_num_parameters(solo) == 2, "single-qubit params"); + circuit_free(solo); + basic_entangler_layers_free(single); + + /* QAOA: the H layer plus cost/mixer evolution for one rep. The + * exact operation count depends on the Pauli-evolution lowering, + * so only smoke-check the floor while pinning the shape. */ + struct CHamiltonian* cost = pauli_hamiltonian("ZZ"); + CHECK(cost != NULL, "smoke qaoa cost fixture"); + struct CQAOAAnsatz* qaoa = qaoa_ansatz_new(cost); + CHECK(qaoa != NULL, "smoke qaoa_ansatz_new"); + struct CCircuit* qc = qaoa_ansatz_build_circuit(qaoa, "t"); + CHECK(qc != NULL, "qaoa_ansatz_build_circuit"); + CHECK(circuit_num_qubits(qc) == 2, "QAOA built qubits"); + CHECK(circuit_num_parameters(qc) == 2, "QAOA built params"); + CHECK(circuit_num_operations(qc) >= 2, "QAOA built ops floor"); + CHECK(qaoa_ansatz_build_circuit(qaoa, NULL) == NULL, "QAOA build null prefix"); + circuit_free(qc); + qaoa_ansatz_free(qaoa); + /* The clone-based constructor keeps the cost Hamiltonian owned by + * this test, so it is freed here. */ + hamiltonian_free(cost); +} + +int main(void) { + test_two_local_structure(); + test_two_local_invalid_inputs(); + test_basic_entangler_layers_structure(); + test_qaoa_ansatz_structure(); + test_ansatz_null_handles(); + test_ansatz_build_circuit_smoke(); + + printf("\n%d failure(s)\n", g_failures); + return (g_failures == 0) ? 0 : 1; +} diff --git a/crates/binding-c/tests/test_circuit_cfg.c b/crates/binding-c/tests/test_circuit_cfg.c new file mode 100644 index 00000000..dae8998d --- /dev/null +++ b/crates/binding-c/tests/test_circuit_cfg.c @@ -0,0 +1,600 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +#include +#include +#include +#include + +#include "cqlib_c.h" + +static int g_failures = 0; + +#define CHECK(cond, name) \ + do { \ + if (cond) { \ + printf("ok: %s\n", (name)); \ + } else { \ + printf("FAIL: %s\n", (name)); \ + g_failures++; \ + } \ + } while (0) + +/* Loop-body callback appending one gate; `user_data` is ignored. */ +static int32_t add_h(struct CCircuit* circuit, void* user_data) { + (void)user_data; + return circuit_h(circuit, 0); +} + +static void test_cfg_from_circuit_roundtrip(void) { + struct CCircuit* circuit = circuit_new(2); + CHECK(circuit != NULL, "cfg fixture circuit_new"); + CHECK(circuit_h(circuit, 0) == 0, "cfg fixture circuit_h"); + CHECK(circuit_cx(circuit, 0, 1) == 0, "cfg fixture circuit_cx"); + CHECK(circuit_num_operations(circuit) == 2, "cfg fixture two ops"); + + /* A straight-line circuit expands into a single entry block. */ + struct CCircuitCFG* cfg = circuit_cfg_from_circuit(circuit); + CHECK(cfg != NULL, "circuit_cfg_from_circuit"); + CHECK(circuit_cfg_validate(cfg) == 0, "circuit_cfg_validate"); + CHECK(circuit_cfg_num_qubits(cfg) == 2, "circuit_cfg_num_qubits"); + CHECK(circuit_cfg_num_blocks(cfg) == 1, "circuit_cfg_num_blocks"); + CHECK(circuit_cfg_entry_block(cfg) == 0, "circuit_cfg_entry_block"); + + uint32_t ids[2] = {9, 9}; + CHECK(circuit_cfg_qubits(cfg, ids, 2) == 0, "circuit_cfg_qubits"); + CHECK(ids[0] == 0 && ids[1] == 1, "circuit_cfg_qubits values"); + + char* entry_label = circuit_cfg_block_label(cfg, 0); + CHECK(entry_label != NULL, "circuit_cfg_block_label"); + CHECK(entry_label != NULL && strcmp(entry_label, "entry") == 0, "entry block label value"); + cqlib_string_free(entry_label); + + /* Entry block: two ordinary operations plus a `Return` terminator. */ + CHECK(circuit_cfg_block_is_empty(cfg, 0) == 0, "entry block not empty"); + CHECK(circuit_cfg_block_operations_len(cfg, 0) == 2, "entry ops len"); + CHECK(circuit_cfg_block_has_terminator(cfg, 0) == 1, "entry has terminator"); + CHECK(circuit_cfg_block_terminator_tag(cfg, 0) == (int32_t)CFG_TERMINATOR_RETURN, + "entry terminator tag"); + CHECK(circuit_cfg_terminator_target(cfg, 0) == UINT32_MAX, "return has no target"); + CHECK(circuit_cfg_terminator_condition(cfg, 0) == NULL, "return has no condition"); + + /* Cloned operation handles are independent of the cfg and of each other. */ + struct COperation* cloned[2] = {NULL, NULL}; + CHECK(circuit_cfg_block_operations(cfg, 0, cloned, 2) == 0, "block operations copy"); + CHECK(cloned[0] != NULL && cloned[1] != NULL, "cloned operations non-null"); + operation_free(cloned[0]); + operation_free(cloned[1]); + + /* An undersized buffer is rejected before any write. */ + CHECK(circuit_cfg_block_operations(cfg, 0, cloned, 1) == -8, "block operations short buffer"); + + /* Straight-line circuits carry no classical state. */ + CHECK(circuit_cfg_classical_vars_len(cfg) == 0, "no classical vars"); + CHECK(circuit_cfg_classical_values_len(cfg) == 0, "no classical values"); + + /* Round trip: qubit count and operation count are preserved. The source + * circuit stays alive and unchanged. */ + struct CCircuit* lowered = circuit_cfg_to_circuit(cfg); + CHECK(lowered != NULL, "circuit_cfg_to_circuit"); + CHECK(circuit_num_qubits(lowered) == 2, "lowered num_qubits"); + CHECK(circuit_num_operations(lowered) == 2, "lowered num_operations"); + CHECK(circuit_num_operations(circuit) == 2, "source circuit unchanged"); + + circuit_free(lowered); + circuit_cfg_free(cfg); + circuit_free(circuit); +} + +static void test_cfg_layout_and_blocks(void) { + const uint32_t zero[1] = {0}; + struct COperation* op = operation_new("H", zero, 1, NULL, 0); + CHECK(op != NULL, "cfg layout fixture"); + + /* A fresh CFG is empty and has no designated entry. */ + struct CCircuitCFG* cfg = circuit_cfg_new(2); + CHECK(cfg != NULL, "circuit_cfg_new"); + CHECK(circuit_cfg_num_qubits(cfg) == 2, "fresh num_qubits"); + CHECK(circuit_cfg_num_blocks(cfg) == 0, "fresh num_blocks"); + CHECK(circuit_cfg_entry_block(cfg) == UINT32_MAX, "fresh entry sentinel"); + CHECK(circuit_cfg_blocks(cfg, NULL, 0) == 0, "fresh blocks copy"); + + /* Blocks are appended with stable sequential indices. */ + uint32_t b0 = circuit_cfg_add_block(cfg); + uint32_t b1 = circuit_cfg_add_block(cfg); + CHECK(b0 == 0 && b1 == 1, "add_block indices"); + CHECK(circuit_cfg_num_blocks(cfg) == 2, "num_blocks 2"); + uint32_t b2 = circuit_cfg_add_block(cfg); + CHECK(b2 == 2, "third block index"); + CHECK(circuit_cfg_num_blocks(cfg) == 3, "num_blocks 3"); + + /* Block enumeration into the caller-supplied buffer. */ + uint32_t nodes[3] = {9, 9, 9}; + CHECK(circuit_cfg_blocks(cfg, nodes, 3) == 0, "blocks copy"); + CHECK(nodes[0] == 0 && nodes[1] == 1 && nodes[2] == 2, "blocks values"); + CHECK(circuit_cfg_blocks(cfg, nodes, 2) == -8, "blocks short buffer"); + CHECK(circuit_cfg_blocks(cfg, NULL, 3) == -1, "blocks null buffer"); + + /* Entry switching accepts any non-sentinel index (validate checks it). */ + CHECK(circuit_cfg_set_entry_block(cfg, b1) == 0, "set_entry_block"); + CHECK(circuit_cfg_entry_block(cfg) == b1, "entry_block readback"); + CHECK(circuit_cfg_set_entry_block(cfg, UINT32_MAX) == -2, "set_entry_block sentinel"); + + /* Fresh blocks are empty and unlabeled. */ + CHECK(circuit_cfg_block_is_empty(cfg, b2) == 1, "fresh block empty"); + CHECK(circuit_cfg_block_label(cfg, b2) == NULL, "fresh block unlabeled"); + CHECK(circuit_cfg_block_operations_len(cfg, b2) == 0, "fresh block ops len"); + CHECK(circuit_cfg_block_operations(cfg, b2, NULL, 0) == 0, "fresh block operations copy"); + + /* Semantic edges between existing blocks; flow tags are checked first. */ + CHECK(circuit_cfg_add_edge(cfg, b0, b1, CFG_FLOW_UNCONDITIONAL, 0, 0) == 0, "add_edge"); + CHECK(circuit_cfg_add_edge(cfg, b0, 3, CFG_FLOW_UNCONDITIONAL, 0, 0) == -2, + "add_edge missing target"); + CHECK(circuit_cfg_add_edge(cfg, UINT32_MAX, b1, CFG_FLOW_UNCONDITIONAL, 0, 0) == -2, + "add_edge sentinel source"); + CHECK(circuit_cfg_add_edge(cfg, b0, 3, 99, 0, 0) == -8, "add_edge bad flow"); + CHECK(circuit_cfg_add_edge(cfg, b0, b1, 99, 0, 0) == -8, "add_edge bad flow 2"); + + uint32_t targets[1] = {9}; + uint32_t tags[1] = {9}; + uint64_t lo[1] = {9}; + uint64_t hi[1] = {9}; + CHECK(circuit_cfg_outgoing_edges_len(cfg, b0) == 1, "outgoing_edges_len"); + CHECK(circuit_cfg_outgoing_edges(cfg, b0, targets, tags, lo, hi, 1) == 0, "outgoing_edges"); + CHECK(targets[0] == b1, "edge target"); + CHECK(tags[0] == CFG_FLOW_UNCONDITIONAL, "edge flow tag"); + CHECK(lo[0] == 0 && hi[0] == 0, "edge case halves"); + /* Undersized then NULL parallel buffers. */ + CHECK(circuit_cfg_outgoing_edges(cfg, b0, targets, tags, lo, hi, 0) == -8, + "outgoing_edges short buffer"); + CHECK(circuit_cfg_outgoing_edges(cfg, b0, NULL, tags, lo, hi, 1) == -1, + "outgoing_edges null buffer"); + + /* Pushing one operation flips `block_is_empty` and grows the count. */ + CHECK(circuit_cfg_push_operation(cfg, b2, op) == 0, "push_operation"); + CHECK(circuit_cfg_block_is_empty(cfg, b2) == 0, "block no longer empty"); + CHECK(circuit_cfg_block_operations_len(cfg, b2) == 1, "block ops len 1"); + struct COperation* single[1] = {NULL}; + CHECK(circuit_cfg_block_operations(cfg, b2, single, 1) == 0, "block operations clone"); + CHECK(single[0] != NULL, "cloned op non-null"); + operation_free(single[0]); + CHECK(circuit_cfg_block_operations(cfg, b2, single, 0) == -8, "block operations short clone"); + CHECK(circuit_cfg_block_operations(cfg, b2, NULL, 1) == -1, "block operations null clone"); + + /* Unknown blocks are rejected by mutation entry points. */ + CHECK(circuit_cfg_push_operation(cfg, 7, op) == -2, "push unknown block"); + CHECK(circuit_cfg_push_operation(cfg, UINT32_MAX, op) == -2, "push sentinel block"); + CHECK(circuit_cfg_push_operation(cfg, b2, NULL) == -1, "push null op"); + + circuit_cfg_free(cfg); + operation_free(op); +} + +static void test_cfg_terminators(void) { + struct CCircuitCFG* cfg = circuit_cfg_new(1); + uint32_t b0 = circuit_cfg_add_block(cfg); + uint32_t b1 = circuit_cfg_add_block(cfg); + uint32_t b2 = circuit_cfg_add_block(cfg); + + /* Setters succeed and the getter round-trips each terminator flavor. */ + CHECK(circuit_cfg_set_terminator_jump(cfg, b0, b1) == 0, "set jump"); + CHECK(circuit_cfg_set_terminator_break(cfg, b1, b0) == 0, "set break"); + CHECK(circuit_cfg_set_terminator_continue(cfg, b2, b0) == 0, "set continue"); + + CHECK(circuit_cfg_block_has_terminator(cfg, b0) == 1, "b0 has terminator"); + CHECK(circuit_cfg_block_terminator_tag(cfg, b0) == (int32_t)CFG_TERMINATOR_JUMP, "b0 jump tag"); + CHECK(circuit_cfg_terminator_target(cfg, b0) == b1, "jump target"); + CHECK(circuit_cfg_block_terminator_tag(cfg, b1) == (int32_t)CFG_TERMINATOR_BREAK, + "b1 break tag"); + CHECK(circuit_cfg_terminator_target(cfg, b1) == b0, "break target"); + CHECK(circuit_cfg_block_terminator_tag(cfg, b2) == (int32_t)CFG_TERMINATOR_CONTINUE, + "b2 continue tag"); + CHECK(circuit_cfg_terminator_target(cfg, b2) == b0, "continue target"); + + /* A terminator without operations still makes the block non-empty. */ + CHECK(circuit_cfg_block_is_empty(cfg, b0) == 0, "terminator makes block non-empty"); + CHECK(circuit_cfg_block_operations_len(cfg, b0) == 0, "terminator block no ops"); + + /* Jump/Break/Continue carry no condition and no loop info. */ + CHECK(circuit_cfg_terminator_condition(cfg, b0) == NULL, "jump no condition"); + struct CClassicalVar* var = NULL; + struct CClassicalExpr* start = NULL; + struct CClassicalExpr* stop = NULL; + struct CClassicalExpr* step = NULL; + CHECK(circuit_cfg_terminator_for_info(cfg, b0, &var, &start, &stop, &step) == -8, + "jump no for info"); + + /* `Return` has no target and overwrites the previous terminator. */ + CHECK(circuit_cfg_set_terminator_return(cfg, b2) == 0, "set return"); + CHECK(circuit_cfg_block_terminator_tag(cfg, b2) == (int32_t)CFG_TERMINATOR_RETURN, + "return tag"); + CHECK(circuit_cfg_terminator_target(cfg, b2) == UINT32_MAX, "return no target"); + + /* NULL and unknown blocks. */ + CHECK(circuit_cfg_set_terminator_jump(NULL, 0, 0) == -1, "jump null cfg"); + CHECK(circuit_cfg_set_terminator_return(NULL, 0) == -1, "return null cfg"); + CHECK(circuit_cfg_set_terminator_break(NULL, 0, 0) == -1, "break null cfg"); + CHECK(circuit_cfg_set_terminator_continue(NULL, 0, 0) == -1, "continue null cfg"); + CHECK(circuit_cfg_set_terminator_jump(cfg, 7, 0) == -2, "jump unknown block"); + CHECK(circuit_cfg_set_terminator_jump(cfg, UINT32_MAX, 0) == -2, "jump sentinel block"); + + circuit_cfg_free(cfg); +} + +static void test_cfg_classical_state(void) { + /* Circuit carrying two variables plus one measured value. */ + struct CCircuit* circuit = circuit_new(2); + struct CClassicalVar* bit_var = circuit_var(circuit, CQLIB_CLASSICAL_TYPE_BIT, 0); + struct CClassicalVar* uint_var = circuit_var(circuit, CQLIB_CLASSICAL_TYPE_UINT, 8); + CHECK(bit_var != NULL, "bit var"); + CHECK(uint_var != NULL, "uint var"); + + const uint32_t measured[2] = {0, 1}; + struct CClassicalExpr* measured_expr = circuit_measure_bits(circuit, measured, 2); + CHECK(measured_expr != NULL, "measure expr"); + CHECK(circuit_classical_vars_len(circuit) == 2, "circuit vars len"); + + struct CCircuitCFG* cfg = circuit_cfg_from_circuit(circuit); + CHECK(cfg != NULL, "cfg from classical circuit"); + + /* The CFG snapshots the circuit's static classical tables. */ + CHECK(circuit_cfg_classical_vars_len(cfg) == 2, "cfg vars len"); + CHECK(circuit_cfg_classical_values_len(cfg) == 1, "cfg values len"); + + CClassicalType vars[2] = {{9, 9}, {9, 9}}; + CHECK(circuit_cfg_classical_vars(cfg, vars, 2) == 0, "vars copy"); + /* Compare per field: the repr(C) snapshot has no comparison support. */ + CHECK(vars[0].tag == CQLIB_CLASSICAL_TYPE_BIT && vars[0].width == 1, "bit var snapshot"); + CHECK(vars[1].tag == CQLIB_CLASSICAL_TYPE_UINT && vars[1].width == 8, "uint var snapshot"); + + CClassicalType values[1] = {{9, 9}}; + CHECK(circuit_cfg_classical_values(cfg, values, 1) == 0, "values copy"); + CHECK(values[0].tag == CQLIB_CLASSICAL_TYPE_BIT_VEC && values[0].width == 2, "value snapshot"); + + /* Undersized then NULL buffers. */ + CHECK(circuit_cfg_classical_vars(cfg, vars, 1) == -8, "vars short buffer"); + CHECK(circuit_cfg_classical_vars(cfg, NULL, 2) == -1, "vars null buffer"); + CHECK(circuit_cfg_classical_values(cfg, values, 0) == -8, "values short buffer"); + CHECK(circuit_cfg_classical_values(cfg, NULL, 1) == -1, "values null buffer"); + + classical_expr_free(measured_expr); + classical_var_free(uint_var); + classical_var_free(bit_var); + circuit_cfg_free(cfg); + + /* A circuit without classical state produces empty CFG tables. */ + struct CCircuit* plain = circuit_new(1); + struct CCircuitCFG* plain_cfg = circuit_cfg_from_circuit(plain); + CHECK(plain_cfg != NULL, "plain cfg"); + CHECK(circuit_cfg_classical_vars_len(plain_cfg) == 0, "plain vars len"); + CHECK(circuit_cfg_classical_values_len(plain_cfg) == 0, "plain values len"); + CHECK(circuit_cfg_classical_vars(plain_cfg, NULL, 0) == 0, "plain vars copy"); + CHECK(circuit_cfg_classical_values(plain_cfg, NULL, 0) == 0, "plain values copy"); + circuit_cfg_free(plain_cfg); + circuit_free(plain); + + /* A hand-built CFG starts with empty tables as well. */ + struct CCircuitCFG* empty = circuit_cfg_new(0); + CHECK(circuit_cfg_classical_vars_len(empty) == 0, "hand-built vars len"); + CHECK(circuit_cfg_classical_values_len(empty) == 0, "hand-built values len"); + CHECK(circuit_cfg_classical_vars(empty, NULL, 0) == 0, "hand-built vars copy"); + CHECK(circuit_cfg_classical_values(empty, NULL, 0) == 0, "hand-built values copy"); + circuit_cfg_free(empty); + + circuit_free(circuit); +} + +static void test_cfg_loop_regions(void) { + /* `while (flag) { h(0); }` expands into entry / cond / body / exit. */ + struct CCircuit* circuit = circuit_new(2); + struct CClassicalVar* condition_var = circuit_var(circuit, CQLIB_CLASSICAL_TYPE_BOOL, 0); + CHECK(condition_var != NULL, "while condition var"); + struct CClassicalExpr* condition = classical_expr_var(condition_var); + CHECK(condition != NULL, "while condition expr"); + CHECK(circuit_while(circuit, condition, add_h, NULL) == 0, "circuit_while"); + classical_expr_free(condition); + classical_var_free(condition_var); + + struct CCircuitCFG* cfg = circuit_cfg_from_circuit(circuit); + CHECK(cfg != NULL, "while cfg"); + CHECK(circuit_cfg_validate(cfg) == 0, "while cfg validate"); + CHECK(circuit_cfg_num_blocks(cfg) == 4, "while blocks"); + CHECK(circuit_cfg_entry_block(cfg) == 0, "while entry"); + + /* Entry jumps to the condition header, which owns the While region. */ + CHECK(circuit_cfg_block_terminator_tag(cfg, 0) == (int32_t)CFG_TERMINATOR_JUMP, "entry jump"); + CHECK(circuit_cfg_terminator_target(cfg, 0) == 1, "entry target"); + CHECK(circuit_cfg_region_tag(cfg, 1) == (int32_t)CFG_REGION_WHILE, "cond region tag"); + CHECK(circuit_cfg_is_loop_header(cfg, 1) == 1, "cond is loop header"); + CHECK(circuit_cfg_region_tag(cfg, 0) == 0, "entry no region"); + CHECK(circuit_cfg_is_loop_header(cfg, 0) == 0, "entry not loop header"); + CHECK(circuit_cfg_block_terminator_tag(cfg, 1) == (int32_t)CFG_TERMINATOR_BRANCH, + "cond branch tag"); + CHECK(circuit_cfg_terminator_target(cfg, 1) == UINT32_MAX, "branch no target"); + + /* Structured labels are attached for diagnostics. */ + char* body_label = circuit_cfg_block_label(cfg, 2); + CHECK(body_label != NULL, "body label"); + CHECK(body_label != NULL && strncmp(body_label, "while_body_", 11) == 0, "body label prefix"); + cqlib_string_free(body_label); + + /* The Branch condition is exposed as a fresh expression handle. */ + struct CClassicalExpr* branch_condition = circuit_cfg_terminator_condition(cfg, 1); + CHECK(branch_condition != NULL, "branch condition"); + classical_expr_free(branch_condition); + + /* The loop layout reads back body and exit blocks. */ + uint32_t body_block = 9; + uint32_t exit_block = 9; + CHECK(circuit_cfg_region_loop(cfg, 1, &body_block, &exit_block) == 0, "while region loop"); + CHECK(body_block == 2 && exit_block == 3, "while body/exit"); + /* Non-loop blocks have no region and are rejected. */ + CHECK(circuit_cfg_region_loop(cfg, 0, &body_block, &exit_block) == -8, "non-loop region_loop"); + CHECK(circuit_cfg_terminator_for_info(cfg, 1, NULL, NULL, NULL, NULL) == -8, "branch for_info"); + + /* Header edges: TrueBranch into the body, FalseBranch into the exit. */ + uint32_t edge_targets[2] = {9, 9}; + uint32_t flow_tags[2] = {9, 9}; + uint64_t edge_lo[2] = {9, 9}; + uint64_t edge_hi[2] = {9, 9}; + CHECK(circuit_cfg_outgoing_edges_len(cfg, 1) == 2, "cond edges len"); + CHECK(circuit_cfg_outgoing_edges(cfg, 1, edge_targets, flow_tags, edge_lo, edge_hi, 2) == 0, + "cond edges"); + CHECK(edge_targets[0] == 2 && edge_targets[1] == 3, "cond edge targets"); + CHECK(flow_tags[0] == CFG_FLOW_TRUE_BRANCH && flow_tags[1] == CFG_FLOW_FALSE_BRANCH, + "cond flow tags"); + + /* The body jumps back to the header; the exit returns. */ + CHECK(circuit_cfg_block_operations_len(cfg, 2) == 1, "body ops len"); + CHECK(circuit_cfg_terminator_target(cfg, 2) == 1, "body jumps back"); + CHECK(circuit_cfg_block_terminator_tag(cfg, 3) == (int32_t)CFG_TERMINATOR_RETURN, + "exit return"); + + circuit_cfg_free(cfg); + circuit_free(circuit); + + /* `for` loops expose their loop variable through `terminator_for_info`. */ + struct CCircuit* for_circuit = circuit_new(1); + struct CClassicalVar* counter = circuit_var(for_circuit, CQLIB_CLASSICAL_TYPE_UINT, 8); + CHECK(counter != NULL, "for counter var"); + struct CClassicalExpr* start = classical_expr_uint_literal(8, 0, 0); + struct CClassicalExpr* stop = classical_expr_uint_literal(8, 4, 0); + struct CClassicalExpr* step = classical_expr_uint_literal(8, 1, 0); + CHECK(start != NULL && stop != NULL && step != NULL, "for literals"); + CHECK(circuit_for_uint(for_circuit, counter, start, stop, step, add_h, NULL) == 0, + "circuit_for_uint"); + + struct CCircuitCFG* for_cfg = circuit_cfg_from_circuit(for_circuit); + CHECK(for_cfg != NULL, "for cfg"); + CHECK(circuit_cfg_num_blocks(for_cfg) == 4, "for blocks"); + CHECK(circuit_cfg_block_terminator_tag(for_cfg, 1) == (int32_t)CFG_TERMINATOR_FOR_LOOP, + "for terminator"); + CHECK(circuit_cfg_region_tag(for_cfg, 1) == (int32_t)CFG_REGION_FOR, "for region tag"); + CHECK(circuit_cfg_is_loop_header(for_cfg, 1) == 1, "for is loop header"); + CHECK(circuit_cfg_region_loop(for_cfg, 1, &body_block, &exit_block) == 0, "for region loop"); + CHECK(body_block == 2 && exit_block == 3, "for body/exit"); + + struct CClassicalVar* var = NULL; + struct CClassicalExpr* for_start = NULL; + struct CClassicalExpr* for_stop = NULL; + struct CClassicalExpr* for_step = NULL; + CHECK(circuit_cfg_terminator_for_info(for_cfg, 1, &var, &for_start, &for_stop, &for_step) == 0, + "for_info"); + CHECK(var != NULL && for_start != NULL && for_stop != NULL && for_step != NULL, + "for_info handles"); + classical_var_free(var); + classical_expr_free(for_step); + classical_expr_free(for_stop); + classical_expr_free(for_start); + + /* NULL out-parameters on a present ForLoop header fail before writing. */ + var = NULL; + for_start = NULL; + for_stop = NULL; + for_step = NULL; + CHECK(circuit_cfg_terminator_for_info(for_cfg, 1, NULL, &for_start, &for_stop, &for_step) == -1, + "for_info null out-param"); + /* Non-loop blocks and unknown blocks report their failure mode. */ + CHECK(circuit_cfg_terminator_for_info(for_cfg, 0, &var, &for_start, &for_stop, &for_step) == -8, + "for_info non-loop"); + CHECK(circuit_cfg_terminator_for_info(for_cfg, UINT32_MAX, &var, &for_start, &for_stop, + &for_step) == -2, + "for_info unknown block"); + + circuit_cfg_free(for_cfg); + classical_expr_free(step); + classical_expr_free(stop); + classical_expr_free(start); + classical_var_free(counter); + circuit_free(for_circuit); + + /* Region setters: manual attachment and replacement on a bare CFG. */ + struct CCircuitCFG* manual = circuit_cfg_new(1); + uint32_t header = circuit_cfg_add_block(manual); + uint32_t body_b = circuit_cfg_add_block(manual); + uint32_t exit_b = circuit_cfg_add_block(manual); + uint32_t merge_b = circuit_cfg_add_block(manual); + + CHECK(circuit_cfg_set_while_region(manual, header, body_b, exit_b, NULL) == 0, + "set_while_region"); + CHECK(circuit_cfg_region_tag(manual, header) == (int32_t)CFG_REGION_WHILE, "while region tag"); + CHECK(circuit_cfg_is_loop_header(manual, header) == 1, "while header"); + + /* Replacing the region with a For region keeps it a loop header. */ + CHECK(circuit_cfg_set_for_region(manual, header, body_b, exit_b, NULL) == 0, "set_for_region"); + CHECK(circuit_cfg_region_tag(manual, header) == (int32_t)CFG_REGION_FOR, "for region tag"); + CHECK(circuit_cfg_is_loop_header(manual, header) == 1, "for header"); + CHECK(circuit_cfg_region_loop(manual, header, &body_block, &exit_block) == 0, + "for region loop readback"); + CHECK(body_block == body_b && exit_block == exit_b, "for region values"); + + /* An If region is not a loop header and carries the four-way layout. */ + CHECK(circuit_cfg_set_if_region(manual, header, body_b, exit_b, merge_b, 0, NULL) == 0, + "set_if_region"); + CHECK(circuit_cfg_region_tag(manual, header) == (int32_t)CFG_REGION_IF, "if region tag"); + CHECK(circuit_cfg_is_loop_header(manual, header) == 0, "if not loop header"); + uint32_t then_entry = 9; + uint32_t else_entry = 9; + uint32_t merge_block = 9; + int32_t has_else = 9; + CHECK(circuit_cfg_region_if(manual, header, &then_entry, &else_entry, &merge_block, + &has_else) == 0, + "region_if"); + CHECK(then_entry == body_b && else_entry == exit_b && merge_block == merge_b, "if layout"); + CHECK(has_else == 0, "if has_else"); + CHECK(circuit_cfg_region_loop(manual, header, &body_block, &exit_block) == -8, + "if region not loop"); + + /* A Switch region enumerates its match values and entries. */ + const uint64_t case_lo[1] = {7}; + const uint64_t case_hi[1] = {0}; + const uint32_t case_entries[1] = {body_b}; + CHECK(circuit_cfg_set_switch_region(manual, header, case_lo, case_hi, case_entries, 1, exit_b, + merge_b, 1, NULL) == 0, + "set_switch_region"); + CHECK(circuit_cfg_region_tag(manual, header) == (int32_t)CFG_REGION_SWITCH, + "switch region tag"); + CHECK(circuit_cfg_is_loop_header(manual, header) == 0, "switch not header"); + CHECK(circuit_cfg_region_switch_cases_len(manual, header) == 1, "switch cases len"); + uint64_t out_lo[1] = {9}; + uint64_t out_hi[1] = {9}; + uint32_t out_entries[1] = {9}; + CHECK(circuit_cfg_region_switch(manual, header, out_lo, out_hi, out_entries, 1, &exit_block, + &merge_block, &has_else) == 0, + "region_switch"); + CHECK(out_lo[0] == 7 && out_hi[0] == 0, "switch case value halves"); + CHECK(out_entries[0] == body_b, "switch case entry"); + CHECK(exit_block == exit_b && merge_block == merge_b && has_else == 1, + "switch default/merge/has_default"); + + /* Setter failure modes: NULL handles, sentinel, and unknown headers. */ + CHECK(circuit_cfg_set_while_region(NULL, 0, 0, 0, NULL) == -1, "set_while null"); + CHECK(circuit_cfg_set_if_region(NULL, 0, 0, 0, 0, 0, NULL) == -1, "set_if null"); + CHECK(circuit_cfg_set_for_region(NULL, 0, 0, 0, NULL) == -1, "set_for null"); + CHECK(circuit_cfg_set_switch_region(NULL, 0, NULL, NULL, NULL, 0, 0, 0, 0, NULL) == -1, + "set_switch null"); + CHECK(circuit_cfg_set_switch_region(manual, header, NULL, NULL, NULL, 1, exit_b, merge_b, 1, + NULL) == -1, + "set_switch null arrays"); + CHECK(circuit_cfg_set_while_region(manual, 7, 0, 0, NULL) == -2, "set_while unknown header"); + CHECK(circuit_cfg_set_while_region(manual, UINT32_MAX, 0, 0, NULL) == -2, + "set_while sentinel header"); + + /* Hand-attached regions point at real blocks everywhere. */ + const uint32_t region_targets[1] = {0}; + CHECK(circuit_cfg_set_switch_region(manual, header, case_lo, case_hi, region_targets, 1, exit_b, + merge_b, 1, NULL) == 0, + "set_switch existing targets"); + CHECK(circuit_cfg_region_switch_cases_len(manual, header) == 1, + "switch cases len after reattach"); + + circuit_cfg_free(manual); +} + +static void test_cfg_null_and_error_paths(void) { + const uint32_t zero[1] = {0}; + struct COperation* op = operation_new("H", zero, 1, NULL, 0); + CHECK(op != NULL, "null-path fixture"); + struct CClassicalVar* var = NULL; + struct CClassicalExpr* start = NULL; + struct CClassicalExpr* stop = NULL; + struct CClassicalExpr* step = NULL; + uint32_t entry = 9; + + /* Every exported function tolerates a NULL handle. */ + CHECK(circuit_cfg_from_circuit(NULL) == NULL, "from_circuit null"); + CHECK(circuit_cfg_from_qubits(NULL, 1) == NULL, "from_qubits null buffer"); + CHECK(circuit_cfg_to_circuit(NULL) == NULL, "to_circuit null"); + CHECK(circuit_cfg_validate(NULL) == -1, "validate null"); + CHECK(circuit_cfg_num_qubits(NULL) == 0, "num_qubits null"); + CHECK(circuit_cfg_num_blocks(NULL) == 0, "num_blocks null"); + CHECK(circuit_cfg_qubits(NULL, NULL, 0) == -1, "qubits null"); + CHECK(circuit_cfg_classical_vars_len(NULL) == 0, "vars_len null"); + CHECK(circuit_cfg_classical_values_len(NULL) == 0, "values_len null"); + CHECK(circuit_cfg_classical_vars(NULL, NULL, 0) == -1, "vars null"); + CHECK(circuit_cfg_classical_values(NULL, NULL, 0) == -1, "values null"); + CHECK(circuit_cfg_entry_block(NULL) == UINT32_MAX, "entry_block null"); + CHECK(circuit_cfg_add_block(NULL) == UINT32_MAX, "add_block null"); + CHECK(circuit_cfg_add_edge(NULL, 0, 0, CFG_FLOW_UNCONDITIONAL, 0, 0) == -1, "add_edge null"); + CHECK(circuit_cfg_set_entry_block(NULL, 0) == -1, "set_entry null"); + CHECK(circuit_cfg_push_operation(NULL, 0, op) == -1, "push null cfg"); + CHECK(circuit_cfg_blocks(NULL, NULL, 0) == -1, "blocks null"); + CHECK(circuit_cfg_block_label(NULL, 0) == NULL, "block_label null"); + CHECK(circuit_cfg_block_is_empty(NULL, 0) == -1, "block_is_empty null"); + CHECK(circuit_cfg_block_operations_len(NULL, 0) == 0, "block_operations_len null"); + CHECK(circuit_cfg_block_operations(NULL, 0, NULL, 0) == -1, "block_operations null"); + CHECK(circuit_cfg_block_has_terminator(NULL, 0) == -1, "has_terminator null"); + CHECK(circuit_cfg_block_terminator_tag(NULL, 0) == -1, "terminator_tag null"); + CHECK(circuit_cfg_terminator_target(NULL, 0) == UINT32_MAX, "terminator_target null"); + CHECK(circuit_cfg_terminator_condition(NULL, 0) == NULL, "terminator_condition null"); + CHECK(circuit_cfg_terminator_for_info(NULL, 0, &var, &start, &stop, &step) == -1, + "for_info null"); + CHECK(circuit_cfg_outgoing_edges_len(NULL, 0) == 0, "edges_len null"); + CHECK(circuit_cfg_outgoing_edges(NULL, 0, NULL, NULL, NULL, NULL, 0) == -1, "edges null"); + CHECK(circuit_cfg_region_tag(NULL, 0) == -1, "region_tag null"); + CHECK(circuit_cfg_is_loop_header(NULL, 0) == -1, "is_loop_header null"); + CHECK(circuit_cfg_region_if(NULL, 0, &entry, &entry, &entry, NULL) == -1, "region_if null"); + CHECK(circuit_cfg_region_loop(NULL, 0, &entry, &entry) == -1, "region_loop null"); + CHECK(circuit_cfg_region_switch_cases_len(NULL, 0) == 0, "cases_len null"); + CHECK(circuit_cfg_region_switch(NULL, 0, NULL, NULL, NULL, 0, &entry, &entry, NULL) == -1, + "region_switch null"); + CHECK(circuit_cfg_set_terminator_jump(NULL, 0, 0) == -1, "set jump null"); + CHECK(circuit_cfg_set_terminator_return(NULL, 0) == -1, "set return null"); + circuit_cfg_free(NULL); + CHECK(1, "cfg_free null"); + + /* Out-of-range and sentinel node ids fail every block-level query. */ + struct CCircuitCFG* cfg = circuit_cfg_new(1); + CHECK(circuit_cfg_add_block(cfg) == 0, "single block fixture"); + const uint32_t invalid[2] = {7, UINT32_MAX}; + for (int i = 0; i < 2; i++) { + uint32_t bad = invalid[i]; + CHECK(circuit_cfg_block_is_empty(cfg, bad) == -2, "is_empty bad node"); + CHECK(circuit_cfg_block_label(cfg, bad) == NULL, "label bad node"); + CHECK(circuit_cfg_block_operations_len(cfg, bad) == 0, "ops len bad node"); + CHECK(circuit_cfg_block_has_terminator(cfg, bad) == -2, "has_terminator bad node"); + CHECK(circuit_cfg_block_terminator_tag(cfg, bad) == -2, "tag bad node"); + CHECK(circuit_cfg_terminator_target(cfg, bad) == UINT32_MAX, "target bad node"); + CHECK(circuit_cfg_terminator_condition(cfg, bad) == NULL, "condition bad node"); + CHECK(circuit_cfg_block_operations(cfg, bad, NULL, 1) == -2, "block ops bad node"); + CHECK(circuit_cfg_outgoing_edges_len(cfg, bad) == 0, "edges len bad node"); + CHECK(circuit_cfg_outgoing_edges(cfg, bad, NULL, NULL, NULL, NULL, 1) == -2, + "edges bad node"); + CHECK(circuit_cfg_region_tag(cfg, bad) == -2, "region tag bad node"); + CHECK(circuit_cfg_is_loop_header(cfg, bad) == -2, "loop header bad node"); + CHECK(circuit_cfg_region_if(cfg, bad, &entry, &entry, &entry, NULL) == -2, + "region_if bad node"); + CHECK(circuit_cfg_region_loop(cfg, bad, &entry, &entry) == -2, "region_loop bad node"); + CHECK(circuit_cfg_region_switch_cases_len(cfg, bad) == 0, "cases len bad node"); + CHECK(circuit_cfg_region_switch(cfg, bad, NULL, NULL, NULL, 0, &entry, &entry, NULL) == -2, + "region_switch bad node"); + CHECK(circuit_cfg_push_operation(cfg, bad, op) == -2, "push bad node"); + CHECK(circuit_cfg_set_terminator_return(cfg, bad) == -2, "set return bad node"); + CHECK(circuit_cfg_set_while_region(cfg, bad, 0, 0, NULL) == -2, "set while bad node"); + } + CHECK(circuit_cfg_set_entry_block(cfg, UINT32_MAX) == -2, "set_entry sentinel"); + + circuit_cfg_free(cfg); + operation_free(op); +} + +int main(void) { + test_cfg_from_circuit_roundtrip(); + test_cfg_layout_and_blocks(); + test_cfg_terminators(); + test_cfg_classical_state(); + test_cfg_loop_regions(); + test_cfg_null_and_error_paths(); + + printf("\n%d failure(s)\n", g_failures); + return (g_failures == 0) ? 0 : 1; +} diff --git a/crates/binding-c/tests/test_circuit_classical.c b/crates/binding-c/tests/test_circuit_classical.c new file mode 100644 index 00000000..db624d74 --- /dev/null +++ b/crates/binding-c/tests/test_circuit_classical.c @@ -0,0 +1,230 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +/* + * Smoke test for the classical variable / expression C ABI (checklist 1.1 + 1.2). + * + * Compile (msvc): cl /I ../../crates/binding-c/include test_circuit_classical.c /link cqlib_c.lib + * Compile (gcc): gcc -I ../../crates/binding-c/include test_circuit_classical.c -L /debug + * -lcqlib_c -o test_circuit_classical + */ + +#include +#include + +#include "cqlib_c.h" + +static void test_classical_vars_and_measure(void) { + CCircuit* circuit = circuit_new(3); + assert(circuit != NULL); + + /* Allocate classical variables of every type. */ + CClassicalVar* bit = circuit_var(circuit, CQLIB_CLASSICAL_TYPE_BIT, 0); + CClassicalVar* flag = circuit_var(circuit, CQLIB_CLASSICAL_TYPE_BOOL, 0); + CClassicalVar* counter = circuit_var(circuit, CQLIB_CLASSICAL_TYPE_UINT, 8); + CClassicalVar* register_var = circuit_var(circuit, CQLIB_CLASSICAL_TYPE_BIT_VEC, 2); + assert(bit && flag && counter && register_var); + + assert(classical_var_id(bit) == 0); + assert(classical_var_index(register_var) == 3); + + uint32_t tag = 99, width = 99; + assert(classical_var_ty(counter, &tag, &width) == 0); + assert(tag == CQLIB_CLASSICAL_TYPE_UINT && width == 8); + + /* Invalid type parameters are rejected. */ + assert(circuit_var(circuit, 99, 4) == NULL); + assert(circuit_var(circuit, CQLIB_CLASSICAL_TYPE_UINT, 0) == NULL); + assert(circuit_var(NULL, CQLIB_CLASSICAL_TYPE_BIT, 0) == NULL); + + /* Type table snapshot (two-step). */ + assert(circuit_classical_vars_len(circuit) == 4); + CClassicalType types[4]; + assert(circuit_classical_vars(circuit, types, 4) == 0); + assert(types[0].tag == CQLIB_CLASSICAL_TYPE_BIT && types[0].width == 1); + assert(types[2].tag == CQLIB_CLASSICAL_TYPE_UINT && types[2].width == 8); + assert(types[3].tag == CQLIB_CLASSICAL_TYPE_BIT_VEC && types[3].width == 2); + assert(circuit_classical_vars(circuit, types, 3) == -8); + + /* Multi-qubit measurement returns a BitVec value expression. */ + uint32_t qubits[2] = {0, 1}; + CClassicalExpr* measured = circuit_measure_bits(circuit, qubits, 2); + assert(measured != NULL); + assert(classical_expr_ty(measured, &tag, &width) == 0); + assert(tag == CQLIB_CLASSICAL_TYPE_BIT_VEC && width == 2); + assert(classical_expr_values_len(measured) == 1); + assert(classical_expr_vars_len(measured) == 0); + CClassicalValueInfo values[1]; + assert(classical_expr_values(measured, values, 1) == 0); + assert(values[0].index == 0 && values[0].tag == CQLIB_CLASSICAL_TYPE_BIT_VEC); + assert(circuit_validate_classical_expr(circuit, measured) == 0); + + /* Measure into a Bit variable: appends measure + store. */ + CClassicalExpr* stored_bit = circuit_measure_into(circuit, 2, bit); + assert(stored_bit != NULL); + assert(circuit_num_operations(circuit) == 3); /* measure_bits + measure + store */ + + /* Measure bits into a matching-width BitVec variable. */ + CClassicalExpr* stored_vec = circuit_measure_bits_into(circuit, qubits, 2, register_var); + assert(stored_vec != NULL); + assert(circuit_num_operations(circuit) == 5); + + /* Error paths. */ + assert(circuit_measure_bits(circuit, NULL, 0) == NULL); + uint32_t oob[1] = {9}; + assert(circuit_measure_bits(circuit, oob, 1) == NULL); + assert(circuit_measure_into(circuit, 0, flag) == NULL); /* Bool target */ + + /* Validation. */ + assert(circuit_validate_classical_var(circuit, bit) == 0); + assert(circuit_validate_classical_var(NULL, bit) == -1); + assert(circuit_validate(circuit) == 0); + + classical_expr_free(stored_bit); + classical_expr_free(stored_vec); + classical_expr_free(measured); + classical_var_free(bit); + classical_var_free(flag); + classical_var_free(counter); + classical_var_free(register_var); + circuit_free(circuit); +} + +static void test_expressions_and_store(void) { + CCircuit* circuit = circuit_new(2); + assert(circuit != NULL); + + /* Literals. */ + CClassicalExpr* bit_true = classical_expr_bit_literal(true); + CClassicalExpr* bool_true = classical_expr_bool_literal(true); + CClassicalExpr* bool_false = classical_expr_bool_literal(false); + CClassicalExpr* ten = classical_expr_uint_literal(8, 10, 0); + CClassicalExpr* twenty = classical_expr_uint_literal(8, 20, 0); + assert(bit_true && bool_true && bool_false && ten && twenty); + + uint32_t kind = 99, tag = 99, width = 99; + assert(classical_expr_kind(bool_true, &kind) == 0); + assert(kind == CQLIB_CLASSICAL_EXPR_BOOL_LITERAL); + assert(classical_expr_ty(ten, &tag, &width) == 0); + assert(tag == CQLIB_CLASSICAL_TYPE_UINT && width == 8); + assert(classical_expr_uint_literal(8, 256, 0) == NULL); /* value overflow */ + assert(classical_expr_uint_literal(0, 0, 0) == NULL); /* zero width */ + + /* Bit <-> Bool conversion and logic. */ + CClassicalExpr* cond = classical_expr_bit_to_bool(bit_true); + assert(cond != NULL); + CClassicalExpr* not_cond = classical_expr_not(cond); + assert(not_cond != NULL); + CClassicalExpr* both = classical_expr_and(bool_true, bool_false); + assert(both != NULL); + assert(classical_expr_and(bool_true, bit_true) == NULL); /* type mismatch */ + assert(classical_expr_or(bool_true, NULL) == NULL); + + /* Comparisons on UInt operands produce Bool. */ + CClassicalExpr* lt = classical_expr_lt(ten, twenty); + assert(lt != NULL); + assert(classical_expr_ty(lt, &tag, &width) == 0); + assert(tag == CQLIB_CLASSICAL_TYPE_BOOL); + assert(classical_expr_gt(bool_true, bool_false) == NULL); /* Bool operands */ + + /* Select / extract / pack / concat / cast. */ + CClassicalExpr* chosen = classical_expr_select(bool_true, ten, twenty); + assert(chosen != NULL); + CClassicalExpr* lsb = classical_expr_extract_bit(ten, 0); + assert(lsb != NULL); + assert(classical_expr_extract_bit(ten, 8) == NULL); /* out of bounds */ + CClassicalExpr* nibble = classical_expr_extract_bits(ten, 4, 4); + assert(nibble != NULL); + + const CClassicalExpr* bits[2] = {bit_true, bit_true}; + CClassicalExpr* packed = classical_expr_pack_bits(bits, 2); + assert(packed != NULL); + assert(classical_expr_ty(packed, &tag, &width) == 0); + assert(tag == CQLIB_CLASSICAL_TYPE_BIT_VEC && width == 2); + + const CClassicalExpr* parts[2] = {packed, bit_true}; + CClassicalExpr* concatenated = classical_expr_concat(parts, 2); + assert(concatenated != NULL); + assert(classical_expr_ty(concatenated, &tag, &width) == 0); + assert(tag == CQLIB_CLASSICAL_TYPE_BIT_VEC && width == 3); + + CClassicalExpr* as_uint = classical_expr_bit_vec_to_uint(packed); + assert(as_uint != NULL); + + /* Simplification: and(b, true) folds to b. */ + CClassicalExpr* simplified = classical_expr_simplified(both); + assert(simplified != NULL); + assert(classical_expr_kind(simplified, &kind) == 0); + assert(kind == CQLIB_CLASSICAL_EXPR_BOOL_LITERAL); + + /* Store: Bool literal into a Bool variable. */ + CClassicalVar* flag = circuit_var(circuit, CQLIB_CLASSICAL_TYPE_BOOL, 0); + assert(flag != NULL); + assert(circuit_store(circuit, flag, bool_true) == 0); + assert(circuit_num_operations(circuit) == 1); + + /* Store a measured bit converted to Bool. */ + CClassicalVar* bit_target = circuit_var(circuit, CQLIB_CLASSICAL_TYPE_BIT, 0); + CClassicalExpr* measured = circuit_measure_into(circuit, 0, bit_target); + assert(measured != NULL); + CClassicalExpr* condition = classical_expr_to_bool(measured); + assert(condition != NULL); + assert(circuit_store(circuit, flag, condition) == 0); + assert(circuit_num_operations(circuit) == 4); + assert(circuit_validate(circuit) == 0); + + /* Store error paths. */ + assert(circuit_store(NULL, flag, bool_true) == -1); + assert(circuit_store(circuit, NULL, bool_true) == -1); + assert(circuit_store(circuit, flag, NULL) == -1); + assert(circuit_store(circuit, bit_target, bool_true) == -3); /* type mismatch */ + + /* vars collection returns owned handles. */ + CClassicalExpr* flag_expr = classical_var_expr(flag); + assert(flag_expr != NULL); + assert(classical_expr_vars_len(flag_expr) == 1); + CClassicalVar* collected[1]; + assert(classical_expr_vars(flag_expr, collected, 1) == 0); + assert(classical_var_id(collected[0]) == 0); + assert(circuit_validate_classical_var(circuit, collected[0]) == 0); + + classical_var_free(collected[0]); + classical_expr_free(flag_expr); + classical_expr_free(condition); + classical_expr_free(measured); + classical_expr_free(simplified); + classical_expr_free(as_uint); + classical_expr_free(concatenated); + classical_expr_free(packed); + classical_expr_free(nibble); + classical_expr_free(lsb); + classical_expr_free(chosen); + classical_expr_free(lt); + classical_expr_free(both); + classical_expr_free(not_cond); + classical_expr_free(cond); + classical_expr_free(bit_true); + classical_expr_free(bool_true); + classical_expr_free(bool_false); + classical_expr_free(ten); + classical_expr_free(twenty); + classical_var_free(flag); + classical_var_free(bit_target); + circuit_free(circuit); +} + +int main(void) { + test_classical_vars_and_measure(); + test_expressions_and_store(); + printf("binding-c circuit classical tests passed\n"); + return 0; +} diff --git a/crates/binding-c/tests/test_circuit_control.c b/crates/binding-c/tests/test_circuit_control.c new file mode 100644 index 00000000..47f2170e --- /dev/null +++ b/crates/binding-c/tests/test_circuit_control.c @@ -0,0 +1,107 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +/* + * Smoke tests for the callback-based classical control-flow builders. + */ + +#include +#include + +#include "cqlib_c.h" + +typedef struct { + int calls; +} BodyCtx; + +static int add_x_body(CCircuit* circuit, void* user_data) { + BodyCtx* ctx = (BodyCtx*)user_data; + ctx->calls += 1; + return circuit_x(circuit, 1); +} + +static int measure_then_break_body(CCircuit* circuit, void* user_data) { + BodyCtx* ctx = (BodyCtx*)user_data; + ctx->calls += 1; + + int rc = circuit_measure(circuit, 0); + if (rc != 0) { + return rc; + } + rc = circuit_x(circuit, 1); + if (rc != 0) { + return rc; + } + return circuit_break_loop(circuit); +} + +static void test_if_with_callback(void) { + CCircuit* circuit = circuit_new(2); + assert(circuit != NULL); + + CClassicalExpr* cond = classical_expr_bool_literal(true); + assert(cond != NULL); + + BodyCtx ctx = {0}; + assert(circuit_if(circuit, cond, add_x_body, &ctx) == 0); + assert(ctx.calls == 1); + /* Only the control operation itself is visible at the top level. */ + assert(circuit_num_operations(circuit) == 1); + /* Recursive depth: control op boundary + body x(1) -> 1 + 1 = 2. */ + assert(circuit_depth(circuit, true) == 2); + assert(circuit_validate(circuit) == 0); + + classical_expr_free(cond); + circuit_free(circuit); +} + +static void test_while_with_loop_variable(void) { + CCircuit* circuit = circuit_new(2); + assert(circuit != NULL); + + CClassicalVar* flag = circuit_var(circuit, CQLIB_CLASSICAL_TYPE_BOOL, 0); + assert(flag != NULL); + CClassicalExpr* cond = classical_var_expr(flag); + assert(cond != NULL); + + BodyCtx ctx = {0}; + assert(circuit_while(circuit, cond, measure_then_break_body, &ctx) == 0); + assert(ctx.calls == 1); + assert(circuit_num_operations(circuit) == 1); + assert(circuit_validate(circuit) == 0); + + classical_expr_free(cond); + classical_var_free(flag); + circuit_free(circuit); +} + +static void test_null_and_scope_errors(void) { + assert(circuit_if(NULL, NULL, NULL, NULL) == -1); + assert(circuit_break_loop(NULL) == -1); + assert(circuit_continue_loop(NULL) == -1); + assert(circuit_append_control(NULL, NULL, 0) == -1); + + CCircuit* circuit = circuit_new(1); + assert(circuit != NULL); + /* break/continue outside any loop body are rejected */ + assert(circuit_break_loop(circuit) == -3); + assert(circuit_continue_loop(circuit) == -3); + circuit_free(circuit); +} + +int main(void) { + test_if_with_callback(); + test_while_with_loop_variable(); + test_null_and_scope_errors(); + printf("binding-c circuit control-flow tests passed\n"); + return 0; +} diff --git a/crates/binding-c/tests/test_circuit_dag.c b/crates/binding-c/tests/test_circuit_dag.c new file mode 100644 index 00000000..3aa52abd --- /dev/null +++ b/crates/binding-c/tests/test_circuit_dag.c @@ -0,0 +1,530 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +#include +#include +#include +#include +#include + +#include "cqlib_c.h" + +static int g_failures = 0; + +#define CHECK(cond, name) \ + do { \ + if (cond) { \ + printf("ok: %s\n", (name)); \ + } else { \ + printf("FAIL: %s\n", (name)); \ + g_failures++; \ + } \ + } while (0) + +static void test_dag_from_circuit(void) { + struct CCircuit* circuit = circuit_new(2); + CHECK(circuit != NULL, "dag fixture circuit_new"); + CHECK(circuit_h(circuit, 0) == 0, "dag fixture circuit_h"); + CHECK(circuit_cx(circuit, 0, 1) == 0, "dag fixture circuit_cx"); + CHECK(circuit_num_operations(circuit) == 2, "dag fixture two ops"); + + struct CCircuitDag* dag = circuit_dag_from_circuit(circuit); + CHECK(dag != NULL, "circuit_dag_from_circuit"); + CHECK(circuit_dag_validate(dag) == 0, "circuit_dag_validate"); + CHECK(circuit_dag_is_empty(dag) == 0, "circuit_dag_is_empty"); + CHECK(circuit_dag_num_qubits(dag) == 2, "circuit_dag_num_qubits"); + CHECK(circuit_dag_num_ops(dag) == 2, "circuit_dag_num_ops"); + CHECK(circuit_dag_qubits_len(dag) == 2, "circuit_dag_qubits_len"); + + uint32_t ids[2] = {9, 9}; + CHECK(circuit_dag_qubits(dag, ids, 2) == 0, "circuit_dag_qubits"); + CHECK(ids[0] == 0 && ids[1] == 1, "circuit_dag_qubits values"); + + /* Wire inventory: the global-order resource first, then qubit timelines. */ + CHECK(circuit_dag_wires_len(dag) == 3, "circuit_dag_wires_len"); + uint32_t tags[3] = {99, 99, 99}; + uint32_t wire_ids[3] = {99, 99, 99}; + CHECK(circuit_dag_wires(dag, tags, wire_ids, 3) == 0, "circuit_dag_wires"); + CHECK( + tags[0] == DAG_WIRE_GLOBAL_ORDER && tags[1] == DAG_WIRE_QUBIT && tags[2] == DAG_WIRE_QUBIT, + "circuit_dag_wires tags"); + CHECK(wire_ids[0] == 0 && wire_ids[1] == 0 && wire_ids[2] == 1, "circuit_dag_wires ids"); + + /* Wire membership probes. */ + CHECK(circuit_dag_has_wire(dag, DAG_WIRE_QUBIT, 0) == 1, "circuit_dag_has_wire qubit 0"); + CHECK(circuit_dag_has_wire(dag, DAG_WIRE_QUBIT, 1) == 1, "circuit_dag_has_wire qubit 1"); + CHECK(circuit_dag_has_wire(dag, DAG_WIRE_QUBIT, 5) == 0, "circuit_dag_has_wire missing qubit"); + CHECK(circuit_dag_has_wire(dag, DAG_WIRE_GLOBAL_ORDER, 0) == 1, + "circuit_dag_has_wire global order"); + + /* Round trip back to a circuit of identical shape. The source handle + * stays alive and unchanged. */ + struct CCircuit* lowered = circuit_dag_to_circuit(dag); + CHECK(lowered != NULL, "circuit_dag_to_circuit"); + CHECK(circuit_num_qubits(lowered) == 2, "lowered num_qubits"); + CHECK(circuit_num_operations(lowered) == 2, "lowered num_operations"); + + circuit_free(lowered); + circuit_dag_free(dag); + CHECK(circuit_num_operations(circuit) == 2, "source circuit unchanged"); + circuit_free(circuit); +} + +static void test_dag_from_operations(void) { + const uint32_t zero[1] = {0}; + const uint32_t one[1] = {1}; + const uint32_t pair[2] = {0, 1}; + const double half[1] = {0.5}; + struct COperation* rx = operation_new("RX", zero, 1, half, 1); + struct COperation* cx = operation_new("CX", pair, 2, NULL, 0); + struct COperation* h = operation_new("H", one, 1, NULL, 0); + CHECK(rx != NULL && cx != NULL && h != NULL, "dag ops fixture"); + + const uint32_t qubits[2] = {0, 1}; + const struct COperation* ops[3]; + ops[0] = rx; + ops[1] = cx; + ops[2] = h; + struct CCircuitDag* dag = circuit_dag_from_operations(qubits, 2, ops, 3); + CHECK(dag != NULL, "circuit_dag_from_operations"); + CHECK(circuit_dag_validate(dag) == 0, "from_operations validate"); + CHECK(circuit_dag_num_qubits(dag) == 2, "from_operations num_qubits"); + CHECK(circuit_dag_num_ops(dag) == 3, "from_operations num_ops"); + CHECK(circuit_dag_is_empty(dag) == 0, "from_operations is_empty"); + + /* The narrow constructor has no parameter table: numeric angles yield + * neither parameters nor symbols. */ + CHECK(circuit_dag_parameters_len(dag) == 0, "from_operations parameters_len"); + CHECK(circuit_dag_symbols_len(dag) == 0, "from_operations symbols_len"); + + /* Round trip keeps all three operations. */ + struct CCircuit* lowered = circuit_dag_to_circuit(dag); + CHECK(lowered != NULL, "from_operations to_circuit"); + CHECK(circuit_num_qubits(lowered) == 2, "from_operations lowered qubits"); + CHECK(circuit_num_operations(lowered) == 3, "from_operations lowered ops"); + + circuit_free(lowered); + circuit_dag_free(dag); + operation_free(rx); + operation_free(cx); + operation_free(h); +} + +static void test_dag_builder_rejects_invalid_inputs(void) { + const uint32_t zero[1] = {0}; + struct COperation* h = operation_new("H", zero, 1, NULL, 0); + CHECK(h != NULL, "dag builder fixture"); + const struct COperation* ops[1]; + ops[0] = h; + + /* NULL qubit buffer with a non-zero length. */ + CHECK(circuit_dag_from_operations(NULL, 1, NULL, 0) == NULL, "from_operations null qubits"); + + /* NULL op buffer with a non-zero length. */ + const uint32_t qubits[1] = {0}; + CHECK(circuit_dag_from_operations(qubits, 1, NULL, 1) == NULL, "from_operations null ops"); + + /* Duplicate qubit ids fail the build. */ + const uint32_t dup[2] = {3, 3}; + CHECK(circuit_dag_from_operations(dup, 2, ops, 1) == NULL, "from_operations duplicate qubits"); + + /* Operations acting on qubits that were never registered. */ + const uint32_t sparse[2] = {7, 8}; + CHECK(circuit_dag_from_operations(sparse, 2, ops, 1) == NULL, + "from_operations unregistered qubits"); + + /* NULL entries inside the op pointer list. */ + const struct COperation* mixed[2]; + mixed[0] = h; + mixed[1] = NULL; + CHECK(circuit_dag_from_operations(qubits, 1, mixed, 2) == NULL, + "from_operations null op entry"); + + /* Building from a NULL circuit is rejected too. */ + CHECK(circuit_dag_from_circuit(NULL) == NULL, "from_circuit null"); + + operation_free(h); +} + +static void test_dag_parameters_and_symbols(void) { + struct CParameter* theta = param_parse("theta"); + CHECK(theta != NULL, "symbolic fixture param_parse"); + + struct CCircuit* circuit = circuit_new(1); + CHECK(circuit_rx_param(circuit, 0, theta) == 0, "circuit_rx_param"); + CHECK(circuit_num_parameters(circuit) == 1, "circuit_num_parameters"); + + struct CCircuitDag* dag = circuit_dag_from_circuit(circuit); + CHECK(dag != NULL, "symbolic dag from_circuit"); + + /* Parameter names in table order. */ + CHECK(circuit_dag_parameters_len(dag) == 1, "circuit_dag_parameters_len"); + char* names[1] = {NULL}; + CHECK(circuit_dag_parameters(dag, names, 1) == 0, "circuit_dag_parameters"); + CHECK(names[0] != NULL && strcmp(names[0], "theta") == 0, "circuit_dag_parameters value"); + cqlib_string_free(names[0]); + + /* Free-symbol inventory mirrors the parameter expression. */ + CHECK(circuit_dag_symbols_len(dag) == 1, "circuit_dag_symbols_len"); + char* symbols[1] = {NULL}; + CHECK(circuit_dag_symbols(dag, symbols, 1) == 0, "circuit_dag_symbols"); + CHECK(symbols[0] != NULL && strcmp(symbols[0], "theta") == 0, "circuit_dag_symbols value"); + cqlib_string_free(symbols[0]); + + /* Undersized or NULL output buffers. */ + CHECK(circuit_dag_parameters(dag, names, 0) == -8, "circuit_dag_parameters short buffer"); + CHECK(circuit_dag_parameters(dag, NULL, 1) == -1, "circuit_dag_parameters null buffer"); + CHECK(circuit_dag_symbols(dag, symbols, 0) == -8, "circuit_dag_symbols short buffer"); + CHECK(circuit_dag_symbols(dag, NULL, 1) == -1, "circuit_dag_symbols null buffer"); + + circuit_dag_free(dag); + circuit_free(circuit); + param_free(theta); +} + +static void test_dag_empty_and_null_paths(void) { + /* Every exported function tolerates a NULL handle. */ + CHECK(circuit_dag_from_circuit(NULL) == NULL, "from_circuit(null)"); + CHECK(circuit_dag_to_circuit(NULL) == NULL, "to_circuit(null)"); + CHECK(circuit_dag_validate(NULL) == -1, "validate(null)"); + CHECK(circuit_dag_is_empty(NULL) == -1, "is_empty(null)"); + CHECK(circuit_dag_num_qubits(NULL) == 0, "num_qubits(null)"); + CHECK(circuit_dag_num_ops(NULL) == 0, "num_ops(null)"); + CHECK(circuit_dag_qubits_len(NULL) == 0, "qubits_len(null)"); + CHECK(circuit_dag_parameters_len(NULL) == 0, "parameters_len(null)"); + CHECK(circuit_dag_symbols_len(NULL) == 0, "symbols_len(null)"); + CHECK(circuit_dag_wires_len(NULL) == 0, "wires_len(null)"); + CHECK(circuit_dag_qubits(NULL, NULL, 0) == -1, "qubits(null)"); + CHECK(circuit_dag_parameters(NULL, NULL, 0) == -1, "parameters(null)"); + CHECK(circuit_dag_symbols(NULL, NULL, 0) == -1, "symbols(null)"); + CHECK(circuit_dag_wires(NULL, NULL, NULL, 0) == -1, "wires(null)"); + CHECK(circuit_dag_has_wire(NULL, DAG_WIRE_QUBIT, 0) == -1, "has_wire(null)"); + circuit_dag_free(NULL); + CHECK(1, "circuit_dag_free(null)"); + + /* An empty DAG carries only the global-order wire. */ + struct CCircuitDag* dag = circuit_dag_from_operations(NULL, 0, NULL, 0); + CHECK(dag != NULL, "empty dag from_operations"); + CHECK(circuit_dag_validate(dag) == 0, "empty dag validate"); + CHECK(circuit_dag_is_empty(dag) == 1, "empty dag is_empty"); + CHECK(circuit_dag_num_qubits(dag) == 0, "empty dag num_qubits"); + CHECK(circuit_dag_num_ops(dag) == 0, "empty dag num_ops"); + CHECK(circuit_dag_qubits_len(dag) == 0, "empty dag qubits_len"); + CHECK(circuit_dag_wires_len(dag) == 1, "empty dag wires_len"); + CHECK(circuit_dag_has_wire(dag, DAG_WIRE_GLOBAL_ORDER, 0) == 1, + "empty dag has_wire(global order)"); + CHECK(circuit_dag_has_wire(dag, DAG_WIRE_QUBIT, 0) == 0, "empty dag has_wire(qubit)"); + + /* Unknown or unsupported wire tags are rejected, not dereferenced. */ + CHECK(circuit_dag_has_wire(dag, DAG_WIRE_CLASSICAL_VAR, 0) == -8, "has_wire classical var tag"); + CHECK(circuit_dag_has_wire(dag, 99, 0) == -8, "has_wire unknown tag"); + + /* Wire buffers: undersized then NULL. */ + uint32_t tags[1] = {99}; + uint32_t ids[1] = {99}; + CHECK(circuit_dag_wires(dag, tags, ids, 0) == -8, "wires short buffer"); + CHECK(circuit_dag_wires(dag, NULL, ids, 1) == -1, "wires null tags"); + CHECK(circuit_dag_wires(dag, tags, NULL, 1) == -1, "wires null ids"); + CHECK(circuit_dag_wires(dag, tags, ids, 1) == 0, "wires copy"); + CHECK(tags[0] == DAG_WIRE_GLOBAL_ORDER, "wires tag value"); + CHECK(ids[0] == 0, "wires id value"); + + /* A qubit buffer is fine to omit when the DAG has no qubits; the empty + * parameter/symbol readers also accept len = 0 with no buffer. */ + CHECK(circuit_dag_qubits(dag, NULL, 0) == 0, "empty dag qubits copy"); + CHECK(circuit_dag_parameters(dag, NULL, 0) == 0, "empty dag parameters copy"); + CHECK(circuit_dag_symbols(dag, NULL, 0) == 0, "empty dag symbols copy"); + + circuit_dag_free(dag); +} + +/* Looks up the ASAP layer index of `node` in a (node, layer) pair table. */ +static int node_layer_of(const uint32_t* nodes, const uintptr_t* layers, uintptr_t len, + uint32_t node) { + for (uintptr_t i = 0; i < len; i++) { + if (nodes[i] == node) { + return (int)layers[i]; + } + } + return -1; +} + +static void test_dag_node_identification_and_traversal(void) { + /* H(0) -> CX(0,1) -> H(1): a chain on qubit 0 with a tail on qubit 1. */ + struct CCircuit* circuit = circuit_new(2); + CHECK(circuit_h(circuit, 0) == 0, "traversal fixture h0"); + CHECK(circuit_cx(circuit, 0, 1) == 0, "traversal fixture cx"); + CHECK(circuit_h(circuit, 1) == 0, "traversal fixture h1"); + + struct CCircuitDag* dag = circuit_dag_from_circuit(circuit); + CHECK(dag != NULL, "traversal dag from_circuit"); + CHECK(circuit_dag_depth(dag) == 3, "circuit_dag_depth chain"); + + /* Operation nodes in source order; a too-small buffer reports -8. */ + CHECK(circuit_dag_op_nodes_len(dag) == 3, "circuit_dag_op_nodes_len"); + uint32_t ops[3] = {9, 9, 9}; + CHECK(circuit_dag_op_nodes(dag, ops, 3) == 0, "circuit_dag_op_nodes"); + CHECK(circuit_dag_op_nodes(dag, ops, 2) == -8, "circuit_dag_op_nodes short buffer"); + + /* Every op node reports the operation kind; the middle node holds CX. */ + for (int i = 0; i < 3; i++) { + CHECK(circuit_dag_is_operation(dag, ops[i]) == 1, "circuit_dag_is_operation"); + char* kind = circuit_dag_node_kind(dag, ops[i]); + CHECK(kind != NULL && strcmp(kind, "operation") == 0, "circuit_dag_node_kind op"); + cqlib_string_free(kind); + } + struct COperation* op = circuit_dag_operation(dag, ops[1]); + CHECK(op != NULL, "circuit_dag_operation"); + char* name = operation_name(op); + CHECK(name != NULL && strcmp(name, "CX") == 0, "circuit_dag_operation name"); + cqlib_string_free(name); + operation_free(op); + + /* Topological order matches source order for this chain. */ + CHECK(circuit_dag_topological_op_nodes_len(dag) == 3, "circuit_dag_topological_op_nodes_len"); + uint32_t topo[3] = {9, 9, 9}; + CHECK(circuit_dag_topological_op_nodes(dag, topo, 3) == 0, "circuit_dag_topological_op_nodes"); + CHECK(topo[0] == ops[0] && topo[1] == ops[1] && topo[2] == ops[2], + "topological order matches source order"); + + /* Predecessors of CX: H(0) plus the qubit-1 input sentinel; quantum + * neighbors filter the sentinel out. */ + CHECK(circuit_dag_predecessors_len(dag, ops[1]) == 2, "circuit_dag_predecessors_len"); + uint32_t preds[2] = {9, 9}; + CHECK(circuit_dag_predecessors(dag, ops[1], preds, 2) == 0, "circuit_dag_predecessors"); + CHECK(preds[0] == ops[0] || preds[1] == ops[0], "predecessors contain H(0)"); + + CHECK(circuit_dag_quantum_predecessors_len(dag, ops[1]) == 1, + "circuit_dag_quantum_predecessors_len"); + uint32_t qpreds[1] = {9}; + CHECK(circuit_dag_quantum_predecessors(dag, ops[1], qpreds, 1) == 0, + "circuit_dag_quantum_predecessors"); + CHECK(qpreds[0] == ops[0], "quantum predecessor is H(0)"); + + /* Successors of CX: H(1) plus the qubit-0 output sentinel. */ + CHECK(circuit_dag_successors_len(dag, ops[1]) == 2, "circuit_dag_successors_len"); + uint32_t succs[2] = {9, 9}; + CHECK(circuit_dag_successors(dag, ops[1], succs, 2) == 0, "circuit_dag_successors"); + CHECK(succs[0] == ops[2] || succs[1] == ops[2], "successors contain H(1)"); + + /* Unknown node ids are reported, not dereferenced. */ + CHECK(circuit_dag_is_operation(dag, 999) == -2, "is_operation unknown node"); + CHECK(circuit_dag_node_kind(dag, 999) == NULL, "node_kind unknown node"); + CHECK(circuit_dag_operation(dag, 999) == NULL, "operation unknown node"); + + circuit_dag_free(dag); + circuit_free(circuit); +} + +static void test_dag_layers_and_depth(void) { + /* Disjoint H(0), H(1) share the front layer; CX depends on both. */ + struct CCircuit* circuit = circuit_new(2); + CHECK(circuit_h(circuit, 0) == 0, "layers fixture h0"); + CHECK(circuit_h(circuit, 1) == 0, "layers fixture h1"); + CHECK(circuit_cx(circuit, 0, 1) == 0, "layers fixture cx"); + + struct CCircuitDag* dag = circuit_dag_from_circuit(circuit); + CHECK(dag != NULL, "layers dag from_circuit"); + CHECK(circuit_dag_depth(dag) == 2, "circuit_dag_depth disjoint"); + CHECK(circuit_dag_depth(NULL) == -1, "circuit_dag_depth null"); + + /* Front layer holds the two disjoint roots. */ + CHECK(circuit_dag_front_layer_len(dag) == 2, "circuit_dag_front_layer_len"); + uint32_t front[2] = {9, 9}; + CHECK(circuit_dag_front_layer(dag, front, 2) == 0, "circuit_dag_front_layer"); + CHECK(circuit_dag_front_layer(dag, front, 1) == -8, "front_layer short buffer"); + + /* ASAP layers in CSR style: two nodes in layer 0, CX in layer 1. */ + CHECK(circuit_dag_layers_len(dag) == 2, "circuit_dag_layers_len"); + uint32_t nodes[3] = {9, 9, 9}; + uintptr_t offsets[3] = {9, 9, 9}; + CHECK(circuit_dag_layers(dag, nodes, 3, offsets, 3) == 0, "circuit_dag_layers"); + CHECK(offsets[0] == 0 && offsets[1] == 2 && offsets[2] == 3, "circuit_dag_layers offsets"); + + /* Per-node layer entries agree: the H nodes sit in layer 0, CX in 1. */ + CHECK(circuit_dag_node_layers_len(dag) == 3, "circuit_dag_node_layers_len"); + uint32_t pair_nodes[3] = {9, 9, 9}; + uintptr_t pair_layers[3] = {9, 9, 9}; + CHECK(circuit_dag_node_layers(dag, pair_nodes, pair_layers, 3) == 0, "circuit_dag_node_layers"); + CHECK(node_layer_of(pair_nodes, pair_layers, 3, front[0]) == 0, "front[0] in layer 0"); + CHECK(node_layer_of(pair_nodes, pair_layers, 3, front[1]) == 0, "front[1] in layer 0"); + CHECK(node_layer_of(pair_nodes, pair_layers, 3, nodes[2]) == 1, "cx in layer 1"); + + circuit_dag_free(dag); + circuit_free(circuit); +} + +static void test_dag_wire_timelines(void) { + /* Only qubit 0 is driven; qubit 1 stays idle. */ + struct CCircuit* circuit = circuit_new(2); + CHECK(circuit_h(circuit, 0) == 0, "wire fixture h"); + + struct CCircuitDag* dag = circuit_dag_from_circuit(circuit); + CHECK(dag != NULL, "wire dag from_circuit"); + + CHECK(circuit_dag_is_wire_idle(dag, DAG_WIRE_QUBIT, 0) == 0, "is_wire_idle driven"); + CHECK(circuit_dag_is_wire_idle(dag, DAG_WIRE_QUBIT, 1) == 1, "is_wire_idle idle"); + CHECK(circuit_dag_is_wire_idle(dag, DAG_WIRE_QUBIT, 5) == -3, "is_wire_idle unknown qubit"); + CHECK(circuit_dag_is_wire_idle(dag, DAG_WIRE_CLASSICAL_VAR, 0) == -8, + "is_wire_idle unsupported tag"); + + /* The qubit-0 timeline holds exactly the H node, in wire order. */ + CHECK(circuit_dag_nodes_on_wire_len(dag, DAG_WIRE_QUBIT, 0) == 1, + "circuit_dag_nodes_on_wire_len"); + uint32_t wire_ops[1] = {9}; + CHECK(circuit_dag_nodes_on_wire(dag, DAG_WIRE_QUBIT, 0, wire_ops, 1) == 0, + "circuit_dag_nodes_on_wire"); + CHECK(circuit_dag_nodes_on_wire(dag, DAG_WIRE_QUBIT, 0, wire_ops, 0) == -8, + "nodes_on_wire short buffer"); + CHECK(circuit_dag_nodes_on_wire_len(dag, DAG_WIRE_QUBIT, 5) == 0, + "nodes_on_wire_len unknown qubit"); + + /* Sentinel endpoints report their node kinds. */ + int32_t input = circuit_dag_wire_in(dag, DAG_WIRE_QUBIT, 0); + int32_t output = circuit_dag_wire_out(dag, DAG_WIRE_QUBIT, 0); + CHECK(input >= 0 && output >= 0 && input != output, "wire sentinels distinct"); + CHECK(circuit_dag_is_operation(dag, (uint32_t)input) == 0, "sentinel is not operation"); + char* kind = circuit_dag_node_kind(dag, (uint32_t)input); + CHECK(kind != NULL && strcmp(kind, "wire_in") == 0, "wire_in kind"); + cqlib_string_free(kind); + kind = circuit_dag_node_kind(dag, (uint32_t)output); + CHECK(kind != NULL && strcmp(kind, "wire_out") == 0, "wire_out kind"); + cqlib_string_free(kind); + CHECK(circuit_dag_wire_in(dag, DAG_WIRE_QUBIT, 5) == -3, "wire_in unknown qubit"); + + circuit_dag_free(dag); + circuit_free(circuit); +} + +static void test_dag_operation_counts_and_measurement_flag(void) { + struct CCircuit* circuit = circuit_new(2); + CHECK(circuit_h(circuit, 0) == 0, "counts fixture h0"); + CHECK(circuit_cx(circuit, 0, 1) == 0, "counts fixture cx"); + CHECK(circuit_h(circuit, 1) == 0, "counts fixture h1"); + CHECK(circuit_measure(circuit, 0) == 0, "counts fixture measure"); + + struct CCircuitDag* dag = circuit_dag_from_circuit(circuit); + CHECK(dag != NULL, "counts dag from_circuit"); + CHECK(circuit_dag_has_measurement(dag) == 1, "circuit_dag_has_measurement"); + CHECK(circuit_dag_has_measurement(NULL) == -1, "has_measurement null"); + CHECK(circuit_dag_has_control_flow(dag) == 0, "circuit_dag_has_control_flow"); + + /* Top-level counts by instruction name, in insertion order. */ + CHECK(circuit_dag_operation_count_by_name_len(dag) == 3, "operation_count_by_name_len"); + char* names[3] = {NULL, NULL, NULL}; + uintptr_t counts[3] = {0, 0, 0}; + CHECK(circuit_dag_operation_count_by_name(dag, names, counts, 3) == 0, + "circuit_dag_operation_count_by_name"); + CHECK(names[0] != NULL && strcmp(names[0], "H") == 0 && counts[0] == 2, "count H"); + CHECK(names[1] != NULL && strcmp(names[1], "CX") == 0 && counts[1] == 1, "count CX"); + CHECK(names[2] != NULL && strcmp(names[2], "measure_bit") == 0 && counts[2] == 1, + "count measure_bit"); + for (int i = 0; i < 3; i++) { + cqlib_string_free(names[i]); + } + + /* NULL and undersized output buffers. */ + CHECK(circuit_dag_operation_count_by_name(dag, NULL, counts, 3) == -1, "count null names"); + CHECK(circuit_dag_operation_count_by_name(dag, names, counts, 2) == -8, "count short buffer"); + + circuit_dag_free(dag); + circuit_free(circuit); +} + +static void test_dag_mutations(void) { + const uint32_t zero[1] = {0}; + const uint32_t one[1] = {1}; + struct COperation* z_op = operation_new("Z", zero, 1, NULL, 0); + struct COperation* x_op = operation_new("X", one, 1, NULL, 0); + CHECK(z_op != NULL && x_op != NULL, "mutation op fixtures"); + + struct CCircuit* circuit = circuit_new(2); + CHECK(circuit_h(circuit, 0) == 0, "mutation fixture h"); + CHECK(circuit_cx(circuit, 0, 1) == 0, "mutation fixture cx"); + + struct CCircuitDag* dag = circuit_dag_from_circuit(circuit); + CHECK(dag != NULL, "mutation dag from_circuit"); + CHECK(circuit_dag_num_ops(dag) == 2, "mutation initial num_ops"); + + /* Append Z(0) at the back: it lands last in source order. */ + uint32_t appended = 9; + CHECK(circuit_dag_apply_operation_back(dag, z_op, &appended) == 0, + "circuit_dag_apply_operation_back"); + CHECK(circuit_dag_num_ops(dag) == 3, "num_ops after apply back"); + uint32_t ops[3] = {9, 9, 9}; + CHECK(circuit_dag_op_nodes(dag, ops, 3) == 0, "op_nodes after apply back"); + CHECK(ops[2] == appended, "appended node lands last"); + + /* Prepend X(1) at the front: it lands first in source order. */ + uint32_t prepended = 9; + CHECK(circuit_dag_apply_operation_front(dag, x_op, &prepended) == 0, + "circuit_dag_apply_operation_front"); + CHECK(circuit_dag_num_ops(dag) == 4, "num_ops after apply front"); + uint32_t ops4[4] = {9, 9, 9, 9}; + CHECK(circuit_dag_op_nodes(dag, ops4, 4) == 0, "op_nodes after apply front"); + CHECK(ops4[0] == prepended, "prepended node lands first"); + + /* Remove the front node: the removed operation comes back out. */ + struct COperation* removed = circuit_dag_remove_op_node(dag, prepended); + CHECK(removed != NULL, "circuit_dag_remove_op_node"); + char* name = operation_name(removed); + CHECK(name != NULL && strcmp(name, "X") == 0, "removed operation is X"); + cqlib_string_free(name); + operation_free(removed); + CHECK(circuit_dag_num_ops(dag) == 3, "num_ops after remove"); + + /* Substitute the leading H with Z(0); the node id may be reassigned by + * the rebuild, so re-read the position. */ + uint32_t ops3[3] = {9, 9, 9}; + CHECK(circuit_dag_op_nodes(dag, ops3, 3) == 0, "op_nodes before substitute"); + CHECK(circuit_dag_substitute_node(dag, ops3[0], z_op) == 0, "circuit_dag_substitute_node"); + CHECK(circuit_dag_num_ops(dag) == 3, "num_ops after substitute"); + CHECK(circuit_dag_op_nodes(dag, ops3, 3) == 0, "op_nodes after substitute"); + struct COperation* lead = circuit_dag_operation(dag, ops3[0]); + CHECK(lead != NULL, "leading op after substitute"); + name = operation_name(lead); + CHECK(name != NULL && strcmp(name, "Z") == 0, "substituted op is Z"); + cqlib_string_free(name); + operation_free(lead); + CHECK(circuit_dag_validate(dag) == 0, "validate after mutations"); + + /* Error paths: unknown node, NULL operation, and a wire sentinel. */ + CHECK(circuit_dag_substitute_node(dag, 9999, z_op) == -2, "substitute unknown node"); + CHECK(circuit_dag_substitute_node(dag, ops3[0], NULL) == -1, "substitute null op"); + int32_t sentinel = circuit_dag_wire_in(dag, DAG_WIRE_QUBIT, 0); + CHECK(circuit_dag_substitute_node(dag, (uint32_t)sentinel, z_op) == -2, + "substitute wire sentinel"); + CHECK(circuit_dag_apply_operation_back(dag, NULL, NULL) == -1, "apply back null op"); + CHECK(circuit_dag_remove_op_node(dag, 9999) == NULL, "remove unknown node"); + CHECK(circuit_dag_remove_op_node(NULL, 0) == NULL, "remove null dag"); + + circuit_dag_free(dag); + circuit_free(circuit); + operation_free(z_op); + operation_free(x_op); +} + +int main(void) { + test_dag_from_circuit(); + test_dag_from_operations(); + test_dag_builder_rejects_invalid_inputs(); + test_dag_parameters_and_symbols(); + test_dag_empty_and_null_paths(); + test_dag_node_identification_and_traversal(); + test_dag_layers_and_depth(); + test_dag_wire_timelines(); + test_dag_operation_counts_and_measurement_flag(); + test_dag_mutations(); + + printf("\n%d failure(s)\n", g_failures); + return (g_failures == 0) ? 0 : 1; +} diff --git a/crates/binding-c/tests/test_circuit_evolution.c b/crates/binding-c/tests/test_circuit_evolution.c new file mode 100644 index 00000000..73a6f7cc --- /dev/null +++ b/crates/binding-c/tests/test_circuit_evolution.c @@ -0,0 +1,70 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +#include +#include +#include +#include + +#include "cqlib_c.h" + +static void test_pauli_evolution_xz(void) { + CCircuit* circuit = circuit_new(2); + assert(circuit != NULL); + assert(circuit_num_operations(circuit) == 0); + + CPauliString* pauli = pauli_string_parse("XZ"); + assert(pauli != NULL); + + // exp(-i * pi/4 * XZ) appends basis changes, a CNOT ladder, one RZ, + // and the inverse ladder, so the operation count must grow. + const uint32_t qubits[2] = {0, 1}; + assert(circuit_pauli_evolution(circuit, pauli, 1.5707963267948966, qubits, 2) == 0); + assert(circuit_num_operations(circuit) > 0); + + pauli_string_free(pauli); + circuit_free(circuit); +} + +static void test_pauli_evolution_errors(void) { + CCircuit* circuit = circuit_new(2); + assert(circuit != NULL); + + CPauliString* pauli = pauli_string_parse("XZ"); + assert(pauli != NULL); + const uint32_t qubits[2] = {0, 1}; + + // NULL circuit / pauli / qubit buffer. + assert(circuit_pauli_evolution(NULL, pauli, 1.0, qubits, 2) == -1); + assert(circuit_pauli_evolution(circuit, NULL, 1.0, qubits, 2) == -1); + assert(circuit_pauli_evolution(circuit, pauli, 1.0, NULL, 2) == -1); + + // Out-of-bounds qubit index. + const uint32_t out_of_bounds[2] = {0, 5}; + assert(circuit_pauli_evolution(circuit, pauli, 1.0, out_of_bounds, 2) == -2); + + // Position-count mismatch: 3-position Pauli string on 2 positions. + CPauliString* big = pauli_string_parse("XXX"); + assert(big != NULL); + assert(circuit_pauli_evolution(circuit, big, 1.0, qubits, 2) == -3); + + pauli_string_free(big); + pauli_string_free(pauli); + circuit_free(circuit); +} + +int main(void) { + test_pauli_evolution_xz(); + test_pauli_evolution_errors(); + printf("binding-c circuit evolution tests passed\n"); + return 0; +} diff --git a/crates/binding-c/tests/test_circuit_missing.c b/crates/binding-c/tests/test_circuit_missing.c new file mode 100644 index 00000000..0caf60bd --- /dev/null +++ b/crates/binding-c/tests/test_circuit_missing.c @@ -0,0 +1,309 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +#include +#include +#include + +#include "cqlib_c.h" + +static void test_from_operations_and_append(void) { + CCircuit* source = circuit_new(3); + assert(source != NULL); + assert(circuit_h(source, 0) == 0); + assert(circuit_cx(source, 0, 1) == 0); + CParameter* theta = param_parse("theta"); + assert(theta != NULL); + assert(circuit_rx_param(source, 2, theta) == 0); + assert(circuit_num_operations(source) == 3); + assert(circuit_num_parameters(source) == 1); + + /* Snapshot every top-level operation. */ + CValueOperation* snapshots[3]; + for (uintptr_t i = 0; i < 3; i++) { + snapshots[i] = circuit_index(source, i); + assert(snapshots[i] != NULL); + } + /* Read-only view used by the value-level construction API. */ + const CValueOperation* ops[3] = {snapshots[0], snapshots[1], snapshots[2]}; + assert(circuit_index(source, 3) == NULL); + assert(circuit_index(NULL, 0) == NULL); + + /* Rebuild an equivalent circuit from the snapshots. */ + const uint32_t qubits[3] = {0, 1, 2}; + CCircuit* rebuilt = circuit_from_operations(qubits, 3, ops, 3); + assert(rebuilt != NULL); + assert(circuit_num_qubits(rebuilt) == 3); + assert(circuit_num_operations(rebuilt) == 3); + assert(circuit_num_parameters(rebuilt) == 1); + assert(circuit_operations_structurally_equal(source, rebuilt) == 1); + assert(circuit_validate(rebuilt) == 0); + + /* Symbolic parameters survive the round trip. */ + CCircuit* assigned = circuit_assign_params(rebuilt, "theta:0.5"); + assert(assigned != NULL); + assert(circuit_num_parameters(assigned) == 0); + circuit_free(assigned); + + /* Appending a snapshot to a fresh circuit. */ + CCircuit* target = circuit_new(3); + assert(circuit_append_value_operation(target, snapshots[0]) == 0); + assert(circuit_num_operations(target) == 1); + assert(circuit_operations_structurally_equal(target, source) == 0); + + /* Appending fails when the snapshot references an unknown qubit. */ + CCircuit* wide = circuit_new(10); + assert(circuit_x(wide, 9) == 0); + CValueOperation* far = circuit_index(wide, 0); + assert(far != NULL); + CCircuit* small = circuit_new(2); + assert(circuit_append_value_operation(small, far) == -3); + + /* Error paths for from_operations. */ + CCircuit* empty = circuit_from_operations(NULL, 0, NULL, 0); + assert(empty != NULL); + assert(circuit_num_qubits(empty) == 0); + assert(circuit_num_operations(empty) == 0); + circuit_free(empty); + + assert(circuit_from_operations(NULL, 1, NULL, 0) == NULL); /* qubits NULL */ + assert(circuit_from_operations(qubits, 3, NULL, 1) == NULL); /* ops NULL */ + const uint32_t duplicate[2] = {0, 0}; + assert(circuit_from_operations(duplicate, 2, NULL, 0) == NULL); + const CValueOperation* with_hole[2] = {ops[0], NULL}; + assert(circuit_from_operations(qubits, 3, with_hole, 2) == NULL); + const CValueOperation* out_of_domain[1] = {far}; + assert(circuit_from_operations(qubits, 3, out_of_domain, 1) == NULL); + + /* Error paths for append. */ + assert(circuit_append_value_operation(NULL, snapshots[0]) == -1); + assert(circuit_append_value_operation(target, NULL) == -1); + + value_operation_free(far); + for (uintptr_t i = 0; i < 3; i++) { + value_operation_free(snapshots[i]); + } + circuit_free(small); + circuit_free(wide); + circuit_free(target); + circuit_free(rebuilt); + circuit_free(source); + param_free(theta); +} + +static void test_membership_and_equality(void) { + CCircuit* a = circuit_new(2); + CCircuit* b = circuit_new(2); + CCircuit* c = circuit_new(3); + assert(a && b && c); + assert(circuit_h(a, 0) == 0); + assert(circuit_h(b, 0) == 0); + + assert(circuit_contains_qubit(a, 0) == 1); + assert(circuit_contains_qubit(a, 1) == 1); + assert(circuit_contains_qubit(a, 2) == 0); + assert(circuit_contains_qubit(NULL, 0) == -1); + + assert(circuit_has_same_qubits(a, b) == 1); + assert(circuit_has_same_qubits(a, c) == 0); + assert(circuit_has_same_qubits(a, NULL) == -1); + assert(circuit_has_same_qubits(NULL, b) == -1); + + assert(circuit_operations_structurally_equal(a, b) == 1); + assert(circuit_operations_structurally_equal(a, c) == 0); + assert(circuit_x(b, 0) == 0); + assert(circuit_operations_structurally_equal(a, b) == 0); + assert(circuit_operations_structurally_equal(NULL, b) == -1); + assert(circuit_operations_structurally_equal(a, NULL) == -1); + + /* Different parameter values break structural equality. */ + CCircuit* p1 = circuit_new(1); + CCircuit* p2 = circuit_new(1); + assert(circuit_rx(p1, 0, 0.25) == 0); + assert(circuit_rx(p2, 0, 0.5) == 0); + assert(circuit_operations_structurally_equal(p1, p2) == 0); + assert(circuit_operations_structurally_equal(p1, p1) == 1); + + circuit_free(p1); + circuit_free(p2); + circuit_free(c); + circuit_free(b); + circuit_free(a); +} + +static void test_checkpoint_transactions(void) { + CCircuit* circuit = circuit_new(2); + assert(circuit != NULL); + assert(circuit_h(circuit, 0) == 0); + + assert(circuit_checkpoint(NULL) == NULL); + assert(circuit_begin(NULL) == NULL); + + /* Rollback discards everything captured after the checkpoint. */ + CCheckpoint* cp = circuit_checkpoint(circuit); + assert(cp != NULL); + assert(circuit_cx(circuit, 0, 1) == 0); + assert(circuit_rx(circuit, 1, 0.25) == 0); + assert(circuit_num_operations(circuit) == 3); + assert(circuit_rollback_to(circuit, cp) == 0); + assert(circuit_num_operations(circuit) == 1); + assert(circuit_num_qubits(circuit) == 2); + assert(circuit_validate(circuit) == 0); + + /* Commit keeps everything captured after the checkpoint. */ + cp = circuit_begin(circuit); + assert(cp != NULL); + assert(circuit_x(circuit, 1) == 0); + assert(circuit_commit(circuit, cp) == 0); + assert(circuit_num_operations(circuit) == 2); + + /* The control-body transaction variant also rolls back. */ + cp = circuit_checkpoint(circuit); + assert(cp != NULL); + assert(circuit_z(circuit, 1) == 0); + assert(circuit_rollback_control_body_transaction(circuit, cp) == 0); + assert(circuit_num_operations(circuit) == 2); + + /* NULL arguments fail without consuming the token. */ + cp = circuit_checkpoint(circuit); + assert(cp != NULL); + assert(circuit_commit(NULL, cp) == -1); + assert(circuit_rollback_to(circuit, NULL) == -1); + assert(circuit_commit(circuit, NULL) == -1); + checkpoint_free(cp); + checkpoint_free(NULL); + + circuit_free(circuit); +} + +static void test_classical_literal_predicates(void) { + CClassicalExpr* bit_true = classical_expr_bit_literal(true); + CClassicalExpr* bit_false = classical_expr_bit_literal(false); + CClassicalExpr* bool_true = classical_expr_bool_literal(true); + CClassicalExpr* bool_false = classical_expr_bool_literal(false); + assert(bit_true && bit_false && bool_true && bool_false); + + assert(classical_expr_is_bit_true(bit_true) == 1); + assert(classical_expr_is_bit_false(bit_true) == 0); + assert(classical_expr_is_bit_false(bit_false) == 1); + assert(classical_expr_is_bit_true(bool_true) == 0); + assert(classical_expr_is_bool_true(bool_true) == 1); + assert(classical_expr_is_bool_false(bool_true) == 0); + assert(classical_expr_is_bool_false(bool_false) == 1); + assert(classical_expr_is_bool_true(bit_true) == 0); + assert(classical_expr_is_bit_true(NULL) == -1); + assert(classical_expr_is_bool_false(NULL) == -1); + + /* Zero and one literals of every classical type. */ + CClassicalExpr* zero_bit = classical_type_zero_literal(CQLIB_CLASSICAL_TYPE_BIT, 0); + CClassicalExpr* one_bool = classical_type_one_literal(CQLIB_CLASSICAL_TYPE_BOOL, 0); + CClassicalExpr* zero_uint = classical_type_zero_literal(CQLIB_CLASSICAL_TYPE_UINT, 8); + CClassicalExpr* one_vec = classical_type_one_literal(CQLIB_CLASSICAL_TYPE_BIT_VEC, 4); + assert(zero_bit && one_bool && zero_uint && one_vec); + + assert(classical_expr_is_bit_false(zero_bit) == 1); + assert(classical_expr_is_bit_true(zero_bit) == 0); + assert(classical_expr_is_bool_true(one_bool) == 1); + assert(classical_expr_is_bool_false(one_bool) == 0); + + uint32_t tag = 99, width = 99, kind = 99; + assert(classical_expr_ty(zero_uint, &tag, &width) == 0); + assert(tag == CQLIB_CLASSICAL_TYPE_UINT && width == 8); + assert(classical_expr_kind(zero_uint, &kind) == 0); + assert(kind == CQLIB_CLASSICAL_EXPR_UINT_LITERAL); + assert(classical_expr_ty(one_vec, &tag, &width) == 0); + assert(tag == CQLIB_CLASSICAL_TYPE_BIT_VEC && width == 4); + assert(classical_expr_kind(one_vec, &kind) == 0); + assert(kind == CQLIB_CLASSICAL_EXPR_BIT_VEC_LITERAL); + + /* Invalid type parameters. */ + assert(classical_type_zero_literal(99, 8) == NULL); + assert(classical_type_one_literal(99, 8) == NULL); + assert(classical_type_zero_literal(CQLIB_CLASSICAL_TYPE_UINT, 0) == NULL); + assert(classical_type_one_literal(CQLIB_CLASSICAL_TYPE_BIT_VEC, 0) == NULL); + assert(classical_type_one_literal(CQLIB_CLASSICAL_TYPE_UINT, 129) == NULL); + + classical_expr_free(one_vec); + classical_expr_free(zero_uint); + classical_expr_free(one_bool); + classical_expr_free(zero_bit); + classical_expr_free(bool_false); + classical_expr_free(bool_true); + classical_expr_free(bit_false); + classical_expr_free(bit_true); +} + +static void test_remap_classical_ids(void) { + CCircuit* circuit = circuit_new(2); + assert(circuit != NULL); + CClassicalVar* var0 = circuit_var(circuit, CQLIB_CLASSICAL_TYPE_BIT, 0); + CClassicalVar* var1 = circuit_var(circuit, CQLIB_CLASSICAL_TYPE_BIT, 0); + assert(var0 && var1); + assert(classical_var_id(var0) == 0); + assert(classical_var_id(var1) == 1); + + /* Remap a variable reference to a new circuit-local id. */ + CClassicalExpr* var_expr = classical_var_expr(var1); + assert(var_expr != NULL); + assert(classical_expr_vars_len(var_expr) == 1); + const uint32_t old_ids[1] = {1}; + const uint32_t new_ids[1] = {5}; + CClassicalExpr* remapped = + classical_expr_remap_classical_ids(var_expr, old_ids, new_ids, 1, NULL, NULL, 0); + assert(remapped != NULL); + assert(classical_expr_vars_len(remapped) == 1); + CClassicalVar* collected[1]; + assert(classical_expr_vars(remapped, collected, 1) == 0); + assert(classical_var_id(collected[0]) == 5); + classical_var_free(collected[0]); + + /* Every variable read by the expression must be covered. */ + const uint32_t missing[1] = {2}; + assert(classical_expr_remap_classical_ids(var_expr, missing, new_ids, 1, NULL, NULL, 0) == + NULL); + + /* NULL arrays with non-zero length and NULL expressions are rejected. */ + assert(classical_expr_remap_classical_ids(var_expr, NULL, new_ids, 1, NULL, NULL, 0) == NULL); + assert(classical_expr_remap_classical_ids(NULL, old_ids, new_ids, 1, NULL, NULL, 0) == NULL); + + /* Immutable value indices are remapped the same way. */ + uint32_t qubits[2] = {0, 1}; + CClassicalExpr* measured = circuit_measure_bits(circuit, qubits, 2); + assert(measured != NULL); + assert(classical_expr_values_len(measured) == 1); + const uint32_t old_indices[1] = {0}; + const uint32_t new_indices[1] = {3}; + CClassicalExpr* remapped_value = + classical_expr_remap_classical_ids(measured, NULL, NULL, 0, old_indices, new_indices, 1); + assert(remapped_value != NULL); + CClassicalValueInfo values[1]; + assert(classical_expr_values(remapped_value, values, 1) == 0); + assert(values[0].index == 3 && values[0].tag == CQLIB_CLASSICAL_TYPE_BIT_VEC); + + classical_expr_free(remapped_value); + classical_expr_free(remapped); + classical_expr_free(measured); + classical_expr_free(var_expr); + classical_var_free(var0); + classical_var_free(var1); + circuit_free(circuit); +} + +int main(void) { + test_from_operations_and_append(); + test_membership_and_equality(); + test_checkpoint_transactions(); + test_classical_literal_predicates(); + test_remap_classical_ids(); + printf("binding-c circuit missing tests passed\n"); + return 0; +} diff --git a/crates/binding-c/tests/test_circuit_operations.c b/crates/binding-c/tests/test_circuit_operations.c new file mode 100644 index 00000000..c29ab46b --- /dev/null +++ b/crates/binding-c/tests/test_circuit_operations.c @@ -0,0 +1,198 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +#include +#include +#include +#include +#include + +#include "cqlib_c.h" + +static int g_failures = 0; + +#define CHECK(cond, name) \ + do { \ + if (cond) { \ + printf("ok: %s\n", (name)); \ + } else { \ + printf("FAIL: %s\n", (name)); \ + g_failures++; \ + } \ + } while (0) + +/* Compares two doubles with a 1e-9 tolerance. */ +static int close_enough(double a, double b) { return fabs(a - b) < 1e-9; } + +static void test_fixed_param_operation(void) { + const uint32_t qubits[1] = {0}; + const double params[1] = {0.5}; + struct COperation* op = operation_new("RX", qubits, 1, params, 1); + CHECK(op != NULL, "operation_new(RX)"); + + char* name = operation_name(op); + CHECK(name != NULL && strcmp(name, "RX") == 0, "operation_name"); + cqlib_string_free(name); + + CHECK(operation_is_standard_gate(op) == 1, "operation_is_standard_gate"); + CHECK(operation_num_qubits(op) == 1, "operation_num_qubits"); + CHECK(operation_qubits_len(op) == 1, "operation_qubits_len"); + + uint32_t ids[1] = {9}; + CHECK(operation_qubits(op, ids, 1) == 0, "operation_qubits"); + CHECK(ids[0] == 0, "operation_qubits value"); + CHECK(operation_qubits(op, NULL, 1) == -1, "operation_qubits null buffer"); + CHECK(operation_qubits(op, ids, 0) == -8, "operation_qubits short buffer"); + + uint32_t tags[1] = {255}; + double values[1] = {0.0}; + CHECK(operation_params_len(op) == 1, "operation_params_len"); + CHECK(operation_params(op, tags, values, 1) == 0, "operation_params"); + CHECK(tags[0] == OPERATION_PARAM_FIXED, "operation_params tag FIXED"); + CHECK(close_enough(values[0], 0.5), "operation_params value"); + CHECK(operation_params(op, tags, values, 0) == -8, "operation_params short buffer"); + + CHECK(operation_matrix_len(op) == 4, "operation_matrix_len"); + uintptr_t rows = 0; + uintptr_t cols = 0; + CHECK(operation_matrix_dims(op, &rows, &cols) == 0, "operation_matrix_dims"); + CHECK(rows == 2 && cols == 2, "operation_matrix_dims shape"); + CHECK(operation_matrix_dims(op, NULL, &cols) == -1, "operation_matrix_dims null rows"); + CHECK(operation_matrix_dims(op, &rows, NULL) == -1, "operation_matrix_dims null cols"); + + double out[8] = {0}; + CHECK(operation_matrix(op, out, 8) == 4, "operation_matrix"); + /* RX(0.5) = [[cos(0.25), -i*sin(0.25)], + [-i*sin(0.25), cos(0.25)]] row-major interleaved. */ + const double c = cos(0.25); + const double s = sin(0.25); + CHECK(close_enough(out[0], c) && close_enough(out[1], 0.0) && close_enough(out[2], 0.0) && + close_enough(out[3], -s) && close_enough(out[4], 0.0) && close_enough(out[5], -s) && + close_enough(out[6], c) && close_enough(out[7], 0.0), + "operation_matrix RX(0.5) values"); + CHECK(operation_matrix(op, out, 7) == 0, "operation_matrix short buffer"); + CHECK(operation_matrix(op, NULL, 8) == 0, "operation_matrix null buffer"); + + /* Operations built by `operation_new` never carry a label. */ + CHECK(operation_label(op) == NULL, "operation_label"); + + operation_free(op); +} + +static void test_parameterless_gate(void) { + const uint32_t pair[2] = {0, 1}; + struct COperation* op = operation_new("CX", pair, 2, NULL, 0); + CHECK(op != NULL, "operation_new(CX)"); + + uint32_t ids[2] = {9, 9}; + CHECK(operation_qubits_len(op) == 2, "CX qubits_len"); + CHECK(operation_qubits(op, ids, 2) == 0, "CX qubits"); + CHECK(ids[0] == 0 && ids[1] == 1, "CX qubit ids"); + + /* Zero parameters: `operation_params` succeeds with len = 0. */ + CHECK(operation_params_len(op) == 0, "CX params_len"); + CHECK(operation_params(op, NULL, NULL, 0) == 0, "CX params empty"); + + /* The 4x4 matrix is returned as 16 interleaved doubles. */ + CHECK(operation_matrix_len(op) == 16, "CX matrix_len"); + double out[32] = {0}; + CHECK(operation_matrix(op, out, 32) == 16, "CX matrix"); + + operation_free(op); +} + +static void test_index_param_operation(void) { + /* `circuit_rx_param` interns the symbolic parameter into the circuit's + * parameter table; the expanded body operation reports the INDEX tag + * and carries the table index as its payload value. */ + struct CCircuit* circuit = circuit_new(1); + struct CParameter* theta = param_parse("theta"); + CHECK(circuit != NULL && theta != NULL, "index test setup"); + CHECK(circuit_rx_param(circuit, 0, theta) == 0, "circuit_rx_param"); + CHECK(circuit_num_parameters(circuit) == 1, "circuit_num_parameters"); + param_free(theta); + + struct CCircuitCFG* cfg = circuit_cfg_from_circuit(circuit); + CHECK(cfg != NULL, "circuit_cfg_from_circuit"); + CHECK(circuit_cfg_block_operations_len(cfg, 0) == 1, "index test block ops"); + + struct COperation* op = NULL; + CHECK(circuit_cfg_block_operations(cfg, 0, &op, 1) == 0 && op != NULL, + "circuit_cfg_block_operations clone"); + CHECK(circuit_cfg_block_operations(cfg, 0, &op, 0) == -8, + "circuit_cfg_block_operations short buffer"); + + uint32_t tags[1] = {255}; + double values[1] = {-1.0}; + CHECK(operation_params_len(op) == 1, "index params_len"); + CHECK(operation_params(op, tags, values, 1) == 0, "index operation_params"); + CHECK(tags[0] == OPERATION_PARAM_INDEX, "index param tag"); + CHECK(values[0] == 0.0, "index param value"); + + /* An unresolved index parameter has no numeric matrix. */ + CHECK(operation_matrix_len(op) == 0, "index matrix_len"); + uintptr_t rows = 1; + uintptr_t cols = 1; + CHECK(operation_matrix_dims(op, &rows, &cols) == -3, "index matrix_dims"); + + operation_free(op); + circuit_cfg_free(cfg); + circuit_free(circuit); +} + +static void test_operation_new_errors(void) { + const uint32_t qubits[1] = {0}; + CHECK(operation_new(NULL, qubits, 1, NULL, 0) == NULL, "operation_new null name"); + CHECK(operation_new("NOT_A_GATE", NULL, 0, NULL, 0) == NULL, "operation_new unknown gate"); + CHECK(operation_new("H", NULL, 1, NULL, 0) == NULL, "operation_new null qubits"); + CHECK(operation_new("H", qubits, 1, NULL, 1) == NULL, "operation_new null params"); +} + +static void test_null_handles(void) { + const uint32_t qubits[1] = {0}; + CHECK(operation_name(NULL) == NULL, "operation_name(null)"); + CHECK(operation_is_standard_gate(NULL) == -1, "operation_is_standard_gate(null)"); + CHECK(operation_num_qubits(NULL) == 0, "operation_num_qubits(null)"); + CHECK(operation_qubits_len(NULL) == 0, "operation_qubits_len(null)"); + CHECK(operation_qubits(NULL, NULL, 0) == -1, "operation_qubits(null)"); + CHECK(operation_params_len(NULL) == 0, "operation_params_len(null)"); + CHECK(operation_params(NULL, NULL, NULL, 0) == -1, "operation_params(null)"); + CHECK(operation_label(NULL) == NULL, "operation_label(null)"); + CHECK(operation_matrix_len(NULL) == 0, "operation_matrix_len(null)"); + CHECK(operation_matrix_dims(NULL, NULL, NULL) == -1, "operation_matrix_dims(null)"); + CHECK(operation_matrix(NULL, NULL, 0) == 0, "operation_matrix(null)"); + + operation_free(NULL); + CHECK(1, "operation_free(null)"); + + /* Buffer arguments may not be NULL on success paths. */ + struct COperation* op = operation_new("H", qubits, 1, NULL, 0); + CHECK(op != NULL, "null-buffer fixture"); + CHECK(operation_qubits(op, NULL, 1) == -1, "operation_qubits null out"); + uintptr_t rows = 0; + uintptr_t cols = 0; + CHECK(operation_matrix_dims(op, NULL, &cols) == -1, "dims null rows(2)"); + CHECK(operation_matrix_dims(op, &rows, NULL) == -1, "dims null cols(2)"); + CHECK(operation_matrix(op, NULL, 8) == 0, "operation_matrix null out"); + operation_free(op); +} + +int main(void) { + test_fixed_param_operation(); + test_parameterless_gate(); + test_index_param_operation(); + test_operation_new_errors(); + test_null_handles(); + + printf("\n%d failure(s)\n", g_failures); + return (g_failures == 0) ? 0 : 1; +} diff --git a/crates/binding-c/tests/test_circuit_symbolic_matrix.c b/crates/binding-c/tests/test_circuit_symbolic_matrix.c new file mode 100644 index 00000000..ea756cd0 --- /dev/null +++ b/crates/binding-c/tests/test_circuit_symbolic_matrix.c @@ -0,0 +1,427 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +#include +#include +#include +#include +#include + +#include "cqlib_c.h" + +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif + +static int g_failures = 0; + +#define CHECK(cond, name) \ + do { \ + if (cond) { \ + printf("ok: %s\n", (name)); \ + } else { \ + printf("FAIL: %s\n", (name)); \ + g_failures++; \ + } \ + } while (0) + +static void test_symbolic_eye_shape(void) { + /* NULL tolerance for the shape readers and the free routine. */ + CHECK(symbolic_matrix_rows(NULL) == 0, "rows(null)"); + CHECK(symbolic_matrix_cols(NULL) == 0, "cols(null)"); + CHECK(symbolic_matrix_evaluate_len(NULL) == 0, "evaluate_len(null)"); + CHECK(symbolic_matrix_element_str(NULL, 0, 0) == NULL, "element_str(null)"); + CHECK(symbolic_matrix_simplify(NULL) == NULL, "simplify(null)"); + symbolic_matrix_free(NULL); + CHECK(1, "free(null)"); + + struct CSymbolicMatrix* eye = symbolic_eye(2); + CHECK(eye != NULL, "symbolic_eye"); + CHECK(symbolic_matrix_rows(eye) == 2, "eye rows"); + CHECK(symbolic_matrix_cols(eye) == 2, "eye cols"); + CHECK(symbolic_matrix_evaluate_len(eye) == 4, "eye evaluate_len"); + + /* Element strings: both diagonal entries match, both off-diagonal + * entries match, and the two groups differ. */ + char* d00 = symbolic_matrix_element_str(eye, 0, 0); + char* d11 = symbolic_matrix_element_str(eye, 1, 1); + CHECK(d00 != NULL && d11 != NULL, "eye diagonal strings"); + char* o01 = symbolic_matrix_element_str(eye, 0, 1); + char* o10 = symbolic_matrix_element_str(eye, 1, 0); + CHECK(o01 != NULL && o10 != NULL, "eye off-diagonal strings"); + CHECK(strcmp(d00, d11) == 0, "eye diagonal match"); + CHECK(strcmp(o01, o10) == 0, "eye off-diagonal match"); + CHECK(strcmp(d00, o01) != 0, "eye diagonal vs off-diagonal"); + CHECK(strcmp(d11, o10) != 0, "eye other diagonal vs off-diagonal"); + cqlib_string_free(d00); + cqlib_string_free(d11); + cqlib_string_free(o01); + cqlib_string_free(o10); + + /* Out-of-bounds indices return NULL. */ + CHECK(symbolic_matrix_element_str(eye, 2, 0) == NULL, "eye element_str row out of bounds"); + CHECK(symbolic_matrix_element_str(eye, 0, 2) == NULL, "eye element_str col out of bounds"); + + /* Simplification keeps the shape. */ + struct CSymbolicMatrix* simplified = symbolic_matrix_simplify(eye); + CHECK(simplified != NULL, "eye simplify"); + CHECK(symbolic_matrix_rows(simplified) == 2, "simplified rows"); + CHECK(symbolic_matrix_cols(simplified) == 2, "simplified cols"); + + /* A constant matrix evaluates with no bindings at all. */ + Complex64 buf[4]; + CHECK(symbolic_matrix_evaluate(eye, NULL, buf, 4) == 0, "eye evaluate"); + CHECK(buf[0].re == 1.0, "eye evaluate buf[0].re"); + CHECK(buf[0].im == 0.0, "eye evaluate buf[0].im"); + CHECK(buf[1].re == 0.0, "eye evaluate buf[1].re"); + + symbolic_matrix_free(simplified); + symbolic_matrix_free(eye); +} + +static void test_standard_gate_matrix_evaluation(void) { + /* RX(theta) as a symbolic standard-gate matrix. */ + struct CParameter* theta = param_parse("theta"); + CHECK(theta != NULL, "gate evaluate fixture param_parse"); + const struct CParameter* params[1]; + params[0] = theta; + struct CSymbolicMatrix* rx_mat = standard_gate_symbolic_matrix("RX", params, 1); + CHECK(rx_mat != NULL, "standard_gate_symbolic_matrix RX"); + CHECK(symbolic_matrix_rows(rx_mat) == 2, "RX matrix rows"); + CHECK(symbolic_matrix_cols(rx_mat) == 2, "RX matrix cols"); + CHECK(symbolic_matrix_evaluate_len(rx_mat) == 4, "RX evaluate_len"); + + /* The diagonal entry renders as a cos(theta/2) expression. */ + char* diag = symbolic_matrix_element_str(rx_mat, 0, 0); + CHECK(diag != NULL && strstr(diag, "cos") != NULL, "RX diagonal contains cos"); + cqlib_string_free(diag); + + /* Bound evaluation: RX(0.5) = [[cos(0.25), -i*sin(0.25)], + * [-i*sin(0.25), cos(0.25)]] in row-major + * order. */ + Complex64 buf[4]; + CHECK(symbolic_matrix_evaluate(rx_mat, "theta:0.5", buf, 4) == 0, "RX bound evaluate"); + CHECK(fabs(buf[0].re - cos(0.25)) < 1e-12, "RX buf[0].re"); + CHECK(buf[0].im == 0.0, "RX buf[0].im"); + CHECK(buf[1].re == 0.0, "RX buf[1].re"); + CHECK(fabs(buf[1].im + sin(0.25)) < 1e-12, "RX buf[1].im"); + CHECK(fabs(buf[2].re - buf[1].re) < 1e-12 && fabs(buf[2].im - buf[1].im) < 1e-12, + "RX buf[2] mirrors buf[1]"); + CHECK(fabs(buf[3].re - buf[0].re) < 1e-12 && fabs(buf[3].im - buf[0].im) < 1e-12, + "RX buf[3] mirrors buf[0]"); + + /* Cross-check against the matrix of a numeric RX(0.5) circuit. */ + struct CCircuit* circuit = circuit_new(1); + CHECK(circuit != NULL, "gate evaluate fixture circuit"); + CHECK(circuit_rx(circuit, 0, 0.5) == 0, "gate evaluate fixture circuit_rx"); + struct CSymbolicMatrix* circ_mat = circuit_to_symbolic_matrix(circuit, NULL, 0); + CHECK(circ_mat != NULL, "gate evaluate circuit matrix"); + Complex64 buf2[4]; + CHECK(symbolic_matrix_evaluate(circ_mat, NULL, buf2, 4) == 0, "circuit matrix evaluate"); + int match = 1; + for (int i = 0; i < 4; i++) { + if (fabs(buf[i].re - buf2[i].re) >= 1e-12 || fabs(buf[i].im - buf2[i].im) >= 1e-12) { + match = 0; + } + } + CHECK(match, "RX matrix matches circuit matrix"); + + symbolic_matrix_free(circ_mat); + symbolic_matrix_free(rx_mat); + circuit_free(circuit); + param_free(theta); +} + +static void test_standard_gate_matrix_invalid_inputs(void) { + /* The parameterless X gate builds with no params buffer at all. */ + struct CSymbolicMatrix* x_mat = standard_gate_symbolic_matrix("X", NULL, 0); + CHECK(x_mat != NULL, "gate matrix X without params"); + symbolic_matrix_free(x_mat); + + struct CParameter* theta = param_parse("theta"); + CHECK(theta != NULL, "gate matrix fixture param_parse"); + const struct CParameter* params[1]; + params[0] = theta; + + /* NULL gate name. */ + CHECK(standard_gate_symbolic_matrix(NULL, NULL, 0) == NULL, "gate matrix null name"); + + /* Unknown gate name. */ + CHECK(standard_gate_symbolic_matrix("NOT_A_GATE", NULL, 0) == NULL, "gate matrix unknown name"); + + /* RX with a NULL params buffer but a non-zero length. */ + CHECK(standard_gate_symbolic_matrix("RX", NULL, 1) == NULL, "gate matrix null params"); + + /* RX with zero parameters. */ + CHECK(standard_gate_symbolic_matrix("RX", NULL, 0) == NULL, "gate matrix RX without params"); + + /* X with one parameter too many (parameter-count mismatch). */ + CHECK(standard_gate_symbolic_matrix("X", params, 1) == NULL, "gate matrix X with extra param"); + + /* A NULL entry inside the params array. */ + const struct CParameter* null_params[1]; + null_params[0] = NULL; + CHECK(standard_gate_symbolic_matrix("RX", null_params, 1) == NULL, + "gate matrix null param entry"); + + param_free(theta); +} + +static void test_circuit_matrix_conversion_and_order(void) { + /* A 1q parametric circuit yields a 2x2 matrix with a free symbol. */ + struct CParameter* theta = param_parse("theta"); + CHECK(theta != NULL, "circuit matrix fixture param_parse"); + struct CCircuit* rx_circuit = circuit_new(1); + CHECK(rx_circuit != NULL, "circuit matrix fixture rx circuit"); + CHECK(circuit_rx_param(rx_circuit, 0, theta) == 0, "circuit_rx_param"); + struct CSymbolicMatrix* rx_mat = circuit_to_symbolic_matrix(rx_circuit, NULL, 0); + CHECK(rx_mat != NULL, "circuit matrix from parametric circuit"); + CHECK(symbolic_matrix_rows(rx_mat) == 2, "parametric matrix rows"); + CHECK(symbolic_matrix_cols(rx_mat) == 2, "parametric matrix cols"); + + /* The free symbol blocks evaluation without bindings. */ + Complex64 buf[4]; + CHECK(symbolic_matrix_evaluate(rx_mat, NULL, buf, 4) == -3, "parametric matrix needs bindings"); + + /* A constant 2q circuit yields a 4x4 matrix of 16 elements. */ + struct CCircuit* circuit = circuit_new(2); + CHECK(circuit != NULL, "circuit matrix fixture circuit"); + CHECK(circuit_h(circuit, 0) == 0, "circuit matrix fixture h"); + CHECK(circuit_cx(circuit, 0, 1) == 0, "circuit matrix fixture cx"); + struct CSymbolicMatrix* mat = circuit_to_symbolic_matrix(circuit, NULL, 0); + CHECK(mat != NULL, "circuit matrix from constant circuit"); + CHECK(symbolic_matrix_rows(mat) == 4, "circuit matrix rows"); + CHECK(symbolic_matrix_cols(mat) == 4, "circuit matrix cols"); + CHECK(symbolic_matrix_evaluate_len(mat) == 16, "circuit matrix eval_len"); + Complex64 buf16[16]; + CHECK(symbolic_matrix_evaluate(mat, NULL, buf16, 16) == 0, "circuit matrix constant evaluate"); + + /* Order handling: a NULL order buffer with a non-zero length is + * rejected. */ + CHECK(circuit_to_symbolic_matrix(circuit, NULL, 1) == NULL, "circuit matrix null order"); + + /* A full order for the 2q qubit set works. */ + const uint32_t order[2] = {0, 1}; + struct CSymbolicMatrix* ordered = circuit_to_symbolic_matrix(circuit, order, 2); + CHECK(ordered != NULL, "circuit matrix full order"); + + /* A one-entry order misses qubit 1 of the 2q set. */ + const uint32_t partial[1] = {0}; + CHECK(circuit_to_symbolic_matrix(circuit, partial, 1) == NULL, "circuit matrix partial order"); + + /* A two-entry order references a qubit the 1q circuit does not + * have. */ + CHECK(circuit_to_symbolic_matrix(rx_circuit, order, 2) == NULL, + "circuit matrix order beyond qubit set"); + + symbolic_matrix_free(ordered); + symbolic_matrix_free(mat); + symbolic_matrix_free(rx_mat); + circuit_free(circuit); + circuit_free(rx_circuit); + param_free(theta); +} + +static void test_matrix_evaluate_error_codes(void) { + struct CParameter* theta = param_parse("theta"); + CHECK(theta != NULL, "evaluate errors fixture param_parse"); + const struct CParameter* params[1]; + params[0] = theta; + struct CSymbolicMatrix* rx_mat = standard_gate_symbolic_matrix("RX", params, 1); + CHECK(rx_mat != NULL, "evaluate errors fixture rx matrix"); + + Complex64 buf[4]; + + /* NULL matrix handle. */ + CHECK(symbolic_matrix_evaluate(NULL, "theta:0.5", buf, 4) == -1, "evaluate null matrix"); + + /* NULL output buffer with valid bindings and sufficient capacity. */ + CHECK(symbolic_matrix_evaluate(rx_mat, "theta:0.5", NULL, 4) == -1, "evaluate null buffer"); + + /* Undersized buffer. */ + CHECK(symbolic_matrix_evaluate(rx_mat, "theta:0.5", buf, 3) == -8, "evaluate short buffer"); + + /* Malformed bindings value. */ + CHECK(symbolic_matrix_evaluate(rx_mat, "theta:abc", buf, 4) == -4, + "evaluate malformed bindings"); + + /* Bindings for an unrelated symbol leave theta unevaluable. */ + CHECK(symbolic_matrix_evaluate(rx_mat, "ghost:1.0", buf, 4) == -3, + "evaluate unrelated bindings"); + + /* No bindings at all. */ + CHECK(symbolic_matrix_evaluate(rx_mat, NULL, buf, 4) == -3, "evaluate without bindings"); + + /* Constant matrices evaluate without any bindings. */ + struct CSymbolicMatrix* eye = symbolic_eye(2); + CHECK(eye != NULL, "evaluate errors fixture eye"); + CHECK(symbolic_matrix_evaluate(eye, NULL, buf, 4) == 0, "evaluate constant eye"); + + symbolic_matrix_free(eye); + symbolic_matrix_free(rx_mat); + param_free(theta); +} + +static void test_equivalence_checks(void) { + /* Matrix-level equivalence: a NULL side is an error. */ + struct CSymbolicMatrix* x_gate = standard_gate_symbolic_matrix("X", NULL, 0); + CHECK(x_gate != NULL, "equivalence fixture X gate matrix"); + CHECK(symbolic_matrices_equivalent(NULL, x_gate) == -1, "matrix equivalent null lhs"); + CHECK(symbolic_matrices_equivalent(x_gate, NULL) == -1, "matrix equivalent null rhs"); + + struct CCircuit* x_circuit = circuit_new(1); + CHECK(x_circuit != NULL, "equivalence fixture X circuit"); + CHECK(circuit_x(x_circuit, 0) == 0, "equivalence fixture circuit_x"); + struct CSymbolicMatrix* x_mat = circuit_to_symbolic_matrix(x_circuit, NULL, 0); + CHECK(x_mat != NULL, "equivalence fixture X circuit matrix"); + struct CCircuit* h_circuit = circuit_new(1); + CHECK(h_circuit != NULL, "equivalence fixture H circuit"); + CHECK(circuit_h(h_circuit, 0) == 0, "equivalence fixture circuit_h"); + struct CSymbolicMatrix* h_mat = circuit_to_symbolic_matrix(h_circuit, NULL, 0); + CHECK(h_mat != NULL, "equivalence fixture H circuit matrix"); + + /* The X gate matrix matches the X circuit matrix. */ + CHECK(symbolic_matrices_equivalent(x_gate, x_mat) == 1, "X gate equals X circuit"); + /* X and H differ by more than a global phase. */ + CHECK(symbolic_matrices_equivalent(x_mat, h_mat) == 0, "X and H circuits differ"); + + /* Different shapes (2x2 identity vs 4x4 controlled-X) are not + * equivalent. */ + struct CSymbolicMatrix* eye = symbolic_eye(2); + CHECK(eye != NULL, "equivalence fixture eye"); + struct CSymbolicMatrix* cnot = symbolic_matrix_controlled(x_gate, 1); + CHECK(cnot != NULL, "equivalence fixture controlled X"); + CHECK(symbolic_matrices_equivalent(eye, cnot) == 0, "different shapes not equivalent"); + + /* Circuit-level equivalence: RX(pi) equals X up to a global phase. */ + struct CCircuit* rx_pi = circuit_new(1); + CHECK(rx_pi != NULL, "equivalence fixture RX(pi) circuit"); + CHECK(circuit_rx(rx_pi, 0, M_PI) == 0, "equivalence fixture circuit_rx"); + CHECK(circuits_equivalent(x_circuit, rx_pi, NULL, 0) == 1, "X equals RX(pi)"); + CHECK(circuits_equivalent(x_circuit, x_circuit, NULL, 0) == 1, "X circuit self equivalence"); + CHECK(circuits_equivalent(x_circuit, h_circuit, NULL, 0) == 0, + "X and H circuits not equivalent"); + + /* NULL circuits. */ + CHECK(circuits_equivalent(NULL, x_circuit, NULL, 0) == -1, "circuit equivalent null lhs"); + CHECK(circuits_equivalent(x_circuit, NULL, NULL, 0) == -1, "circuit equivalent null rhs"); + + /* A NULL order buffer with a non-zero length. */ + CHECK(circuits_equivalent(x_circuit, rx_pi, NULL, 1) == -8, "circuit equivalent null order"); + + /* An order that does not cover the 2q qubit set fails matrix + * construction. */ + struct CCircuit* empty_a = circuit_new(2); + struct CCircuit* empty_b = circuit_new(2); + CHECK(empty_a != NULL && empty_b != NULL, "equivalence fixture empty circuits"); + const uint32_t order[1] = {0}; + CHECK(circuits_equivalent(empty_a, empty_b, order, 1) == -3, + "circuit equivalent order mismatch"); + + symbolic_matrix_free(cnot); + symbolic_matrix_free(eye); + symbolic_matrix_free(h_mat); + symbolic_matrix_free(x_mat); + symbolic_matrix_free(x_gate); + circuit_free(empty_b); + circuit_free(empty_a); + circuit_free(rx_pi); + circuit_free(h_circuit); + circuit_free(x_circuit); +} + +static void test_matrix_substitute_and_controlled(void) { + /* Substitute theta -> 0.5 in the RX(theta) matrix. */ + struct CParameter* theta = param_parse("theta"); + CHECK(theta != NULL, "substitute fixture param_parse"); + const struct CParameter* params[1]; + params[0] = theta; + struct CSymbolicMatrix* rx_mat = standard_gate_symbolic_matrix("RX", params, 1); + CHECK(rx_mat != NULL, "substitute fixture RX(theta)"); + + struct CParameter* half = param_parse("0.5"); + CHECK(half != NULL, "substitute fixture half"); + const char* names[1]; + names[0] = "theta"; + const struct CParameter* repl[1]; + repl[0] = half; + struct CSymbolicMatrix* substituted = symbolic_matrix_substitute(rx_mat, names, repl, 1); + CHECK(substituted != NULL, "symbolic_matrix_substitute"); + + /* The substituted matrix evaluates without bindings... */ + Complex64 buf[4]; + CHECK(symbolic_matrix_evaluate(substituted, NULL, buf, 4) == 0, "substituted matrix evaluates"); + CHECK(fabs(buf[0].re - cos(0.25)) < 1e-12, "substituted matrix diagonal value"); + + /* ...while the original matrix still carries the free symbol. */ + CHECK(symbolic_matrix_evaluate(rx_mat, NULL, buf, 4) == -3, + "original matrix keeps free symbol"); + + /* Controlled-X embeds the X matrix in the bottom-right of a 4x4 + * identity. */ + struct CSymbolicMatrix* x_mat = standard_gate_symbolic_matrix("X", NULL, 0); + CHECK(x_mat != NULL, "controlled fixture X matrix"); + struct CSymbolicMatrix* cnot = symbolic_matrix_controlled(x_mat, 1); + CHECK(cnot != NULL, "controlled X"); + CHECK(symbolic_matrix_rows(cnot) == 4, "controlled X rows"); + CHECK(symbolic_matrix_cols(cnot) == 4, "controlled X cols"); + Complex64 buf4[16]; + CHECK(symbolic_matrix_evaluate(cnot, NULL, buf4, 16) == 0, "controlled X evaluate"); + /* Top-left identity block. */ + CHECK(buf4[0].re == 1.0, "controlled identity block 00"); + CHECK(buf4[5].re == 1.0, "controlled identity block 11"); + /* Bottom-right X block (CNOT pattern) in row-major order. */ + CHECK(buf4[6].re == 0.0, "controlled X block 12 re"); + CHECK(buf4[7].re == 0.0, "controlled X block 13 re"); + CHECK(buf4[10].re == 0.0, "controlled X block 23 re"); + CHECK(buf4[11].re == 1.0, "controlled X block 24 re"); + CHECK(buf4[14].re == 1.0, "controlled X block 32 re"); + CHECK(buf4[15].re == 0.0, "controlled X block 33 re"); + + /* One more control level doubles the dimension again. */ + struct CSymbolicMatrix* ccx = symbolic_matrix_controlled(cnot, 1); + CHECK(ccx != NULL, "controlled cnot"); + CHECK(symbolic_matrix_rows(ccx) == 8, "controlled cnot rows"); + CHECK(symbolic_matrix_cols(ccx) == 8, "controlled cnot cols"); + + /* Error paths. */ + CHECK(symbolic_matrix_controlled(NULL, 1) == NULL, "controlled null base"); + CHECK(symbolic_matrix_substitute(NULL, names, repl, 1) == NULL, "substitute null matrix"); + CHECK(symbolic_matrix_substitute(rx_mat, NULL, repl, 1) == NULL, "substitute null names"); + CHECK(symbolic_matrix_substitute(rx_mat, names, NULL, 1) == NULL, "substitute null params"); + /* A NULL entry inside the names array. */ + const char* null_names[1]; + null_names[0] = NULL; + CHECK(symbolic_matrix_substitute(rx_mat, null_names, repl, 1) == NULL, + "substitute null name entry"); + + symbolic_matrix_free(ccx); + symbolic_matrix_free(cnot); + symbolic_matrix_free(x_mat); + symbolic_matrix_free(substituted); + symbolic_matrix_free(rx_mat); + param_free(half); + param_free(theta); +} + +int main(void) { + test_symbolic_eye_shape(); + test_standard_gate_matrix_evaluation(); + test_standard_gate_matrix_invalid_inputs(); + test_circuit_matrix_conversion_and_order(); + test_matrix_evaluate_error_codes(); + test_equivalence_checks(); + test_matrix_substitute_and_controlled(); + + printf("\n%d failure(s)\n", g_failures); + return (g_failures == 0) ? 0 : 1; +} diff --git a/crates/binding-c/tests/test_compile.c b/crates/binding-c/tests/test_compile.c new file mode 100644 index 00000000..1a7aacb4 --- /dev/null +++ b/crates/binding-c/tests/test_compile.c @@ -0,0 +1,82 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +#include +#include +#include +#include + +#include "cqlib_c.h" + +static CCircuit* build_bell_circuit(void) { + CCircuit* circuit = circuit_new(2); + assert(circuit != NULL); + assert(circuit_h(circuit, 0) == 0); + assert(circuit_cx(circuit, 0, 1) == 0); + return circuit; +} + +static CompileConfigC logical_config(uint8_t mode) { + CompileConfigC config; + memset(&config, 0, sizeof(config)); + config.mode = mode; + config.target = COMPILE_TARGET_LOGICAL; + return config; +} + +static void test_compile_logical_normal(void) { + CCircuit* circuit = build_bell_circuit(); + + CCompileResult* result = compile(circuit, logical_config(COMPILE_MODE_NORMAL)); + assert(result != NULL); + assert(compile_result_mode(result) == COMPILE_MODE_NORMAL); + + CCircuit* compiled = compile_result_circuit(result); + assert(compiled != NULL); + assert(circuit_num_qubits(compiled) == 2); + + assert(compile_result_num_steps(result) >= 1); + char* step = compile_result_step_name(result, 0); + assert(step != NULL); + cqlib_string_free(step); + + compile_result_free(result); + circuit_free(compiled); + circuit_free(circuit); +} + +static void test_compile_enhanced(void) { + CCircuit* circuit = build_bell_circuit(); + + CCompileResult* result = compile(circuit, logical_config(COMPILE_MODE_ENHANCED)); + assert(result != NULL); + assert(compile_result_mode(result) == COMPILE_MODE_ENHANCED); + assert(compile_result_changed(result) == 0 || compile_result_changed(result) == 1); + + compile_result_free(result); + circuit_free(circuit); +} + +static void test_compile_errors(void) { + assert(compile(NULL, logical_config(COMPILE_MODE_NORMAL)) == NULL); + assert(compile_result_circuit(NULL) == NULL); + assert(compile_result_num_steps(NULL) == 0); + compile_result_free(NULL); +} + +int main(void) { + test_compile_logical_normal(); + test_compile_enhanced(); + test_compile_errors(); + printf("binding-c compile tests passed\n"); + return 0; +} diff --git a/crates/binding-c/tests/test_compile_em.c b/crates/binding-c/tests/test_compile_em.c new file mode 100644 index 00000000..ab2367e0 --- /dev/null +++ b/crates/binding-c/tests/test_compile_em.c @@ -0,0 +1,341 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +#include +#include +#include +#include + +#include "cqlib_c.h" + +static CCircuit* build_bell_circuit(void) { + CCircuit* circuit = circuit_new(2); + assert(circuit != NULL); + assert(circuit_h(circuit, 0) == 0); + assert(circuit_cx(circuit, 0, 1) == 0); + return circuit; +} + +/* Demo estimator: reports the folded circuit's operation count as the + * "expectation value", so the sequence mirrors the folding shape. */ +static void counting_estimator(const CCircuit* circuit, const CHamiltonian* hamiltonian, + uintptr_t shots, double* expectation, double* variance) { + (void)hamiltonian; + (void)shots; + *expectation = (double)circuit_num_operations(circuit); + *variance = 0.0; +} + +/* Static estimator for virtual distillation: numerator invocations + * (Hamiltonian present) report (4.0, 0.1); denominator invocations + * (Hamiltonian NULL) report (2.0, 0.05). */ +static void static_vd_estimator(const CCircuit* circuit, const CHamiltonian* hamiltonian, + uintptr_t shots, double* expectation, double* variance) { + (void)circuit; + (void)shots; + if (hamiltonian != NULL) { + *expectation = 4.0; + *variance = 0.1; + } else { + *expectation = 2.0; + *variance = 0.05; + } +} + +/* ===== Compile: step query by name + device-target compilation ===== */ + +static void test_compile_result_step_by_name(void) { + CCircuit* circuit = build_bell_circuit(); + + CompileConfigC config; + memset(&config, 0, sizeof(config)); + config.mode = COMPILE_MODE_ENHANCED; + config.target = COMPILE_TARGET_LOGICAL; + + CCompileResult* result = compile(circuit, config); + assert(result != NULL); + + /* A logical-target workflow reports route.sabre as skipped. */ + int32_t changed = -1; + int32_t skipped = -1; + assert(compile_result_step(result, "route.sabre", &changed, &skipped) == 1); + assert(changed == 0 || changed == 1); + assert(skipped == 1); + + /* Every reported step name must be findable by name. */ + uintptr_t steps = compile_result_num_steps(result); + assert(steps > 0); + for (uintptr_t i = 0; i < steps; i++) { + char* name = compile_result_step_name(result, i); + assert(name != NULL); + assert(compile_result_step(result, name, &changed, &skipped) == 1); + cqlib_string_free(name); + } + + /* Unknown names report "not found"; NULL arguments report -1. */ + assert(compile_result_step(result, "no.such_step", &changed, &skipped) == 0); + assert(compile_result_step(NULL, "route.sabre", &changed, &skipped) == -1); + assert(compile_result_step(result, NULL, &changed, &skipped) == -1); + + compile_result_free(result); + circuit_free(circuit); +} + +static void test_compile_with_device_layouts(void) { + CCircuit* circuit = build_bell_circuit(); + + CDevice* device = device_bidirectional_line("line-2", 2); + assert(device != NULL); + assert(device_with_native_gates(device, "H,CX") == 0); + + /* Device-target compilation routes on the topology and records layouts. */ + CCompileResult* result = compile_with_device(circuit, COMPILE_MODE_NORMAL, device, NULL, 7); + assert(result != NULL); + + CLayout* initial = compile_result_initial_layout(result); + assert(initial != NULL); + assert(layout_num_logical(initial) == 2); + layout_free(initial); + + CLayout* final_layout = compile_result_final_layout(result); + assert(final_layout != NULL); + layout_free(final_layout); + + /* Logical-target compilation carries no device metadata. */ + CompileConfigC config; + memset(&config, 0, sizeof(config)); + config.mode = COMPILE_MODE_NORMAL; + config.target = COMPILE_TARGET_LOGICAL; + CCompileResult* logical = compile(circuit, config); + assert(logical != NULL); + assert(compile_result_initial_layout(logical) == NULL); + assert(compile_result_final_layout(logical) == NULL); + + /* NULL and invalid-argument handling. */ + assert(compile_with_device(NULL, COMPILE_MODE_NORMAL, device, NULL, 7) == NULL); + assert(compile_with_device(circuit, COMPILE_MODE_NORMAL, NULL, NULL, 7) == NULL); + assert(compile_with_device(circuit, 42, device, NULL, 7) == NULL); + assert(compile_result_initial_layout(NULL) == NULL); + assert(compile_result_final_layout(NULL) == NULL); + + compile_result_free(result); + compile_result_free(logical); + device_free(device); + circuit_free(circuit); +} + +/* ===== ZNE: fine-grained folding / extrapolation / sampling ===== */ + +static CZneMitigation* build_zne(CCircuit* circuit) { + const int32_t levels[] = {0, 1, 2}; + CZneMitigation* zne = zne_mitigation_new(circuit, levels, 3); + assert(zne != NULL); + return zne; +} + +static void test_zne_circuit_levels_factors(void) { + CCircuit* circuit = build_bell_circuit(); + CZneMitigation* zne = build_zne(circuit); + + /* zne_circuit returns the original (unfolded) circuit. */ + CCircuit* original = zne_circuit(zne); + assert(original != NULL); + assert(circuit_num_operations(original) == 2); + circuit_free(original); + + /* Fold levels and noise factors roundtrip through the two-step pattern. */ + int32_t levels[3] = {0, 0, 0}; + assert(zne_fold_levels_len(zne) == 3); + assert(zne_fold_levels(zne, levels, 3) == 0); + assert(levels[0] == 0 && levels[1] == 1 && levels[2] == 2); + assert(zne_fold_levels(zne, levels, 2) == -8); + + int32_t factors[3] = {0, 0, 0}; + assert(zne_noise_factors_len(zne) == 3); + assert(zne_noise_factors(zne, factors, 3) == 0); + assert(factors[0] == 1 && factors[1] == 3 && factors[2] == 5); + assert(zne_noise_factors(zne, factors, 4) == -8); + + zne_mitigation_free(zne); + circuit_free(circuit); +} + +static void test_zne_fold_circuits_two_step(void) { + CCircuit* circuit = build_bell_circuit(); + CZneMitigation* zne = build_zne(circuit); + + /* Global folding scales the circuit to (2 * level + 1) copies: the + * 2-operation Bell circuit becomes 2 / 6 / 10 operations. */ + CCircuit* folded[3] = {NULL, NULL, NULL}; + assert(zne_fold_circuits_len(zne, NULL) == 3); + assert(zne_fold_circuits(zne, NULL, folded, 3) == 0); + assert(circuit_num_operations(folded[0]) == 2); + assert(circuit_num_operations(folded[1]) == 6); + assert(circuit_num_operations(folded[2]) == 10); + for (int i = 0; i < 3; i++) { + circuit_free(folded[i]); + } + + /* Selective folding on H only keeps CX untouched (4 / 6 operations). */ + assert(zne_fold_circuits_len(zne, "H") == 3); + assert(zne_fold_circuits(zne, "H", folded, 3) == 0); + assert(circuit_num_operations(folded[1]) == 4); + assert(circuit_num_operations(folded[2]) == 6); + for (int i = 0; i < 3; i++) { + circuit_free(folded[i]); + } + + /* Length mismatch and unknown gate names must fail. */ + CCircuit* sink = NULL; + assert(zne_fold_circuits(zne, NULL, &sink, 2) == -8); + assert(zne_fold_circuits(zne, "NOT_A_GATE", &sink, 3) == -4); + assert(zne_fold_circuits_len(zne, "NOT_A_GATE") == 0); + + zne_mitigation_free(zne); + circuit_free(circuit); +} + +static void test_zne_extrapolate_known_values(void) { + CCircuit* circuit = build_bell_circuit(); + CZneMitigation* zne = build_zne(circuit); + + /* Linear data y = 2x + 1 over noise factors x = {1, 3, 5}: the degree-1 + * fit extrapolates exactly to y(0) = 1. */ + const double linear[3] = {3.0, 7.0, 11.0}; + double value = NAN; + assert(zne_poly_extrapolate(zne, linear, 3, 1, &value) == 0); + assert(fabs(value - 1.0) < 1e-9); + + /* The unified entry point with the method tag must agree. */ + assert(zne_extrapolate(zne, linear, 3, ZNE_EXTRAPOLATE_POLYNOMIAL, 1, &value) == 0); + assert(fabs(value - 1.0) < 1e-9); + + /* Exponential decay y = exp(-x) extrapolates to A = 1 at x = 0. */ + const double decay[3] = {exp(-1.0), exp(-3.0), exp(-5.0)}; + assert(zne_exp_extrapolate(zne, decay, 3, &value) == 0); + assert(fabs(value - 1.0) < 1e-9); + assert(zne_extrapolate(zne, decay, 3, ZNE_EXTRAPOLATE_EXPONENTIAL, 0, &value) == 0); + assert(fabs(value - 1.0) < 1e-9); + + /* Invalid inputs must fail: degree too large, wrong length, non-positive + * data, unknown method tag. */ + assert(zne_poly_extrapolate(zne, linear, 3, 3, &value) != 0); + assert(zne_poly_extrapolate(zne, linear, 2, 1, &value) != 0); + const double bad[3] = {0.0, 1.0, 2.0}; + assert(zne_exp_extrapolate(zne, bad, 3, &value) != 0); + assert(zne_extrapolate(zne, linear, 3, 42, 1, &value) != 0); + + zne_mitigation_free(zne); + circuit_free(circuit); +} + +static void test_zne_run_em_sequence(void) { + CCircuit* circuit = build_bell_circuit(); + CZneMitigation* zne = build_zne(circuit); + + CPauliString* pauli = pauli_string_parse("ZZ"); + assert(pauli != NULL); + CHamiltonian* ham = hamiltonian_from_pauli(pauli); /* takes ownership */ + assert(ham != NULL); + + /* The counting estimator reports the folded circuit lengths. */ + double sequence[3] = {NAN, NAN, NAN}; + assert(zne_run_em_sequence_len(zne) == 3); + assert(zne_run_em_sequence(zne, NULL, ham, counting_estimator, sequence, 3) == 0); + assert(sequence[0] == 2.0 && sequence[1] == 6.0 && sequence[2] == 10.0); + + /* The with-shots variant supports selective folding. */ + assert(zne_run_em_sequence_with_shots_len(zne) == 3); + assert(zne_run_em_sequence_with_shots(zne, "H", ham, 128, counting_estimator, sequence, 3) == + 0); + assert(sequence[0] == 2.0 && sequence[1] == 4.0 && sequence[2] == 6.0); + + /* Buffer mismatch and unknown gate names must fail. */ + double sink[2] = {0.0, 0.0}; + assert(zne_run_em_sequence(zne, NULL, ham, counting_estimator, sink, 2) == -8); + assert(zne_run_em_sequence(zne, "NOT_A_GATE", ham, counting_estimator, sequence, 3) == -4); + assert(zne_run_em_sequence(NULL, NULL, ham, counting_estimator, sequence, 3) == -1); + + hamiltonian_free(ham); + zne_mitigation_free(zne); + circuit_free(circuit); +} + +/* ===== Virtual distillation: copies / sampling ===== */ + +static void test_virtual_distillation_fine_grained(void) { + CCircuit* circuit = circuit_new(1); + assert(circuit != NULL); + assert(circuit_h(circuit, 0) == 0); + + CVirtualDistillation* vd = virtual_distillation_new(circuit, 2); + assert(vd != NULL); + + /* Copies roundtrip; values below 2 are rejected. */ + assert(virtual_distillation_copies(vd) == 2); + assert(virtual_distillation_set_copies(vd, 3) == 0); + assert(virtual_distillation_copies(vd) == 3); + assert(virtual_distillation_set_copies(vd, 1) == -8); + assert(virtual_distillation_copies(vd) == 3); + assert(virtual_distillation_set_copies(vd, 2) == 0); + + /* The copy-swap circuit is non-empty. */ + CCircuit* protocol = virtual_distillation_build_circuit(vd); + assert(protocol != NULL); + assert(circuit_num_operations(protocol) > 0); + circuit_free(protocol); + + /* run_vd normalizes the numerator by the denominator: 4 / 2 = 2 with + * var = 0.1 / 4 + 16 * 0.05 / 16 = 0.075. */ + CPauliString* z_pauli = pauli_string_parse("Z"); + assert(z_pauli != NULL); + CHamiltonian* ham = hamiltonian_from_pauli(z_pauli); /* takes ownership */ + assert(ham != NULL); + + double mean = NAN; + double variance = NAN; + assert(virtual_distillation_run_vd(vd, ham, 100, 100, static_vd_estimator, &mean, &variance) == + 0); + assert(fabs(mean - 2.0) < 1e-12); + assert(fabs(variance - 0.075) < 1e-12); + + /* A Hamiltonian wider than the circuit must fail. */ + CPauliString* wide_pauli = pauli_string_parse("ZZ"); + assert(wide_pauli != NULL); + CHamiltonian* wide = hamiltonian_from_pauli(wide_pauli); /* takes ownership */ + assert(wide != NULL); + assert(virtual_distillation_run_vd(vd, wide, 100, 100, static_vd_estimator, &mean, &variance) == + -8); + hamiltonian_free(wide); + + /* NULL handling. */ + assert(virtual_distillation_copies(NULL) == 0); + assert(virtual_distillation_set_copies(NULL, 3) == -1); + assert(virtual_distillation_run_vd(NULL, ham, 100, 100, static_vd_estimator, &mean, + &variance) == -1); + + hamiltonian_free(ham); + virtual_distillation_free(vd); + circuit_free(circuit); +} + +int main(void) { + test_compile_result_step_by_name(); + test_compile_with_device_layouts(); + test_zne_circuit_levels_factors(); + test_zne_fold_circuits_two_step(); + test_zne_extrapolate_known_values(); + test_zne_run_em_sequence(); + test_virtual_distillation_fine_grained(); + printf("binding-c compile + error mitigation fine-grained tests passed\n"); + return 0; +} diff --git a/crates/binding-c/tests/test_compile_knowledge.c b/crates/binding-c/tests/test_compile_knowledge.c new file mode 100644 index 00000000..5ac18307 --- /dev/null +++ b/crates/binding-c/tests/test_compile_knowledge.c @@ -0,0 +1,501 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating that +// they have been altered from the originals. + +// Smoke tests for the knowledge matching-engine C ABI: rule listing and +// lookup, transactional matching, condition checks, equivalence, candidate +// and key filtering, free symbols, rule extension, and target instantiation. + +#include +#include +#include +#include + +#include "cqlib_c.h" + +// DSL sources for the rules used below. `\xcf\x80` is the UTF-8 encoding of +// the Greek letter pi. +static const char MERGE_DSL[] = + "rule merge_rz {\n" + " match {\n" + " RZ(a) 0\n" + " RZ(b) 0\n" + " }\n" + " rewrite {\n" + " RZ(a + b) 0\n" + " }\n" + "}"; +static const char CANCEL_H_DSL[] = + "rule cancel_h {\n" + " match {\n" + " H 0\n" + " H 0\n" + " }\n" + " rewrite {}\n" + "}"; +static const char CANCEL_X_DSL[] = + "rule cancel_x {\n" + " match {\n" + " X 0\n" + " X 0\n" + " }\n" + " rewrite {}\n" + "}"; +static const char CONDITION_DSL[] = + "rule cancel_rz_inverse {\n" + " match { RZ(a) 0, RZ(b) 0 }\n" + " require { a + b == 0 mod 4*\xcf\x80 }\n" + " rewrite {}\n" + "}"; +static const char SHIFT_DSL[] = + "rule shift_rz {\n" + " match { RZ(a) 0, RZ(b) 1 }\n" + " rewrite { RZ(a + b) 0, RZ(a + b) 1 }\n" + "}"; + +static struct CKnowledgeRule* make_rule(const char* dsl) { + struct CKnowledgeRule* rule = NULL; + assert(knowledge_rule_from_dsl(dsl, &rule) == 0); + assert(rule != NULL); + return rule; +} + +static struct COperation* make_op(const char* gate, const uint32_t* qubits, uintptr_t qubits_len, + const double* params, uintptr_t params_len) { + struct COperation* op = operation_new(gate, qubits, qubits_len, params, params_len); + assert(op != NULL); + return op; +} + +static struct COperation* make_gate_op(const char* gate, uint32_t qubit) { + return make_op(gate, &qubit, 1u, NULL, 0u); +} + +static struct COperation* make_rz_op(uint32_t qubit, double angle) { + return make_op("RZ", &qubit, 1u, &angle, 1u); +} + +static int string_equals(char* ptr, const char* expected) { + if (ptr == NULL) { + return 0; + } + int equal = strcmp(ptr, expected) == 0; + cqlib_string_free(ptr); + return equal; +} + +static void test_knowledge_bindings_lifecycle(void) { + struct CKnowledgeBindings* bindings = knowledge_bindings_new(); + assert(bindings != NULL); + knowledge_bindings_free(bindings); + knowledge_bindings_free(NULL); +} + +static void test_knowledge_rules_listing(void) { + char source[512]; + snprintf(source, sizeof(source), "%s\n%s", CANCEL_H_DSL, CANCEL_X_DSL); + + struct CKnowledgeLibrary* library = NULL; + assert(knowledge_library_from_dsl_str(source, RULE_KIND_CANCEL, &library) == 0); + assert(library != NULL); + + assert(knowledge_rules_len(library) == 2u); + uint32_t ids[2] = {99u, 99u}; + assert(knowledge_rules(library, ids, 2u) == 2u); + assert(ids[0] == 0u && ids[1] == 1u); + + // Truncated buffer reports the full total but only writes the prefix. + uint32_t truncated[1] = {99u}; + assert(knowledge_rules(library, truncated, 1u) == 2u); + assert(truncated[0] == 0u); + + // NULL out buffer still reports the total; NULL library reports none. + assert(knowledge_rules(library, NULL, 2u) == 2u); + assert(knowledge_rules_len(NULL) == 0u); + assert(knowledge_rules(NULL, ids, 2u) == 0u); + + assert(knowledge_library_len(library) == 2u); + assert(knowledge_library_is_empty(library) == 0); + assert(knowledge_library_contains(library, "cancel_h") == 1); + assert(knowledge_library_contains(library, "missing") == 0); + assert(knowledge_library_contains(NULL, "cancel_h") == -1); + + knowledge_library_free(library); + knowledge_library_free(NULL); +} + +static void test_knowledge_get_by_name(void) { + char source[512]; + snprintf(source, sizeof(source), "%s\n%s", CANCEL_H_DSL, CANCEL_X_DSL); + + struct CKnowledgeLibrary* library = NULL; + assert(knowledge_library_from_dsl_str(source, RULE_KIND_CANCEL, &library) == 0); + + struct CKnowledgeRule* rule = knowledge_get_by_name(library, "cancel_h"); + assert(rule != NULL); + char* name = knowledge_rule_name(rule); + int ok = name != NULL && strcmp(name, "cancel_h") == 0; + cqlib_string_free(name); + assert(ok); + knowledge_rule_free(rule); + + assert(knowledge_get_by_name(library, "no_such_rule") == NULL); + assert(knowledge_get_by_name(NULL, "cancel_h") == NULL); + assert(knowledge_get_by_name(library, NULL) == NULL); + + knowledge_library_free(library); +} + +static void test_knowledge_match_rule_item(void) { + struct CKnowledgeRule* rule = make_rule(MERGE_DSL); + struct COperation* op_a = make_rz_op(7u, 0.25); + struct COperation* op_b = make_rz_op(7u, 0.5); + struct COperation* op_conflict = make_rz_op(9u, 0.5); + struct COperation* op_h = make_gate_op("H", 7u); + + struct CKnowledgeBindings* bindings = knowledge_bindings_new(); + assert(bindings != NULL); + + // Both pattern items bind against the same window, transactionally. + assert(knowledge_match_rule_item(rule, 0u, op_a, bindings) == 1); + assert(knowledge_match_rule_item(rule, 1u, op_b, bindings) == 1); + + // A conflicting qubit fails without corrupting existing bindings. + assert(knowledge_match_rule_item(rule, 0u, op_conflict, bindings) == 0); + // A different gate never matches the RZ pattern item. + assert(knowledge_match_rule_item(rule, 0u, op_h, bindings) == 0); + + // The rolled-back item can be re-matched on the original qubit. + assert(knowledge_match_rule_item(rule, 1u, op_b, bindings) == 1); + + // Error paths. + assert(knowledge_match_rule_item(rule, 5u, op_a, bindings) == -2); + assert(knowledge_match_rule_item(NULL, 0u, op_a, bindings) == -1); + assert(knowledge_match_rule_item(rule, 0u, NULL, bindings) == -1); + assert(knowledge_match_rule_item(rule, 0u, op_a, NULL) == -1); + + knowledge_bindings_free(bindings); + operation_free(op_h); + operation_free(op_conflict); + operation_free(op_b); + operation_free(op_a); + knowledge_rule_free(rule); +} + +static void test_knowledge_rule_matches_operations(void) { + struct CKnowledgeRule* rule = make_rule(CANCEL_H_DSL); + struct COperation* op_h1 = make_gate_op("H", 7u); + struct COperation* op_h2 = make_gate_op("H", 7u); + struct COperation* op_x = make_gate_op("X", 7u); + + const struct COperation* const adjacent[2] = {op_h1, op_h2}; + const struct COperation* const interrupted[2] = {op_h1, op_x}; + + struct CKnowledgeBindings* bindings = NULL; + assert(knowledge_rule_matches_operations(rule, adjacent, 2u, &bindings) == 1); + assert(bindings != NULL); + knowledge_bindings_free(bindings); + + struct CKnowledgeBindings* no_match = NULL; + assert(knowledge_rule_matches_operations(rule, interrupted, 2u, &no_match) == 0); + assert(no_match == NULL); + + // The operation count must equal the pattern length. + assert(knowledge_rule_matches_operations(rule, adjacent, 1u, &bindings) == -8); + assert(knowledge_rule_matches_operations(NULL, adjacent, 2u, &bindings) == -1); + assert(knowledge_rule_matches_operations(rule, NULL, 2u, &bindings) == -1); + assert(knowledge_rule_matches_operations(rule, adjacent, 2u, NULL) == -1); + + operation_free(op_x); + operation_free(op_h2); + operation_free(op_h1); + knowledge_rule_free(rule); +} + +static void test_knowledge_conditions_hold(void) { + struct CKnowledgeRule* rule = make_rule(CONDITION_DSL); + struct COperation* op_hold_a = make_rz_op(3u, 0.0); + struct COperation* op_hold_b = make_rz_op(3u, 0.0); + struct COperation* op_fail_a = make_rz_op(3u, 0.1); + struct COperation* op_fail_b = make_rz_op(3u, 0.2); + + const struct COperation* const holding[2] = {op_hold_a, op_hold_b}; + const struct COperation* const failing[2] = {op_fail_a, op_fail_b}; + + struct CKnowledgeBindings* bindings = NULL; + assert(knowledge_rule_matches_operations(rule, holding, 2u, &bindings) == 1); + assert(knowledge_conditions_hold(rule, bindings) == 1); + knowledge_bindings_free(bindings); + + struct CKnowledgeBindings* failing_bindings = NULL; + assert(knowledge_rule_matches_operations(rule, failing, 2u, &failing_bindings) == 1); + assert(knowledge_conditions_hold(rule, failing_bindings) == 0); + knowledge_bindings_free(failing_bindings); + + assert(knowledge_conditions_hold(NULL, NULL) == -1); + + operation_free(op_fail_b); + operation_free(op_fail_a); + operation_free(op_hold_b); + operation_free(op_hold_a); + knowledge_rule_free(rule); +} + +static void test_knowledge_equivalent_to(void) { + struct CKnowledgeRule* cancel_h_1 = make_rule(CANCEL_H_DSL); + struct CKnowledgeRule* cancel_h_2 = make_rule(CANCEL_H_DSL); + struct CKnowledgeRule* cancel_x = make_rule(CANCEL_X_DSL); + struct CKnowledgeRule* merge = make_rule(MERGE_DSL); + struct CKnowledgeRule* condition = make_rule(CONDITION_DSL); + + // Identical rules have equivalent items. + assert(knowledge_equivalent_to(cancel_h_1, 0u, cancel_h_2, 0u) == 1); + assert(knowledge_equivalent_to(cancel_h_1, 1u, cancel_h_2, 1u) == 1); + // Different gates are not equivalent. + assert(knowledge_equivalent_to(cancel_h_1, 0u, cancel_x, 0u) == 0); + // RZ(a) in the merge rule matches RZ(a) in the condition rule. + assert(knowledge_equivalent_to(merge, 0u, condition, 0u) == 1); + + // Out-of-bounds item indices and NULL guards. + assert(knowledge_equivalent_to(cancel_h_1, 7u, cancel_h_2, 0u) == -2); + assert(knowledge_equivalent_to(cancel_h_1, 0u, cancel_h_2, 7u) == -2); + assert(knowledge_equivalent_to(NULL, 0u, cancel_h_2, 0u) == -1); + + knowledge_rule_free(condition); + knowledge_rule_free(merge); + knowledge_rule_free(cancel_x); + knowledge_rule_free(cancel_h_2); + knowledge_rule_free(cancel_h_1); +} + +static void test_knowledge_candidates_for_first_instruction(void) { + struct CKnowledgeLibrary* library = knowledge_library_builtin(); + assert(library != NULL); + + const uintptr_t total = knowledge_candidates_for_first_instruction_len(library, "H"); + assert(total >= 1u); + + uint32_t ids[64]; + assert(total <= 64u); + assert(knowledge_candidates_for_first_instruction(library, "H", ids, total) == total); + + // cancel_h is a builtin rule starting with H. + const int64_t cancel_h_id = knowledge_library_id_by_name(library, "cancel_h"); + assert(cancel_h_id >= 0); + int found = 0; + for (uintptr_t i = 0; i < total; i++) { + if (ids[i] == (uint32_t)cancel_h_id) { + found = 1; + } + } + assert(found == 1); + + // Truncated copies still report the full total. + uint32_t truncated[1] = {0u}; + assert(knowledge_candidates_for_first_instruction(library, "H", truncated, 1u) == total); + assert(truncated[0] == ids[0]); + + // Unknown gate names and NULL inputs report no candidates. + assert(knowledge_candidates_for_first_instruction_len(library, "NOPE") == 0u); + assert(knowledge_candidates_for_first_instruction_len(NULL, "H") == 0u); + assert(knowledge_candidates_for_first_instruction(library, NULL, NULL, 0u) == 0u); + + knowledge_library_free(library); +} + +static void test_knowledge_collect_free_symbols(void) { + struct CKnowledgeRule* merge = make_rule(MERGE_DSL); + struct CKnowledgeRule* cancel = make_rule(CANCEL_H_DSL); + + assert(knowledge_collect_free_symbols_len(merge) == 2u); + char* symbols[2] = {NULL, NULL}; + assert(knowledge_collect_free_symbols(merge, symbols, 2u) == 2u); + int ok_a = string_equals(symbols[0], "a"); + int ok_b = string_equals(symbols[1], "b"); + assert(ok_a && ok_b); + + assert(knowledge_collect_free_symbols_len(cancel) == 0u); + assert(knowledge_collect_free_symbols(cancel, NULL, 0u) == 0u); + + assert(knowledge_collect_free_symbols_len(NULL) == 0u); + assert(knowledge_collect_free_symbols(NULL, NULL, 0u) == 0u); + + knowledge_rule_free(cancel); + knowledge_rule_free(merge); +} + +static void test_knowledge_filter_rule_ids_by_instruction_keys(void) { + struct CKnowledgeLibrary* library = NULL; + assert(knowledge_library_from_dsl_str(MERGE_DSL, RULE_KIND_MERGE, &library) == 0); + + const char* const op_names[1] = {"RZ"}; + const char* const target_names[1] = {"RZ"}; + + assert(knowledge_filter_rule_ids_by_instruction_keys_len(library, op_names, 1u, target_names, + 1u) == 1u); + uint32_t ids[1] = {0u}; + assert(knowledge_filter_rule_ids_by_instruction_keys(library, op_names, 1u, target_names, 1u, + ids, 1u) == 1u); + assert(ids[0] == 0u); + + // A match side that does not cover RZ filters everything out. + const char* const h_names[1] = {"H"}; + assert(knowledge_filter_rule_ids_by_instruction_keys_len(library, h_names, 1u, target_names, + 1u) == 0u); + + // Unknown gate names and NULL inputs report nothing. + const char* const unknown_names[1] = {"NOPE"}; + assert(knowledge_filter_rule_ids_by_instruction_keys_len(library, unknown_names, 1u, + target_names, 1u) == 0u); + assert(knowledge_filter_rule_ids_by_instruction_keys_len(NULL, op_names, 1u, target_names, + 1u) == 0u); + + knowledge_library_free(library); +} + +static void test_knowledge_extend_rules(void) { + struct CKnowledgeRule* cancel_h = make_rule(CANCEL_H_DSL); + struct CKnowledgeRule* cancel_x = make_rule(CANCEL_X_DSL); + const struct CKnowledgeRule* const rules[2] = {cancel_h, cancel_x}; + + struct CKnowledgeLibrary* library = knowledge_library_new(); + assert(library != NULL); + + uint32_t ids[2] = {0u, 0u}; + assert(knowledge_extend_rules(library, rules, 2u, RULE_KIND_CANCEL, ids, 2u) == 0); + assert(ids[0] == 0u && ids[1] == 1u); + assert(knowledge_library_len(library) == 2u); + assert(knowledge_library_rule_kind(library, 0u) == (int32_t)RULE_KIND_CANCEL); + + // Duplicate names are rejected and the library stays unchanged. + assert(knowledge_extend_rules(library, rules, 2u, RULE_KIND_CANCEL, ids, 2u) == -4); + assert(knowledge_library_len(library) == 2u); + + // Unknown kind tags and small buffers are rejected before insertion. + assert(knowledge_extend_rules(library, rules, 2u, 99u, ids, 2u) == -8); + assert(knowledge_extend_rules(library, rules, 2u, RULE_KIND_CANCEL, ids, 1u) == -8); + + // NULL guards. + assert(knowledge_extend_rules(NULL, rules, 2u, RULE_KIND_CANCEL, ids, 2u) == -1); + assert(knowledge_extend_rules(library, NULL, 2u, RULE_KIND_CANCEL, ids, 2u) == -1); + + knowledge_library_free(library); + knowledge_rule_free(cancel_x); + knowledge_rule_free(cancel_h); +} + +static void test_knowledge_target_qubits(void) { + struct CKnowledgeRule* merge = make_rule(MERGE_DSL); + struct CKnowledgeRule* cancel = make_rule(CANCEL_H_DSL); + struct CKnowledgeRule* shift = make_rule(SHIFT_DSL); + + assert(knowledge_target_qubits_len(merge) == 1u); + uint32_t qubits[1] = {0u}; + assert(knowledge_target_qubits(merge, qubits, 1u) == 1u); + assert(qubits[0] == 0u); + + // The shift rule rewrites on rule-local labels 0 and 1. + assert(knowledge_target_qubits_len(shift) == 2u); + uint32_t shift_qubits[2] = {0u, 0u}; + assert(knowledge_target_qubits(shift, shift_qubits, 2u) == 2u); + assert(shift_qubits[0] == 0u && shift_qubits[1] == 1u); + + // An empty rewrite target has no qubits. + assert(knowledge_target_qubits_len(cancel) == 0u); + assert(knowledge_target_qubits(cancel, NULL, 0u) == 0u); + + assert(knowledge_target_qubits_len(NULL) == 0u); + assert(knowledge_target_qubits(NULL, NULL, 0u) == 0u); + + knowledge_rule_free(shift); + knowledge_rule_free(cancel); + knowledge_rule_free(merge); +} + +static void test_knowledge_instantiate_target(void) { + struct CKnowledgeRule* rule = make_rule(MERGE_DSL); + struct COperation* op_a = make_rz_op(7u, 0.25); + struct COperation* op_b = make_rz_op(7u, 0.5); + + // Match the full window, then instantiate the rewrite target. + const struct COperation* const adjacent[2] = {op_a, op_b}; + struct CKnowledgeBindings* bindings = NULL; + assert(knowledge_rule_matches_operations(rule, adjacent, 2u, &bindings) == 1); + assert(bindings != NULL); + + struct CKnowledgeReplacements* replacements = NULL; + assert(knowledge_instantiate_target(rule, bindings, &replacements) == 0); + assert(replacements != NULL); + assert(knowledge_replacements_len(replacements) == 1u); + + char* instruction = knowledge_replacement_instruction(replacements, 0u); + int ok = instruction != NULL && strcmp(instruction, "RZ") == 0; + cqlib_string_free(instruction); + assert(ok); + + assert(knowledge_replacement_qubits_len(replacements, 0u) == 1u); + uint32_t qubits[1] = {0u}; + assert(knowledge_replacement_qubits(replacements, 0u, qubits, 1u) == 1u); + assert(qubits[0] == 7u); + + assert(knowledge_replacement_params_len(replacements, 0u) == 1u); + double params[1] = {0.0}; + assert(knowledge_replacement_params(replacements, 0u, params, 1u) == 1u); + const double diff = params[0] - 0.75; + assert(diff > -1e-12 && diff < 1e-12); + + // Out-of-bounds replacement indices are benign. + assert(knowledge_replacement_instruction(replacements, 3u) == NULL); + assert(knowledge_replacement_qubits_len(replacements, 3u) == 0u); + assert(knowledge_replacement_qubits(replacements, 3u, NULL, 0u) == 0u); + assert(knowledge_replacement_params_len(replacements, 3u) == 0u); + assert(knowledge_replacement_params(replacements, 3u, NULL, 0u) == 0u); + knowledge_replacements_free(replacements); + + // Instantiating against empty bindings fails on the unbound qubit. + struct CKnowledgeBindings* empty = knowledge_bindings_new(); + struct CKnowledgeReplacements* unbound = NULL; + assert(knowledge_instantiate_target(rule, empty, &unbound) == -2); + knowledge_bindings_free(empty); + + // NULL guards. + assert(knowledge_instantiate_target(NULL, bindings, &unbound) == -1); + assert(knowledge_instantiate_target(rule, NULL, &unbound) == -1); + assert(knowledge_instantiate_target(rule, bindings, NULL) == -1); + knowledge_bindings_free(bindings); + + // NULL-safe accessors and frees. + assert(knowledge_replacements_len(NULL) == 0u); + assert(knowledge_replacement_instruction(NULL, 0u) == NULL); + knowledge_replacements_free(NULL); + + operation_free(op_b); + operation_free(op_a); + knowledge_rule_free(rule); +} + +int main(void) { + test_knowledge_bindings_lifecycle(); + test_knowledge_rules_listing(); + test_knowledge_get_by_name(); + test_knowledge_match_rule_item(); + test_knowledge_rule_matches_operations(); + test_knowledge_conditions_hold(); + test_knowledge_equivalent_to(); + test_knowledge_candidates_for_first_instruction(); + test_knowledge_collect_free_symbols(); + test_knowledge_filter_rule_ids_by_instruction_keys(); + test_knowledge_extend_rules(); + test_knowledge_target_qubits(); + test_knowledge_instantiate_target(); + printf("binding-c compile knowledge tests passed\n"); + return 0; +} diff --git a/crates/binding-c/tests/test_compile_pass.c b/crates/binding-c/tests/test_compile_pass.c new file mode 100644 index 00000000..da3001e2 --- /dev/null +++ b/crates/binding-c/tests/test_compile_pass.c @@ -0,0 +1,389 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// use this file except in compliance with the License. You may obtain +// a copy of the License in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +#include +#include +#include + +#include "cqlib_c.h" + +// Directed line 0 -> 1 -> 2 built from an edge list. +static CDevice* build_line3_device(const char* name) { + uint32_t edges[4] = {0, 1, 1, 2}; + return device_from_edges(name, 3, edges, 2); +} + +// Circuit with one cx(0, 2) interaction that does not fit the line. +static CCircuit* build_line3_circuit(void) { + CCircuit* circuit = circuit_new(3); + assert(circuit != NULL); + assert(circuit_cx(circuit, 0, 2) == 0); + return circuit; +} + +static void test_layout_passes(void) { + CDevice* device = build_line3_device("line-3"); + CCircuit* circuit = build_line3_circuit(); + assert(device != NULL); + + // Trivial layout maps logical i to physical i. + CLayoutResult* trivial = trivial_layout(circuit, device, LAYOUT_OBJECTIVE_TOPOLOGY_ONLY); + assert(trivial != NULL); + CLayout* layout = layout_result_layout(trivial); + assert(layout != NULL); + assert(layout_num_logical(layout) == 3); + assert(layout_get(layout, 0) == 0); + assert(layout_get(layout, 2) == 2); + layout_free(layout); + layout_result_free(trivial); + + // Greedy layout runs on the same circuit. + CLayoutResult* greedy = greedy_layout(circuit, device, LAYOUT_OBJECTIVE_TOPOLOGY_ONLY); + assert(greedy != NULL); + layout_result_free(greedy); + + // SABRE layout with the default config reports candidates. + CLayoutResult* sabre = sabre_layout(circuit, device, LAYOUT_OBJECTIVE_TOPOLOGY_ONLY, NULL); + assert(sabre != NULL); + assert(layout_result_candidates_evaluated(sabre) >= 1); + layout_result_free(sabre); + + // Unknown objective tag is rejected. + assert(trivial_layout(circuit, device, 99) == NULL); + + circuit_free(circuit); + device_free(device); +} + +static void test_vf2_layout(void) { + CDevice* device = build_line3_device("line-3"); + assert(device != NULL); + + // Interactions (0, 1) and (1, 2) fit the line perfectly. + CCircuit* circuit = circuit_new(3); + assert(circuit != NULL); + assert(circuit_cx(circuit, 0, 1) == 0); + assert(circuit_cx(circuit, 1, 2) == 0); + + CLayoutResult* result = + vf2_perfect_layout(circuit, device, LAYOUT_OBJECTIVE_TOPOLOGY_ONLY, NULL); + assert(result != NULL); + assert(layout_result_is_perfect(result) == 1); + layout_result_free(result); + + circuit_free(circuit); + device_free(device); +} + +static void test_analysis_and_prepared(void) { + CDevice* device = build_line3_device("line-3"); + CCircuit* circuit = build_line3_circuit(); + assert(device != NULL); + // Exact device-native SABRE requires declared routing capability. + assert(device_with_native_gates(device, "H,CX,SWAP") == 0); + + // Layout analysis collapses both operations into one interaction pair. + CCircuitLayoutAnalysis* analysis = analyze_circuit_for_layout(circuit); + assert(analysis != NULL); + assert(circuit_layout_analysis_num_logical(analysis) == 3); + assert(circuit_layout_analysis_interactions_len(analysis) == 1); + CInteraction interaction; + memset(&interaction, 0, sizeof(interaction)); + assert(circuit_layout_analysis_interaction(analysis, 0, &interaction) == 0); + assert(interaction.left == 0); + assert(interaction.right == 2); + assert(interaction.weight == 1.0); + + // Physical graph view of the device. + CPhysicalLayoutGraph* graph = physical_layout_graph_from_device(device); + assert(graph != NULL); + assert(physical_layout_graph_num_physical(graph) == 3); + assert(physical_layout_graph_distance(graph, 0, 2) == 2); + assert(physical_layout_graph_is_adjacent_undirected(graph, 0, 2) == 0); + assert(physical_layout_graph_has_fidelity_data(graph) == 0); + + // Trivial layout over the prepared analysis and graph. + CLayoutResult* trivial = + trivial_layout_prepared(analysis, graph, LAYOUT_OBJECTIVE_TOPOLOGY_ONLY); + assert(trivial != NULL); + layout_result_free(trivial); + + // Prepared SABRE circuit/target pipeline. + CPreparedSabreCircuit* prepared = prepare_sabre_circuit(circuit); + assert(prepared != NULL); + CPreparedSabreTarget* target = prepare_sabre_device_target(prepared, device); + assert(target != NULL); + CPhysicalLayoutGraph* target_graph = prepared_sabre_target_physical(target); + assert(target_graph != NULL); + assert(physical_layout_graph_num_physical(target_graph) == 3); + physical_layout_graph_free(target_graph); + + CLayoutResult* sabre_prepared = + sabre_layout_prepared(prepared, target, LAYOUT_OBJECTIVE_TOPOLOGY_ONLY, NULL); + assert(sabre_prepared != NULL); + layout_result_free(sabre_prepared); + prepared_sabre_target_free(target); + prepared_sabre_circuit_free(prepared); + + physical_layout_graph_free(graph); + circuit_layout_analysis_free(analysis); + circuit_free(circuit); + device_free(device); +} + +static void test_routing(void) { + CDevice* device = build_line3_device("line-3"); + CCircuit* circuit = build_line3_circuit(); + assert(device != NULL); + + uint32_t logical[3] = {0, 1, 2}; + uint32_t physical[3] = {0, 1, 2}; + CLayout* layout = layout_new(logical, 3, physical, 3); + assert(layout != NULL); + + // Identity layout needs at least one SWAP for cx(0, 2) on the line. + CRoutedCircuit* routed = route_with_layout(circuit, device, layout, NULL); + assert(routed != NULL); + assert(routed_circuit_swap_count(routed) >= 1); + assert(routed_circuit_changed(routed, circuit) == 1); + + CLayout* final_layout = routed_circuit_final_layout(routed); + assert(final_layout != NULL); + assert(layout_num_logical(final_layout) == 3); + layout_free(final_layout); + + CCircuit* physical_circuit = routed_circuit_circuit(routed); + assert(physical_circuit != NULL); + assert(circuit_num_operations(physical_circuit) >= 1); + circuit_free(physical_circuit); + + CSabreRoutingDiagnostics diagnostics; + memset(&diagnostics, 0, sizeof(diagnostics)); + assert(routed_circuit_diagnostics(routed, &diagnostics) == 0); + assert(diagnostics.trials_evaluated >= 1); + + routed_circuit_free(routed); + layout_free(layout); + + // route_sabre selects a layout and routes in one call. + CSabreRouteResult* result = route_sabre(circuit, device, LAYOUT_OBJECTIVE_TOPOLOGY_ONLY, NULL); + assert(result != NULL); + assert(sabre_route_result_layout_candidates_evaluated(result) >= 1); + + CLayoutScore score; + memset(&score, 0, sizeof(score)); + assert(sabre_route_result_layout_score(result, &score) == 0); + + CRoutedCircuit* inner = sabre_route_result_routed(result); + assert(inner != NULL); + assert(sabre_route_result_swap_count(result) == routed_circuit_swap_count(inner)); + routed_circuit_free(inner); + sabre_route_result_free(result); + + circuit_free(circuit); + device_free(device); +} + +static void test_sabre_route_and_validate(void) { + CDevice* device = build_line3_device("line-3"); + CCircuit* circuit = build_line3_circuit(); + assert(device != NULL); + + uint32_t logical[3] = {0, 1, 2}; + uint32_t physical[3] = {0, 1, 2}; + CLayout* layout = layout_new(logical, 3, physical, 3); + assert(layout != NULL); + + // Low-level SABRE route from the identity layout. + CSabreRoutingResult* result = sabre_route(circuit, device, layout, NULL); + assert(result != NULL); + assert(sabre_routing_result_swap_count(result) >= 1); + + CCircuit* physical_circuit = sabre_routing_result_circuit(result); + assert(physical_circuit != NULL); + circuit_free(physical_circuit); + sabre_routing_result_free(result); + + // normalize_initial_layout accepts an already-normal identity layout. + CLayout* normalized = normalize_initial_layout(logical, 3, device, layout); + assert(normalized != NULL); + assert(layout_num_logical(normalized) == 3); + layout_free(normalized); + + // Connected interactions are reachable from the identity layout. + CCircuit* adjacent = circuit_new(3); + assert(circuit_cx(adjacent, 0, 1) == 0); + assert(circuit_cx(adjacent, 0, 2) == 0); + assert(validate_reachable_interactions(adjacent, device, layout) == 0); + circuit_free(adjacent); + + layout_free(layout); + circuit_free(circuit); + device_free(device); + + // A qubit with no couplings cannot host a two-qubit interaction. + uint32_t iso_edges[4] = {0, 1, 1, 2}; + CDevice* iso_device = device_from_edges("iso-4", 4, iso_edges, 2); + assert(iso_device != NULL); + uint32_t logical4[4] = {0, 1, 2, 3}; + uint32_t physical4[4] = {0, 1, 2, 3}; + CLayout* iso_layout = layout_new(logical4, 4, physical4, 4); + assert(iso_layout != NULL); + CCircuit* split = circuit_new(4); + assert(circuit_cx(split, 0, 3) == 0); + assert(validate_reachable_interactions(split, iso_device, iso_layout) == -6); + circuit_free(split); + layout_free(iso_layout); + device_free(iso_device); +} + +static void test_transforms(void) { + // Canonicalize runs and reports its rounds. + CCircuit* bell = circuit_new(2); + assert(circuit_h(bell, 0) == 0); + assert(circuit_cx(bell, 0, 1) == 0); + + CCanonicalizeResult* canon = canonicalize_circuit(bell); + assert(canon != NULL); + assert(canonicalize_result_rounds(canon) >= 1); + CCircuit* canonical = canonicalize_result_circuit(canon); + assert(canonical != NULL); + assert(circuit_num_operations(canonical) == 2); + circuit_free(canonical); + canonicalize_result_free(canon); + + // Standalone canonicalize wrapper returns a new circuit. + CCircuit* wrapped = transform_canonicalize(bell); + assert(wrapped != NULL); + assert(circuit_num_operations(wrapped) == 2); + circuit_free(wrapped); + circuit_free(bell); + + // One-qubit runs fuse: the optimized stream never grows. + CCircuit* runs = circuit_new(1); + assert(circuit_h(runs, 0) == 0); + assert(circuit_t(runs, 0) == 0); + assert(circuit_tdg(runs, 0) == 0); + assert(circuit_s(runs, 0) == 0); + assert(circuit_sdg(runs, 0) == 0); + assert(circuit_num_operations(runs) == 5); + CCircuit* optimized = transform_optimize_one_qubit_runs(runs); + assert(optimized != NULL); + assert(circuit_num_operations(optimized) <= 5); + assert(circuit_num_operations(optimized) >= 1); + circuit_free(optimized); + circuit_free(runs); + + // Knowledge rewrite with the default configuration. + CCircuit* twice_h = circuit_new(1); + assert(circuit_h(twice_h, 0) == 0); + assert(circuit_h(twice_h, 0) == 0); + CKnowledgeRewriteResult* rewritten = rewrite_circuit(twice_h, rewrite_config_default()); + assert(rewritten != NULL); + CKnowledgeRewriteStats stats; + memset(&stats, 0, sizeof(stats)); + assert(knowledge_rewrite_result_stats(rewritten, &stats) == 0); + CKnowledgeRewriteDiagnostics diagnostics; + memset(&diagnostics, 0, sizeof(diagnostics)); + assert(knowledge_rewrite_result_diagnostics(rewritten, &diagnostics) == 0); + knowledge_rewrite_result_free(rewritten); + circuit_free(twice_h); + + // Commutative cancellation and routing-basis lowering smoke runs. + CCircuit* simple = circuit_new(2); + assert(circuit_h(simple, 0) == 0); + assert(circuit_cx(simple, 0, 1) == 0); + CCircuit* cancelled = transform_commutative_cancellation(simple); + assert(cancelled != NULL); + circuit_free(cancelled); + CCircuit* lowered = transform_lower_to_routing_basis(simple); + assert(lowered != NULL); + circuit_free(lowered); + circuit_free(simple); + + // Structural analysis snapshot. + CCircuit* probe = circuit_new(2); + assert(circuit_h(probe, 0) == 0); + CCircuitAnalysis analysis; + memset(&analysis, 0, sizeof(analysis)); + assert(circuit_analyze(probe, &analysis) == 0); + assert(analysis.has_classical_control == 0); + assert(analysis.has_unitary_gates == 0); + circuit_free(probe); +} + +static void test_null_guards(void) { + assert(trivial_layout(NULL, NULL, LAYOUT_OBJECTIVE_TOPOLOGY_ONLY) == NULL); + assert(greedy_layout(NULL, NULL, LAYOUT_OBJECTIVE_TOPOLOGY_ONLY) == NULL); + assert(vf2_perfect_layout(NULL, NULL, LAYOUT_OBJECTIVE_TOPOLOGY_ONLY, NULL) == NULL); + assert(sabre_layout(NULL, NULL, LAYOUT_OBJECTIVE_TOPOLOGY_ONLY, NULL) == NULL); + assert(analyze_circuit_for_layout(NULL) == NULL); + assert(physical_layout_graph_from_device(NULL) == NULL); + assert(prepare_sabre_circuit(NULL) == NULL); + assert(trivial_layout_prepared(NULL, NULL, LAYOUT_OBJECTIVE_TOPOLOGY_ONLY) == NULL); + assert(sabre_layout_prepared(NULL, NULL, LAYOUT_OBJECTIVE_TOPOLOGY_ONLY, NULL) == NULL); + assert(score_layout(LAYOUT_OBJECTIVE_TOPOLOGY_ONLY, NULL, NULL, NULL, NULL) == -1); + + assert(route_with_layout(NULL, NULL, NULL, NULL) == NULL); + assert(route_sabre(NULL, NULL, LAYOUT_OBJECTIVE_TOPOLOGY_ONLY, NULL) == NULL); + assert(sabre_route(NULL, NULL, NULL, NULL) == NULL); + assert(normalize_initial_layout(NULL, 0, NULL, NULL) == NULL); + assert(validate_reachable_interactions(NULL, NULL, NULL) == -1); + + assert(canonicalize_circuit(NULL) == NULL); + assert(rewrite_circuit(NULL, rewrite_config_default()) == NULL); + assert(transform_canonicalize(NULL) == NULL); + assert(transform_knowledge_rewrite(NULL, rewrite_config_default()) == NULL); + assert(transform_optimize_one_qubit_runs(NULL) == NULL); + assert(transform_commutative_cancellation(NULL) == NULL); + assert(transform_lower_to_routing_basis(NULL) == NULL); + assert(circuit_analyze(NULL, NULL) == -1); + + // Accessors on NULL handles. + assert(layout_result_layout(NULL) == NULL); + assert(layout_result_is_perfect(NULL) == -1); + assert(layout_result_candidates_evaluated(NULL) == 0); + assert(routed_circuit_circuit(NULL) == NULL); + assert(routed_circuit_swap_count(NULL) == 0); + assert(routed_circuit_changed(NULL, NULL) == -1); + assert(sabre_route_result_routed(NULL) == NULL); + assert(sabre_route_result_layout_score(NULL, NULL) == -1); + assert(sabre_routing_result_swap_count(NULL) == 0); + assert(canonicalize_result_rounds(NULL) == 0); + assert(knowledge_rewrite_result_stats(NULL, NULL) == -1); + assert(sabre_config_validate(NULL) == -1); + + // Free functions accept NULL. + layout_result_free(NULL); + circuit_layout_analysis_free(NULL); + physical_layout_graph_free(NULL); + prepared_sabre_circuit_free(NULL); + prepared_sabre_target_free(NULL); + routed_circuit_free(NULL); + sabre_route_result_free(NULL); + sabre_routing_result_free(NULL); + canonicalize_result_free(NULL); + knowledge_rewrite_result_free(NULL); +} + +int main(void) { + test_layout_passes(); + test_vf2_layout(); + test_analysis_and_prepared(); + test_routing(); + test_sabre_route_and_validate(); + test_transforms(); + test_null_guards(); + printf("binding-c compile pass tests passed\n"); + return 0; +} diff --git a/crates/binding-c/tests/test_compile_pass2.c b/crates/binding-c/tests/test_compile_pass2.c new file mode 100644 index 00000000..1cc30e08 --- /dev/null +++ b/crates/binding-c/tests/test_compile_pass2.c @@ -0,0 +1,534 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// use this file except in compliance with the License. You may obtain +// a copy of the License in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +#include +#include +#include +#include +#include +#include + +#include "cqlib_c.h" + +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif + +static void test_decompose_passes(void) { + // expand_definitions: a frozen circuit-gate is expanded back into plain H. + CCircuit* inner = circuit_new(1); + assert(inner != NULL); + assert(circuit_h(inner, 0) == 0); + CCircuitGate* gate = circuit_to_gate(inner, "my_h"); + assert(gate != NULL); + CCircuit* outer = circuit_new(2); + assert(outer != NULL); + uint32_t gate_qubits[1] = {0}; + assert(circuit_circuit_gate(outer, gate, gate_qubits, 1, NULL, 0) == 0); + CCircuit* expanded = decompose_expand_definitions(outer); + assert(expanded != NULL); + assert(circuit_num_operations(expanded) == 1); + circuit_free(expanded); + circuit_gate_free(gate); + circuit_free(outer); + circuit_free(inner); + + // unitaries: a matrix-backed H is synthesized into standard operations. + Complex64 h_matrix[4] = { + {1.0 / sqrt(2.0), 0.0}, + {1.0 / sqrt(2.0), 0.0}, + {1.0 / sqrt(2.0), 0.0}, + {-1.0 / sqrt(2.0), 0.0}, + }; + CCircuit* unitary_circuit = circuit_new(2); + assert(unitary_circuit != NULL); + uint32_t unitary_targets[1] = {0}; + assert(circuit_unitary(unitary_circuit, "my_u", 1, h_matrix, unitary_targets, 1) == 0); + CCircuit* synthesized = + decompose_unitaries(unitary_circuit, decompose_unitary_config_default()); + assert(synthesized != NULL); + assert(circuit_num_operations(synthesized) >= 1); + circuit_free(synthesized); + CDecompositionRuleStats stats; + memset(&stats, 0, sizeof(stats)); + synthesized = decompose_unitaries_with_rule_stats(unitary_circuit, + decompose_unitary_config_default(), &stats); + assert(synthesized != NULL); + circuit_free(synthesized); + circuit_free(unitary_circuit); + + // mc_gates: a doubly-controlled X is rewritten, bounded or not. + CCircuit* mc_circuit = circuit_new(3); + assert(mc_circuit != NULL); + uint32_t controls[2] = {0, 1}; + uint32_t targets[1] = {2}; + assert(circuit_multi_control(mc_circuit, "X", controls, 2, targets, 1, NULL, 0) == 0); + CCircuit* rewritten = decompose_mc_gates(mc_circuit, mc_gate_config_default()); + assert(rewritten != NULL); + assert(circuit_num_operations(rewritten) >= 1); + circuit_free(rewritten); + memset(&stats, 0, sizeof(stats)); + rewritten = decompose_mc_gates_with_rule_stats(mc_circuit, mc_gate_config_default(), &stats); + assert(rewritten != NULL); + circuit_free(rewritten); + uint32_t edges[4] = {0, 1, 1, 2}; + CDevice* device = device_from_edges("line-3", 3, edges, 2); + assert(device != NULL); + rewritten = decompose_mc_gates_for_device(mc_circuit, device, NULL); + assert(rewritten != NULL); + circuit_free(rewritten); + device_free(device); + circuit_free(mc_circuit); + + // resynthesis: both budgets rebuild the two-qubit blocks. + CCircuit* block_circuit = circuit_new(2); + assert(block_circuit != NULL); + assert(circuit_h(block_circuit, 0) == 0); + assert(circuit_cx(block_circuit, 0, 1) == 0); + CCircuit* resynthesized = + resynthesize_two_qubit_blocks(block_circuit, resynthesis_config_normal()); + assert(resynthesized != NULL); + assert(circuit_num_operations(resynthesized) >= 1); + circuit_free(resynthesized); + resynthesized = resynthesize_two_qubit_blocks(block_circuit, resynthesis_config_enhanced()); + assert(resynthesized != NULL); + circuit_free(resynthesized); + circuit_free(block_circuit); +} + +static void test_numeric_synthesis_and_lowering(void) { + // 1q synthesis of the identity yields U(0, 0, 0) with zero global phase. + Complex64 identity2[4] = {{1.0, 0.0}, {0.0, 0.0}, {0.0, 0.0}, {1.0, 0.0}}; + COneQubitUnitaryDecomposition one_q; + memset(&one_q, 0, sizeof(one_q)); + assert(synthesize_numeric_1q_unitary(identity2, &one_q) == 0); + assert(fabs(one_q.theta) < 1e-10); + assert(fabs(one_q.phi) < 1e-10); + assert(fabs(one_q.lambda) < 1e-10); + assert(fabs(one_q.global_phase) < 1e-10); + + // 2q synthesis of the identity succeeds and exposes its operations. + Complex64 identity4[16]; + memset(identity4, 0, sizeof(identity4)); + for (int i = 0; i < 16; i += 5) { + identity4[i].re = 1.0; + } + CTwoQubitUnitarySynthesis* synthesis = + synthesize_numeric_2q_unitary(identity4, 0, 1, TWO_QUBIT_BASIS_CX); + assert(synthesis != NULL); + assert(isfinite(two_qubit_synthesis_global_phase(synthesis))); + for (uintptr_t i = 0; i < two_qubit_synthesis_num_operations(synthesis); i++) { + char* op_name = two_qubit_synthesis_operation_name(synthesis, i); + assert(op_name != NULL); + cqlib_string_free(op_name); + uintptr_t qubits_len = two_qubit_synthesis_operation_qubits_len(synthesis, i); + assert(qubits_len >= 1); + assert(qubits_len <= 8); + uint32_t qubits[8]; + assert(two_qubit_synthesis_operation_qubits(synthesis, i, qubits, qubits_len) == + qubits_len); + } + two_qubit_synthesis_free(synthesis); + + // KAK decomposition of the identity has zero interaction coordinates. + CKakDecomposition* kak = kak_decompose(identity4); + assert(kak != NULL); + double a = 0.0; + double b = 0.0; + double c = 0.0; + assert(kak_decomposition_coordinates(kak, &a, &b, &c) == 0); + assert(fabs(a) < 1e-6); + assert(fabs(b) < 1e-6); + assert(fabs(c) < 1e-6); + assert(fabs(kak_decomposition_global_phase(kak)) < 1e-6); + Complex64 factor[4]; + memset(factor, 0, sizeof(factor)); + assert(kak_decomposition_local_factor(kak, KAK_FACTOR_K1L, factor) == 0); + assert(kak_decomposition_local_factor(kak, 99, factor) == -8); + kak_decomposition_free(kak); + + // Target-basis lowering: T is outside {H, RZ, CX}. + const char* basis_names[3] = {"H", "RZ", "CX"}; + CTargetBasisLowerer* lowerer = target_basis_lowerer_new(basis_names, 3); + assert(lowerer != NULL); + assert(target_basis_lowerer_num_gates(lowerer) == 3); + CCircuit* t_circuit = circuit_new(1); + assert(t_circuit != NULL); + assert(circuit_t(t_circuit, 0) == 0); + assert(target_basis_lowerer_requires_lowering(lowerer, t_circuit) == 1); + CCircuit* lowered = target_basis_lowerer_apply(lowerer, t_circuit); + assert(lowered != NULL); + assert(circuit_num_operations(lowered) >= 1); + circuit_free(lowered); + circuit_free(t_circuit); + + // A circuit already in the basis needs no lowering. + CCircuit* basis_circuit = circuit_new(2); + assert(basis_circuit != NULL); + assert(circuit_h(basis_circuit, 0) == 0); + assert(circuit_cx(basis_circuit, 0, 1) == 0); + assert(target_basis_lowerer_requires_lowering(lowerer, basis_circuit) == 0); + circuit_free(basis_circuit); + target_basis_lowerer_free(lowerer); + + // Unknown gate names and empty bases are rejected. + const char* bad_names[1] = {"NOPE"}; + assert(target_basis_lowerer_new(bad_names, 1) == NULL); + assert(target_basis_lowerer_new(NULL, 0) == NULL); + + // Device lowering: a native circuit passes through unchanged. + uint32_t edges[2] = {0, 1}; + CDevice* device = device_from_edges("line-2", 2, edges, 1); + assert(device != NULL); + assert(device_with_native_gates(device, "H,RZ,CX") == 0); + CCircuit* native_circuit = circuit_new(2); + assert(native_circuit != NULL); + assert(circuit_h(native_circuit, 0) == 0); + assert(circuit_cx(native_circuit, 0, 1) == 0); + CCircuit* device_lowered = decompose_lower_to_device(native_circuit, device); + assert(device_lowered != NULL); + assert(circuit_num_operations(device_lowered) == 2); + circuit_free(device_lowered); + circuit_free(native_circuit); + device_free(device); +} + +static void test_resource_lifecycle(void) { + CCircuit* circuit = circuit_new(2); + assert(circuit != NULL); + assert(circuit_h(circuit, 0) == 0); + + CResourcePolicy policy = resource_policy_default(); + policy.max_pre_layout_clean_ancillas = 2; + + CResourceManager* manager = resource_manager_from_circuit(circuit, &policy, NULL); + assert(manager != NULL); + assert(resource_manager_verify_consistency(manager) == 0); + assert(resource_manager_verify_idle(manager) == 0); + + // Invalid requirement tags and oversized requests are rejected. + assert(resource_manager_preview(manager, 99, 1, NULL, 0) == NULL); + assert(resource_manager_preview(manager, RESOURCE_REQUIREMENT_CLEAN_ZERO, 5, NULL, 0) == NULL); + + // Preview plans one fresh clean ancilla beyond the input width. + CResourcePlan* plan = + resource_manager_preview(manager, RESOURCE_REQUIREMENT_CLEAN_ZERO, 1, NULL, 0); + assert(plan != NULL); + assert(resource_plan_qubits_len(plan) == 1); + assert(resource_plan_num_new_qubits(plan) == 1); + assert(resource_plan_requirement(plan) == RESOURCE_REQUIREMENT_CLEAN_ZERO); + uint32_t planned[1] = {7}; + assert(resource_plan_qubits(plan, planned, 1) == 1); + assert(planned[0] == 2); + + // Commit mutates the manager's working circuit and reserves the qubit. + CResourceLease* lease = resource_manager_commit(manager, plan); + assert(lease != NULL); + assert(resource_lease_qubits_len(lease) == 1); + assert(resource_lease_requirement(lease) == RESOURCE_REQUIREMENT_CLEAN_ZERO); + CCircuit* working = resource_manager_circuit(manager); + assert(working != NULL); + assert(circuit_num_qubits(working) == 3); + circuit_free(working); + + // While the lease is active the manager is consistent but not idle. + assert(resource_manager_verify_consistency(manager) == 0); + assert(resource_manager_verify_idle(manager) == -6); + + // Release frees the lease; a second release is rejected. + assert(resource_manager_release(manager, lease) == 0); + assert(resource_manager_verify_idle(manager) == 0); + assert(resource_manager_release(manager, lease) == -6); + + resource_lease_free(lease); + resource_manager_free(manager); + circuit_free(circuit); +} + +static void test_knowledge_library(void) { + // The builtin library carries rules under their compiler kinds. + CKnowledgeLibrary* builtin = knowledge_library_builtin(); + assert(builtin != NULL); + assert(knowledge_library_len(builtin) > 0); + assert(knowledge_library_is_empty(builtin) == 0); + assert(knowledge_library_rules_by_kind_len(builtin, RULE_KIND_DECOMPOSE) > 0); + assert(knowledge_library_rules_by_kind_len(builtin, RULE_KIND_CANCEL) > 0); + uintptr_t decompose_len = knowledge_library_rules_by_kind_len(builtin, RULE_KIND_DECOMPOSE); + uint32_t* decompose_ids = malloc(decompose_len * sizeof(uint32_t)); + assert(decompose_ids != NULL); + assert(knowledge_library_rules_by_kind(builtin, RULE_KIND_DECOMPOSE, decompose_ids, + decompose_len) == decompose_len); + free(decompose_ids); + + // Metadata accessors cover every builtin rule. + for (uintptr_t i = 0; i < knowledge_library_len(builtin); i++) { + char* rule_name = knowledge_library_rule_name(builtin, i); + assert(rule_name != NULL); + cqlib_string_free(rule_name); + int32_t kind = knowledge_library_rule_kind(builtin, i); + assert(kind >= RULE_KIND_SIMPLIFY); + assert(kind <= RULE_KIND_OTHER); + assert(knowledge_library_rule_pattern_len(builtin, i) >= 1); + } + char* first = knowledge_library_rule_first_instruction(builtin, 0); + assert(first != NULL); + cqlib_string_free(first); + + // Out-of-bounds indices are rejected. + assert(knowledge_library_rule_name(builtin, UINTPTR_MAX) == NULL); + assert(knowledge_library_rule_kind(builtin, UINTPTR_MAX) == -8); + knowledge_library_free(builtin); + + // DSL text parsing with a valid merge rule. + const char* source = + "rule merge_rz {\n" + " match {\n" + " RZ(a) 0\n" + " RZ(b) 0\n" + " }\n" + " rewrite {\n" + " RZ(a + b) 0\n" + " }\n" + "}"; + CKnowledgeLibrary* library = NULL; + assert(knowledge_library_from_dsl_str(source, RULE_KIND_MERGE, &library) == 0); + assert(library != NULL); + assert(knowledge_library_len(library) == 1); + assert(knowledge_library_contains(library, "merge_rz") == 1); + assert(knowledge_library_id_by_name(library, "merge_rz") == 0); + assert(knowledge_library_rule_pattern_len(library, 0) == 2); + assert(knowledge_library_rule_rewrite_len(library, 0) == 1); + assert(knowledge_library_rule_qubit_count(library, 0) == 1); + assert(knowledge_library_rule_cost_delta(library, 0) == -1); + assert(knowledge_library_rule_has_conditions(library, 0) == 0); + assert(knowledge_library_rule_kind(library, 0) == RULE_KIND_MERGE); + char* rule_name = knowledge_library_rule_name(library, 0); + assert(rule_name != NULL); + assert(strcmp(rule_name, "merge_rz") == 0); + cqlib_string_free(rule_name); + + // Unknown names report absence, not errors. + assert(knowledge_library_contains(library, "no_such_rule") == 0); + assert(knowledge_library_id_by_name(library, "no_such_rule") == -1); + + // Unknown kind tags are rejected without touching `out`. + assert(knowledge_library_from_dsl_str(source, 99, &library) == -8); + knowledge_library_free(library); + + // Malformed DSL text and missing files map to their error codes. + CKnowledgeLibrary* bad = NULL; + assert(knowledge_library_from_dsl_str("this is not a rule", RULE_KIND_OTHER, &bad) == -4); + assert(knowledge_library_from_dsl_file("no_such_rule_file.rule", RULE_KIND_OTHER, &bad) == -5); + assert(knowledge_library_from_dsl_str(NULL, RULE_KIND_OTHER, NULL) == -1); + assert(knowledge_library_from_dsl_file(NULL, RULE_KIND_OTHER, NULL) == -1); +} + +static void test_knowledge_rule_add(void) { + const char* source = + "rule cancel_h {\n" + " match {\n" + " H 0\n" + " H 0\n" + " }\n" + " rewrite {}\n" + "}"; + CKnowledgeRule* rule = NULL; + assert(knowledge_rule_from_dsl(source, &rule) == 0); + assert(rule != NULL); + assert(knowledge_rule_validate(rule) == 0); + assert(knowledge_rule_pattern_len(rule) == 2); + assert(knowledge_rule_rewrite_len(rule) == 0); + assert(knowledge_rule_num_qubits(rule) == 1); + assert(knowledge_rule_has_conditions(rule) == 0); + char* name = knowledge_rule_name(rule); + assert(name != NULL); + assert(strcmp(name, "cancel_h") == 0); + cqlib_string_free(name); + + // Insertion into a fresh library assigns id 0. + CKnowledgeLibrary* library = knowledge_library_new(); + assert(library != NULL); + assert(knowledge_library_is_empty(library) == 1); + assert(knowledge_library_add_rule(library, rule, RULE_KIND_CANCEL) == 0); + assert(knowledge_library_len(library) == 1); + assert(knowledge_library_rule_kind(library, 0) == RULE_KIND_CANCEL); + + // Duplicate names and unknown kind tags are rejected. + assert(knowledge_library_add_rule(library, rule, RULE_KIND_CANCEL) == -4); + assert(knowledge_library_add_rule(library, rule, 99) == -8); + knowledge_library_free(library); + knowledge_rule_free(rule); + + // Malformed DSL text fails with the parse error code. + CKnowledgeRule* bad = NULL; + assert(knowledge_rule_from_dsl("this is not a rule", &bad) == -4); + assert(knowledge_rule_from_dsl(NULL, NULL) == -1); +} + +static void test_commutation(void) { + uint32_t q0[1] = {0}; + uint32_t q1[1] = {1}; + double angle_a[1] = {0.3}; + double angle_b[1] = {0.5}; + double phase = 0.0; + + // Disjoint supports commute exactly. + assert(commutation_check("X", q0, 1, NULL, 0, "X", q1, 1, NULL, 0, &phase) == + COMMUTATION_RESULT_EXACT); + assert(phase == 0.0); + + // The same application commutes exactly. + assert(commutation_check("X", q0, 1, NULL, 0, "X", q0, 1, NULL, 0, &phase) == + COMMUTATION_RESULT_EXACT); + + // X and Z on the same qubit commute up to a global phase of pi. + phase = 0.0; + assert(commutation_check("X", q0, 1, NULL, 0, "Z", q0, 1, NULL, 0, &phase) == + COMMUTATION_RESULT_UP_TO_GLOBAL_PHASE); + assert(fabs(phase - M_PI) < 1e-9); + + // Diagonal rotations commute exactly with numeric parameters. + assert(commutation_check("RZ", q0, 1, angle_a, 1, "RZ", q0, 1, angle_b, 1, &phase) == + COMMUTATION_RESULT_EXACT); + + // The algebraic-only variant proves the same facts. + assert(commutation_check_algebraic("X", q0, 1, NULL, 0, "X", q1, 1, NULL, 0, &phase) == + COMMUTATION_RESULT_EXACT); + phase = 0.0; + assert(commutation_check_algebraic("X", q0, 1, NULL, 0, "Z", q0, 1, NULL, 0, &phase) == + COMMUTATION_RESULT_UP_TO_GLOBAL_PHASE); + assert(fabs(phase - M_PI) < 1e-9); + + // Unknown gate names and NULL inputs are rejected. + assert(commutation_check("NOPE", q0, 1, NULL, 0, "X", q0, 1, NULL, 0, &phase) == -8); + assert(commutation_check(NULL, q0, 1, NULL, 0, "X", q0, 1, NULL, 0, &phase) == -1); + + // Reusable checkers with an explicit configuration. + CCommutationConfig config = commutation_config_default(); + assert(config.enable_rule_oracle == 1); + assert(config.enable_matrix_fallback == 1); + assert(config.max_matrix_qubits == 4); + CCommutationChecker* checker = commutation_checker_new(config); + assert(checker != NULL); + assert(commutation_checker_check(checker, "X", q0, 1, NULL, 0, "X", q1, 1, NULL, 0, &phase) == + COMMUTATION_RESULT_EXACT); + assert(commutation_checker_check(NULL, "X", q0, 1, NULL, 0, "X", q1, 1, NULL, 0, &phase) == -1); + commutation_checker_free(checker); + + // A checker can draw its commutation rules from a knowledge library. + CKnowledgeLibrary* library = knowledge_library_builtin(); + assert(library != NULL); + CCommutationChecker* from_library = commutation_checker_from_library(library, config); + assert(from_library != NULL); + assert(commutation_checker_from_library(NULL, config) == NULL); + commutation_checker_free(from_library); + knowledge_library_free(library); +} + +static void test_null_guards(void) { + double a = 0.0; + double b = 0.0; + double c = 0.0; + + // Decompose entry points. + assert(decompose_expand_definitions(NULL) == NULL); + assert(decompose_unitaries(NULL, decompose_unitary_config_default()) == NULL); + assert(decompose_unitaries_with_rule_stats(NULL, decompose_unitary_config_default(), NULL) == + NULL); + assert(decompose_mc_gates(NULL, mc_gate_config_default()) == NULL); + assert(decompose_mc_gates_with_rule_stats(NULL, mc_gate_config_default(), NULL) == NULL); + assert(decompose_mc_gates_for_device(NULL, NULL, NULL) == NULL); + assert(resynthesize_two_qubit_blocks(NULL, resynthesis_config_normal()) == NULL); + assert(synthesize_numeric_2q_unitary(NULL, 0, 1, TWO_QUBIT_BASIS_CX) == NULL); + assert(kak_decompose(NULL) == NULL); + assert(synthesize_numeric_1q_unitary(NULL, NULL) == -1); + assert(kak_decomposition_coordinates(NULL, &a, &b, &c) == -1); + Complex64 factor[4]; + memset(factor, 0, sizeof(factor)); + assert(kak_decomposition_local_factor(NULL, KAK_FACTOR_K1L, factor) == -1); + assert(two_qubit_synthesis_num_operations(NULL) == 0); + assert(two_qubit_synthesis_global_phase(NULL) == 0.0); + assert(two_qubit_synthesis_operation_name(NULL, 0) == NULL); + assert(two_qubit_synthesis_operation_qubits(NULL, 0, NULL, 0) == 0); + assert(two_qubit_synthesis_operation_qubits_len(NULL, 0) == 0); + assert(kak_decomposition_global_phase(NULL) == 0.0); + assert(target_basis_lowerer_apply(NULL, NULL) == NULL); + assert(target_basis_lowerer_requires_lowering(NULL, NULL) == -1); + assert(target_basis_lowerer_num_gates(NULL) == 0); + assert(decompose_lower_to_device(NULL, NULL) == NULL); + two_qubit_synthesis_free(NULL); + kak_decomposition_free(NULL); + target_basis_lowerer_free(NULL); + + // Resource entry points. + assert(resource_manager_from_circuit(NULL, NULL, NULL) == NULL); + assert(resource_manager_preview(NULL, RESOURCE_REQUIREMENT_CLEAN_ZERO, 1, NULL, 0) == NULL); + assert(resource_manager_commit(NULL, NULL) == NULL); + assert(resource_manager_circuit(NULL) == NULL); + assert(resource_manager_release(NULL, NULL) == -1); + assert(resource_manager_enter_post_layout(NULL) == -1); + assert(resource_manager_verify_consistency(NULL) == -1); + assert(resource_manager_verify_idle(NULL) == -1); + assert(resource_plan_qubits_len(NULL) == 0); + assert(resource_plan_num_new_qubits(NULL) == 0); + assert(resource_plan_requirement(NULL) == -1); + assert(resource_lease_qubits_len(NULL) == 0); + assert(resource_lease_requirement(NULL) == -1); + resource_manager_free(NULL); + resource_plan_free(NULL); + resource_lease_free(NULL); + + // Knowledge entry points. + assert(knowledge_library_len(NULL) == 0); + assert(knowledge_library_is_empty(NULL) == -1); + assert(knowledge_library_contains(NULL, NULL) == -1); + assert(knowledge_library_id_by_name(NULL, NULL) == -1); + assert(knowledge_library_rules_by_kind_len(NULL, RULE_KIND_OTHER) == 0); + assert(knowledge_library_rules_by_kind(NULL, RULE_KIND_OTHER, NULL, 0) == 0); + assert(knowledge_library_rule_name(NULL, 0) == NULL); + assert(knowledge_library_rule_kind(NULL, 0) == -1); + assert(knowledge_library_rule_first_instruction(NULL, 0) == NULL); + assert(knowledge_library_rule_pattern_len(NULL, 0) == 0); + assert(knowledge_library_rule_rewrite_len(NULL, 0) == 0); + assert(knowledge_library_rule_qubit_count(NULL, 0) == 0); + assert(knowledge_library_rule_cost_delta(NULL, 0) == 0); + assert(knowledge_library_rule_has_conditions(NULL, 0) == -1); + assert(knowledge_library_add_rule(NULL, NULL, RULE_KIND_OTHER) == -1); + assert(knowledge_rule_name(NULL) == NULL); + assert(knowledge_rule_num_qubits(NULL) == 0); + assert(knowledge_rule_pattern_len(NULL) == 0); + assert(knowledge_rule_rewrite_len(NULL) == 0); + assert(knowledge_rule_has_conditions(NULL) == -1); + assert(knowledge_rule_validate(NULL) == -1); + knowledge_library_free(NULL); + knowledge_rule_free(NULL); + + // Commutation entry points. + assert(commutation_check(NULL, NULL, 0, NULL, 0, NULL, NULL, 0, NULL, 0, NULL) == -1); + assert(commutation_check_algebraic(NULL, NULL, 0, NULL, 0, NULL, NULL, 0, NULL, 0, NULL) == -1); + assert(commutation_checker_check(NULL, NULL, NULL, 0, NULL, 0, NULL, NULL, 0, NULL, 0, NULL) == + -1); + commutation_checker_free(NULL); +} + +int main(void) { + test_decompose_passes(); + test_numeric_synthesis_and_lowering(); + test_resource_lifecycle(); + test_knowledge_library(); + test_knowledge_rule_add(); + test_commutation(); + test_null_guards(); + printf("binding-c compile pass2 tests passed\n"); + return 0; +} diff --git a/crates/binding-c/tests/test_compile_transform_config.c b/crates/binding-c/tests/test_compile_transform_config.c new file mode 100644 index 00000000..1905e334 --- /dev/null +++ b/crates/binding-c/tests/test_compile_transform_config.c @@ -0,0 +1,313 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating that +// they have been altered from the originals. + +#include +#include +#include +#include + +#include "cqlib_c.h" + +// Sorts a small array of freshly allocated gate names in place. +static void sort_names(char** names, uintptr_t len) { + for (uintptr_t i = 0; i + 1 < len; i++) { + for (uintptr_t j = i + 1; j < len; j++) { + if (strcmp(names[i], names[j]) > 0) { + char* tmp = names[i]; + names[i] = names[j]; + names[j] = tmp; + } + } + } +} + +static void test_transform_config_defaults(void) { + // Production defaults: conservative optimization mode. + struct CTransformConfig* config = transform_config_default(); + assert(config != NULL); + assert(transform_config_mode(config) == REWRITE_MODE_OPTIMIZE); + assert(transform_config_max_rounds(config) == 8u); + assert(transform_config_max_window_ops(config) == 16u); + assert(transform_config_max_pattern_len(config) == 8u); + assert(transform_config_recurse_control_flow(config) == 1); + assert(transform_config_skip_labeled_ops(config) == 1); + assert(transform_config_target_instruction_basis_len(config) == 0u); + + // Default rule categories: simplify, cancel, merge, canonicalize. + assert(transform_config_enabled_kinds_len(config) == 4u); + uint8_t kinds[4] = {0}; + assert(transform_config_enabled_kinds(config, kinds, 4u) == 4u); + assert(kinds[0] == REWRITE_KIND_SIMPLIFY); + assert(kinds[1] == REWRITE_KIND_CANCEL); + assert(kinds[2] == REWRITE_KIND_MERGE); + assert(kinds[3] == REWRITE_KIND_CANONICALIZE); + + transform_config_free(config); + + // Lowering defaults switch the mode and widen the rule set. + struct CTransformConfig* lowering = transform_config_lowering(); + assert(lowering != NULL); + assert(transform_config_mode(lowering) == REWRITE_MODE_LOWERING); + assert(transform_config_enabled_kinds_len(lowering) == 6u); + transform_config_free(lowering); + + // NULL handling: getters degrade, setters reject, free allows NULL. + assert(transform_config_mode(NULL) == -1); + assert(transform_config_max_rounds(NULL) == 0u); + assert(transform_config_enabled_kinds_len(NULL) == 0u); + assert(transform_config_with_mode(NULL, REWRITE_MODE_OPTIMIZE) == -1); + transform_config_free(NULL); +} + +static void test_transform_config_setters(void) { + struct CTransformConfig* config = transform_config_default(); + assert(config != NULL); + + // Setters mutate the config in place and return a status code. + assert(transform_config_with_mode(config, REWRITE_MODE_LOWERING) == 0); + assert(transform_config_mode(config) == REWRITE_MODE_LOWERING); + assert(transform_config_with_mode(config, 99) == -8); + + assert(transform_config_with_max_rounds(config, 3) == 0); + assert(transform_config_max_rounds(config) == 3u); + assert(transform_config_with_max_window_ops(config, 64) == 0); + assert(transform_config_max_window_ops(config) == 64u); + assert(transform_config_with_max_pattern_len(config, 12) == 0); + assert(transform_config_max_pattern_len(config) == 12u); + + // Replacing the enabled rule categories. + const uint8_t kinds[2] = {REWRITE_KIND_CANCEL, REWRITE_KIND_DECOMPOSE}; + assert(transform_config_with_enabled_kinds(config, kinds, 2u) == 0); + assert(transform_config_enabled_kinds_len(config) == 2u); + uint8_t read_back[2] = {0}; + assert(transform_config_enabled_kinds(config, read_back, 2u) == 2u); + assert(read_back[0] == REWRITE_KIND_CANCEL); + assert(read_back[1] == REWRITE_KIND_DECOMPOSE); + + // Unknown kind tags are rejected and the config stays unchanged. + const uint8_t bad_kinds[1] = {200}; + assert(transform_config_with_enabled_kinds(config, bad_kinds, 1u) == -8); + assert(transform_config_enabled_kinds_len(config) == 2u); + + // An explicit standard-gate target basis is parsed by name. + const char* const gates[2] = {"H", "CX"}; + assert(transform_config_with_target_instructions(config, gates, 2u) == 0); + assert(transform_config_target_instruction_basis_len(config) == 2u); + char* basis[2] = {NULL, NULL}; + assert(transform_config_target_instruction_basis(config, basis, 2u) == 2u); + assert(basis[0] != NULL && strcmp(basis[0], "H") == 0); + assert(basis[1] != NULL && strcmp(basis[1], "CX") == 0); + cqlib_string_free(basis[0]); + cqlib_string_free(basis[1]); + + // Unknown gate names map to -4 and leave the basis unchanged. + const char* const bad_gates[2] = {"H", "NOPE"}; + assert(transform_config_with_target_instructions(config, bad_gates, 2u) == -4); + assert(transform_config_target_instruction_basis_len(config) == 2u); + + // An empty basis is rejected (-8). + assert(transform_config_with_target_instructions(config, NULL, 0) == -8); + assert(transform_config_target_instruction_basis_len(config) == 2u); + + // NULL setters are rejected. + assert(transform_config_with_max_rounds(NULL, 1) == -1); + + transform_config_free(config); +} + +static void test_canonicalize_config(void) { + struct CCanonicalizeConfig* config = canonicalize_config_default(); + assert(config != NULL); + + // Production defaults: round limit 8, every optional behavior enabled. + assert(canonicalize_config_round_limit(config) == 8u); + assert(canonicalize_config_recurse_control_flow(config) == 1); + assert(canonicalize_config_fold_gphase(config) == 1); + assert(canonicalize_config_canonicalize_instruction_form(config) == 1); + assert(canonicalize_config_drop_noops(config) == 1); + assert(canonicalize_config_canonicalize_barriers(config) == 1); + + // Setters mutate in place and report success. + assert(canonicalize_config_with_round_limit(config, 2) == 0); + assert(canonicalize_config_round_limit(config) == 2u); + assert(canonicalize_config_with_recurse_control_flow(config, 0) == 0); + assert(canonicalize_config_recurse_control_flow(config) == 0); + assert(canonicalize_config_with_fold_gphase(config, 0) == 0); + assert(canonicalize_config_fold_gphase(config) == 0); + assert(canonicalize_config_with_canonicalize_instruction_form(config, 0) == 0); + assert(canonicalize_config_canonicalize_instruction_form(config) == 0); + assert(canonicalize_config_with_drop_noops(config, 0) == 0); + assert(canonicalize_config_drop_noops(config) == 0); + assert(canonicalize_config_with_canonicalize_barriers(config, 0) == 0); + assert(canonicalize_config_canonicalize_barriers(config) == 0); + + // NULL handling. + assert(canonicalize_config_with_round_limit(NULL, 1) == -1); + assert(canonicalize_config_round_limit(NULL) == 0u); + assert(canonicalize_config_recurse_control_flow(NULL) == -1); + canonicalize_config_free(NULL); + + canonicalize_config_free(config); +} + +static void test_canonicalize_circuit_with_config(void) { + CCircuit* circuit = circuit_new(2); + assert(circuit != NULL); + assert(circuit_h(circuit, 0) == 0); + assert(circuit_cx(circuit, 0, 1) == 0); + assert(circuit_t(circuit, 1) == 0); + + struct CCanonicalizeConfig* config = canonicalize_config_default(); + assert(config != NULL); + + // Canonicalizing with the default config yields a valid 2-qubit circuit. + struct CCanonicalizeResult* result = canonicalize_circuit_with_config(circuit, config); + assert(result != NULL); + CCircuit* canon = canonicalize_result_circuit(result); + assert(canon != NULL); + assert(circuit_num_qubits(canon) == 2u); + assert(circuit_num_operations(canon) >= 1u); + assert(canonicalize_result_changed(result) == 0 || canonicalize_result_changed(result) == 1); + assert(canonicalize_result_rounds(result) <= canonicalize_config_round_limit(config)); + circuit_free(canon); + canonicalize_result_free(result); + + // The config is not consumed and can be reused with a tighter limit. + assert(canonicalize_config_with_round_limit(config, 1) == 0); + struct CCanonicalizeResult* limited = canonicalize_circuit_with_config(circuit, config); + assert(limited != NULL); + CCircuit* limited_circuit = canonicalize_result_circuit(limited); + assert(limited_circuit != NULL); + assert(circuit_num_qubits(limited_circuit) == 2u); + assert(canonicalize_result_rounds(limited) <= 1u); + circuit_free(limited_circuit); + canonicalize_result_free(limited); + + // NULL inputs are rejected. + assert(canonicalize_circuit_with_config(NULL, config) == NULL); + assert(canonicalize_circuit_with_config(circuit, NULL) == NULL); + + canonicalize_config_free(config); + circuit_free(circuit); +} + +static void test_decompose_unitary_target(void) { + // Unconstrained target: no native gates, neutral Pauli fallback. + struct CDecomposeUnitaryTarget* unconstrained = decompose_unitary_target_unconstrained(); + assert(unconstrained != NULL); + assert(decompose_unitary_target_fallback_pauli(unconstrained) == 1); + assert(decompose_unitary_target_native_1q_len(unconstrained) == 0u); + assert(decompose_unitary_target_native_2q_len(unconstrained) == 0u); + assert(decompose_unitary_target_basis_len(unconstrained) == 0u); + + // Rebuilding the fallback flag needs a non-empty native basis. + assert(decompose_unitary_target_set_fallback_pauli(unconstrained, 0) == -8); + assert(decompose_unitary_target_fallback_pauli(unconstrained) == 1); + + // An unconstrained target has no basis to cost against. + const char* const ops[3] = {"H", "CX", "H"}; + const uint32_t qubit_ids[4] = {0, 0, 1, 1}; + const uint32_t op_counts[3] = {1, 2, 1}; + CTargetBasisCost cost; + memset(&cost, 0, sizeof(cost)); + assert(decompose_unitary_target_cost_of_fixed_operations(unconstrained, ops, qubit_ids, + op_counts, 3u, &cost) == -8); + + // A workflow-style basis list splits native gates by arity. + const char* const basis_gates[3] = {"H", "RZ", "CX"}; + struct CDecomposeUnitaryTarget* target = + decompose_unitary_target_from_instructions(basis_gates, 3u); + assert(target != NULL); + assert(decompose_unitary_target_fallback_pauli(target) == 1); + assert(decompose_unitary_target_native_1q_len(target) == 2u); + assert(decompose_unitary_target_native_2q_len(target) == 1u); + assert(decompose_unitary_target_basis_len(target) == 3u); + + // The combined basis contains every native gate (1q gates first). + char* basis[3] = {NULL, NULL, NULL}; + assert(decompose_unitary_target_basis(target, basis, 3u) == 3u); + sort_names(basis, 3u); + assert(strcmp(basis[0], "CX") == 0); + assert(strcmp(basis[1], "H") == 0); + assert(strcmp(basis[2], "RZ") == 0); + for (uintptr_t i = 0; i < 3u; i++) { + cqlib_string_free(basis[i]); + } + + // Empty and unknown basis lists are rejected. + assert(decompose_unitary_target_from_instructions(NULL, 0) == NULL); + const char* const unknown_gates[1] = {"NOT_A_GATE"}; + assert(decompose_unitary_target_from_instructions(unknown_gates, 1u) == NULL); + + // A native sequence lowers to itself: 3 ops, depth 3, one 2q op. + memset(&cost, 0, sizeof(cost)); + assert(decompose_unitary_target_cost_of_fixed_operations(target, ops, qubit_ids, op_counts, 3u, + &cost) == 0); + assert(cost.total_ops == 3u); + assert(cost.two_qubit_ops == 1u); + assert(cost.parameterized_ops == 0u); + assert(cost.depth == 3u); + + // A parameterized gate is counted as such. + const char* const rz_ops[1] = {"RZ"}; + const uint32_t rz_counts[1] = {1}; + assert(decompose_unitary_target_cost_of_fixed_operations(target, rz_ops, qubit_ids, rz_counts, + 1u, &cost) == 0); + assert(cost.total_ops == 1u); + assert(cost.parameterized_ops == 1u); + + // Error paths: unknown gate name, NULL out, NULL target. + const char* const bad_ops[1] = {"NOT_A_GATE"}; + assert(decompose_unitary_target_cost_of_fixed_operations(target, bad_ops, qubit_ids, rz_counts, + 1u, &cost) == -4); + assert(decompose_unitary_target_cost_of_fixed_operations(target, ops, qubit_ids, op_counts, 3u, + NULL) == -1); + assert(decompose_unitary_target_cost_of_fixed_operations(NULL, ops, qubit_ids, op_counts, 3u, + &cost) == -1); + + // Toggling the fallback rebuilds the target around its native lists. + assert(decompose_unitary_target_set_fallback_pauli(target, 0) == 0); + assert(decompose_unitary_target_fallback_pauli(target) == 0); + assert(decompose_unitary_target_native_1q_len(target) == 2u); + assert(decompose_unitary_target_set_fallback_pauli(target, 1) == 0); + assert(decompose_unitary_target_fallback_pauli(target) == 1); + assert(decompose_unitary_target_set_fallback_pauli(NULL, 1) == -1); + + // Explicit standard-gate lists with a wrong-arity or unknown gate fail. + const char* const one_qubit[2] = {"H", "RZ"}; + const char* const two_qubit[2] = {"CX", "RZZ"}; + struct CDecomposeUnitaryTarget* explicit_target = + decompose_unitary_target_from_standard_gates(one_qubit, 2u, two_qubit, 2u, 1); + assert(explicit_target != NULL); + assert(decompose_unitary_target_fallback_pauli(explicit_target) == 1); + assert(decompose_unitary_target_native_1q_len(explicit_target) == 2u); + assert(decompose_unitary_target_native_2q_len(explicit_target) == 2u); + assert(decompose_unitary_target_basis_len(explicit_target) == 4u); + decompose_unitary_target_free(explicit_target); + assert(decompose_unitary_target_from_standard_gates(two_qubit, 2u, two_qubit, 2u, 1) == NULL); + assert(decompose_unitary_target_from_standard_gates(unknown_gates, 1u, two_qubit, 2u, 1) == + NULL); + + decompose_unitary_target_free(target); + decompose_unitary_target_free(unconstrained); + decompose_unitary_target_free(NULL); +} + +int main(void) { + test_transform_config_defaults(); + test_transform_config_setters(); + test_canonicalize_config(); + test_canonicalize_circuit_with_config(); + test_decompose_unitary_target(); + printf("binding-c compile transform config tests passed\n"); + return 0; +} diff --git a/crates/binding-c/tests/test_compile_workflow.c b/crates/binding-c/tests/test_compile_workflow.c new file mode 100644 index 00000000..db96c49a --- /dev/null +++ b/crates/binding-c/tests/test_compile_workflow.c @@ -0,0 +1,129 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +#include +#include +#include +#include + +#include "cqlib_c.h" + +static CCircuit* build_bell_circuit(void) { + CCircuit* circuit = circuit_new(2); + assert(circuit != NULL); + assert(circuit_h(circuit, 0) == 0); + assert(circuit_cx(circuit, 0, 1) == 0); + return circuit; +} + +static CompileConfigC logical_config(uint8_t mode) { + CompileConfigC config; + memset(&config, 0, sizeof(config)); + config.mode = mode; + config.target = COMPILE_TARGET_LOGICAL; + return config; +} + +static void test_workflow_new_and_free(void) { + CCompilerWorkflow* workflow = compiler_workflow_new(logical_config(COMPILE_MODE_NORMAL)); + assert(workflow != NULL); + compiler_workflow_free(workflow); + + workflow = compiler_workflow_new(logical_config(COMPILE_MODE_ENHANCED)); + assert(workflow != NULL); + compiler_workflow_free(workflow); + + // Invalid mode and unsupported target tags return NULL. + assert(compiler_workflow_new(logical_config(42)) == NULL); + + CompileConfigC config = logical_config(COMPILE_MODE_NORMAL); + config.target = COMPILE_TARGET_DEVICE; + assert(compiler_workflow_new(config) == NULL); + + // Freeing NULL is allowed. + compiler_workflow_free(NULL); +} + +static void test_workflow_run_logical(void) { + CCircuit* circuit = build_bell_circuit(); + CCompilerWorkflow* workflow = compiler_workflow_new(logical_config(COMPILE_MODE_ENHANCED)); + assert(workflow != NULL); + + CCompileResult* result = compiler_workflow_run(workflow, circuit); + assert(result != NULL); + assert(compile_result_mode(result) == COMPILE_MODE_ENHANCED); + assert(compile_result_num_steps(result) > 0); + + CCircuit* optimized = compile_result_circuit(result); + assert(optimized != NULL); + assert(circuit_num_qubits(optimized) == 2); + + // The workflow is reusable: a second run on the same circuit works. + CCompileResult* second = compiler_workflow_run(workflow, circuit); + assert(second != NULL); + compile_result_free(second); + + compile_result_free(result); + circuit_free(optimized); + compiler_workflow_free(workflow); + circuit_free(circuit); +} + +static void test_workflow_run_null_arguments(void) { + CCircuit* circuit = build_bell_circuit(); + CCompilerWorkflow* workflow = compiler_workflow_new(logical_config(COMPILE_MODE_NORMAL)); + assert(workflow != NULL); + + assert(compiler_workflow_run(NULL, circuit) == NULL); + assert(compiler_workflow_run(workflow, NULL) == NULL); + + compiler_workflow_free(workflow); + circuit_free(circuit); +} + +static void test_device_metadata_empty_after_logical_compile(void) { + CCircuit* circuit = build_bell_circuit(); + CCompilerWorkflow* workflow = compiler_workflow_new(logical_config(COMPILE_MODE_NORMAL)); + assert(workflow != NULL); + + CCompileResult* result = compiler_workflow_run(workflow, circuit); + assert(result != NULL); + + // A logical-target compile carries no device metadata: numeric fields + // return 0, handle fields return NULL, and query functions report + // absence. + assert(compile_result_has_device_metadata(result) == 0); + assert(compile_result_device_metadata_initial_layout(result) == NULL); + assert(compile_result_device_metadata_final_layout(result) == NULL); + assert(compile_result_device_metadata_permutation_len(result) == 0); + assert(compile_result_device_metadata_permutation(result, NULL, NULL, 0) == 0); + + // NULL result handling. + assert(compile_result_has_device_metadata(NULL) == -1); + assert(compile_result_device_metadata_initial_layout(NULL) == NULL); + assert(compile_result_device_metadata_final_layout(NULL) == NULL); + assert(compile_result_device_metadata_permutation_len(NULL) == 0); + assert(compile_result_device_metadata_permutation(NULL, NULL, NULL, 0) == -1); + + compile_result_free(result); + compiler_workflow_free(workflow); + circuit_free(circuit); +} + +int main(void) { + test_workflow_new_and_free(); + test_workflow_run_logical(); + test_workflow_run_null_arguments(); + test_device_metadata_empty_after_logical_compile(); + printf("binding-c compile workflow tests passed\n"); + return 0; +} diff --git a/crates/binding-c/tests/test_device.c b/crates/binding-c/tests/test_device.c new file mode 100644 index 00000000..3ef98f4a --- /dev/null +++ b/crates/binding-c/tests/test_device.c @@ -0,0 +1,710 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +#include +#include +#include +#include +#include +#include + +#include "cqlib_c.h" + +static void test_device_topology_variants(void) { + CDevice* line = device_line("line", 4); + assert(line != NULL); + CDevice* biline = device_bidirectional_line("biline", 4); + assert(biline != NULL); + CDevice* ring = device_ring("ring", 4); + assert(ring != NULL); + CDevice* grid = device_grid("grid", 2, 2); + assert(grid != NULL); + CDevice* star = device_star("star", 4, 0); + assert(star != NULL); + + device_free(line); + device_free(biline); + device_free(ring); + device_free(grid); + device_free(star); + device_free(NULL); + assert(device_new(NULL, 2) == NULL); +} + +static void test_device_edges_and_properties(void) { + const uint32_t edges[] = {0, 1, 1, 2, 1, 3, 3, 4}; + CDevice* device = device_from_edges("t-shape", 5, edges, 4); + assert(device != NULL); + + char* name = device_name(device); + assert(name != NULL); + assert(strcmp(name, "t-shape") == 0); + cqlib_string_free(name); + + assert(device_num_qubits(device) == 5); + + CTopology* topology = device_topology(device); + assert(topology != NULL); + assert(topology_num_qubits(topology) == 5); + assert(topology_num_couplings(topology) == 4); + topology_free(topology); + topology_free(NULL); + + /* Edge referencing a qubit beyond num_qubits must be rejected. */ + const uint32_t bad_edges[] = {0, 9}; + assert(device_from_edges("bad", 2, bad_edges, 1) == NULL); + + device_free(device); +} + +static void test_device_native_gates_and_validation(void) { + CDevice* device = device_line("dev", 2); + assert(device != NULL); + assert(device_with_native_gates(device, "H,CX,RZ") == 0); + + char* gates = device_native_gates(device); + assert(gates != NULL); + assert(strstr(gates, "H") != NULL); + assert(strstr(gates, "CX") != NULL); + cqlib_string_free(gates); + + /* Circuit uses only native gates -> valid. */ + CCircuit* circuit = circuit_new(2); + assert(circuit_h(circuit, 0) == 0); + assert(circuit_cx(circuit, 0, 1) == 0); + assert(device_validate_circuit(device, circuit) == 0); + + /* T is not a native gate here -> invalid. */ + assert(circuit_t(circuit, 0) == 0); + assert(device_validate_circuit(device, circuit) == -3); + + circuit_free(circuit); + assert(device_with_native_gates(device, "NOT_A_GATE") == -4); + device_free(device); +} + +static void test_layout(void) { + const uint32_t logical[] = {0, 1, 2}; + const uint32_t physical[] = {4, 3, 1}; + CLayout* layout = layout_new(logical, 3, physical, 3); + assert(layout != NULL); + assert(layout_num_logical(layout) == 3); + assert(layout_num_physical(layout) == 3); + assert(layout_get(layout, 0) == 4); + assert(layout_get(layout, 2) == 1); + /* Unmapped logical qubit -> u32 max sentinel. */ + assert(layout_get(layout, 9) == UINT32_MAX); + layout_free(layout); + + const uint32_t pairs[] = {0, 4, 1, 3, 2, 1}; + CLayout* from_pairs = layout_from_pairs(pairs, 3, 5); + assert(from_pairs != NULL); + assert(layout_get(from_pairs, 1) == 3); + layout_free(from_pairs); + + /* Mismatched array lengths must be rejected. */ + assert(layout_new(logical, 3, physical, 2) == NULL); + layout_free(NULL); +} + +static void test_noise_model(void) { + CNoiseModel* model = noise_model_new(); + assert(model != NULL); + + assert(noise_model_add_single_qubit(model, "H", 0, NOISE_DEPOLARIZING, 0.001) == 0); + assert(noise_model_add_single_qubit(model, "BOGUS", 0, NOISE_DEPOLARIZING, 0.001) == -4); + assert(noise_model_add_single_qubit(model, "H", 0, 99, 0.001) == -8); + assert(noise_model_add_single_qubit(model, "H", 0, NOISE_DEPOLARIZING, 1.5) == -8); + + assert(noise_model_add_two_qubit(model, "CX", 0, 1, NOISE_TWO_DEPOLARIZING, 0.01) == 0); + assert(noise_model_add_two_qubit(model, "CX", 1, 1, NOISE_TWO_DEPOLARIZING, 0.01) == -8); + + assert(noise_model_add_readout(model, 0, 0.02, 0.01) == 0); + assert(noise_model_add_readout(model, 1, 0.5, 1.7) == -8); + + noise_model_free(model); + noise_model_free(NULL); +} + +static void test_execution_result(void) { + const char* bitstrings[] = {"00", "01", "11"}; + const uint64_t counts[] = {480, 40, 480}; + CExecutionResult* result = + execution_result_from_counts("task-001", 2, 1000, bitstrings, counts, 3); + assert(result != NULL); + assert(execution_result_shots(result) == 1000); + assert(execution_result_num_qubits(result) == 2); + + CCountsList* counts_list = execution_result_counts(result); + assert(counts_list != NULL); + assert(counts_list_len(counts_list) == 3); + double total = 0.0; + for (uintptr_t i = 0; i < counts_list_len(counts_list); i++) { + char* key = counts_list_get_key(counts_list, i); + assert(key != NULL); + total += counts_list_get_value(counts_list, i); + cqlib_string_free(key); + } + assert(total == 1000.0); + counts_list_free(counts_list); + + CCountsList* probs = execution_result_probabilities(result); + assert(probs != NULL); + double prob_total = 0.0; + for (uintptr_t i = 0; i < counts_list_len(probs); i++) { + prob_total += counts_list_get_value(probs, i); + } + assert(fabs(prob_total - 1.0) < 1e-9); + counts_list_free(probs); + + execution_result_free(result); + execution_result_free(NULL); + assert(execution_result_from_counts(NULL, 2, 10, bitstrings, counts, 3) == NULL); + counts_list_free(NULL); +} + +/* ===== Section 3.1: device attribute queries and updates ===== */ + +static void test_device_attributes(void) { + CDevice* device = device_new("dev", 4); + assert(device != NULL); + + /* Defaults start unset (-8, NaN) and round-trip after set. */ + double v = 0.0; + assert(device_default_t1(device, &v) == -8 && isnan(v)); + assert(device_default_t2(device, &v) == -8); + assert(device_default_readout_error(device, &v) == -8); + assert(device_default_single_qubit_error(device, &v) == -8); + assert(device_default_two_qubit_error(device, &v) == -8); + assert(device_set_default_t1(device, 40.0) == 0); + assert(device_set_default_t2(device, 35.0) == 0); + assert(device_set_default_readout_error(device, 0.02) == 0); + assert(device_set_default_single_qubit_error(device, 0.005) == 0); + assert(device_set_default_two_qubit_error(device, 0.01) == 0); + assert(device_default_t1(device, &v) == 0 && v == 40.0); + assert(device_default_two_qubit_error(device, &v) == 0 && v == 0.01); + + /* Per-qubit queries fall back to the defaults. */ + assert(device_get_t1(device, 0, &v) == 0 && v == 40.0); + assert(device_get_t2(device, 2, &v) == 0 && v == 35.0); + assert(device_get_readout_error(device, 3, &v) == 0 && v == 0.02); + + int64_t ms = 0; + assert(device_calibration_time(device, &ms) == -8); + + /* Invalid-qubit bookkeeping (two-step array output). */ + const uint32_t invalid[2] = {1, 3}; + assert(device_set_invalid_qubits(device, invalid, 2) == 0); + assert(device_invalid_qubits_len(device) == 2); + uint32_t got_invalid[2] = {0}; + assert(device_invalid_qubits(device, got_invalid, 2) == 2); + assert(((got_invalid[0] == 1 && got_invalid[1] == 3)) || + (got_invalid[0] == 3 && got_invalid[1] == 1)); + assert(device_is_usable_qubit(device, 0) == 1); + assert(device_is_usable_qubit(device, 1) == 0); + assert(device_is_usable_qubit(device, 9) == 0); + assert(device_num_usable_qubits(device) == 2); + uint32_t usable[2] = {0}; + assert(device_usable_qubits_len(device) == 2); + assert(device_usable_qubits(device, usable, 2) == 2); + assert(((usable[0] == 0 && usable[1] == 2)) || (usable[0] == 2 && usable[1] == 0)); + assert(device_qubits_len(device) == 4); + uint32_t all[4] = {0}; + assert(device_qubits(device, all, 4) == 4); + for (uintptr_t i = 0; i < 4; i++) { + assert(all[i] == (uint32_t)i); + } + + /* Unregistered qubits are rejected and the set survives. */ + const uint32_t bad[1] = {9}; + assert(device_set_invalid_qubits(device, bad, 1) == -2); + assert(device_invalid_qubits_len(device) == 2); + /* An empty set restores full usability. */ + assert(device_set_invalid_qubits(device, NULL, 0) == 0); + assert(device_num_usable_qubits(device) == 4); + + /* NULL guards. */ + assert(device_get_t1(NULL, 0, &v) == -1); + assert(device_set_default_t1(NULL, 1.0) == -1); + assert(device_set_invalid_qubits(NULL, invalid, 2) == -1); + assert(device_is_usable_qubit(NULL, 0) == -1); + + device_free(device); +} + +static void test_device_qubit_edge_properties(void) { + CDevice* device = device_line("line", 3); + assert(device != NULL); + + CQubitProp qprop = {0}; + CEdgeProp eprop = {0}; + double v = 0.0; + + /* Nothing recorded yet. */ + assert(device_qubit_properties(device, 0, &qprop) == -8); + assert(device_edge_properties(device, 0, 1, &eprop) == -8); + /* H is not a device-wide native gate -> unsupported. */ + assert(device_single_qubit_error(device, "H", 0, &v) == -8); + + /* Record qubit-0 properties with one native H instruction. */ + const char* names[1] = {"H"}; + const double rates[1] = {0.001}; + const double lengths[1] = {50.0}; + CQubitPropInput qin = { + .readout_error = 0.01, + .t1 = 20.0, + .t2 = 30.0, + .prob_meas0_prep1 = 0.02, + .prob_meas1_prep0 = 0.03, + .frequency = 5.0, + .native_gate_names = names, + .native_error_rates = rates, + .native_lengths = lengths, + .num_native_instructions = 1, + }; + assert(device_add_qubit_properties(device, 0, &qin) == 0); + assert(device_qubit_properties(device, 0, &qprop) == 0); + assert(qprop.readout_error == 0.01 && qprop.t1 == 20.0 && qprop.t2 == 30.0); + assert(qprop.prob_meas0_prep1 == 0.02 && qprop.prob_meas1_prep0 == 0.03); + assert(qprop.frequency == 5.0 && qprop.num_native_instructions == 1); + + CNativeInstruction native = {0}; + assert(device_qubit_prop_native_instruction(device, 0, 0, &native) == 0); + assert(native.name != NULL && strcmp(native.name, "H") == 0); + assert(native.error_rate == 0.001 && native.length == 50.0); + cqlib_string_free(native.name); + assert(device_qubit_prop_native_instruction(device, 0, 1, &native) == -8); + + /* Per-qubit values win over the unset defaults. */ + assert(device_get_t1(device, 0, &v) == 0 && v == 20.0); + assert(device_get_readout_error(device, 0, &v) == 0 && v == 0.01); + + /* A non-empty local list is authoritative. */ + assert(device_single_qubit_error(device, "H", 0, &v) == 0 && v == 0.001); + assert(device_single_qubit_error(device, "X", 0, &v) == -8); + assert(device_single_qubit_error(device, "CX", 0, &v) == -8); + assert(device_single_qubit_error(device, "BOGUS", 0, &v) == -4); + + /* Record edge (0 -> 1) properties with one native CX instruction. */ + const char* edge_names[1] = {"CX"}; + const double edge_rates[1] = {0.02}; + const double edge_lengths[1] = {100.0}; + CEdgePropInput ein = { + .native_gate_names = edge_names, + .native_error_rates = edge_rates, + .native_lengths = edge_lengths, + .num_native_instructions = 1, + }; + assert(device_add_edge_properties(device, 0, 1, &ein) == 0); + assert(device_edge_properties(device, 0, 1, &eprop) == 0); + assert(eprop.num_native_instructions == 1); + assert(device_edge_prop_native_instruction(device, 0, 1, 0, &native) == 0); + assert(native.name != NULL && strcmp(native.name, "CX") == 0); + assert(native.error_rate == 0.02 && native.length == 100.0); + cqlib_string_free(native.name); + assert(device_edge_prop_native_instruction(device, 0, 1, 1, &native) == -8); + + assert(device_two_qubit_error(device, "CX", 0, 1, &v) == 0 && v == 0.02); + assert(device_two_qubit_error(device, "CX", 1, 0, &v) == -8); + assert(device_two_qubit_error(device, "CX", 0, 2, &v) == -8); + assert(device_two_qubit_error(device, "H", 0, 1, &v) == -8); + assert(device_two_qubit_error(device, "BOGUS", 0, 1, &v) == -4); + + assert(device_edge_error(device, 0, 1, &v) == 0 && v == 0.02); + assert(device_edge_error(device, 1, 0, &v) == -8); + assert(device_edge_error(device, 1, 2, &v) == -8); + + /* Unregistered targets are rejected; bad payloads map to -4/-8. */ + assert(device_add_qubit_properties(device, 9, &qin) == -2); + assert(device_add_edge_properties(device, 2, 1, &ein) == -2); + assert(device_add_qubit_properties(device, 1, NULL) == -1); + const char* bogus_names[1] = {"BOGUS"}; + CEdgePropInput bad_ein = { + .native_gate_names = bogus_names, + .native_error_rates = edge_rates, + .native_lengths = NULL, + .num_native_instructions = 1, + }; + assert(device_add_edge_properties(device, 0, 1, &bad_ein) == -4); + const double bad_rate[1] = {1.5}; + CEdgePropInput bad_rate_ein = { + .native_gate_names = edge_names, + .native_error_rates = bad_rate, + .native_lengths = NULL, + .num_native_instructions = 1, + }; + assert(device_add_edge_properties(device, 0, 1, &bad_rate_ein) == -8); + + /* Capability queries: exact ordered qargs decide. */ + const uint32_t q0[1] = {0}; + const uint32_t q1[1] = {1}; + const uint32_t pair01[2] = {0, 1}; + const uint32_t pair10[2] = {1, 0}; + assert(device_supports_native_instruction(device, "H", q0, 1) == 1); + assert(device_supports_native_instruction(device, "H", q1, 1) == 0); + assert(device_supports_native_instruction(device, "CX", pair01, 2) == 1); + assert(device_supports_native_instruction(device, "CX", pair10, 2) == 0); + assert(device_supports_native_instruction(device, "CX", q0, 1) == 0); + assert(device_supports_native_instruction(device, "BOGUS", q0, 1) == -4); + assert(device_supports_native_instruction(device, "H", NULL, 1) == -1); + assert(device_supports_native_instruction(device, "GPhase", NULL, 0) == 0); + + device_free(device); +} + +static void test_device_line_from_qubits_and_validate_operation(void) { + const uint32_t qubits[3] = {5, 6, 7}; + CDevice* device = device_line_from_qubits("line7", qubits, 3); + assert(device != NULL); + assert(device_qubits_len(device) == 3); + uint32_t all[3] = {0}; + assert(device_qubits(device, all, 3) == 3); + assert(all[0] == 5 && all[1] == 6 && all[2] == 7); + device_free(device); + assert(device_line_from_qubits(NULL, qubits, 3) == NULL); + assert(device_line_from_qubits("x", NULL, 3) == NULL); + + /* validate_operation consumes a CValueOperation snapshot from circuit_index. */ + CDevice* dev = device_line("dev", 2); + assert(dev != NULL); + assert(device_with_native_gates(dev, "H,CX") == 0); + CCircuit* circuit = circuit_new(2); + assert(circuit_h(circuit, 0) == 0); + assert(circuit_cx(circuit, 0, 1) == 0); + CValueOperation* op = circuit_index(circuit, 0); + assert(op != NULL); + assert(device_validate_operation(dev, op) == 0); + assert(device_validate_value_operation(dev, op) == 0); + value_operation_free(op); + assert(circuit_t(circuit, 0) == 0); + CValueOperation* bad_op = circuit_index(circuit, 2); + assert(bad_op != NULL); + assert(device_validate_operation(dev, bad_op) == -3); + assert(device_validate_value_operation(dev, bad_op) == -3); + value_operation_free(bad_op); + assert(device_validate_operation(dev, NULL) == -1); + circuit_free(circuit); + device_free(dev); +} + +/* ===== Section 3.2: layout binding and map introspection ===== */ + +static void test_layout_mapping(void) { + const uint32_t pairs[4] = {0, 2, 1, 0}; /* L0->P2, L1->P0 */ + CLayout* layout = layout_from_pairs(pairs, 2, 4); + assert(layout != NULL); + + /* Reverse lookup and vacancy checks. */ + assert(layout_get_logical(layout, 2) == 0); + assert(layout_get_logical(layout, 0) == 1); + assert(layout_get_logical(layout, 1) == UINT32_MAX); + assert(layout_is_physical_vacant(layout, 1) == 1); + assert(layout_is_physical_vacant(layout, 3) == 1); + assert(layout_is_physical_vacant(layout, 0) == 0); + assert(layout_is_physical_vacant(layout, 9) == 0); + + /* Bind / unbind. */ + assert(layout_bind(layout, 2, 1) == 0); + assert(layout_get(layout, 2) == 1); + assert(layout_bind(layout, 2, 3) == -8); + assert(layout_bind(layout, 3, 9) == -2); + uint32_t released = 0; + assert(layout_unbind(layout, 9, &released) == -8); + + /* Full map introspection (two-step arrays). */ + uint32_t l2p[6] = {0}; + assert(layout_l2p_map_len(layout) == 3); + assert(layout_l2p_map(layout, l2p, 3) == 3); + assert(l2p[0] == 0 && l2p[1] == 2 && l2p[2] == 1 && l2p[3] == 0 && l2p[4] == 2 && l2p[5] == 1); + uint32_t p2l[6] = {0}; + assert(layout_p2l_map_len(layout) == 3); + assert(layout_p2l_map(layout, p2l, 3) == 3); + assert(p2l[0] == 0 && p2l[1] == 1 && p2l[2] == 1 && p2l[3] == 2 && p2l[4] == 2 && p2l[5] == 0); + uint32_t logical[3] = {0}; + assert(layout_logical_qubits_len(layout) == 3); + assert(layout_logical_qubits(layout, logical, 3) == 3); + uint32_t physical[4] = {0}; + assert(layout_physical_qubits_len(layout) == 4); + assert(layout_physical_qubits(layout, physical, 4) == 4); + assert(physical[0] == 0 && physical[1] == 1 && physical[2] == 2 && physical[3] == 3); + uint32_t vacant[1] = {0}; + assert(layout_num_vacant_physical(layout) == 1); + assert(layout_vacant_physical_qubits_len(layout) == 1); + assert(layout_vacant_physical_qubits(layout, vacant, 1) == 1); + assert(vacant[0] == 3); + + /* Unbind releases the physical qubit; swap can move onto a vacant one. */ + assert(layout_unbind(layout, 0, &released) == 0 && released == 2); + assert(layout_get(layout, 0) == UINT32_MAX); + assert(layout_num_vacant_physical(layout) == 2); + assert(layout_swap_physical(layout, 1, 3) == 0); + assert(layout_get(layout, 2) == 3); + assert(layout_get_logical(layout, 1) == UINT32_MAX); + assert(layout_get_logical(layout, 3) == 2); + assert(layout_swap_physical(layout, 1, 99) == -2); + + /* NULL guards. */ + assert(layout_bind(NULL, 0, 0) == -1); + assert(layout_unbind(NULL, 0, &released) == -1); + assert(layout_get_logical(NULL, 0) == UINT32_MAX); + assert(layout_is_physical_vacant(NULL, 0) == -1); + assert(layout_num_vacant_physical(NULL) == 0); + assert(layout_swap_physical(NULL, 0, 1) == -1); + + layout_free(layout); +} + +/* ===== Section 3.3: topology graph queries and updates ===== */ + +static void test_topology_graph(void) { + const uint32_t qubits[3] = {0, 1, 2}; + CTopology* topo = topology_line(qubits, 3); + assert(topo != NULL); + assert(topology_num_qubits(topo) == 3); + assert(topology_num_couplings(topo) == 2); + + assert(topology_contains_qubit(topo, 0) == 1); + assert(topology_contains_qubit(topo, 9) == 0); + assert(topology_contains_qubit(NULL, 0) == -1); + + uint32_t all[3] = {0}; + assert(topology_qubits_len(topo) == 3); + assert(topology_qubits(topo, all, 3) == 3); + for (uintptr_t i = 0; i < 3; i++) { + assert(all[i] == (uint32_t)i); + } + + /* Directed line 0 -> 1 -> 2: neighbors, degrees, adjacency. */ + uint32_t neighbors[2] = {0}; + assert(topology_neighbors_undirected_len(topo, 1) == 2); + assert(topology_neighbors_undirected(topo, 1, neighbors, 2) == 2); + assert((neighbors[0] == 0 && neighbors[1] == 2) || (neighbors[0] == 2 && neighbors[1] == 0)); + uint32_t preds[1] = {0}; + uint32_t succs[1] = {0}; + assert(topology_predecessors_len(topo, 1) == 1); + assert(topology_predecessors(topo, 1, preds, 1) == 1 && preds[0] == 0); + assert(topology_successors_len(topo, 1) == 1); + assert(topology_successors(topo, 1, succs, 1) == 1 && succs[0] == 2); + assert(topology_in_degree(topo, 1) == 1); + assert(topology_out_degree(topo, 1) == 1); + assert(topology_neighbors_undirected_len(topo, 9) == 0); + assert(topology_in_degree(topo, 9) == 0); + + assert(topology_supports_directed_coupling(topo, 0, 1) == 1); + assert(topology_supports_directed_coupling(topo, 1, 0) == 0); + assert(topology_supports_coupling_either_direction(topo, 1, 0) == 1); + assert(topology_supports_coupling_either_direction(topo, 0, 2) == 0); + assert(topology_supports_directed_coupling(NULL, 0, 1) == -1); + + uint32_t undirected[4] = {0}; + assert(topology_undirected_edges_len(topo) == 2); + assert(topology_undirected_edges(topo, undirected, 2) == 2); + assert(undirected[0] == 0 && undirected[1] == 1 && undirected[2] == 1 && undirected[3] == 2); + + /* Qubit and coupling updates. */ + const uint32_t five[1] = {5}; + assert(topology_add_qubits(topo, five, 1) == 0); + assert(topology_qubits_len(topo) == 4); + assert(topology_add_qubits(topo, five, 1) == -8); + const uint32_t nine[1] = {9}; + assert(topology_remove_qubits(topo, nine, 1) == -2); + + const uint32_t edge[2] = {1, 5}; + const char* names[1] = {"c15"}; + assert(topology_add_couplings(topo, edge, 1, names) == 0); + assert(topology_num_couplings(topo) == 3); + char* name = topology_get_coupling_name(topo, 1, 5); + assert(name != NULL && strcmp(name, "c15") == 0); + cqlib_string_free(name); + char* unnamed = topology_get_coupling_name(topo, 0, 1); + assert(unnamed != NULL && unnamed[0] == '\0'); + cqlib_string_free(unnamed); + assert(topology_get_coupling_name(topo, 5, 1) == NULL); + assert(topology_get_coupling_name(NULL, 0, 1) == NULL); + + assert(topology_supports_directed_coupling(topo, 1, 5) == 1); + assert(topology_supports_coupling_either_direction(topo, 5, 1) == 1); + assert(topology_out_degree(topo, 1) == 2); + uint32_t succs2[2] = {0}; + assert(topology_successors_len(topo, 1) == 2); + assert(topology_successors(topo, 1, succs2, 2) == 2); + assert(succs2[0] == 2 && succs2[1] == 5); + + assert(topology_remove_couplings(topo, edge, 1) == 0); + assert(topology_supports_directed_coupling(topo, 1, 5) == 0); + assert(topology_remove_couplings(topo, edge, 1) == -2); + assert(topology_add_couplings(topo, edge, 1, NULL) == 0); + assert(topology_add_couplings(topo, edge, 1, NULL) == -8); + const uint32_t bad_edge[2] = {0, 9}; + assert(topology_add_couplings(topo, bad_edge, 1, NULL) == -2); + const uint32_t self_edge[2] = {0, 0}; + assert(topology_add_couplings(topo, self_edge, 1, NULL) == -8); + assert(topology_add_qubits(NULL, five, 1) == -1); + assert(topology_add_couplings(NULL, edge, 1, NULL) == -1); + assert(topology_remove_couplings(NULL, edge, 1) == -1); + assert(topology_add_qubits(topo, NULL, 1) == -1); + + /* Removing a qubit drops every coupling touching it. */ + assert(topology_remove_qubits(topo, five, 1) == 0); + assert(topology_contains_qubit(topo, 5) == 0); + assert(topology_num_couplings(topo) == 2); + + topology_free(topo); + assert(topology_line(NULL, 3) == NULL); +} + +/* ===== Section 3.4: Pauli channels and noise queries ===== */ + +static void test_noise_queries(void) { + CNoiseModel* model = noise_model_new(); + assert(model != NULL); + + /* Pauli channel add + query. */ + assert(noise_model_add_single_qubit_pauli(model, "H", 0, 0.01, 0.02, 0.03) == 0); + assert(noise_model_add_single_qubit_pauli(model, "BOGUS", 0, 0.01, 0.0, 0.0) == -4); + assert(noise_model_add_single_qubit_pauli(model, "H", 0, 0.5, 0.5, 0.5) == -8); + assert(noise_model_add_single_qubit_pauli(NULL, "H", 0, 0.01, 0.0, 0.0) == -1); + + assert(noise_model_get_single_qubit_errors_len(model, "H", 0) == 1); + CSingleQubitNoise channels[1] = {{0, 0.0, 0.0, 0.0, 0.0}}; + assert(noise_model_get_single_qubit_errors(model, "H", 0, channels, 1) == 1); + assert(channels[0].tag == NOISE_PAULI); + assert(channels[0].px == 0.01 && channels[0].py == 0.02 && channels[0].pz == 0.03); + assert(noise_model_get_single_qubit_errors_len(model, "H", 1) == 0); + assert(noise_model_get_single_qubit_errors_len(model, "BOGUS", 0) == 0); + + /* Two-qubit queries: key includes gate and the ordered pair. */ + assert(noise_model_add_two_qubit(model, "CX", 0, 1, NOISE_TWO_DEPOLARIZING, 0.01) == 0); + assert(noise_model_get_two_qubit_errors_len(model, "CX", 0, 1) == 1); + CTwoQubitNoise two_q[1]; + memset(two_q, 0, sizeof(two_q)); + assert(noise_model_get_two_qubit_errors(model, "CX", 0, 1, two_q, 1) == 1); + assert(two_q[0].tag == NOISE_TWO_DEPOLARIZING && two_q[0].p == 0.01); + assert(noise_model_get_two_qubit_errors_len(model, "CX", 1, 0) == 0); + assert(noise_model_get_two_qubit_errors_len(model, "CX", 1, 1) == 0); + assert(noise_model_get_two_qubit_errors_len(NULL, "CX", 0, 1) == 0); + + /* Readout queries. */ + assert(noise_model_add_readout(model, 0, 0.02, 0.03) == 0); + double p01 = 0.0; + double p10 = 0.0; + assert(noise_model_get_readout_error(model, 0, &p01, &p10) == 0); + assert(p01 == 0.02 && p10 == 0.03); + assert(noise_model_get_readout_error(model, 5, &p01, &p10) == -8); + assert(noise_model_get_readout_error(NULL, 0, &p01, &p10) == -1); + + noise_model_free(model); +} + +/* ===== Section 3.5: execution result lifecycle and metadata ===== */ + +static void test_execution_lifecycle(void) { + const uint32_t qubits[2] = {0, 1}; + CExecutionResult* result = execution_result_new("task-2", qubits, 2, 100, NULL); + assert(result != NULL); + + /* Freshly created: queued, no backend, only the creation timestamp. */ + uint8_t tag = 0; + int32_t code = 0; + assert(execution_result_status(result, &tag, &code) == 0); + assert(tag == EXECUTION_STATUS_QUEUED && code == 0); + int64_t ms = 0; + assert(execution_result_created_at(result, &ms) == 0 && ms > 1600000000000LL); + assert(execution_result_started_at(result, &ms) == -8); + assert(execution_result_finished_at(result, &ms) == -8); + assert(execution_result_shots(result) == 100); + assert(execution_result_num_qubits(result) == 2); + uint32_t got_q[2] = {0}; + assert(execution_result_qubits_len(result) == 2); + assert(execution_result_qubits(result, got_q, 2) == 2); + assert(got_q[0] == 0 && got_q[1] == 1); + char* task = execution_result_task_id(result); + assert(task != NULL && strcmp(task, "task-2") == 0); + cqlib_string_free(task); + assert(execution_result_backend(result) == NULL); + assert(execution_result_error_message(result) == NULL); + + /* start -> running -> finish with accumulated counts. */ + assert(execution_result_start(result) == 0); + assert(execution_result_status(result, &tag, &code) == 0 && tag == EXECUTION_STATUS_RUNNING); + assert(execution_result_started_at(result, &ms) == 0); + const char* bitstrings[3] = {"00", "11", "00"}; + const uint64_t counts[3] = {600, 400, 50}; + assert(execution_result_finish(result, bitstrings, counts, 3) == 0); + assert(execution_result_status(result, &tag, &code) == 0 && tag == EXECUTION_STATUS_COMPLETED); + assert(execution_result_finished_at(result, &ms) == 0); + CCountsList* list = execution_result_counts(result); + assert(list != NULL && counts_list_len(list) == 2); + double total = 0.0; + for (uintptr_t i = 0; i < counts_list_len(list); i++) { + total += counts_list_get_value(list, i); + } + assert(total == 1050.0); + counts_list_free(list); + assert(execution_result_error_message(result) == NULL); + + /* Invalid bitstrings are rejected. */ + const char* bad_bits[1] = {"0x"}; + const uint64_t one[1] = {1}; + assert(execution_result_finish(result, bad_bits, one, 1) == -4); + + /* fail -> failed with code and message; cancel -> cancelled. */ + assert(execution_result_fail(result, "device offline", 42) == 0); + assert(execution_result_status(result, &tag, &code) == 0); + assert(tag == EXECUTION_STATUS_FAILED && code == 42); + char* err = execution_result_error_message(result); + assert(err != NULL && strcmp(err, "device offline") == 0); + cqlib_string_free(err); + assert(execution_result_cancel(result) == 0); + assert(execution_result_status(result, &tag, &code) == 0 && tag == EXECUTION_STATUS_CANCELLED); + execution_result_free(result); + + /* Backend metadata echoes back. */ + CExecutionResult* with_backend = execution_result_new("task-3", qubits, 2, 10, "sim-ctx"); + assert(with_backend != NULL); + char* backend = execution_result_backend(with_backend); + assert(backend != NULL && strcmp(backend, "sim-ctx") == 0); + cqlib_string_free(backend); + execution_result_free(with_backend); + + /* NULL guards. */ + assert(execution_result_new(NULL, qubits, 2, 10, NULL) == NULL); + assert(execution_result_new("t", NULL, 2, 10, NULL) == NULL); + assert(execution_result_start(NULL) == -1); + assert(execution_result_finish(NULL, NULL, NULL, 0) == -1); + assert(execution_result_cancel(NULL) == -1); + assert(execution_result_status(NULL, &tag, &code) == -1); + assert(execution_result_task_id(NULL) == NULL); + assert(execution_result_backend(NULL) == NULL); + assert(execution_result_created_at(NULL, &ms) == -1); + assert(execution_result_started_at(NULL, &ms) == -1); + assert(execution_result_finished_at(NULL, &ms) == -1); + assert(execution_result_qubits_len(NULL) == 0); +} + +int main(void) { + test_device_topology_variants(); + test_device_edges_and_properties(); + test_device_native_gates_and_validation(); + test_layout(); + test_noise_model(); + test_execution_result(); + test_device_attributes(); + test_device_qubit_edge_properties(); + test_device_line_from_qubits_and_validate_operation(); + test_layout_mapping(); + test_topology_graph(); + test_noise_queries(); + test_execution_lifecycle(); + printf("binding-c device tests passed\n"); + return 0; +} diff --git a/crates/binding-c/tests/test_device_missing.c b/crates/binding-c/tests/test_device_missing.c new file mode 100644 index 00000000..d18fbc52 --- /dev/null +++ b/crates/binding-c/tests/test_device_missing.c @@ -0,0 +1,363 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +// Smoke tests for the newly added device-module FFI: per-qubit property +// setters/getters, native-instruction editors, layout-result helpers, +// execution-result status/probability helpers, and noise-model queries. + +#include +#include +#include +#include +#include + +#include "cqlib_c.h" + +/* Builds a CQubitPropInput with only the readout error set. */ +static CQubitPropInput bare_input(double readout) { + CQubitPropInput input = {0}; + input.readout_error = readout; + input.t1 = NAN; + input.t2 = NAN; + input.prob_meas0_prep1 = NAN; + input.prob_meas1_prep0 = NAN; + input.frequency = NAN; + input.native_gate_names = NULL; + input.native_error_rates = NULL; + input.native_lengths = NULL; + input.num_native_instructions = 0; + return input; +} + +static void test_device_property_setters_and_getters(void) { + CDevice* device = device_line("prop-line", 3); + assert(device != NULL); + + /* Record full properties for qubit 0: readout 0.01, t1 20, t2 30, + * prob01 0.02, prob10 0.03, freq 5.0, native H 0.001/50, X 0.002/60. */ + const char* gates[2] = {"H", "X"}; + const double rates[2] = {0.001, 0.002}; + const double lengths[2] = {50.0, 60.0}; + CQubitPropInput input = bare_input(0.01); + input.t1 = 20.0; + input.t2 = 30.0; + input.prob_meas0_prep1 = 0.02; + input.prob_meas1_prep0 = 0.03; + input.frequency = 5.0; + input.native_gate_names = gates; + input.native_error_rates = rates; + input.native_lengths = lengths; + input.num_native_instructions = 2; + assert(device_add_qubit_properties(device, 0, &input) == 0); + + /* Getters read back the stored values. */ + double v = NAN; + assert(device_get_frequency(device, 0, &v) == 0 && v == 5.0); + assert(device_get_prob_meas0_prep1(device, 0, &v) == 0 && v == 0.02); + assert(device_get_prob_meas1_prep0(device, 0, &v) == 0 && v == 0.03); + assert(device_get_error_rate(device, 0, 1, &v) == 0 && v == 0.002); + assert(device_get_length(device, 0, 0, &v) == 0 && v == 50.0); + assert(device_get_t1(device, 0, &v) == 0 && v == 20.0); + assert(device_get_t2(device, 0, &v) == 0 && v == 30.0); + + /* Getters fail for a qubit without recorded properties. */ + assert(device_get_frequency(device, 1, &v) == -8); + assert(device_get_error_rate(device, 1, 0, &v) == -8); + + /* NULL handling. */ + assert(device_get_frequency(NULL, 0, &v) == -1); + assert(device_set_frequency(NULL, 0, 1.0) == -1); + + /* Setters update one field and preserve the others; all reads happen + * after the last setter to verify the rebuild keeps every field. */ + assert(device_set_t1(device, 0, 21.0) == 0); + assert(device_set_t2(device, 0, 31.0) == 0); + assert(device_set_frequency(device, 0, 5.5) == 0); + assert(device_set_prob_meas0_prep1(device, 0, 0.021) == 0); + assert(device_set_prob_meas1_prep0(device, 0, 0.031) == 0); + assert(device_get_t1(device, 0, &v) == 0 && v == 21.0); + assert(device_get_t2(device, 0, &v) == 0 && v == 31.0); + assert(device_get_frequency(device, 0, &v) == 0 && v == 5.5); + assert(device_get_prob_meas0_prep1(device, 0, &v) == 0 && v == 0.021); + assert(device_get_prob_meas1_prep0(device, 0, &v) == 0 && v == 0.031); + /* Native instructions survive the property rebuilds. */ + assert(device_get_error_rate(device, 0, 1, &v) == 0 && v == 0.002); + + /* Setters fail for a qubit without recorded properties. */ + assert(device_set_t1(device, 2, 1.0) == -8); + assert(device_set_prob_meas0_prep1(device, 2, 0.1) == -8); + + device_free(device); +} + +static void test_device_native_instruction_editors(void) { + CDevice* device = device_line("gate-line", 2); + assert(device != NULL); + + const char* gates[2] = {"H", "X"}; + const double rates[2] = {0.001, 0.002}; + const double lengths[2] = {50.0, 60.0}; + CQubitPropInput input = bare_input(0.01); + input.native_gate_names = gates; + input.native_error_rates = rates; + input.native_lengths = lengths; + input.num_native_instructions = 2; + assert(device_add_qubit_properties(device, 0, &input) == 0); + + double v = NAN; + /* Edit error rate and duration in place. */ + assert(device_set_error_rate(device, 0, 1, 0.009) == 0); + assert(device_set_length(device, 0, 0, 70.0) == 0); + assert(device_get_error_rate(device, 0, 1, &v) == 0 && v == 0.009); + assert(device_get_length(device, 0, 0, &v) == 0 && v == 70.0); + /* Untouched entries keep their values. */ + assert(device_get_error_rate(device, 0, 0, &v) == 0 && v == 0.001); + assert(device_get_length(device, 0, 1, &v) == 0 && v == 60.0); + + /* Out-of-bounds index handling. */ + assert(device_set_error_rate(device, 0, 5, 0.1) == -8); + assert(device_set_length(device, 0, 5, 1.0) == -8); + assert(device_get_error_rate(device, 0, 5, &v) == -8); + assert(device_set_error_rate(device, 1, 0, 0.1) == -8); + + /* Replace the carried gate of entry 0 (H -> X) and verify the snapshot. */ + assert(device_set_instruction(device, 0, 0, "X") == 0); + CNativeInstruction inst = {0}; + assert(device_qubit_prop_native_instruction(device, 0, 0, &inst) == 0); + assert(inst.name != NULL && strcmp(inst.name, "X") == 0); + cqlib_string_free(inst.name); + assert(inst.error_rate == 0.001 && inst.length == 70.0); + + /* Append a new native instruction. */ + assert(device_set_native_instruction(device, 0, "Z", 0.003, 80.0) == 0); + CQubitProp snap = {0}; + assert(device_qubit_properties(device, 0, &snap) == 0); + assert(snap.num_native_instructions == 3); + CNativeInstruction third = {0}; + assert(device_qubit_prop_native_instruction(device, 0, 2, &third) == 0); + assert(third.name != NULL && strcmp(third.name, "Z") == 0); + cqlib_string_free(third.name); + assert(third.error_rate == 0.003 && third.length == 80.0); + + /* A two-qubit gate is rejected as a qubit-native instruction (arity). */ + assert(device_set_native_instruction(device, 0, "CX", 0.01, 300.0) == -8); + + /* Unknown gate names. */ + assert(device_set_instruction(device, 0, 0, "nope") == -4); + assert(device_set_native_instruction(device, 0, "nope", 0.01, 1.0) == -4); + + /* NULL handling. */ + assert(device_set_instruction(device, 0, 0, NULL) == -1); + assert(device_set_native_instruction(device, 0, NULL, 0.01, 1.0) == -1); + + device_free(device); +} + +static void test_layout_result_distances_and_activity(void) { + CDevice* device = device_line("dist-line", 3); + assert(device != NULL); + + CPhysicalLayoutGraph* graph = physical_layout_graph_from_device(device); + assert(graph != NULL); + + /* Two-step pattern: length query, then a full copy of the 3x3 table. */ + assert(layout_result_distances(graph, NULL, 0) == 9); + const uint32_t expected[9] = {0, 1, 2, 1, 0, 1, 2, 1, 0}; + uint32_t buf[9] = {0}; + assert(layout_result_distances(graph, buf, 9) == 9); + assert(memcmp(buf, expected, sizeof(expected)) == 0); + + /* A partial copy only touches the requested prefix. */ + uint32_t part[9]; + for (uintptr_t i = 0; i < 9; i++) { + part[i] = 7; + } + assert(layout_result_distances(graph, part, 3) == 9); + assert(part[0] == 0 && part[1] == 1 && part[2] == 2); + for (uintptr_t i = 3; i < 9; i++) { + assert(part[i] == 7); + } + assert(layout_result_distances(NULL, NULL, 0) == 0); + physical_layout_graph_free(graph); + + /* Activity from interactions cx(0,1) x2 and cx(1,2) x1. */ + CCircuit* circuit = circuit_new(3); + assert(circuit != NULL); + assert(circuit_cx(circuit, 0, 1) == 0); + assert(circuit_cx(circuit, 0, 1) == 0); + assert(circuit_cx(circuit, 1, 2) == 0); + + CCircuitLayoutAnalysis* analysis = analyze_circuit_for_layout(circuit); + assert(analysis != NULL); + assert(layout_result_logical_activity(analysis, NULL, NULL, 0) == 3); + uint32_t qubits[3] = {0}; + double act[3] = {0.0}; + assert(layout_result_logical_activity(analysis, qubits, act, 3) == 3); + assert(qubits[0] == 0 && qubits[1] == 1 && qubits[2] == 2); + const double expected_act[3] = {2.0, 3.0, 1.0}; + for (uintptr_t i = 0; i < 3; i++) { + assert(fabs(act[i] - expected_act[i]) < 1e-12); + } + circuit_layout_analysis_free(analysis); + + /* layout_result_analysis reuses the CCircuitLayoutAnalysis handle type. */ + CPreparedSabreCircuit* prepared = prepare_sabre_circuit(circuit); + assert(prepared != NULL); + CCircuitLayoutAnalysis* reused = layout_result_analysis(prepared); + assert(reused != NULL); + assert(circuit_layout_analysis_num_logical(reused) == 3); + circuit_layout_analysis_free(reused); + prepared_sabre_circuit_free(prepared); + assert(layout_result_analysis(NULL) == NULL); + + circuit_free(circuit); + device_free(device); +} + +static void test_execution_result_status_flags_and_bitstring(void) { + const uint32_t qubits[3] = {0, 1, 2}; + CExecutionResult* result = execution_result_new("task-1", qubits, 3, 100, NULL); + assert(result != NULL); + + /* Queued: neither success nor terminal. */ + assert(execution_result_is_success(result) == 0); + assert(execution_result_is_terminal(result) == 0); + + /* Running is still not terminal. */ + assert(execution_result_start(result) == 0); + assert(execution_result_is_terminal(result) == 0); + + /* Complete with counts. */ + const char* keys[3] = {"000", "111", "101"}; + const uint64_t counts[3] = {50, 30, 20}; + assert(execution_result_finish(result, keys, counts, 3) == 0); + assert(execution_result_is_success(result) == 1); + assert(execution_result_is_terminal(result) == 1); + + /* Bitstring formatting: bit i of the mask is measured qubit i. */ + const uint64_t masks[4] = {1, 5, 0, UINT64_MAX}; + const char* wants[4] = {"001", "101", "000", "111"}; + for (uintptr_t i = 0; i < 4; i++) { + char* text = execution_result_to_bitstring(result, masks[i]); + assert(text != NULL && strcmp(text, wants[i]) == 0); + cqlib_string_free(text); + } + assert(execution_result_to_bitstring(NULL, 0) == NULL); + + /* Failure and cancellation are terminal but not successful. */ + assert(execution_result_fail(result, "boom", 7) == 0); + assert(execution_result_is_success(result) == 0); + assert(execution_result_is_terminal(result) == 1); + assert(execution_result_cancel(result) == 0); + assert(execution_result_is_terminal(result) == 1); + + assert(execution_result_is_success(NULL) == -1); + assert(execution_result_is_terminal(NULL) == -1); + + execution_result_free(result); +} + +static void test_execution_result_calc_probabilities(void) { + const uint32_t qubits[2] = {0, 1}; + CExecutionResult* result = execution_result_new("task-2", qubits, 2, 50, NULL); + assert(result != NULL); + const char* keys[2] = {"00", "11"}; + const uint64_t counts[2] = {30, 20}; + assert(execution_result_finish(result, keys, counts, 2) == 0); + + /* Two-step pattern with ascending outcome order ("00" then "11"). */ + assert(execution_result_calc_probabilities_len(result) == 2); + double probs[2] = {0.0}; + assert(execution_result_calc_probabilities(result, probs, 2) == 2); + assert(fabs(probs[0] - 0.6) < 1e-12); + assert(fabs(probs[1] - 0.4) < 1e-12); + + /* A partial copy only touches the requested prefix. */ + double part[2] = {NAN, NAN}; + assert(execution_result_calc_probabilities(result, part, 1) == 2); + assert(fabs(part[0] - 0.6) < 1e-12); + assert(isnan(part[1])); + + assert(execution_result_calc_probabilities_len(NULL) == 0); + assert(execution_result_calc_probabilities(NULL, NULL, 0) == 0); + + execution_result_free(result); +} + +static void test_noise_model_validity_and_kraus(void) { + CNoiseModel* model = noise_model_new(); + assert(model != NULL); + + assert(noise_model_add_single_qubit(model, "H", 0, NOISE_DEPOLARIZING, 0.1) == 0); + assert(noise_model_is_valid(model, "H", 0, 0) == 1); + + /* Kraus output for depolarizing p=0.1: I*sqrt(0.9), X/Y/Z*sqrt(p/3), + * four 2x2 operators written back to back in row-major order. */ + assert(noise_model_to_kraus_len(model, "H", 0, 0) == 16); + Complex64 buf[16]; + memset(buf, 0, sizeof(buf)); + assert(noise_model_to_kraus(model, "H", 0, 0, buf, 16) == 16); + assert(fabs(buf[0].re - sqrt(0.9)) < 1e-12); + assert(fabs(buf[0].im) < 1e-12); + assert(fabs(buf[1].re) < 1e-12); + assert(fabs(buf[3].re - sqrt(0.9)) < 1e-12); + const double p_other = sqrt(0.1 / 3.0); + assert(fabs(buf[4].re) < 1e-12); + assert(fabs(buf[5].re - p_other) < 1e-12); + assert(fabs(buf[6].re - p_other) < 1e-12); + assert(fabs(buf[7].re) < 1e-12); + + /* A second channel under a different gate key (X@0, its own index 0). */ + assert(noise_model_add_single_qubit(model, "X", 0, NOISE_AMPLITUDE_DAMPING, 0.25) == 0); + assert(noise_model_is_valid(model, "X", 0, 0) == 1); + assert(noise_model_to_kraus_len(model, "X", 0, 0) == 8); + Complex64 buf2[8]; + memset(buf2, 0, sizeof(buf2)); + assert(noise_model_to_kraus(model, "X", 0, 0, buf2, 8) == 8); + assert(buf2[0].re == 1.0); + assert(fabs(buf2[3].re - sqrt(0.75)) < 1e-12); + assert(fabs(buf2[4].re) < 1e-12 && fabs(buf2[4].im) < 1e-12); + assert(fabs(buf2[5].re - 0.5) < 1e-12); + assert(fabs(buf2[7].re) < 1e-12); + + /* A partial copy only touches the requested prefix. */ + Complex64 part[8]; + for (uintptr_t i = 0; i < 8; i++) { + part[i].re = NAN; + part[i].im = NAN; + } + assert(noise_model_to_kraus(model, "X", 0, 0, part, 2) == 8); + assert(fabs(part[0].re - 1.0) < 1e-12); + assert(isnan(part[3].re)); + + /* Errors: absent key, out-of-bounds index, unknown gate, NULL. */ + assert(noise_model_is_valid(model, "H", 1, 0) == -8); + assert(noise_model_to_kraus_len(model, "H", 1, 0) == 0); + assert(noise_model_is_valid(model, "H", 0, 5) == -8); + assert(noise_model_is_valid(model, "nope", 0, 0) == -4); + assert(noise_model_to_kraus_len(model, "nope", 0, 0) == 0); + assert(noise_model_is_valid(NULL, "H", 0, 0) == -1); + assert(noise_model_to_kraus_len(NULL, "H", 0, 0) == 0); + + noise_model_free(model); +} + +int main(void) { + test_device_property_setters_and_getters(); + test_device_native_instruction_editors(); + test_layout_result_distances_and_activity(); + test_execution_result_status_flags_and_bitstring(); + test_execution_result_calc_probabilities(); + test_noise_model_validity_and_kraus(); + printf("binding-c device missing tests passed\n"); + return 0; +} diff --git a/crates/binding-c/tests/test_error_mitigation.c b/crates/binding-c/tests/test_error_mitigation.c new file mode 100644 index 00000000..808e7306 --- /dev/null +++ b/crates/binding-c/tests/test_error_mitigation.c @@ -0,0 +1,147 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +#include +#include +#include +#include + +#include "cqlib_c.h" + +/* Demo estimator: stands in for a real backend execution. The ideal + * expectation is 1.0; each additional gate copy contributes decay, so + * heavily folded circuits yield noisier raw estimates. */ +static void demo_estimator(const CCircuit* circuit, const CHamiltonian* hamiltonian, + uintptr_t shots, double* expectation, double* variance) { + (void)hamiltonian; + (void)shots; + const double ideal = 1.0; + const double decay = 0.99; + *expectation = ideal * pow(decay, (double)circuit_num_operations(circuit)); + *variance = 0.0; +} + +static void test_zne_mitigation_fold(void) { + CCircuit* circuit = circuit_new(2); + assert(circuit != NULL); + assert(circuit_h(circuit, 0) == 0); + assert(circuit_cx(circuit, 0, 1) == 0); + + const int32_t levels[] = {0, 1, 2}; + CZneMitigation* zne = zne_mitigation_new(circuit, levels, 3); + assert(zne != NULL); + + assert(zne_mitigation_noise_factor(zne, 0) == 1); + assert(zne_mitigation_noise_factor(zne, 1) == 3); + assert(zne_mitigation_noise_factor(zne, 2) == 5); + + /* Global folding produces one circuit per level. */ + CCircuitList* list = zne_mitigation_fold_circuit(zne, NULL); + assert(list != NULL); + assert(circuit_list_len(list) == 3); + uintptr_t ops0 = circuit_num_operations(circuit_list_get(list, 0)); + uintptr_t ops2 = circuit_num_operations(circuit_list_get(list, 2)); + assert(ops0 == 2); + assert(ops2 > ops0); + circuit_list_free(list); + + /* Selective folding on H only. */ + list = zne_mitigation_fold_circuit(zne, "H"); + assert(list != NULL); + assert(circuit_list_len(list) == 3); + circuit_list_free(list); + + /* Unknown gate name must fail. */ + assert(zne_mitigation_fold_circuit(zne, "NOT_A_GATE") == NULL); + + zne_mitigation_free(zne); + zne_mitigation_free(NULL); + circuit_free(circuit); +} + +static void test_zne_full_pipeline(void) { + CCircuit* circuit = circuit_new(2); + assert(circuit != NULL); + assert(circuit_h(circuit, 0) == 0); + assert(circuit_cx(circuit, 0, 1) == 0); + + const int32_t levels[] = {0, 1, 2}; + CErrorMitigation* em = error_mitigation_new(circuit, MITIGATION_ZNE, levels, 3, 0); + assert(em != NULL); + + CPauliString* pauli = pauli_string_parse("ZZ"); + assert(pauli != NULL); + CHamiltonian* ham = hamiltonian_from_pauli(pauli); + assert(ham != NULL); + + assert(error_mitigation_run(em, MITIGATION_ZNE, ham, 0, 0, demo_estimator) == 0); + + double expectation = NAN; + double variance = NAN; + assert(error_mitigation_get_mitigated(em, PROCESS_ZNE_POLYNOMIAL, 1, &expectation, &variance) == + 0); + assert(isfinite(expectation)); + + hamiltonian_free(ham); + error_mitigation_free(em); + error_mitigation_free(NULL); + circuit_free(circuit); +} + +static void test_get_mitigated_requires_run(void) { + CCircuit* circuit = circuit_new(1); + assert(circuit != NULL); + + const int32_t levels[] = {0, 1}; + CErrorMitigation* em = error_mitigation_new(circuit, MITIGATION_ZNE, levels, 2, 0); + assert(em != NULL); + + double expectation = NAN; + double variance = NAN; + assert(error_mitigation_get_mitigated(em, PROCESS_ZNE_POLYNOMIAL, 0, &expectation, &variance) == + -7); + + /* Method mismatch between new() and run() must fail. */ + assert(error_mitigation_run(em, MITIGATION_VIRTUAL_DISTILLATION, NULL, 0, 0, demo_estimator) != + 0); + + error_mitigation_free(em); + circuit_free(circuit); +} + +static void test_virtual_distillation(void) { + CCircuit* circuit = circuit_new(2); + assert(circuit != NULL); + assert(circuit_h(circuit, 0) == 0); + assert(circuit_cx(circuit, 0, 1) == 0); + + CVirtualDistillation* vd = virtual_distillation_new(circuit, 2); + assert(vd != NULL); + + CCircuit* built = virtual_distillation_build_circuit(vd); + assert(built != NULL); + assert(circuit_num_qubits(built) >= 2); + circuit_free(built); + + virtual_distillation_free(vd); + virtual_distillation_free(NULL); + circuit_free(circuit); +} + +int main(void) { + test_zne_mitigation_fold(); + test_zne_full_pipeline(); + test_get_mitigated_requires_run(); + test_virtual_distillation(); + printf("binding-c error mitigation tests passed\n"); + return 0; +} diff --git a/crates/binding-c/tests/test_ir.c b/crates/binding-c/tests/test_ir.c new file mode 100644 index 00000000..9cd54f5e --- /dev/null +++ b/crates/binding-c/tests/test_ir.c @@ -0,0 +1,109 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +#include +#include +#include +#include + +#include "cqlib_c.h" + +static const char* QASM2_SAMPLE = + "OPENQASM 2.0;\n" + "include \"qelib1.inc\";\n" + "qreg q[2];\n" + "h q[0];\n" + "cx q[0], q[1];\n"; + +static const char* QCIS_SAMPLE = "H Q0\nCZ Q0 Q1\nM Q0 Q1"; + +static void test_qasm2_loads_and_dumps(void) { + CCircuit* circuit = qasm2_loads(QASM2_SAMPLE); + assert(circuit != NULL); + assert(circuit_num_qubits(circuit) == 2); + + char* dumped = qasm2_dumps(circuit); + assert(dumped != NULL); + assert(strstr(dumped, "h") != NULL); + assert(strstr(dumped, "cx") != NULL); + cqlib_string_free(dumped); + circuit_free(circuit); +} + +static void test_qasm2_errors(void) { + assert(qasm2_loads(NULL) == NULL); + assert(qasm2_loads("not valid qasm") == NULL); + assert(qasm2_dumps(NULL) == NULL); +} + +static void test_qasm2_file_roundtrip(void) { + CCircuit* circuit = qasm2_loads(QASM2_SAMPLE); + assert(circuit != NULL); + + const char* tmpdir = getenv("TEMP"); + if (tmpdir == NULL) { + tmpdir = "."; + } + char path[512]; + snprintf(path, sizeof(path), "%s\\cqlib_c_test_qasm2_dump.qasm", tmpdir); + + assert(qasm2_dump(circuit, path) == 0); + CCircuit* loaded = qasm2_load(path); + assert(loaded != NULL); + assert(circuit_num_qubits(loaded) == 2); + circuit_free(loaded); + remove(path); + + assert(qasm2_dump(NULL, path) == -1); + assert(qasm2_dump(circuit, NULL) == -1); + + circuit_free(circuit); +} + +static void test_qasm3_dumps(void) { + CCircuit* circuit = qasm2_loads(QASM2_SAMPLE); + assert(circuit != NULL); + + char* dumped = qasm3_dumps(circuit); + assert(dumped != NULL); + cqlib_string_free(dumped); + circuit_free(circuit); +} + +static void test_qcis_loads_and_dumps(void) { + CCircuit* circuit = qcis_loads(QCIS_SAMPLE); + assert(circuit != NULL); + assert(circuit_num_qubits(circuit) == 2); + + char* dumped = qcis_dumps(circuit); + assert(dumped != NULL); + assert(strchr(dumped, 'H') != NULL || strchr(dumped, 'h') != NULL); + cqlib_string_free(dumped); + circuit_free(circuit); +} + +static void test_qcis_errors(void) { + assert(qcis_loads(NULL) == NULL); + assert(qcis_loads("@@@INVALID_GARBAGE@@@") == NULL); + assert(qcis_dumps(NULL) == NULL); +} + +int main(void) { + test_qasm2_loads_and_dumps(); + test_qasm2_errors(); + test_qasm2_file_roundtrip(); + test_qasm3_dumps(); + test_qcis_loads_and_dumps(); + test_qcis_errors(); + printf("binding-c ir tests passed\n"); + return 0; +} diff --git a/crates/binding-c/tests/test_misc_missing.c b/crates/binding-c/tests/test_misc_missing.c new file mode 100644 index 00000000..6e4823ef --- /dev/null +++ b/crates/binding-c/tests/test_misc_missing.c @@ -0,0 +1,210 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +#include +#include +#include +#include +#include + +#include "cqlib_c.h" + +static int close_to(double a, double b) { return fabs(a - b) < 1e-9; } + +/* |Phi+> = (|00> + |11>) / sqrt(2). */ +static CStatevector* make_bell_state(void) { + CStatevector* sv = statevector_new(2); + assert(sv != NULL); + assert(statevector_apply_h(sv, 0) == 0); + assert(statevector_apply_cx(sv, 0, 1) == 0); + return sv; +} + +static CHamiltonian* make_zz_hamiltonian(void) { + CPauliString* zz = pauli_string_parse("ZZ"); + assert(zz != NULL); + CHamiltonian* ham = hamiltonian_new(2); + assert(ham != NULL); + assert(hamiltonian_add_term(ham, zz, 1.0, 0.0) == 0); + pauli_string_free(zz); + return ham; +} + +static void test_hamiltonian_expectation_probs(void) { + CStatevector* sv = make_bell_state(); + CHamiltonian* ham = make_zz_hamiltonian(); + + /* Full computational-basis distribution of the Bell state. */ + CPauliString* basis = pauli_string_parse("ZZ"); + assert(basis != NULL); + const char* states[4] = {"00", "01", "10", "11"}; + double probs[4] = {0.5, 0.0, 0.0, 0.5}; + CProbMeasurement measurement; + measurement.basis = basis; + measurement.states = states; + measurement.probs = probs; + measurement.len = 4; + + /* = 0.5*(+1) + 0.5*(+1) = 1.0 on the Bell state. */ + double out = NAN; + assert(hamiltonian_expectation_probs(ham, &measurement, 1, &out) == 0); + assert(close_to(out, 1.0)); + + /* Only the observed (non-zero) outcomes may be supplied; the outcome + * eigenvalues of "00" and "11" under Z⊗Z are both +1. */ + const char* observed[2] = {"00", "11"}; + double observed_probs[2] = {0.5, 0.5}; + measurement.states = observed; + measurement.probs = observed_probs; + measurement.len = 2; + assert(hamiltonian_expectation_probs(ham, &measurement, 1, &out) == 0); + assert(close_to(out, 1.0)); + + /* A term whose support is contained in the measured basis: Z on qubit 1 + * evaluated in the ZZ basis gives = 0 on the Bell state. */ + CPauliString* zi = pauli_string_parse("ZI"); + assert(zi != NULL); + CHamiltonian* ham_zi = hamiltonian_new(2); + assert(ham_zi != NULL); + assert(hamiltonian_add_term(ham_zi, zi, 1.0, 0.0) == 0); + measurement.states = states; + measurement.probs = probs; + measurement.len = 4; + assert(hamiltonian_expectation_probs(ham_zi, &measurement, 1, &out) == 0); + assert(close_to(out, 0.0)); + hamiltonian_free(ham_zi); + pauli_string_free(zi); + + /* Errors: NULL out, NULL measurements with a non-zero count, NULL basis, + * and an empty measurement list for a non-identity Hamiltonian (-7). */ + assert(hamiltonian_expectation_probs(ham, &measurement, 1, NULL) == -1); + assert(hamiltonian_expectation_probs(ham, NULL, 1, &out) == -1); + CProbMeasurement bad = measurement; + bad.basis = NULL; + assert(hamiltonian_expectation_probs(ham, &bad, 1, &out) == -1); + assert(hamiltonian_expectation_probs(ham, NULL, 0, &out) == -7); + + pauli_string_free(basis); + hamiltonian_free(ham); + statevector_free(sv); + statevector_free(NULL); +} + +static void test_hamiltonian_variance_statevector(void) { + CStatevector* sv = make_bell_state(); + + /* The Bell state is an eigenstate of Z⊗Z: = ^2 = 1, so the + * variance is 0. */ + CHamiltonian* ham = make_zz_hamiltonian(); + double variance = NAN; + assert(hamiltonian_variance_statevector(ham, sv, &variance) == 0); + assert(close_to(variance, 0.0)); + hamiltonian_free(ham); + + /* Z on qubit 1 is not sharp: = 0 and = 1, so the variance + * is 1. */ + CPauliString* zi = pauli_string_parse("ZI"); + assert(zi != NULL); + CHamiltonian* ham_zi = hamiltonian_new(2); + assert(ham_zi != NULL); + assert(hamiltonian_add_term(ham_zi, zi, 1.0, 0.0) == 0); + assert(hamiltonian_variance_statevector(ham_zi, sv, &variance) == 0); + assert(close_to(variance, 1.0)); + hamiltonian_free(ham_zi); + pauli_string_free(zi); + + /* Errors: NULL pointers and a qubit-count mismatch (-8). */ + CPauliString* zzz = pauli_string_parse("ZZZ"); + assert(zzz != NULL); + CHamiltonian* ham3 = hamiltonian_new(3); + assert(ham3 != NULL); + assert(hamiltonian_add_term(ham3, zzz, 1.0, 0.0) == 0); + assert(hamiltonian_variance_statevector(ham3, sv, &variance) == -8); + pauli_string_free(zzz); + hamiltonian_free(ham3); + + assert(hamiltonian_variance_statevector(NULL, sv, &variance) == -1); + CHamiltonian* ham_tmp = make_zz_hamiltonian(); + assert(hamiltonian_variance_statevector(ham_tmp, NULL, &variance) == -1); + assert(hamiltonian_variance_statevector(ham_tmp, sv, NULL) == -1); + hamiltonian_free(ham_tmp); + + statevector_free(sv); +} + +static void test_local_bloch_vectors(void) { + CStatevector* sv = make_bell_state(); + + /* Each qubit of the Bell state is maximally mixed: all components 0. */ + assert(local_bloch_vectors_len(sv) == 6); + double bloch[6]; + assert(local_bloch_vectors(sv, bloch, 6) == 0); + for (int i = 0; i < 6; i++) { + assert(close_to(bloch[i], 0.0)); + } + + /* Product state |+> on qubit 0 and |0> on qubit 1. */ + CStatevector* product = statevector_new(2); + assert(product != NULL); + assert(statevector_apply_h(product, 0) == 0); + assert(local_bloch_vectors(product, bloch, 6) == 0); + assert(close_to(bloch[0], 1.0)); /* x0 */ + assert(close_to(bloch[1], 0.0)); /* y0 */ + assert(close_to(bloch[2], 0.0)); /* z0 */ + assert(close_to(bloch[3], 0.0)); /* x1 */ + assert(close_to(bloch[4], 0.0)); /* y1 */ + assert(close_to(bloch[5], 1.0)); /* z1 */ + statevector_free(product); + + /* Errors: wrong length (-8) and NULL pointers (-1). */ + assert(local_bloch_vectors(sv, bloch, 5) == -8); + assert(local_bloch_vectors(sv, NULL, 6) == -1); + assert(local_bloch_vectors(NULL, bloch, 6) == -1); + assert(local_bloch_vectors_len(NULL) == 0); + + statevector_free(sv); +} + +static void test_state_to_density_matrix(void) { + CStatevector* sv = make_bell_state(); + + /* 4^2 = 16 entries forming the 4x4 density matrix of the Bell state: + * 0.5 on the diagonal corners and on the (0,3)/(3,0) coherences. */ + assert(state_to_density_matrix_len(sv) == 16); + Complex64 matrix[16]; + assert(state_to_density_matrix(sv, matrix, 16) == 0); + for (int row = 0; row < 4; row++) { + for (int col = 0; col < 4; col++) { + const Complex64 entry = matrix[row * 4 + col]; + const int corner = (row == 0 || row == 3) && (col == 0 || col == 3); + assert(close_to(entry.re, corner ? 0.5 : 0.0)); + assert(close_to(entry.im, 0.0)); + } + } + + /* Errors: wrong length (-8) and NULL pointers (-1). */ + assert(state_to_density_matrix(sv, matrix, 15) == -8); + assert(state_to_density_matrix(sv, NULL, 16) == -1); + assert(state_to_density_matrix(NULL, matrix, 16) == -1); + assert(state_to_density_matrix_len(NULL) == 0); + + statevector_free(sv); +} + +int main(void) { + test_hamiltonian_expectation_probs(); + test_hamiltonian_variance_statevector(); + test_local_bloch_vectors(); + test_state_to_density_matrix(); + printf("binding-c misc missing tests passed\n"); + return 0; +} diff --git a/crates/binding-c/tests/test_parameter.c b/crates/binding-c/tests/test_parameter.c new file mode 100644 index 00000000..28bb009a --- /dev/null +++ b/crates/binding-c/tests/test_parameter.c @@ -0,0 +1,171 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +#include +#include +#include +#include +#include + +#include "cqlib_c.h" + +static void test_construction(void) { + CParameter* theta = param_symbol("theta"); + assert(theta != NULL); + + char* name = param_as_symbol(theta); + assert(name != NULL); + assert(strcmp(name, "theta") == 0); + cqlib_string_free(name); + + CParameter* pi = param_pi(); + assert(fabs(param_evaluate(pi, NULL) - M_PI) < 1e-12); + assert(param_is_constant(pi) == 1); + + CParameter* e = param_e(); + assert(fabs(param_evaluate(e, NULL) - exp(1.0)) < 1e-12); + + CParameter* half = param_from_double(0.5); + assert(fabs(param_evaluate(half, NULL) - 0.5) < 1e-12); + + assert(param_from_double(NAN) == NULL); + assert(param_from_double(INFINITY) == NULL); + assert(param_symbol(NULL) == NULL); + + param_free(theta); + param_free(pi); + param_free(e); + param_free(half); +} + +static void test_arithmetic_and_math(void) { + CParameter* theta = param_symbol("theta"); + CParameter* phi = param_symbol("phi"); + + CParameter* sum = param_add(theta, phi); + assert(fabs(param_evaluate(sum, "theta:0.5,phi:1.5") - 2.0) < 1e-12); + + CParameter* neg = param_neg(theta); + assert(fabs(param_evaluate(neg, "theta:0.5") + 0.5) < 1e-12); + + CParameter* x = param_from_double(2.0); + CParameter* cube = param_pow(x, param_from_double(3.0)); + assert(fabs(param_evaluate(cube, NULL) - 8.0) < 1e-12); + + CParameter* log2_8 = param_log(param_from_double(8.0), param_from_double(2.0)); + assert(fabs(param_evaluate(log2_8, NULL) - 3.0) < 1e-12); + + CParameter* rounded = param_round(param_from_double(1.75)); + assert(fabs(param_evaluate(rounded, NULL) - 2.0) < 1e-12); + + CParameter* sinus = param_sin(param_from_double(0.5)); + assert(fabs(param_evaluate(sinus, NULL) - sin(0.5)) < 1e-12); + + assert(param_add(NULL, theta) == NULL); + assert(param_sin(NULL) == NULL); + + param_free(theta); + param_free(phi); + param_free(sum); + param_free(neg); + param_free(x); + param_free(cube); + param_free(log2_8); + param_free(rounded); + param_free(sinus); +} + +static void test_symbol_queries(void) { + CParameter* expr = param_parse("phi + theta * 2"); + assert(expr != NULL); + + uintptr_t len = param_symbols_len(expr); + assert(len == 2); + + char** names = malloc(len * sizeof(char*)); + assert(names != NULL); + assert(param_symbols(expr, names, len) == 0); + assert(strcmp(names[0], "phi") == 0); + assert(strcmp(names[1], "theta") == 0); + for (uintptr_t i = 0; i < len; ++i) { + cqlib_string_free(names[i]); + } + free(names); + + assert(param_symbols(expr, NULL, 1) == -8); + assert(param_symbols(NULL, NULL, 0) == -1); + assert(param_as_symbol(expr) == NULL); + assert(param_is_constant(expr) == 0); + + CParameter* zero = param_from_double(0.0); + assert(param_is_zero(zero) == 1); + assert(param_is_one(zero) == 0); + assert(param_is_exact_zero(zero) == 1); + assert(param_is_constant(zero) == 1); + assert(param_is_zero(NULL) == -1); + + param_free(expr); + param_free(zero); +} + +static void test_simplify_substitute_derivative(void) { + CParameter* theta = param_symbol("theta"); + CParameter* expr = param_add(theta, param_from_double(0.0)); + + CParameter* simplified = param_simplify(expr); + assert(simplified != NULL); + assert(param_provably_equal(simplified, theta, 1e-12) == 1); + + CParameter* canonical = param_canonicalized(param_parse("pi / 2")); + assert(canonical != NULL); + assert(fabs(param_evaluate(canonical, NULL) - M_PI / 2.0) < 1e-12); + + CParameter* replaced = param_replace(param_parse("x + 2.0"), "x", param_from_double(1.0)); + assert(fabs(param_evaluate(replaced, NULL) - 3.0) < 1e-12); + + const char* names[2] = {"x", "y"}; + const CParameter* values[2] = {param_from_double(2.0), param_from_double(3.0)}; + CParameter* sum = param_substitute_many(param_parse("x + y"), names, values, 2); + assert(fabs(param_evaluate(sum, NULL) - 5.0) < 1e-12); + + CParameter* square = param_mul(theta, theta); + CParameter* deriv = param_derivative(square, "theta"); + assert(deriv != NULL); + assert(fabs(param_evaluate(deriv, "theta:3.0") - 6.0) < 1e-12); + + CParameter* two_pi = param_from_double(2.0 * M_PI); + assert(param_provably_equal_modulo(param_from_double(0.5), param_from_double(0.5 + 2.0 * M_PI), + two_pi, 1e-12) == 1); + assert(param_provably_equal(param_from_double(0.5), param_from_double(0.6), 1e-12) == 0); + assert(param_provably_equal(NULL, two_pi, 1e-12) == -1); + + param_free(theta); + param_free(expr); + param_free(simplified); + param_free(canonical); + param_free(replaced); + param_free(sum); + param_free(square); + param_free(deriv); + param_free(two_pi); + param_free((CParameter*)values[0]); + param_free((CParameter*)values[1]); +} + +int main(void) { + test_construction(); + test_arithmetic_and_math(); + test_symbol_queries(); + test_simplify_substitute_derivative(); + printf("test_parameter passed\n"); + return 0; +} diff --git a/crates/binding-c/tests/test_parity_circuit_compile3.c b/crates/binding-c/tests/test_parity_circuit_compile3.c new file mode 100644 index 00000000..58fc2dc3 --- /dev/null +++ b/crates/binding-c/tests/test_parity_circuit_compile3.c @@ -0,0 +1,298 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating that +// they have been altered from the originals. + +#include +#include +#include +#include +#include + +#include "cqlib_c.h" + +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif + +static int close_enough(double a, double b) { return fabs(a - b) < 1e-9; } + +static void assert_complex(Complex64 actual, double re, double im) { + assert(close_enough(actual.re, re)); + assert(close_enough(actual.im, im)); +} + +/* Builds one symbolic complex from real/imaginary expression strings and + * returns the parsed parameter handles so the caller can free them. */ +static struct CSymbolicComplex* make_complex(const char* re_expr, const char* im_expr, + struct CParameter** re_out, + struct CParameter** im_out) { + *re_out = param_parse(re_expr); + *im_out = param_parse(im_expr); + assert(*re_out != NULL); + assert(*im_out != NULL); + struct CSymbolicComplex* complex = symbolic_complex_new(*re_out, *im_out); + assert(complex != NULL); + return complex; +} + +static void test_unitary_gate_matrix_for_params(void) { + /* U(theta) = cos(theta) * I + i sin(theta) * X. */ + struct CParameter* re00 = NULL; + struct CParameter* im00 = NULL; + struct CParameter* re01 = NULL; + struct CParameter* im01 = NULL; + struct CParameter* re10 = NULL; + struct CParameter* im10 = NULL; + struct CParameter* re11 = NULL; + struct CParameter* im11 = NULL; + struct CSymbolicComplex* e00 = make_complex("cos(theta)", "0", &re00, &im00); + struct CSymbolicComplex* e01 = make_complex("0", "sin(theta)", &re01, &im01); + struct CSymbolicComplex* e10 = make_complex("0", "sin(theta)", &re10, &im10); + struct CSymbolicComplex* e11 = make_complex("cos(theta)", "0", &re11, &im11); + const struct CSymbolicComplex* elements[4] = {e00, e01, e10, e11}; + struct CSymbolicMatrix* matrix = symbolic_matrix_new(2, 2, elements, 4); + assert(matrix != NULL); + assert(unitary_gate_matrix_for_params_len(matrix) == 4u); + + const char* names[1] = {"theta"}; + Complex64 buffer[4] = {{0.0, 0.0}}; + + /* theta = 0 evaluates to the identity. */ + double zero[1] = {0.0}; + assert(unitary_gate_matrix_for_params(matrix, names, 1, zero, 1, buffer, 4) == 0); + assert_complex(buffer[0], 1.0, 0.0); + assert_complex(buffer[1], 0.0, 0.0); + assert_complex(buffer[2], 0.0, 0.0); + assert_complex(buffer[3], 1.0, 0.0); + + /* theta = pi/2 evaluates to iX. */ + double half_pi[1] = {M_PI / 2.0}; + assert(unitary_gate_matrix_for_params(matrix, names, 1, half_pi, 1, buffer, 4) == 0); + assert_complex(buffer[0], 0.0, 0.0); + assert_complex(buffer[1], 0.0, 1.0); + assert_complex(buffer[2], 0.0, 1.0); + assert_complex(buffer[3], 0.0, 0.0); + + /* Error paths. */ + assert(unitary_gate_matrix_for_params(NULL, names, 1, zero, 1, buffer, 4) == -1); + assert(unitary_gate_matrix_for_params(matrix, names, 1, zero, 1, NULL, 4) == -1); + assert(unitary_gate_matrix_for_params(matrix, NULL, 1, zero, 1, buffer, 4) == -1); + assert(unitary_gate_matrix_for_params(matrix, names, 1, NULL, 1, buffer, 4) == -1); + + const char* null_names[1] = {NULL}; + assert(unitary_gate_matrix_for_params(matrix, null_names, 1, zero, 1, buffer, 4) == -1); + + /* Parameter-count mismatch and non-finite values map to -3. */ + assert(unitary_gate_matrix_for_params(matrix, names, 1, NULL, 0, buffer, 4) == -3); + double nan_value[1] = {NAN}; + assert(unitary_gate_matrix_for_params(matrix, names, 1, nan_value, 1, buffer, 4) == -3); + + /* A too-small buffer maps to -8. */ + assert(unitary_gate_matrix_for_params(matrix, names, 1, zero, 1, buffer, 3) == -8); + + symbolic_matrix_free(matrix); + symbolic_complex_free(e00); + symbolic_complex_free(e01); + symbolic_complex_free(e10); + symbolic_complex_free(e11); + param_free(re00); + param_free(im00); + param_free(re01); + param_free(im01); + param_free(re10); + param_free(im10); + param_free(re11); + param_free(im11); + + /* A parameterless constant Pauli-X matrix evaluates without names. */ + struct CSymbolicComplex* zero_c = symbolic_complex_zero(); + struct CSymbolicComplex* one_c = symbolic_complex_one(); + assert(zero_c != NULL && one_c != NULL); + const struct CSymbolicComplex* x_elements[4] = {zero_c, one_c, one_c, zero_c}; + struct CSymbolicMatrix* x_matrix = symbolic_matrix_new(2, 2, x_elements, 4); + assert(x_matrix != NULL); + Complex64 x_buffer[4] = {{9.0, 9.0}, {9.0, 9.0}, {9.0, 9.0}, {9.0, 9.0}}; + assert(unitary_gate_matrix_for_params(x_matrix, NULL, 0, NULL, 0, x_buffer, 4) == 0); + assert_complex(x_buffer[0], 0.0, 0.0); + assert_complex(x_buffer[1], 1.0, 0.0); + assert_complex(x_buffer[2], 1.0, 0.0); + assert_complex(x_buffer[3], 0.0, 0.0); + + /* A singular constant matrix fails the core unitarity check (-3). */ + const struct CSymbolicComplex* singular_elements[4] = {one_c, zero_c, zero_c, zero_c}; + struct CSymbolicMatrix* singular = symbolic_matrix_new(2, 2, singular_elements, 4); + assert(singular != NULL); + assert(unitary_gate_matrix_for_params(singular, NULL, 0, NULL, 0, x_buffer, 4) == -3); + + symbolic_matrix_free(singular); + symbolic_matrix_free(x_matrix); + symbolic_complex_free(zero_c); + symbolic_complex_free(one_c); + + /* NULL/unsupported length queries return 0. */ + assert(unitary_gate_matrix_for_params_len(NULL) == 0u); +} + +static void test_rewrite_config_target_instructions_setter(void) { + CRewriteConfig config = rewrite_config_default(); + assert(config.mode == REWRITE_MODE_OPTIMIZE); + assert(config.target_gate_names == NULL); + assert(config.target_gate_names_len == 0u); + + /* NULL config, an empty basis and NULL arrays with a non-zero length. */ + assert(rewrite_config_with_target_instructions(NULL, NULL, 2) == -1); + assert(rewrite_config_with_target_instructions(&config, NULL, 0) == -8); + assert(rewrite_config_with_target_instructions(&config, NULL, 2) == -1); + + /* An unknown gate name maps to -4 and leaves the field unset. */ + const char* bad[1] = {"NOPE"}; + assert(rewrite_config_with_target_instructions(&config, bad, 1) == -4); + assert(config.target_gate_names == NULL); + assert(config.target_gate_names_len == 0u); + + /* A NULL name entry maps to -1. */ + const char* null_entry[1] = {NULL}; + assert(rewrite_config_with_target_instructions(&config, null_entry, 1) == -1); + + /* Valid names are stored on the by-value config. */ + const char* gates[2] = {"H", "CZ"}; + assert(rewrite_config_with_target_instructions(&config, gates, 2) == 0); + assert(config.target_gate_names_len == 2u); + assert(config.target_gate_names == gates); +} + +static void test_rewrite_target_basis_lowering(void) { + struct CCircuit* circuit = circuit_new(2); + assert(circuit != NULL); + assert(circuit_cx(circuit, 0, 1) == 0); + + /* CX lowers to H-CZ-H against the {H, CZ} target basis. */ + const char* gates[2] = {"H", "CZ"}; + CRewriteConfig config = rewrite_config_lowering(); + assert(config.mode == REWRITE_MODE_LOWERING); + assert(rewrite_config_with_target_instructions(&config, gates, 2) == 0); + + struct CKnowledgeRewriteResult* result = rewrite_circuit(circuit, config); + assert(result != NULL); + assert(knowledge_rewrite_result_changed(result) == 1); + struct CCircuit* rewritten = knowledge_rewrite_result_circuit(result); + assert(rewritten != NULL); + assert(circuit_num_operations(rewritten) == 3u); + circuit_free(rewritten); + knowledge_rewrite_result_free(result); + + /* The standalone transform wrapper consumes the same config. */ + struct CCircuit* wrapped = transform_knowledge_rewrite(circuit, config); + assert(wrapped != NULL); + assert(circuit_num_operations(wrapped) == 3u); + circuit_free(wrapped); + + /* A basis that cannot represent CX fails final-target validation. */ + const char* x_only[1] = {"X"}; + CRewriteConfig impossible = rewrite_config_lowering(); + assert(rewrite_config_with_target_instructions(&impossible, x_only, 1) == 0); + assert(rewrite_circuit(circuit, impossible) == NULL); + + circuit_free(circuit); +} + +static struct CDevice* line3_device(void) { + const uint32_t edges[4] = {0, 1, 1, 2}; + struct CDevice* device = device_from_edges("line-3", 3, edges, 2); + assert(device != NULL); + return device; +} + +static void test_layout_objective_tag(void) { + /* The C tag follows the core constructor ordering. */ + assert(LAYOUT_OBJECTIVE_FIDELITY_REQUIRED == 3); +} + +static void test_layout_fidelity_required_without_calibration(void) { + struct CDevice* device = line3_device(); + struct CCircuit* circuit = circuit_new(2); + assert(circuit_cx(circuit, 0, 1) == 0); + + struct CPhysicalLayoutGraph* graph = physical_layout_graph_from_device(device); + assert(graph != NULL); + assert(physical_layout_graph_has_fidelity_data(graph) == 0); + + /* Pointer-returning entries surface missing calibration as NULL. */ + assert(trivial_layout(circuit, device, LAYOUT_OBJECTIVE_FIDELITY_REQUIRED) == NULL); + struct CCircuitLayoutAnalysis* analysis = analyze_circuit_for_layout(circuit); + assert(analysis != NULL); + assert(trivial_layout_prepared(analysis, graph, LAYOUT_OBJECTIVE_FIDELITY_REQUIRED) == NULL); + + /* The scoring entry reports the invalid configuration as -8. */ + const uint32_t qubits[2] = {0, 1}; + struct CLayout* layout = layout_new(qubits, 2, qubits, 2); + assert(layout != NULL); + CLayoutScore score = {0}; + assert(score_layout(LAYOUT_OBJECTIVE_FIDELITY_REQUIRED, analysis, graph, layout, &score) == -8); + + /* An unknown objective tag is rejected the same way. */ + assert(trivial_layout(circuit, device, 99) == NULL); + assert(score_layout(99, analysis, graph, layout, &score) == -8); + + layout_free(layout); + circuit_layout_analysis_free(analysis); + physical_layout_graph_free(graph); + circuit_free(circuit); + device_free(device); +} + +static void test_layout_fidelity_required_with_calibration(void) { + struct CDevice* device = line3_device(); + assert(device_set_default_readout_error(device, 0.01) == 0); + struct CCircuit* circuit = circuit_new(2); + assert(circuit_cx(circuit, 0, 1) == 0); + + struct CPhysicalLayoutGraph* graph = physical_layout_graph_from_device(device); + assert(graph != NULL); + assert(physical_layout_graph_has_fidelity_data(graph) == 1); + + struct CLayoutResult* result = + trivial_layout(circuit, device, LAYOUT_OBJECTIVE_FIDELITY_REQUIRED); + assert(result != NULL); + layout_result_free(result); + + struct CCircuitLayoutAnalysis* analysis = analyze_circuit_for_layout(circuit); + assert(analysis != NULL); + struct CLayoutResult* prepared = + trivial_layout_prepared(analysis, graph, LAYOUT_OBJECTIVE_FIDELITY_REQUIRED); + assert(prepared != NULL); + layout_result_free(prepared); + + /* The resolved objective carries fidelity terms into scoring. */ + const uint32_t qubits[2] = {0, 1}; + struct CLayout* layout = layout_new(qubits, 2, qubits, 2); + assert(layout != NULL); + CLayoutScore score = {0}; + assert(score_layout(LAYOUT_OBJECTIVE_FIDELITY_REQUIRED, analysis, graph, layout, &score) == 0); + assert(score.used_fidelity == 1u); + + layout_free(layout); + circuit_layout_analysis_free(analysis); + physical_layout_graph_free(graph); + circuit_free(circuit); + device_free(device); +} + +int main(void) { + test_unitary_gate_matrix_for_params(); + test_rewrite_config_target_instructions_setter(); + test_rewrite_target_basis_lowering(); + test_layout_objective_tag(); + test_layout_fidelity_required_without_calibration(); + test_layout_fidelity_required_with_calibration(); + printf("binding-c parity circuit/compile tests passed\n"); + return 0; +} diff --git a/crates/binding-c/tests/test_parity_circuit_qis.c b/crates/binding-c/tests/test_parity_circuit_qis.c new file mode 100644 index 00000000..1d6dc7ba --- /dev/null +++ b/crates/binding-c/tests/test_parity_circuit_qis.c @@ -0,0 +1,180 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +#include +#include +#include +#include +#include + +#include "cqlib_c.h" + +static void test_circuit_dag_add_parameter(void) { + CParameter* theta = param_parse("theta"); + assert(theta != NULL); + + // A DAG built from a circuit already interns "theta" via the RX gate. + CCircuit* circuit = circuit_new(1); + assert(circuit_rx_param(circuit, 0, theta) == 0); + CCircuitDag* dag = circuit_dag_from_circuit(circuit); + assert(dag != NULL); + assert(circuit_dag_parameters_len(dag) == 1); + + // Re-adding the resident parameter reports "not inserted". + uintptr_t index = 99; + int32_t inserted = -1; + assert(circuit_dag_add_parameter(dag, theta, &index, &inserted) == 0); + assert(index == 0); + assert(inserted == 0); + assert(circuit_dag_parameters_len(dag) == 1); + + // A fresh parameter lands at the next table index. + CParameter* phi = param_parse("phi"); + assert(phi != NULL); + assert(circuit_dag_add_parameter(dag, phi, &index, &inserted) == 0); + assert(index == 1); + assert(inserted == 1); + assert(circuit_dag_parameters_len(dag) == 2); + + // Adding it again keeps the index and reports "not inserted". + assert(circuit_dag_add_parameter(dag, phi, &index, &inserted) == 0); + assert(index == 1); + assert(inserted == 0); + + // Output pointers may be omitted. + assert(circuit_dag_add_parameter(dag, phi, NULL, NULL) == 0); + + // NULL handles and NULL parameters are rejected. + assert(circuit_dag_add_parameter(NULL, phi, &index, &inserted) == -1); + assert(circuit_dag_add_parameter(dag, NULL, &index, &inserted) == -1); + + circuit_dag_free(dag); + circuit_free(circuit); + param_free(theta); + param_free(phi); +} + +static void test_pauli_string_expectation(void) { + CPauliString* ps = pauli_string_new(2); + assert(ps != NULL); + assert(pauli_string_set_pauli(ps, 0, PAULI_Z) == 0); + + // Z0 on |00> gives +1; on little-endian "01" (qubit 0 = 1) gives -1. + const char* state00 = "00"; + const char* state01 = "01"; + const char* bits1[1] = {state00}; + const double one[1] = {1.0}; + double out = 99.0; + + assert(pauli_string_expectation(ps, bits1, one, 1, &out) == 0); + assert(out == 1.0); + + const char* bits01[1] = {state01}; + assert(pauli_string_expectation(ps, bits01, one, 1, &out) == 0); + assert(out == -1.0); + + // An even mixture of the two eigenstates averages to zero. + const char* bits2[2] = {state00, state01}; + const double half[2] = {0.5, 0.5}; + assert(pauli_string_expectation(ps, bits2, half, 2, &out) == 0); + assert(fabs(out) < 1e-12); + + // Off-diagonal strings vanish on computational-basis distributions. + assert(pauli_string_set_pauli(ps, 0, PAULI_X) == 0); + assert(pauli_string_expectation(ps, bits1, one, 1, &out) == 0); + assert(out == 0.0); + + // NULL handle, NULL output, and NULL arrays with data are rejected. + assert(pauli_string_expectation(NULL, bits1, one, 1, &out) == -1); + assert(pauli_string_expectation(ps, bits1, one, 1, NULL) == -1); + assert(pauli_string_expectation(ps, NULL, one, 1, &out) == -1); + assert(pauli_string_expectation(ps, bits1, NULL, 1, &out) == -1); + + // A NULL bitstring entry inside the arrays is an invalid parameter. + const char* bits_null[1] = {NULL}; + assert(pauli_string_expectation(ps, bits_null, one, 1, &out) == -8); + + // An empty distribution is valid and yields zero. + assert(pauli_string_expectation(ps, NULL, NULL, 0, &out) == 0); + assert(out == 0.0); + + pauli_string_free(ps); +} + +static void test_density_matrix_zeros_and_validate_physical(void) { + CDensityMatrix* dm = density_matrix_zeros(2); + assert(dm != NULL); + assert(density_matrix_num_qubits(dm) == 2); + + // Every element is zero, so the trace vanishes. + double trace_re = 9.0, trace_im = 9.0; + assert(density_matrix_trace(dm, &trace_re, &trace_im) == 0); + assert(trace_re == 0.0 && trace_im == 0.0); + + // Zero trace fails the unit-trace check (NotNormalized -> -8). + assert(density_matrix_validate_physical(dm, 1e-10) == -8); + density_matrix_free(dm); + + // Physical states pass: the pure |0> state and the maximally mixed I/2. + CDensityMatrix* pure = density_matrix_new(1); + assert(pure != NULL); + assert(density_matrix_validate_physical(pure, 1e-10) == 0); + density_matrix_free(pure); + + CDensityMatrix* mixed = density_matrix_maximally_mixed(1); + assert(mixed != NULL); + assert(density_matrix_validate_physical(mixed, 1e-10) == 0); + + // NULL handle and non-finite tolerance are rejected; an oversized + // qubit count fails the constructor instead of crashing. + assert(density_matrix_validate_physical(NULL, 1e-10) == -1); + assert(density_matrix_validate_physical(mixed, NAN) == -8); + density_matrix_free(mixed); + assert(density_matrix_zeros(100) == NULL); +} + +static void test_statevector_entanglement_entropy_pure(void) { + // Bell state |Phi+> = (|00> + |11>)/sqrt(2). + CStatevector* sv = statevector_new(2); + assert(sv != NULL); + assert(statevector_apply_h(sv, 0) == 0); + assert(statevector_apply_cx(sv, 0, 1) == 0); + + // One bit (log2) of entanglement across the 0|1 split. + const uint32_t subsys[1] = {0}; + double out = 99.0; + assert(statevector_entanglement_entropy_pure(sv, subsys, 1, &out) == 0); + assert(fabs(out - 1.0) < 1e-9); + + // A product state carries no entanglement. + CStatevector* product = statevector_new(2); + assert(product != NULL); + assert(statevector_entanglement_entropy_pure(product, subsys, 1, &out) == 0); + assert(fabs(out) < 1e-9); + statevector_free(product); + + // NULL handle, NULL output, and a NULL subsystem buffer with data. + assert(statevector_entanglement_entropy_pure(NULL, subsys, 1, &out) == -1); + assert(statevector_entanglement_entropy_pure(sv, subsys, 1, NULL) == -1); + assert(statevector_entanglement_entropy_pure(sv, NULL, 1, &out) == -1); + + statevector_free(sv); +} + +int main(void) { + test_circuit_dag_add_parameter(); + test_pauli_string_expectation(); + test_density_matrix_zeros_and_validate_physical(); + test_statevector_entanglement_entropy_pure(); + printf("binding-c parity circuit/qis tests passed\n"); + return 0; +} diff --git a/crates/binding-c/tests/test_parity_compile_em.c b/crates/binding-c/tests/test_parity_compile_em.c new file mode 100644 index 00000000..839620ee --- /dev/null +++ b/crates/binding-c/tests/test_parity_compile_em.c @@ -0,0 +1,213 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +#include +#include +#include +#include + +#include "cqlib_c.h" + +static void test_evolution_info(void) { + CHamiltonian* h = hamiltonian_new(2); + assert(h != NULL); + + CPauliString* zz = pauli_string_parse("ZZ"); + assert(zz != NULL); + assert(hamiltonian_add_term(h, zz, 0.5, 0.0) == 0); + pauli_string_free(zz); + + CPauliString* zi = pauli_string_parse("ZI"); + assert(zi != NULL); + assert(hamiltonian_add_term(h, zi, 0.3, 0.0) == 0); + pauli_string_free(zi); + + CPauliEvolutionAnsatz* ansatz = pauli_evolution_ansatz_new(h); + assert(ansatz != NULL); + + // ZZ and ZI mutually commute: Auto takes the exact single-pass path. + CPauliEvolutionInfo info; + memset(&info, 0, sizeof(info)); + assert(pauli_evolution_ansatz_evolution_info(ansatz, &info) == 0); + assert(info.is_exact == 1); + assert(info.all_terms_commute == 1); + assert(info.steps == 1); + assert(info.num_terms == 2); + assert(info.trotter_mode == TROTTER_MODE_NONE); + assert(info.trotter_seed == 0); + + // NULL guards. + CPauliEvolutionInfo sink; + assert(pauli_evolution_ansatz_evolution_info(NULL, &sink) == -1); + assert(pauli_evolution_ansatz_evolution_info(ansatz, NULL) == -1); + + pauli_evolution_ansatz_free(ansatz); + hamiltonian_free(h); +} + +static void test_copy_swap_circuit(void) { + CCircuit* circuit = circuit_new(1); + assert(circuit != NULL); + assert(circuit_h(circuit, 0) == 0); + + CVirtualDistillation* vd = virtual_distillation_new(circuit, 2); + assert(vd != NULL); + + CCircuit* copy_swap = virtual_distillation_build_copy_swap_circuit(vd); + assert(copy_swap != NULL); + assert(circuit_num_qubits(copy_swap) == 2); + assert(circuit_num_operations(copy_swap) > 0); + circuit_free(copy_swap); + + assert(virtual_distillation_build_copy_swap_circuit(NULL) == NULL); + + virtual_distillation_free(vd); + circuit_free(circuit); +} + +static void assert_symbol_list(char** out, uintptr_t len, const char* expected0, + const char* expected1) { + assert(strcmp(out[0], expected0) == 0); + assert(strcmp(out[1], expected1) == 0); + for (uintptr_t i = 0; i < len; i++) { + cqlib_string_free(out[i]); + } +} + +static void test_rule_symbols(void) { + const char* merge_dsl = + "rule merge_rz {\n" + " match {\n" + " RZ(a) 0\n" + " RZ(b) 0\n" + " }\n" + " rewrite {\n" + " RZ(a + b) 0\n" + " }\n" + "}"; + const char* cancel_dsl = + "rule cancel_h {\n" + " match {\n" + " H 0\n" + " H 0\n" + " }\n" + " rewrite {}\n" + "}"; + const char* condition_dsl = + "rule cancel_rz_inverse {\n" + " match { RZ(a) 0, RZ(b) 0 }\n" + " require { a + b == 0 mod 4*π }\n" + " rewrite {}\n" + "}"; + + CKnowledgeRule* merge = NULL; + assert(knowledge_rule_from_dsl(merge_dsl, &merge) == 0); + assert(merge != NULL); + CKnowledgeRule* cancel = NULL; + assert(knowledge_rule_from_dsl(cancel_dsl, &cancel) == 0); + assert(cancel != NULL); + CKnowledgeRule* condition = NULL; + assert(knowledge_rule_from_dsl(condition_dsl, &condition) == 0); + assert(condition != NULL); + + // Match-block symbols: the merge pattern binds a and b (sorted). + assert(knowledge_rule_operation_symbols_len(merge) == 2); + char* ops[2] = {NULL, NULL}; + assert(knowledge_rule_operation_symbols(merge, ops, 2) == 0); + assert_symbol_list(ops, 2, "a", "b"); + + // Rewrite-target symbols: the merge target references a and b; the + // cancel target is empty. + assert(knowledge_rule_target_symbols_len(merge) == 2); + char* targets[2] = {NULL, NULL}; + assert(knowledge_rule_target_symbols(merge, targets, 2) == 0); + assert_symbol_list(targets, 2, "a", "b"); + assert(knowledge_rule_target_symbols_len(cancel) == 0); + assert(knowledge_rule_target_symbols(cancel, NULL, 0) == 0); + + // Condition symbols: only the condition rule carries a require block. + assert(knowledge_rule_condition_symbols_len(condition) == 2); + char* conds[2] = {NULL, NULL}; + assert(knowledge_rule_condition_symbols(condition, conds, 2) == 0); + assert_symbol_list(conds, 2, "a", "b"); + assert(knowledge_rule_condition_symbols_len(merge) == 0); + assert(knowledge_rule_condition_symbols_len(cancel) == 0); + + // Wrong buffer lengths and NULL buffers are rejected. + char* short_out[1] = {NULL}; + assert(knowledge_rule_operation_symbols(merge, short_out, 1) == -8); + assert(short_out[0] == NULL); + assert(knowledge_rule_operation_symbols(merge, NULL, 2) == -1); + + // NULL guards. + assert(knowledge_rule_operation_symbols_len(NULL) == 0); + assert(knowledge_rule_target_symbols_len(NULL) == 0); + assert(knowledge_rule_condition_symbols_len(NULL) == 0); + assert(knowledge_rule_operation_symbols(NULL, NULL, 0) == -1); + assert(knowledge_rule_target_symbols(NULL, NULL, 0) == -1); + assert(knowledge_rule_condition_symbols(NULL, NULL, 0) == -1); + + knowledge_rule_free(condition); + knowledge_rule_free(cancel); + knowledge_rule_free(merge); +} + +static void test_step_reason(void) { + CCircuit* circuit = circuit_new(2); + assert(circuit != NULL); + assert(circuit_h(circuit, 0) == 0); + assert(circuit_cx(circuit, 0, 1) == 0); + + CompileConfigC config; + memset(&config, 0, sizeof(config)); + config.mode = COMPILE_MODE_NORMAL; + config.target = COMPILE_TARGET_LOGICAL; + + CCompileResult* result = compile(circuit, config); + assert(result != NULL); + + uintptr_t steps = compile_result_num_steps(result); + assert(steps > 0); + + // Every step has a name; a logical-target compile skips the target-basis + // steps with documented reasons, so at least one reason must be present. + uintptr_t reasons = 0; + for (uintptr_t i = 0; i < steps; i++) { + char* name = compile_result_step_name(result, i); + assert(name != NULL && strlen(name) > 0); + cqlib_string_free(name); + + char* reason = compile_result_step_reason(result, i); + if (reason != NULL) { + assert(strlen(reason) > 0); + cqlib_string_free(reason); + reasons++; + } + } + assert(reasons > 0); + + // Out-of-bounds indices and NULL input yield NULL. + assert(compile_result_step_reason(result, steps + 10) == NULL); + assert(compile_result_step_reason(NULL, 0) == NULL); + + compile_result_free(result); + circuit_free(circuit); +} + +int main(void) { + test_evolution_info(); + test_copy_swap_circuit(); + test_rule_symbols(); + test_step_reason(); + printf("binding-c parity compile/em tests passed\n"); + return 0; +} diff --git a/crates/binding-c/tests/test_parity_qis3.c b/crates/binding-c/tests/test_parity_qis3.c new file mode 100644 index 00000000..f80a3fff --- /dev/null +++ b/crates/binding-c/tests/test_parity_qis3.c @@ -0,0 +1,145 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating that +// they have been altered from the originals. + +#include +#include +#include +#include +#include + +#include "cqlib_c.h" + +static int near(double a, double b) { return fabs(a - b) < 1e-10; } + +static void test_statevector_data_mut(void) { + CStatevector* sv = statevector_new(1); + assert(sv != NULL); + assert(statevector_data_len(sv) == 2); + + /* Overwrite |0> with |1>, then read the amplitudes back. */ + Complex64 one[2] = {{0.0, 0.0}, {1.0, 0.0}}; + assert(statevector_data_mut(sv, one, 2) == 0); + Complex64 amps[2]; + assert(statevector_data(sv, amps, 2) == 0); + assert(near(amps[0].re, 0.0) && near(amps[0].im, 0.0)); + assert(near(amps[1].re, 1.0) && near(amps[1].im, 0.0)); + + /* The simulator keeps operating on the written data: Z|1> = -|1>. */ + assert(statevector_apply_z(sv, 0) == 0); + assert(statevector_data(sv, amps, 2) == 0); + assert(near(amps[0].re, 0.0)); + assert(near(amps[1].re, -1.0) && near(amps[1].im, 0.0)); + + /* A normalized superposition can also be written directly. */ + double inv = 1.0 / sqrt(2.0); + Complex64 plus[2] = {{inv, 0.0}, {inv, 0.0}}; + assert(statevector_data_mut(sv, plus, 2) == 0); + assert(statevector_data(sv, amps, 2) == 0); + assert(near(amps[0].re, inv) && near(amps[1].re, inv)); + + /* Length mismatch is rejected; NULL pointers return -1. */ + assert(statevector_data_mut(sv, one, 1) == -8); + assert(statevector_data_mut(NULL, one, 2) == -1); + assert(statevector_data_mut(sv, NULL, 2) == -1); + statevector_free(sv); +} + +static void test_density_matrix_psd(void) { + bool positive = false; + + /* Ground state |0><0| is positive semidefinite. */ + CDensityMatrix* dm = density_matrix_new(1); + assert(dm != NULL); + assert(density_matrix_is_positive_semidefinite(dm, 1e-10, &positive) == 0); + assert(positive); + density_matrix_free(dm); + + /* Maximally mixed state I/4 is positive semidefinite. */ + CDensityMatrix* mixed = density_matrix_maximally_mixed(2); + assert(mixed != NULL); + assert(density_matrix_is_positive_semidefinite(mixed, 1e-10, &positive) == 0); + assert(positive); + density_matrix_free(mixed); + + /* Pure |+><+| state is positive semidefinite. */ + double inv = 1.0 / sqrt(2.0); + Complex64 plus[2] = {{inv, 0.0}, {inv, 0.0}}; + CDensityMatrix* pure = density_matrix_from_state(1, plus, 2); + assert(pure != NULL); + assert(density_matrix_is_positive_semidefinite(pure, 1e-10, &positive) == 0); + assert(positive); + density_matrix_free(pure); + + /* The zero matrix has zero eigenvalues and also passes. */ + CDensityMatrix* zero = density_matrix_zeros(1); + assert(zero != NULL); + assert(density_matrix_is_positive_semidefinite(zero, 1e-10, &positive) == 0); + assert(positive); + density_matrix_free(zero); + + /* Error paths: NULL handles and non-finite tolerance. */ + assert(density_matrix_is_positive_semidefinite(NULL, 1e-10, &positive) == -1); + dm = density_matrix_new(1); + assert(density_matrix_is_positive_semidefinite(dm, 1e-10, NULL) == -1); + assert(density_matrix_is_positive_semidefinite(dm, NAN, &positive) == -8); + density_matrix_free(dm); +} + +static void test_pauli_mul_with_phase(void) { + uint8_t tag = 0xFF; + Complex64 phase = {9.0, 9.0}; + + /* X * Z = -iY. */ + assert(pauli_mul_with_phase(PAULI_X, PAULI_Z, &tag, &phase) == 0); + assert(tag == PAULI_Y); + assert(near(phase.re, 0.0) && near(phase.im, -1.0)); + + /* Z * X = iY. */ + assert(pauli_mul_with_phase(PAULI_Z, PAULI_X, &tag, &phase) == 0); + assert(tag == PAULI_Y); + assert(near(phase.re, 0.0) && near(phase.im, 1.0)); + + /* X * X = I with phase +1. */ + assert(pauli_mul_with_phase(PAULI_X, PAULI_X, &tag, &phase) == 0); + assert(tag == PAULI_I); + assert(near(phase.re, 1.0) && near(phase.im, 0.0)); + + /* X * Y = iZ, Y * Z = iX, Z * Y = -iX, Y * X = -iZ. */ + assert(pauli_mul_with_phase(PAULI_X, PAULI_Y, &tag, &phase) == 0); + assert(tag == PAULI_Z && near(phase.im, 1.0)); + assert(pauli_mul_with_phase(PAULI_Y, PAULI_Z, &tag, &phase) == 0); + assert(tag == PAULI_X && near(phase.im, 1.0)); + assert(pauli_mul_with_phase(PAULI_Z, PAULI_Y, &tag, &phase) == 0); + assert(tag == PAULI_X && near(phase.im, -1.0)); + assert(pauli_mul_with_phase(PAULI_Y, PAULI_X, &tag, &phase) == 0); + assert(tag == PAULI_Z && near(phase.im, -1.0)); + + /* Identity leaves the other operator untouched. */ + assert(pauli_mul_with_phase(PAULI_I, PAULI_Y, &tag, &phase) == 0); + assert(tag == PAULI_Y && near(phase.re, 1.0)); + assert(pauli_mul_with_phase(PAULI_Y, PAULI_I, &tag, &phase) == 0); + assert(tag == PAULI_Y && near(phase.re, 1.0)); + + /* Invalid tags and NULL output pointers are rejected. */ + assert(pauli_mul_with_phase(9, PAULI_X, &tag, &phase) == -8); + assert(pauli_mul_with_phase(PAULI_X, 0xFF, &tag, &phase) == -8); + assert(pauli_mul_with_phase(PAULI_X, PAULI_Z, NULL, &phase) == -1); + assert(pauli_mul_with_phase(PAULI_X, PAULI_Z, &tag, NULL) == -1); +} + +int main(void) { + test_statevector_data_mut(); + test_density_matrix_psd(); + test_pauli_mul_with_phase(); + printf("binding-c parity qis3 tests passed\n"); + return 0; +} diff --git a/crates/binding-c/tests/test_qis.c b/crates/binding-c/tests/test_qis.c new file mode 100644 index 00000000..42db62a9 --- /dev/null +++ b/crates/binding-c/tests/test_qis.c @@ -0,0 +1,318 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +#include +#include +#include +#include +#include + +#include "cqlib_c.h" + +static void test_statevector_bell_state(void) { + CStatevector* sv = statevector_new(2); + assert(sv != NULL); + assert(statevector_num_qubits(sv) == 2); + + assert(statevector_apply_h(sv, 0) == 0); + assert(statevector_apply_cx(sv, 0, 1) == 0); + + uintptr_t len = statevector_probabilities_len(sv); + assert(len == 4); + double probs[4]; + assert(statevector_probabilities(sv, probs, len) == 0); + assert(fabs(probs[0] - 0.5) < 1e-9); + assert(fabs(probs[1]) < 1e-9); + assert(fabs(probs[2]) < 1e-9); + assert(fabs(probs[3] - 0.5) < 1e-9); + + char* outcome = statevector_measure_all(sv); + assert(outcome != NULL); + assert(strlen(outcome) == 2); + assert(outcome[0] == outcome[1]); /* |00> or |11> */ + cqlib_string_free(outcome); + + assert(statevector_reset(sv, 0) == 0); + + /* Sampling */ + COutcomeList* shots = statevector_sample_shots(sv, 16); + assert(shots != NULL); + assert(outcome_list_len(shots) == 16); + char* first = outcome_list_get(shots, 0); + assert(first != NULL); + assert(strlen(first) == 2); + cqlib_string_free(first); + outcome_list_free(shots); + + /* Errors */ + assert(statevector_apply_h(sv, 2) == -2); + statevector_free(sv); + statevector_free(NULL); + assert(statevector_num_qubits(NULL) == 0); +} + +static void test_statevector_expectation(void) { + CStatevector* sv = statevector_new(2); + assert(sv != NULL); + + /* = 1.0 on any state. */ + CPauliString* pauli = pauli_string_parse("II"); + assert(pauli != NULL); + CHamiltonian* ham = hamiltonian_from_pauli(pauli); + assert(ham != NULL); + + double expectation = NAN; + assert(statevector_expectation(sv, ham, &expectation) == 0); + assert(fabs(expectation - 1.0) < 1e-9); + + hamiltonian_free(ham); + statevector_free(sv); +} + +static void test_statevector_y_expectation_from_circuit(void) { + for (int sign = -1; sign <= 1; sign += 2) { + CCircuit* circuit = circuit_new(1); + assert(circuit != NULL); + assert(circuit_h(circuit, 0) == 0); + assert(circuit_rz(circuit, 0, sign * acos(-1.0) / 2.0) == 0); + CStatevector* sv = statevector_from_circuit(circuit); + assert(sv != NULL); + CPauliString* pauli = pauli_string_parse("Y"); + assert(pauli != NULL); + /* from_pauli consumes the Pauli handle. */ + CHamiltonian* ham = hamiltonian_from_pauli(pauli); + assert(ham != NULL); + + double expectation = NAN; + assert(statevector_expectation(sv, ham, &expectation) == 0); + assert(fabs(expectation - sign) < 1e-10); + + hamiltonian_free(ham); + statevector_free(sv); + circuit_free(circuit); + } +} + +static void test_statevector_pauli_phases_and_y_parity(void) { + const char* prefixes[] = {"", "-", "i", "-i"}; + /* Each coefficient absorbs its Pauli phase, leaving -0.37 * P. */ + const double coeff_re[] = {-0.37, 0.37, 0.0, 0.0}; + const double coeff_im[] = {0.0, 0.0, 0.37, -0.37}; + for (uintptr_t n = 1; n <= 3; ++n) { + CStatevector* sv = statevector_new(n); + assert(sv != NULL); + double single[3][4]; + for (uintptr_t q = 0; q < n; ++q) { + double theta = 0.7 + 0.2 * q; + double phi = -0.3 + 0.4 * q; + assert(statevector_apply_ry(sv, (uint32_t)q, theta) == 0); + assert(statevector_apply_rz(sv, (uint32_t)q, phi) == 0); + /* RZ(phi) RY(theta)|0>: independently known Bloch components. */ + single[q][0] = 1.0; + single[q][1] = sin(theta) * cos(phi); + single[q][2] = sin(theta) * sin(phi); + single[q][3] = cos(theta); + } + for (uintptr_t encoded = 0; encoded < ((uintptr_t)1 << (2 * n)); ++encoded) { + char label[4] = {0}; + double expected = -0.37; + for (uintptr_t q = 0; q < n; ++q) { + uintptr_t op = (encoded >> (2 * q)) & 3; + label[n - 1 - q] = "IXYZ"[op]; + expected *= single[q][op]; + } + assert(fabs(expected) > 1e-10); + for (uintptr_t phase = 0; phase < 4; ++phase) { + char phased_label[6]; + snprintf(phased_label, sizeof(phased_label), "%s%s", prefixes[phase], label); + CPauliString* pauli = pauli_string_parse(phased_label); + assert(pauli != NULL); + CHamiltonian* ham = hamiltonian_new(n); + assert(ham != NULL); + assert(hamiltonian_add_term(ham, pauli, coeff_re[phase], coeff_im[phase]) == 0); + /* add_term clones the Pauli handle. */ + pauli_string_free(pauli); + double expectation = NAN; + assert(statevector_expectation(sv, ham, &expectation) == 0); + assert(fabs(expectation - expected) < 1e-10); + assert(hamiltonian_simplify(ham) == 0); + assert(statevector_expectation(sv, ham, &expectation) == 0); + assert(fabs(expectation - expected) < 1e-10); + hamiltonian_free(ham); + } + } + statevector_free(sv); + } +} + +static void test_statevector_mixed_hamiltonian_expectation(void) { + CStatevector* sv = statevector_new(1); + assert(sv != NULL); + assert(statevector_apply_h(sv, 0) == 0); + assert(statevector_apply_rz(sv, 0, acos(-1.0) / 4.0) == 0); + CHamiltonian* ham = hamiltonian_new(1); + assert(ham != NULL); + const char* labels[] = {"I", "X", "-iY", "Y"}; + const double coeff_re[] = {0.3, 0.5, 0.0, -0.25}; + const double coeff_im[] = {0.0, 0.0, 0.75, 0.0}; + for (uintptr_t i = 0; i < 4; ++i) { + CPauliString* pauli = pauli_string_parse(labels[i]); + assert(pauli != NULL); + assert(hamiltonian_add_term(ham, pauli, coeff_re[i], coeff_im[i]) == 0); + pauli_string_free(pauli); + } + /* H = 0.3 I + 0.5 X + 0.5 Y, with = = 1/sqrt(2). */ + double expectation = NAN; + double expected = 0.3 + 1.0 / sqrt(2.0); + assert(statevector_expectation(sv, ham, &expectation) == 0); + assert(fabs(expectation - expected) < 1e-10); + assert(hamiltonian_simplify(ham) == 0); + assert(statevector_expectation(sv, ham, &expectation) == 0); + assert(fabs(expectation - expected) < 1e-10); + hamiltonian_free(ham); + statevector_free(sv); +} + +static void test_statevector_from_circuit(void) { + CCircuit* circuit = circuit_new(1); + assert(circuit_h(circuit, 0) == 0); + + CStatevector* sv = statevector_from_circuit(circuit); + assert(sv != NULL); + assert(statevector_num_qubits(sv) == 1); + + double probs[2]; + assert(statevector_probabilities(sv, probs, 2) == 0); + assert(fabs(probs[0] - 0.5) < 1e-9); + assert(fabs(probs[1] - 0.5) < 1e-9); + + statevector_free(sv); + circuit_free(circuit); +} + +static void test_density_matrix(void) { + CDensityMatrix* dm = density_matrix_new(2); + assert(dm != NULL); + assert(density_matrix_num_qubits(dm) == 2); + + assert(density_matrix_apply_h(dm, 0) == 0); + assert(density_matrix_apply_cx(dm, 0, 1) == 0); + + double probs[4]; + assert(density_matrix_probabilities_len(dm) == 4); + assert(density_matrix_probabilities(dm, probs, 4) == 0); + assert(fabs(probs[0] - 0.5) < 1e-9); + assert(fabs(probs[3] - 0.5) < 1e-9); + + /* Partial trace keeping qubit 1 -> single maximally mixed qubit. */ + const uint32_t keep[] = {1}; + CDensityMatrix* reduced = density_matrix_partial_trace(dm, keep, 1); + assert(reduced != NULL); + assert(density_matrix_num_qubits(reduced) == 1); + double rprobs[2]; + assert(density_matrix_probabilities(reduced, rprobs, 2) == 0); + assert(fabs(rprobs[0] - 0.5) < 1e-9); + assert(fabs(rprobs[1] - 0.5) < 1e-9); + density_matrix_free(reduced); + + assert(density_matrix_apply_h(dm, 5) == -2); + density_matrix_free(dm); + density_matrix_free(NULL); +} + +static void test_stabilizer(void) { + CStabilizerState* stab = stabilizer_new(2); + assert(stab != NULL); + assert(stabilizer_num_qubits(stab) == 2); + + assert(stabilizer_apply_h(stab, 0) == 0); + assert(stabilizer_apply_cx(stab, 0, 1) == 0); + + char* outcome = stabilizer_measure_all(stab); + assert(outcome != NULL); + cqlib_string_free(outcome); + + /* = +1 on the Bell state. */ + CPauliString* pauli = pauli_string_parse("ZZ"); + assert(pauli != NULL); + int32_t value = 0; + assert(stabilizer_pauli_expectation(stab, pauli, &value) == 0); + assert(value == 1); + pauli_string_free(pauli); + + assert(stabilizer_get_stabilizers_len(stab) == 2); + char* gen = stabilizer_get_stabilizer(stab, 0); + assert(gen != NULL); + cqlib_string_free(gen); + + assert(stabilizer_apply_h(stab, 2) == -2); + stabilizer_free(stab); + stabilizer_free(NULL); + assert(stabilizer_new(0) == NULL); +} + +static void test_pauli_string(void) { + CPauliString* pauli = pauli_string_parse("XYZ"); + assert(pauli != NULL); + assert(pauli_string_num_qubits(pauli) == 3); + /* Parsing is in reverse order: the first character maps to the highest + * qubit index, so get(idx) is qubit-index based. */ + assert(pauli_string_get_pauli(pauli, 0) == PAULI_Z); + assert(pauli_string_get_pauli(pauli, 1) == PAULI_Y); + assert(pauli_string_get_pauli(pauli, 2) == PAULI_X); + /* Out-of-bounds reads return the 0xFF sentinel. */ + assert(pauli_string_get_pauli(pauli, 3) == 0xFF); + + assert(pauli_string_set_pauli(pauli, 0, PAULI_I) == 0); + assert(pauli_string_get_pauli(pauli, 0) == PAULI_I); + + assert(pauli_string_parse(NULL) == NULL); + pauli_string_free(pauli); + pauli_string_free(NULL); +} + +static void test_hamiltonian(void) { + CHamiltonian* ham = hamiltonian_new(2); + assert(ham != NULL); + assert(hamiltonian_num_qubits(ham) == 2); + assert(hamiltonian_num_terms(ham) == 0); + + CPauliString* zz = pauli_string_parse("ZZ"); + assert(zz != NULL); + assert(hamiltonian_add_term(ham, zz, 0.5, 0.0) == 0); + assert(hamiltonian_num_terms(ham) == 1); + pauli_string_free(zz); + + /* Qubit-count mismatch must be rejected. */ + CPauliString* single = pauli_string_parse("X"); + assert(single != NULL); + assert(hamiltonian_add_term(ham, single, 1.0, 0.0) != 0); + pauli_string_free(single); + + hamiltonian_free(ham); + hamiltonian_free(NULL); +} + +int main(void) { + test_statevector_bell_state(); + test_statevector_expectation(); + test_statevector_y_expectation_from_circuit(); + test_statevector_pauli_phases_and_y_parity(); + test_statevector_mixed_hamiltonian_expectation(); + test_statevector_from_circuit(); + test_density_matrix(); + test_stabilizer(); + test_pauli_string(); + test_hamiltonian(); + printf("binding-c qis tests passed\n"); + return 0; +} diff --git a/crates/binding-c/tests/test_qis_measurement.c b/crates/binding-c/tests/test_qis_measurement.c new file mode 100644 index 00000000..b638a937 --- /dev/null +++ b/crates/binding-c/tests/test_qis_measurement.c @@ -0,0 +1,283 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +/* + * Smoke test for the QIS simulator supplement and Measurement C ABI + * (checklist 2.5 + 2.6). + * + * Compile (msvc): cl /I ../../crates/binding-c/include test_qis_measurement.c /link cqlib_c.lib + * Compile (gcc): gcc -I ../../crates/binding-c/include test_qis_measurement.c -L /debug + * -lcqlib_c -o test_qis_measurement + */ + +#include +#include +#include +#include + +#include "cqlib_c.h" + +static int near(double a, double b) { return fabs(a - b) < 1e-10; } + +static void test_statevector_supplement(void) { + const double inv = 1.0 / sqrt(2.0); + + /* Bell state built with named gates, read back through data(). */ + CStatevector* sv = statevector_new(2); + assert(sv != NULL); + uint32_t targets[1] = {0}; + uint32_t pair[2] = {0, 1}; + assert(statevector_apply_standard_gate(sv, "H", targets, 1, NULL, 0) == 0); + assert(statevector_apply_standard_gate(sv, "CX", pair, 2, NULL, 0) == 0); + + assert(statevector_data_len(sv) == 4); + Complex64 amps[4]; + assert(statevector_data(sv, amps, 4) == 0); + assert(near(amps[0].re, inv) && near(amps[0].im, 0.0)); + assert(near(amps[1].re, 0.0) && near(amps[1].im, 0.0)); + assert(near(amps[2].re, 0.0) && near(amps[2].im, 0.0)); + assert(near(amps[3].re, inv) && near(amps[3].im, 0.0)); + + /* Named "H" matches the dedicated apply_h entry point. */ + CStatevector* a = statevector_new(1); + CStatevector* b = statevector_new(1); + assert(statevector_apply_h(a, 0) == 0); + assert(statevector_apply_standard_gate(b, "H", targets, 1, NULL, 0) == 0); + Complex64 ca[2], cb[2]; + assert(statevector_data(a, ca, 2) == 0); + assert(statevector_data(b, cb, 2) == 0); + assert(near(ca[0].re, cb[0].re) && near(ca[1].re, cb[1].re)); + statevector_free(a); + statevector_free(b); + + /* RX(pi) maps |0> to -i|1>. */ + double pi = acos(-1.0); + assert(statevector_apply_standard_gate(sv, "RX", targets, 1, &pi, 1) == 0); + + /* Pauli rotation exp(-i*pi/2*X) on a fresh qubit maps |0> to -i|1>. */ + CPauliString* pauli = pauli_string_new(1); + assert(pauli != NULL); + assert(pauli_string_set_pauli(pauli, 0, PAULI_X) == 0); + CStatevector* sv1 = statevector_new(1); + assert(statevector_apply_pauli_rotation(sv1, pauli, pi) == 0); + assert(statevector_data(sv1, amps, 2) == 0); + assert(near(amps[0].re, 0.0) && near(amps[1].im, -1.0)); + statevector_free(sv1); + pauli_string_free(pauli); + + /* Arbitrary unitary: X matrix flips |0> to |1>. */ + Complex64 x_matrix[4] = {{0.0, 0.0}, {1.0, 0.0}, {1.0, 0.0}, {0.0, 0.0}}; + CStatevector* svx = statevector_new(1); + assert(statevector_apply_unitary_gate(svx, targets, 1, x_matrix, 2) == 0); + assert(statevector_data(svx, amps, 2) == 0); + assert(near(amps[1].re, 1.0)); + statevector_free(svx); + + /* Rebuild the Bell state from amplitudes via from_state. */ + Complex64 bell[4] = {{inv, 0.0}, {0.0, 0.0}, {0.0, 0.0}, {inv, 0.0}}; + CStatevector* rebuilt = statevector_from_state(2, bell, 4); + assert(rebuilt != NULL); + assert(statevector_data(rebuilt, amps, 4) == 0); + assert(near(amps[0].re, inv) && near(amps[3].re, inv)); + statevector_free(rebuilt); + assert(statevector_from_state(2, bell, 3) == NULL); /* wrong dimension */ + + /* Error paths. */ + assert(statevector_data(sv, amps, 3) == -8); + assert(statevector_data(sv, NULL, 4) == -1); + assert(statevector_apply_standard_gate(sv, "NOPE", targets, 1, NULL, 0) == -8); + assert(statevector_apply_standard_gate(sv, NULL, targets, 1, NULL, 0) == -1); + uint32_t oob[1] = {9}; + assert(statevector_apply_standard_gate(sv, "H", oob, 1, NULL, 0) == -2); + statevector_free(sv); +} + +static void test_density_matrix_supplement(void) { + const double inv = 1.0 / sqrt(2.0); + + /* rho = |+><+| has all entries 0.5 and trace 1. */ + Complex64 plus[2] = {{inv, 0.0}, {inv, 0.0}}; + CDensityMatrix* dm = density_matrix_from_state(1, plus, 2); + assert(dm != NULL); + assert(density_matrix_data_len(dm) == 4); + Complex64 data[16]; + assert(density_matrix_data(dm, data, 4) == 0); + for (int i = 0; i < 4; i++) { + assert(near(data[i].re, 0.5) && near(data[i].im, 0.0)); + } + double re = 0.0, im = 0.0; + assert(density_matrix_trace(dm, &re, &im) == 0); + assert(near(re, 1.0) && near(im, 0.0)); + bool hermitian = false; + assert(density_matrix_is_hermitian(dm, 1e-10, &hermitian) == 0); + assert(hermitian); + + /* Named "X" flips |0><0| to |1><1|. */ + Complex64 ket0[2] = {{1.0, 0.0}, {0.0, 0.0}}; + CDensityMatrix* flipped = density_matrix_from_state(1, ket0, 2); + uint32_t targets[1] = {0}; + assert(density_matrix_apply_standard_gate(flipped, "X", targets, 1, NULL, 0) == 0); + assert(density_matrix_data(flipped, data, 4) == 0); + assert(near(data[0].re, 0.0) && near(data[3].re, 1.0)); + assert(density_matrix_apply_standard_gate(flipped, "NOPE", targets, 1, NULL, 0) == -8); + density_matrix_free(flipped); + density_matrix_free(dm); + + /* Maximally mixed 2-qubit state: diagonal 0.25, trace 1. */ + CDensityMatrix* mm = density_matrix_maximally_mixed(2); + assert(mm != NULL); + assert(density_matrix_data_len(mm) == 16); + assert(density_matrix_data(mm, data, 16) == 0); + for (int i = 0; i < 4; i++) { + assert(near(data[i * 4 + i].re, 0.25)); + } + assert(density_matrix_trace(mm, &re, &im) == 0); + assert(near(re, 1.0) && near(im, 0.0)); + density_matrix_free(mm); + + assert(density_matrix_trace(NULL, &re, &im) == -1); + assert(density_matrix_data_len(NULL) == 0); +} + +static void test_stabilizer_supplement(void) { + /* Fresh 1-qubit state: destabilizer +X, stim export "+Z\n". */ + CStabilizerState* s = stabilizer_new(1); + assert(s != NULL); + assert(stabilizer_get_destabilizers_len(s) == 1); + char* destab = stabilizer_get_destabilizer(s, 0); + assert(destab != NULL && strcmp(destab, "+X") == 0); + cqlib_string_free(destab); + assert(stabilizer_get_destabilizer(s, 5) == NULL); + char* stim = stabilizer_to_stim_format(s); + assert(stim != NULL && strcmp(stim, "+Z\n") == 0); + cqlib_string_free(stim); + stabilizer_free(s); + + /* Bell state outcome probabilities. */ + CStabilizerState* bell = stabilizer_new(2); + uint32_t pair[2] = {0, 1}; + assert(stabilizer_apply_standard_gate(bell, "H", pair, 1, NULL, 0) == 0); + assert(stabilizer_apply_standard_gate(bell, "CX", pair, 2, NULL, 0) == 0); + double p = 0.0; + bool bits00[2] = {false, false}; + bool bits11[2] = {true, true}; + bool bits01[2] = {false, true}; + assert(stabilizer_probability_of(bell, bits00, 2, &p) == 0 && near(p, 0.5)); + assert(stabilizer_probability_of(bell, bits11, 2, &p) == 0 && near(p, 0.5)); + assert(stabilizer_probability_of(bell, bits01, 2, &p) == 0 && p < 1e-12); + bool one_bit[1] = {false}; + assert(stabilizer_probability_of(bell, one_bit, 1, &p) == -8); + stabilizer_free(bell); + + /* Non-Clifford "T" is rejected with -7. */ + CStabilizerState* t = stabilizer_new(1); + uint32_t targets[1] = {0}; + assert(stabilizer_apply_standard_gate(t, "T", targets, 1, NULL, 0) == -7); + assert(stabilizer_apply_standard_gate(t, "NOPE", targets, 1, NULL, 0) == -8); + stabilizer_free(t); + + /* run_circuit collapses terminal measurements. */ + CCircuit* circuit = circuit_new(2); + assert(circuit_h(circuit, 0) == 0); + assert(circuit_cx(circuit, 0, 1) == 0); + uint32_t measured[2] = {0, 1}; + CClassicalExpr* expr = circuit_measure_bits(circuit, measured, 2); + assert(expr != NULL); + classical_expr_free(expr); + + CStabilizerState* collapsed = stabilizer_run_circuit(circuit); + assert(collapsed != NULL); + circuit_free(circuit); + double p00 = 0.0, p11 = 0.0, p01 = 0.0; + assert(stabilizer_probability_of(collapsed, bits00, 2, &p00) == 0); + assert(stabilizer_probability_of(collapsed, bits11, 2, &p11) == 0); + assert(stabilizer_probability_of(collapsed, bits01, 2, &p01) == 0); + assert(near(p00 + p11, 1.0) && p01 < 1e-12); + stabilizer_free(collapsed); + + assert(stabilizer_run_circuit(NULL) == NULL); + assert(stabilizer_to_stim_format(NULL) == NULL); +} + +static void test_measurement_credentials(void) { + /* Standalone descriptor reading qubits [2, 0] (result bit order). */ + uint32_t qubits[2] = {2, 0}; + CMeasurement* m = measurement_new(0, CQLIB_CLASSICAL_TYPE_BIT_VEC, 2, qubits, 2); + assert(m != NULL); + + assert(measurement_qubits_len(m) == 2); + assert(measurement_width(m) == 2); + uint32_t buffer[2] = {99, 99}; + assert(measurement_qubits(m, buffer, 2) == 0); + assert(buffer[0] == 2 && buffer[1] == 0); + assert(measurement_qubits(m, buffer, 1) == -8); + + uint32_t tag = 99, width = 99; + assert(measurement_ty(m, &tag, &width) == 0); + assert(tag == CQLIB_CLASSICAL_TYPE_BIT_VEC && width == 2); + + /* Value / expression chain. */ + CClassicalValue* value = measurement_value(m); + assert(value != NULL); + assert(classical_value_index(value) == 0); + assert(classical_value_ty(value, &tag, &width) == 0); + assert(tag == CQLIB_CLASSICAL_TYPE_BIT_VEC && width == 2); + CClassicalExpr* value_expr = classical_value_expr(value); + assert(value_expr != NULL); + uint32_t kind = 99; + assert(classical_expr_kind(value_expr, &kind) == 0); + assert(kind == CQLIB_CLASSICAL_EXPR_VALUE); + classical_expr_free(value_expr); + classical_value_free(value); + + CClassicalExpr* expr = measurement_expr(m); + assert(expr != NULL); + assert(classical_expr_kind(expr, &kind) == 0); + assert(kind == CQLIB_CLASSICAL_EXPR_VALUE); + classical_expr_free(expr); + + /* Qubit-order validation and projection. */ + assert(measurement_check_qubits(m, 3) == 0); + assert(measurement_check_qubits(m, 2) == -2); + char* projected = measurement_project(m, "101"); + assert(projected != NULL && strcmp(projected, "11") == 0); + cqlib_string_free(projected); + projected = measurement_project_basis(m, 5 /* 0b101 */); + assert(projected != NULL && strcmp(projected, "11") == 0); + cqlib_string_free(projected); + assert(measurement_project(m, "10a") == NULL); + measurement_free(m); + + /* Constructor error paths. */ + assert(measurement_new(0, CQLIB_CLASSICAL_TYPE_BIT_VEC, 2, NULL, 2) == NULL); + assert(measurement_new(0, CQLIB_CLASSICAL_TYPE_BIT_VEC, 2, qubits, 0) == NULL); + assert(measurement_new(0, 99, 2, qubits, 2) == NULL); + + /* NULL handling. */ + measurement_free(NULL); + classical_value_free(NULL); + assert(measurement_value(NULL) == NULL); + assert(measurement_qubits_len(NULL) == 0); + assert(measurement_width(NULL) == 0); + assert(measurement_ty(NULL, &tag, &width) == -1); + assert(classical_value_index(NULL) == UINT32_MAX); + assert(classical_value_expr(NULL) == NULL); +} + +int main(void) { + test_statevector_supplement(); + test_density_matrix_supplement(); + test_stabilizer_supplement(); + test_measurement_credentials(); + printf("binding-c qis measurement tests passed\n"); + return 0; +} diff --git a/crates/binding-c/tests/test_qis_metrics.c b/crates/binding-c/tests/test_qis_metrics.c new file mode 100644 index 00000000..947ccdd6 --- /dev/null +++ b/crates/binding-c/tests/test_qis_metrics.c @@ -0,0 +1,285 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +#include +#include +#include +#include +#include + +#include "cqlib_c.h" + +/* Bell state |Phi+> = (|00> + |11>) / sqrt(2) as a density matrix. */ +static CDensityMatrix* bell_dm(void) { + CDensityMatrix* dm = density_matrix_new(2); + assert(dm != NULL); + assert(density_matrix_apply_h(dm, 0) == 0); + assert(density_matrix_apply_cx(dm, 0, 1) == 0); + return dm; +} + +/* Bell state as a statevector. */ +static CStatevector* bell_sv(void) { + CStatevector* sv = statevector_new(2); + assert(sv != NULL); + assert(statevector_apply_h(sv, 0) == 0); + assert(statevector_apply_cx(sv, 0, 1) == 0); + return sv; +} + +static void test_entropy(void) { + double out = NAN; + + /* Pure state: linear entropy ~ 0. */ + CDensityMatrix* bell = bell_dm(); + assert(density_matrix_linear_entropy(bell, &out) == 0); + assert(fabs(out) < 1e-9); + + /* Pure state: Renyi entropy of any order ~ 0. */ + assert(density_matrix_renyi_entropy(bell, 2.0, &out) == 0); + assert(fabs(out) < 1e-9); + /* Invalid order is rejected. */ + assert(density_matrix_renyi_entropy(bell, 0.0, &out) == -8); + + /* Bell entanglement entropy = 1 bit. */ + CStatevector* sv = bell_sv(); + uint32_t subsys_a[1] = {0}; + assert(statevector_entanglement_entropy(sv, subsys_a, 1, &out) == 0); + assert(fabs(out - 1.0) < 1e-9); + + /* Negativity = 0.5, concurrence = 1, E_F = 1 for the Bell state. */ + assert(density_matrix_negativity(bell, subsys_a, 1, &out) == 0); + assert(fabs(out - 0.5) < 1e-9); + assert(density_matrix_concurrence(bell, &out) == 0); + assert(fabs(out - 1.0) < 1e-9); + assert(density_matrix_entanglement_of_formation(bell, &out) == 0); + assert(fabs(out - 1.0) < 1e-9); + + /* Separable pure state: concurrence = 0, E_F = 0. */ + CDensityMatrix* sep = density_matrix_new(2); + assert(density_matrix_concurrence(sep, &out) == 0); + assert(fabs(out) < 1e-9); + assert(density_matrix_entanglement_of_formation(sep, &out) == 0); + assert(fabs(out) < 1e-9); + + /* NULL handling. */ + assert(density_matrix_linear_entropy(NULL, &out) == -1); + assert(density_matrix_negativity(bell, NULL, 1, &out) == -1); + + density_matrix_free(sep); + statevector_free(sv); + density_matrix_free(bell); +} + +static void test_metrics(void) { + double out = NAN; + + /* Purity of pure states is 1. */ + CStatevector* sv = bell_sv(); + assert(statevector_purity(sv, &out) == 0); + assert(fabs(out - 1.0) < 1e-9); + + CDensityMatrix* bell = bell_dm(); + assert(density_matrix_purity(bell, &out) == 0); + assert(fabs(out - 1.0) < 1e-9); + + /* Identical states: fidelity 1, trace distance 0. */ + CStatevector* sv2 = bell_sv(); + assert(statevector_fidelity(sv, sv2, &out) == 0); + assert(fabs(out - 1.0) < 1e-9); + assert(statevector_trace_distance(sv, sv2, &out) == 0); + assert(fabs(out) < 1e-9); + + assert(statevector_fidelity_pure_mixed(sv, bell, &out) == 0); + assert(fabs(out - 1.0) < 1e-9); + + CDensityMatrix* bell2 = bell_dm(); + assert(density_matrix_trace_distance(bell, bell2, &out) == 0); + assert(fabs(out) < 1e-9); + assert(density_matrix_fidelity(bell, bell2, &out) == 0); + assert(fabs(out - 1.0) < 1e-9); + + /* Pure state: von Neumann entropy ~ 0. */ + assert(density_matrix_entropy(bell, &out) == 0); + assert(fabs(out) < 1e-9); + + /* Logarithmic negativity of the Bell state = 1. */ + uint32_t sys_a[1] = {0}; + assert(density_matrix_logarithmic_negativity(bell, sys_a, 1, &out) == 0); + assert(fabs(out - 1.0) < 1e-9); + + /* Partial transpose: new 2-qubit density matrix. */ + CDensityMatrix* pt = density_matrix_partial_transpose(bell, sys_a, 1); + assert(pt != NULL); + assert(density_matrix_num_qubits(pt) == 2); + /* Eigenvalues (1/2, 1/2, 1/2, -1/2): purity stays 1. */ + assert(density_matrix_purity(pt, &out) == 0); + assert(fabs(out - 1.0) < 1e-9); + /* Out-of-bounds qubit yields NULL. */ + uint32_t oob[1] = {7}; + assert(density_matrix_partial_transpose(bell, oob, 1) == NULL); + + density_matrix_free(pt); + density_matrix_free(bell2); + density_matrix_free(bell); + statevector_free(sv2); + statevector_free(sv); +} + +static CHamiltonian* hamiltonian_zz(double coeff) { + CPauliString* zz = pauli_string_parse("ZZ"); + assert(zz != NULL); + CHamiltonian* h = hamiltonian_new(2); + assert(h != NULL); + assert(hamiltonian_add_term(h, zz, coeff, 0.0) == 0); + pauli_string_free(zz); + return h; +} + +static void test_evolution(void) { + CHamiltonian* h = hamiltonian_zz(1.0); + + /* Trotter circuits for both modes. */ + CCircuit* circuit = hamiltonian_to_trotter_circuit(h, 1.0, 3, TROTTER_MODE_FIRST_ORDER); + assert(circuit != NULL); + assert(circuit_num_qubits(circuit) == 2); + circuit_free(circuit); + + circuit = hamiltonian_to_trotter_circuit(h, 1.0, 3, TROTTER_MODE_SECOND_ORDER); + assert(circuit != NULL); + assert(circuit_num_qubits(circuit) == 2); + circuit_free(circuit); + + /* Invalid mode tag and zero steps yield NULL. */ + assert(hamiltonian_to_trotter_circuit(h, 1.0, 3, 99) == NULL); + assert(hamiltonian_to_trotter_circuit(h, 1.0, 0, TROTTER_MODE_FIRST_ORDER) == NULL); + + /* Auto-selecting evolution circuit (ZZ commutes: exact path). */ + circuit = hamiltonian_to_evolution_circuit(h, 1.0, 1, TROTTER_MODE_FIRST_ORDER); + assert(circuit != NULL); + assert(circuit_num_qubits(circuit) == 2); + circuit_free(circuit); + + hamiltonian_free(h); +} + +static void test_hamiltonian_supplements(void) { + /* Scale: 0.5 * ZZ scaled by 2 equals ZZ. */ + CHamiltonian* h = hamiltonian_zz(0.5); + assert(hamiltonian_scale(h, 2.0, 0.0) == 0); + + uintptr_t len = hamiltonian_to_matrix_len(h); + assert(len == 4); + + Complex64 buf[16]; + assert(hamiltonian_to_matrix(h, buf, len * len) == 0); + /* diag(1, -1, -1, 1) for ZZ. */ + assert(fabs(buf[0].re - 1.0) < 1e-9); + assert(fabs(buf[5].re + 1.0) < 1e-9); + assert(fabs(buf[10].re + 1.0) < 1e-9); + assert(fabs(buf[15].re - 1.0) < 1e-9); + assert(fabs(buf[1].re) < 1e-9 && fabs(buf[1].im) < 1e-9); + + /* Wrong buffer length is rejected. */ + assert(hamiltonian_to_matrix(h, buf, 15) == -8); + assert(hamiltonian_to_matrix_len(NULL) == 0); + + /* All ZZ-type terms commute. */ + assert(hamiltonian_all_terms_commute(h) == 1); + + /* X + Z on one qubit does not commute. */ + CPauliString* px = pauli_string_parse("X"); + CPauliString* pz = pauli_string_parse("Z"); + CHamiltonian* h2 = hamiltonian_new(1); + assert(hamiltonian_add_term(h2, px, 1.0, 0.0) == 0); + assert(hamiltonian_add_term(h2, pz, 1.0, 0.0) == 0); + assert(hamiltonian_all_terms_commute(h2) == 0); + assert(hamiltonian_all_terms_commute(NULL) == -1); + + pauli_string_free(pz); + pauli_string_free(px); + hamiltonian_free(h2); + hamiltonian_free(h); +} + +static void test_pauli_string_supplements(void) { + /* "XZI": qubit 2 = X, qubit 1 = Z, qubit 0 = I. */ + CPauliString* ps = pauli_string_parse("XZI"); + assert(ps != NULL); + + /* Masks: "XZI" has X on qubit 2, Z on qubit 1. */ + assert(pauli_string_x_mask(ps) == 0x4); + assert(pauli_string_z_mask(ps) == 0x2); + + /* Support = [1, 2]. */ + uintptr_t support_len = pauli_string_support_len(ps); + assert(support_len == 2); + uint32_t support[2]; + assert(pauli_string_support(ps, support, 2) == 0); + assert(support[0] == 1 && support[1] == 2); + assert(pauli_string_support(ps, support, 3) == -8); + + /* try_get_pauli roundtrip. */ + uint8_t tag = 0xFF; + assert(pauli_string_try_get_pauli(ps, 0, &tag) == 0); + assert(tag == PAULI_I); + assert(pauli_string_try_get_pauli(ps, 1, &tag) == 0); + assert(tag == PAULI_Z); + assert(pauli_string_try_get_pauli(ps, 2, &tag) == 0); + assert(tag == PAULI_X); + assert(pauli_string_try_get_pauli(ps, 3, &tag) == -2); + + /* Commutation: XZ vs ZX commutes, XI vs ZI anticommutes. */ + CPauliString* xz = pauli_string_parse("XZ"); + CPauliString* zx = pauli_string_parse("ZX"); + CPauliString* xi = pauli_string_parse("XI"); + CPauliString* zi = pauli_string_parse("ZI"); + assert(pauli_string_commutes_with(xz, zx) == 1); + assert(pauli_string_commutes_with(xi, zi) == 0); + /* Qubit-count mismatch. */ + assert(pauli_string_commutes_with(xz, ps) == -8); + assert(pauli_string_commutes_with(NULL, xz) == -1); + + /* y_phase: "Y" contributes i, "XZ" contributes 1. */ + CPauliString* py = pauli_string_parse("Y"); + Complex64 phase = {0.0, 0.0}; + assert(pauli_string_y_phase(py, &phase) == 0); + assert(fabs(phase.re) < 1e-9 && fabs(phase.im - 1.0) < 1e-9); + assert(pauli_string_y_phase(xz, &phase) == 0); + assert(fabs(phase.re - 1.0) < 1e-9 && fabs(phase.im) < 1e-9); + + /* try_set_pauli: set Y on qubit 0 and read it back. */ + CPauliString* editable = pauli_string_new(2); + assert(pauli_string_try_set_pauli(editable, 0, PAULI_Y) == 0); + assert(pauli_string_try_get_pauli(editable, 0, &tag) == 0); + assert(tag == PAULI_Y); + assert(pauli_string_try_set_pauli(editable, 5, PAULI_X) == -2); + assert(pauli_string_try_set_pauli(editable, 0, 200) == -8); + + pauli_string_free(editable); + pauli_string_free(py); + pauli_string_free(zi); + pauli_string_free(xi); + pauli_string_free(zx); + pauli_string_free(xz); + pauli_string_free(ps); +} + +int main(void) { + test_entropy(); + test_metrics(); + test_evolution(); + test_hamiltonian_supplements(); + test_pauli_string_supplements(); + printf("binding-c qis metrics tests passed\n"); + return 0; +} diff --git a/crates/binding-c/tests/test_qis_noise.c b/crates/binding-c/tests/test_qis_noise.c new file mode 100644 index 00000000..0ab17606 --- /dev/null +++ b/crates/binding-c/tests/test_qis_noise.c @@ -0,0 +1,238 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +#include +#include +#include +#include +#include + +#include "cqlib_c.h" + +static void test_lifecycle(void) { + CDensityMatrixNoise* sim = density_matrix_noise_new(2, NULL); + assert(sim != NULL); + assert(density_matrix_noise_num_qubits(sim) == 2); + density_matrix_noise_free(sim); + + density_matrix_noise_free(NULL); + assert(density_matrix_noise_num_qubits(NULL) == 0); + assert(density_matrix_noise_probabilities_len(NULL) == 0); + assert(density_matrix_noise_apply_h(NULL, 0) == -1); + assert(density_matrix_noise_measure(NULL, 0) == -1); + assert(density_matrix_noise_measure_all(NULL) == NULL); + assert(density_matrix_noise_sample_shots(NULL, 4) == NULL); +} + +static void test_ideal_bell_state(void) { + CDensityMatrixNoise* sim = density_matrix_noise_new(2, NULL); + assert(sim != NULL); + + assert(density_matrix_noise_apply_h(sim, 0) == 0); + assert(density_matrix_noise_apply_cx(sim, 0, 1) == 0); + + uintptr_t len = density_matrix_noise_probabilities_len(sim); + assert(len == 4); + double probs[4]; + assert(density_matrix_noise_probabilities(sim, probs, len) == 0); + assert(fabs(probs[0] - 0.5) < 1e-9); + assert(fabs(probs[1]) < 1e-9); + assert(fabs(probs[2]) < 1e-9); + assert(fabs(probs[3] - 0.5) < 1e-9); + + /* Wrong buffer length is rejected. */ + assert(density_matrix_noise_probabilities(sim, probs, 2) == -8); + + /* Collapse the state with a full measurement. */ + char* outcome = density_matrix_noise_measure_all(sim); + assert(outcome != NULL); + assert(strlen(outcome) == 2); + assert(outcome[0] == outcome[1]); /* |00> or |11> */ + cqlib_string_free(outcome); + + /* Sampling the collapsed state keeps results consistent. */ + COutcomeList* shots = density_matrix_noise_sample_shots(sim, 16); + assert(shots != NULL); + assert(outcome_list_len(shots) == 16); + char* first = outcome_list_get(shots, 0); + assert(first != NULL); + assert(strlen(first) == 2); + cqlib_string_free(first); + outcome_list_free(shots); + + /* Reset then measure: qubit 0 is back to |0>. */ + assert(density_matrix_noise_reset(sim, 0) == 0); + assert(density_matrix_noise_measure(sim, 0) == 0); + + /* Qubit out of bounds. */ + assert(density_matrix_noise_apply_h(sim, 2) == -2); + assert(density_matrix_noise_apply_cx(sim, 0, 2) == -2); + density_matrix_noise_free(sim); +} + +static void test_gate_noise(void) { + /* X followed by 10% bit-flip noise: P(|1>) = 0.9, P(|0>) = 0.1. */ + CNoiseModel* nm = noise_model_new(); + assert(nm != NULL); + assert(noise_model_add_single_qubit(nm, "X", 0, NOISE_BIT_FLIP, 0.1) == 0); + + CDensityMatrixNoise* sim = density_matrix_noise_new(1, nm); + assert(sim != NULL); + assert(density_matrix_noise_apply_x(sim, 0) == 0); + + double probs[2]; + assert(density_matrix_noise_probabilities(sim, probs, 2) == 0); + assert(fabs(probs[1] - 0.9) < 1e-9); + assert(fabs(probs[0] - 0.1) < 1e-9); + + density_matrix_noise_free(sim); + noise_model_free(nm); +} + +static void test_readout_noise(void) { + /* |0> with P(measure 1 | true 0) = 0.1. */ + CNoiseModel* nm = noise_model_new(); + assert(noise_model_add_readout(nm, 0, 0.0, 0.1) == 0); + + CDensityMatrixNoise* sim = density_matrix_noise_new(1, nm); + + /* Ideal distribution ignores readout errors. */ + double ideal[2]; + assert(density_matrix_noise_probabilities(sim, ideal, 2) == 0); + assert(fabs(ideal[0] - 1.0) < 1e-9); + assert(fabs(ideal[1]) < 1e-9); + + /* With readout: P(|0>) = 0.9, P(|1>) = 0.1. */ + assert(density_matrix_noise_probabilities_with_readout_len(sim) == 2); + const uint32_t qs[1] = {0}; + double probs[2]; + assert(density_matrix_noise_probabilities_with_readout(sim, probs, 2, qs, 1) == 0); + assert(fabs(probs[0] - 0.9) < 1e-9); + assert(fabs(probs[1] - 0.1) < 1e-9); + + /* Out-of-bounds readout qubit is rejected. */ + const uint32_t bad[1] = {99}; + assert(density_matrix_noise_probabilities_with_readout(sim, probs, 2, bad, 1) == -2); + + density_matrix_noise_free(sim); + noise_model_free(nm); +} + +static void test_expectation(void) { + /* = -1 on |1>. */ + CDensityMatrixNoise* sim = density_matrix_noise_new(1, NULL); + assert(density_matrix_noise_apply_x(sim, 0) == 0); + + CPauliString* pauli = pauli_string_parse("Z"); + assert(pauli != NULL); + CHamiltonian* ham = hamiltonian_from_pauli(pauli); + assert(ham != NULL); + + double value = NAN; + assert(density_matrix_noise_expectation(sim, ham, &value) == 0); + assert(fabs(value + 1.0) < 1e-9); + + hamiltonian_free(ham); + /* `hamiltonian_from_pauli` consumed the `pauli` handle; do not free it. */ + density_matrix_noise_free(sim); +} + +static void test_standard_gate_noise_entry(void) { + CDensityMatrixNoise* sim = density_matrix_noise_new(1, NULL); + + const uint32_t qs[1] = {0}; + assert(density_matrix_noise_apply_standard_gate_noise(sim, "H", qs, 1, NULL, 0) == 0); + + double probs[2]; + assert(density_matrix_noise_probabilities(sim, probs, 2) == 0); + assert(fabs(probs[0] - 0.5) < 1e-9); + assert(fabs(probs[1] - 0.5) < 1e-9); + + /* Parameterised gate via the generic entry point. */ + const double theta = 3.14159265358979323846 / 2.0; + assert(density_matrix_noise_apply_standard_gate_noise(sim, "RY", qs, 1, &theta, 1) == 0); + assert(density_matrix_noise_probabilities(sim, probs, 2) == 0); + assert(fabs(probs[0] + probs[1] - 1.0) < 1e-9); + + /* Unknown gate name -> -8. */ + assert(density_matrix_noise_apply_standard_gate_noise(sim, "NOT_A_GATE", qs, 1, NULL, 0) == -8); + /* Wrong arity -> -8. */ + assert(density_matrix_noise_apply_standard_gate_noise(sim, "CX", qs, 1, NULL, 0) == -8); + /* Wrong parameter count -> -8. */ + assert(density_matrix_noise_apply_standard_gate_noise(sim, "RX", qs, 1, NULL, 0) == -8); + /* Out-of-bounds qubit -> -2. */ + const uint32_t oob[1] = {5}; + assert(density_matrix_noise_apply_standard_gate_noise(sim, "H", oob, 1, NULL, 0) == -2); + + density_matrix_noise_free(sim); +} + +static void test_unitary_gate(void) { + CDensityMatrixNoise* sim = density_matrix_noise_new(1, NULL); + + /* Pauli-X as a 2x2 row-major Complex64 matrix. */ + const Complex64 x_matrix[4] = { + {0.0, 0.0}, + {1.0, 0.0}, + {1.0, 0.0}, + {0.0, 0.0}, + }; + const uint32_t qs[1] = {0}; + assert(density_matrix_noise_apply_unitary_gate(sim, qs, 1, x_matrix, 2) == 0); + + double probs[2]; + assert(density_matrix_noise_probabilities(sim, probs, 2) == 0); + assert(fabs(probs[1] - 1.0) < 1e-9); + assert(fabs(probs[0]) < 1e-9); + + /* Matrix dimension inconsistent with the qubit count -> -8. */ + const uint32_t two_qs[2] = {0, 0}; + assert(density_matrix_noise_apply_unitary_gate(sim, two_qs, 2, x_matrix, 2) == -8); + + density_matrix_noise_free(sim); +} + +static void test_from_circuit(void) { + CCircuit* circuit = circuit_new(1); + assert(circuit != NULL); + assert(circuit_x(circuit, 0) == 0); + + CNoiseModel* nm = noise_model_new(); + assert(noise_model_add_single_qubit(nm, "X", 0, NOISE_BIT_FLIP, 0.1) == 0); + + CDensityMatrixNoise* sim = density_matrix_noise_from_circuit(circuit, nm); + assert(sim != NULL); + assert(density_matrix_noise_num_qubits(sim) == 1); + + double probs[2]; + assert(density_matrix_noise_probabilities(sim, probs, 2) == 0); + assert(fabs(probs[1] - 0.9) < 1e-9); + assert(fabs(probs[0] - 0.1) < 1e-9); + + density_matrix_noise_free(sim); + noise_model_free(nm); + circuit_free(circuit); + assert(density_matrix_noise_from_circuit(NULL, NULL) == NULL); +} + +int main(void) { + test_lifecycle(); + test_ideal_bell_state(); + test_gate_noise(); + test_readout_noise(); + test_expectation(); + test_standard_gate_noise_entry(); + test_unitary_gate(); + test_from_circuit(); + printf("binding-c qis noise tests passed\n"); + return 0; +} diff --git a/crates/binding-c/tests/test_qubit.c b/crates/binding-c/tests/test_qubit.c new file mode 100644 index 00000000..959e58a4 --- /dev/null +++ b/crates/binding-c/tests/test_qubit.c @@ -0,0 +1,100 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating that +// they have been altered from the originals. + +#include +#include +#include +#include + +#include "cqlib_c.h" + +static void test_qubit_basics(void) { + CQubit q = qubit_new(12); + assert(q.id == 12); + assert(qubit_id(q) == 12u); + assert(qubit_index(q) == 12u); + + assert(qubit_equal(q, qubit_new(12)) == 1); + assert(qubit_equal(q, qubit_new(11)) == 0); + + assert(qubit_compare(qubit_new(0), qubit_new(1)) == -1); + assert(qubit_compare(qubit_new(7), qubit_new(7)) == 0); + assert(qubit_compare(qubit_new(2), qubit_new(1)) == 1); + + char* text = qubit_to_string(q); + assert(text != NULL && strcmp(text, "Q12") == 0); + cqlib_string_free(text); +} + +static void test_qubit_checked_conversions(void) { + CQubit out = {0}; + + // Negative or out-of-range signed values are rejected. + assert(qubit_try_from_i64(-1, &out) == -8); + assert(qubit_try_from_i64((int64_t)UINT32_MAX + 1, &out) == -8); + assert(qubit_try_from_i64(3, &out) == 0); + assert(out.id == 3u); + + // Unsigned values above UINT32_MAX are rejected. + assert(qubit_try_from_u64((uint64_t)UINT32_MAX + 1, &out) == -8); + assert(qubit_try_from_u64(20, &out) == 0); + assert(out.id == 20u); + + // NULL out buffer. + assert(qubit_try_from_i64(0, NULL) == -1); + assert(qubit_try_from_u64(0, NULL) == -1); +} + +static void test_logical_qubit_roundtrip(void) { + CQubit wire = qubit_new(3); + CLogicalQubit logical = logical_qubit_from_qubit(wire); + assert(logical.id == 3u); + assert(logical_qubit_id(logical) == 3u); + + CQubit back = logical_qubit_qubit(logical); + assert(back.id == 3u); + + assert(logical_qubit_equal(logical, logical_qubit_new(3)) == 1); + assert(logical_qubit_equal(logical, logical_qubit_new(2)) == 0); + assert(logical_qubit_compare(logical_qubit_new(2), logical_qubit_new(0)) == 1); + + char* text = logical_qubit_to_string(logical); + assert(text != NULL && strcmp(text, "L3") == 0); + cqlib_string_free(text); +} + +static void test_physical_qubit_roundtrip(void) { + CQubit wire = qubit_new(11); + CPhysicalQubit physical = physical_qubit_from_qubit(wire); + assert(physical.id == 11u); + assert(physical_qubit_id(physical) == 11u); + + CQubit back = physical_qubit_qubit(physical); + assert(back.id == 11u); + + assert(physical_qubit_equal(physical, physical_qubit_new(11)) == 1); + assert(physical_qubit_equal(physical, physical_qubit_new(10)) == 0); + assert(physical_qubit_compare(physical_qubit_new(0), physical_qubit_new(2)) == -1); + + char* text = physical_qubit_to_string(physical); + assert(text != NULL && strcmp(text, "P11") == 0); + cqlib_string_free(text); +} + +int main(void) { + test_qubit_basics(); + test_qubit_checked_conversions(); + test_logical_qubit_roundtrip(); + test_physical_qubit_roundtrip(); + printf("binding-c qubit tests passed\n"); + return 0; +} diff --git a/crates/binding-c/tests/test_symbolic_matrix.c b/crates/binding-c/tests/test_symbolic_matrix.c new file mode 100644 index 00000000..4e4d0292 --- /dev/null +++ b/crates/binding-c/tests/test_symbolic_matrix.c @@ -0,0 +1,563 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +// C smoke tests for symbolic complex values, element-level matrix +// construction and the in-place `apply_*` gate family. + +#include +#include +#include +#include +#include + +#include "cqlib_c.h" + +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif + +static int g_failures = 0; + +#define CHECK(cond, name) \ + do { \ + if (cond) { \ + printf("ok: %s\n", (name)); \ + } else { \ + printf("FAIL: %s\n", (name)); \ + g_failures++; \ + } \ + } while (0) + +/* Evaluates a fully numeric matrix with no bindings. */ +static int eval_matrix(const struct CSymbolicMatrix* matrix, Complex64* buffer, uintptr_t len) { + return symbolic_matrix_evaluate(matrix, NULL, buffer, len); +} + +static int close_enough(double a, double b) { return fabs(a - b) < 1e-9; } + +static void test_symbolic_complex_construction(void) { + /* NULL tolerance and invalid-input handling. */ + CHECK(symbolic_complex_new(NULL, NULL) == NULL, "complex new null"); + CHECK(symbolic_complex_from_real(NULL) == NULL, "complex from_real null"); + CHECK(symbolic_complex_exp_i(NULL) == NULL, "complex exp_i null"); + CHECK(!isfinite(0.0 / 0.0) && symbolic_complex_from_complex(0.0 / 0.0, 0.0) == NULL, + "complex from_complex nan"); + CHECK(symbolic_complex_re(NULL) == NULL, "complex re null"); + CHECK(symbolic_complex_im(NULL) == NULL, "complex im null"); + CHECK(symbolic_complex_is_zero_exact(NULL) == -1, "complex is_zero_exact null"); + CHECK(symbolic_complex_is_one_exact(NULL) == -1, "complex is_one_exact null"); + CHECK(symbolic_complex_simplifies_to_zero(NULL) == -1, "complex simplifies_to_zero null"); + CHECK(symbolic_complex_simplify(NULL) == NULL, "complex simplify null"); + symbolic_complex_free(NULL); + CHECK(1, "complex free null"); + + /* zero / one / i constants. */ + struct CSymbolicComplex* zero = symbolic_complex_zero(); + CHECK(symbolic_complex_is_zero_exact(zero) == 1, "zero is_zero_exact"); + CHECK(symbolic_complex_simplifies_to_zero(zero) == 1, "zero simplifies_to_zero"); + Complex64 out; + CHECK(symbolic_complex_evaluate(zero, NULL, &out) == 0, "zero evaluate"); + CHECK(out.re == 0.0 && out.im == 0.0, "zero value"); + + struct CSymbolicComplex* one = symbolic_complex_one(); + CHECK(symbolic_complex_is_one_exact(one) == 1, "one is_one_exact"); + CHECK(symbolic_complex_is_zero_exact(one) == 0, "one not zero"); + CHECK(symbolic_complex_evaluate(one, NULL, &out) == 0, "one evaluate"); + CHECK(out.re == 1.0 && out.im == 0.0, "one value"); + + struct CSymbolicComplex* eye_unit = symbolic_complex_i(); + CHECK(symbolic_complex_is_zero_exact(eye_unit) == 0, "i not zero"); + CHECK(symbolic_complex_evaluate(eye_unit, NULL, &out) == 0, "i evaluate"); + CHECK(out.re == 0.0 && out.im == 1.0, "i value"); + CHECK(symbolic_complex_is_one_exact(eye_unit) == 0, "i not one"); + + /* from_complex / from_real. */ + struct CSymbolicComplex* raw = symbolic_complex_from_complex(0.5, -0.25); + CHECK(symbolic_complex_evaluate(raw, NULL, &out) == 0, "from_complex evaluate"); + CHECK(out.re == 0.5 && out.im == -0.25, "from_complex value"); + + struct CParameter* theta = param_symbol("theta"); + struct CSymbolicComplex* real = symbolic_complex_from_real(theta); + CHECK(symbolic_complex_evaluate(real, "theta:0.75", &out) == 0, "from_real evaluate"); + CHECK(out.re == 0.75 && out.im == 0.0, "from_real value"); + + /* new(re, im) and the re/im accessors. */ + struct CParameter* two = param_from_double(2.0); + struct CSymbolicComplex* value = symbolic_complex_new(theta, two); + CHECK(value != NULL, "complex new"); + struct CParameter* re = symbolic_complex_re(value); + struct CParameter* im = symbolic_complex_im(value); + CHECK(re != NULL && im != NULL, "complex re/im accessors"); + CHECK(close_enough(param_evaluate(re, "theta:0.5"), 0.5), "re accessor value"); + CHECK(close_enough(param_evaluate(im, NULL), 2.0), "im accessor value"); + CHECK(symbolic_complex_evaluate(value, "theta:0.5", &out) == 0, "new evaluate"); + CHECK(out.re == 0.5 && out.im == 2.0, "new value"); + + /* exp_i(theta) evaluates to cos(theta) + i*sin(theta). */ + struct CSymbolicComplex* phase = symbolic_complex_exp_i(theta); + CHECK(symbolic_complex_evaluate(phase, "theta:0", &out) == 0, "exp_i at 0"); + CHECK(close_enough(out.re, 1.0) && close_enough(out.im, 0.0), "exp_i at 0 value"); + CHECK(symbolic_complex_evaluate(phase, "theta:3.141592653589793", &out) == 0, "exp_i at pi"); + CHECK(close_enough(out.re, -1.0) && close_enough(out.im, 0.0), "exp_i at pi value"); + + param_free(im); + param_free(re); + symbolic_complex_free(phase); + symbolic_complex_free(value); + symbolic_complex_free(real); + symbolic_complex_free(raw); + symbolic_complex_free(eye_unit); + symbolic_complex_free(one); + symbolic_complex_free(zero); + param_free(two); + param_free(theta); +} + +static void test_symbolic_complex_evaluate_simplify_replace(void) { + struct CParameter* theta = param_symbol("theta"); + struct CParameter* x = param_symbol("x"); + struct CParameter* diff = param_sub(x, theta); + struct CParameter* zero_param = param_from_double(0.0); + /* (x - theta, 0): after replacing x by theta both parts simplify to + * exact zero. */ + struct CSymbolicComplex* value = symbolic_complex_new(diff, zero_param); + CHECK(value != NULL, "replace fixture value"); + + Complex64 out; + /* Evaluation needs bound symbols: an unbound one yields -3 and a + * malformed bindings string yields -4. */ + CHECK(symbolic_complex_evaluate(value, "theta:1", &out) == -3, "evaluate unbound symbol"); + CHECK(symbolic_complex_evaluate(value, "bogus;", &out) == -4, "evaluate malformed bindings"); + + struct CSymbolicComplex* replaced = symbolic_complex_replace(value, "x", theta); + CHECK(replaced != NULL, "complex replace"); + CHECK(symbolic_complex_simplifies_to_zero(replaced) == 1, "replaced simplifies to zero"); + + struct CSymbolicComplex* simplified = symbolic_complex_simplify(replaced); + CHECK(simplified != NULL, "complex simplify"); + CHECK(symbolic_complex_is_zero_exact(simplified) == 1, "simplified is_zero_exact"); + CHECK(symbolic_complex_evaluate(simplified, NULL, &out) == 0, "simplified evaluate"); + CHECK(out.re == 0.0 && out.im == 0.0, "simplified value"); + + CHECK(symbolic_complex_replace(NULL, "x", theta) == NULL, "replace null value"); + CHECK(symbolic_complex_replace(value, NULL, theta) == NULL, "replace null symbol"); + CHECK(symbolic_complex_replace(value, "x", NULL) == NULL, "replace null replacement"); + + /* Simplify works on composed arithmetic as well. */ + struct CParameter* sum = param_add(theta, theta); + struct CSymbolicComplex* composed = symbolic_complex_from_real(sum); + CHECK(symbolic_complex_simplify(composed) != NULL, "simplify composed"); + + symbolic_complex_free(simplified); + symbolic_complex_free(replaced); + symbolic_complex_free(composed); + symbolic_complex_free(value); + param_free(zero_param); + param_free(sum); + param_free(diff); + param_free(x); + param_free(theta); +} + +static void test_symbolic_matrix_new_and_element(void) { + /* eye(2): build an element snapshot, then rebuild a matrix from it. */ + struct CSymbolicMatrix* eye = symbolic_eye(2); + CHECK(eye != NULL, "new_and_element fixture eye"); + struct CSymbolicComplex* e00 = symbolic_matrix_element(eye, 0, 0); + struct CSymbolicComplex* e01 = symbolic_matrix_element(eye, 0, 1); + struct CSymbolicComplex* e10 = symbolic_matrix_element(eye, 1, 0); + struct CSymbolicComplex* e11 = symbolic_matrix_element(eye, 1, 1); + CHECK(e00 != NULL && e01 != NULL && e10 != NULL && e11 != NULL, "eye elements"); + CHECK(symbolic_complex_is_one_exact(e00) == 1, "eye e00 is one"); + CHECK(symbolic_complex_is_zero_exact(e01) == 1, "eye e01 is zero"); + + const struct CSymbolicComplex* elements[4]; + elements[0] = e00; + elements[1] = e01; + elements[2] = e10; + elements[3] = e11; + struct CSymbolicMatrix* rebuilt = symbolic_matrix_new(2, 2, elements, 4); + CHECK(rebuilt != NULL, "matrix_new from elements"); + CHECK(symbolic_matrix_rows(rebuilt) == 2, "rebuilt rows"); + CHECK(symbolic_matrices_equivalent(eye, rebuilt) == 1, "rebuilt equivalent"); + + /* Out-of-bounds element access returns NULL. */ + CHECK(symbolic_matrix_element(eye, 2, 0) == NULL, "element row out of bounds"); + CHECK(symbolic_matrix_element(eye, 0, 2) == NULL, "element col out of bounds"); + CHECK(symbolic_matrix_element(NULL, 0, 0) == NULL, "element null matrix"); + + /* Constructors reject shape/length mismatches and NULL handles. */ + CHECK(symbolic_matrix_new(2, 2, elements, 3) == NULL, "matrix_new length mismatch"); + CHECK(symbolic_matrix_new(0, 2, elements, 0) == NULL, "matrix_new zero rows"); + CHECK(symbolic_matrix_new(2, 0, elements, 0) == NULL, "matrix_new zero cols"); + CHECK(symbolic_matrix_new(2, 2, NULL, 4) == NULL, "matrix_new null elements"); + const struct CSymbolicComplex* bad[4]; + bad[0] = e00; + bad[1] = NULL; + bad[2] = e10; + bad[3] = e11; + CHECK(symbolic_matrix_new(2, 2, bad, 4) == NULL, "matrix_new null entry"); + + symbolic_complex_free(e00); + symbolic_complex_free(e01); + symbolic_complex_free(e10); + symbolic_complex_free(e11); + symbolic_matrix_free(rebuilt); + symbolic_matrix_free(eye); +} + +static void test_apply_standard_gate_matches_circuit(void) { + /* Reference: a 2-qubit circuit H(0); CX(0, 1). */ + struct CCircuit* circuit = circuit_new(2); + CHECK(circuit != NULL, "standard fixture circuit"); + CHECK(circuit_h(circuit, 0) == 0, "standard fixture h"); + CHECK(circuit_cx(circuit, 0, 1) == 0, "standard fixture cx"); + struct CSymbolicMatrix* reference = circuit_to_symbolic_matrix(circuit, NULL, 0); + CHECK(reference != NULL, "standard fixture reference matrix"); + + /* Same unitary assembled in place from the identity matrix. */ + struct CSymbolicMatrix* matrix = symbolic_eye(4); + const uintptr_t bits[1] = {0}; + CHECK(symbolic_matrix_apply_standard_gate(matrix, "H", bits, 1, NULL, 0) == 0, + "apply standard H"); + /* `apply_gate_to_matrix` expects the target bits in the system's + * Little-Endian order, and `circuit_to_symbolic_matrix` reverses the + * circuit's [control, target] pair before applying, so + * CX(control=0, target=1) maps to bits [1, 0]. */ + const uintptr_t two_bits[2] = {1, 0}; + CHECK(symbolic_matrix_apply_standard_gate(matrix, "CX", two_bits, 2, NULL, 0) == 0, + "apply standard CX"); + CHECK(symbolic_matrices_equivalent(reference, matrix) == 1, "assembled equals reference"); + + /* Error paths: NULL handles, unknown gate, bad bit arrays. */ + CHECK(symbolic_matrix_apply_standard_gate(NULL, "H", bits, 1, NULL, 0) == -1, + "apply standard null matrix"); + CHECK(symbolic_matrix_apply_standard_gate(matrix, "NOPE", bits, 1, NULL, 0) == -8, + "apply standard unknown gate"); + CHECK(symbolic_matrix_apply_standard_gate(matrix, "H", NULL, 1, NULL, 0) == -8, + "apply standard null bits"); + const uintptr_t out_of_range[1] = {9}; + CHECK(symbolic_matrix_apply_standard_gate(matrix, "H", out_of_range, 1, NULL, 0) == -8, + "apply standard bit out of range"); + const uintptr_t duplicated[2] = {1, 1}; + CHECK(symbolic_matrix_apply_standard_gate(matrix, "CX", duplicated, 2, NULL, 0) == -8, + "apply standard duplicated bits"); + + /* RX with one parameter works and NULL parameter handles are rejected. */ + struct CSymbolicMatrix* rebuilt = symbolic_eye(4); + struct CParameter* half = param_from_double(0.5); + const struct CParameter* params[1]; + params[0] = half; + CHECK(symbolic_matrix_apply_standard_gate(rebuilt, "RX", bits, 1, params, 1) == 0, + "apply standard RX"); + const struct CParameter* null_params[1]; + null_params[0] = NULL; + CHECK(symbolic_matrix_apply_standard_gate(rebuilt, "RX", bits, 1, null_params, 1) == -1, + "apply standard null param entry"); + + param_free(half); + symbolic_matrix_free(rebuilt); + symbolic_matrix_free(matrix); + symbolic_matrix_free(reference); + circuit_free(circuit); +} + +static void test_apply_gate_and_single_qubit(void) { + struct CSymbolicMatrix* x_gate = standard_gate_symbolic_matrix("X", NULL, 0); + CHECK(x_gate != NULL, "gate fixture X"); + + /* symbolic apply_gate mirrors the numeric X application. */ + struct CSymbolicMatrix* symbolic = symbolic_eye(4); + const uintptr_t bit[1] = {1}; + CHECK(symbolic_matrix_apply_gate(NULL, x_gate, bit, 1) == -1, "apply gate null matrix"); + CHECK(symbolic_matrix_apply_gate(symbolic, NULL, bit, 1) == -1, "apply gate null gate"); + CHECK(symbolic_matrix_apply_gate(symbolic, x_gate, NULL, 1) == -8, "apply gate null bits"); + CHECK(symbolic_matrix_apply_gate(symbolic, x_gate, bit, 1) == 0, "apply gate X"); + + const Complex64 x_num[4] = {{0.0, 0.0}, {1.0, 0.0}, {1.0, 0.0}, {0.0, 0.0}}; + struct CSymbolicMatrix* numeric = symbolic_eye(4); + CHECK(symbolic_matrix_apply_gate_num(numeric, x_num, 2, bit, 1) == 0, "apply gate_num X"); + /* gate_dim must equal 1 << bits_len. */ + CHECK(symbolic_matrix_apply_gate_num(numeric, x_num, 4, bit, 1) == -8, + "apply gate_num dim mismatch"); + CHECK(symbolic_matrix_apply_gate_num(NULL, x_num, 2, bit, 1) == -1, + "apply gate_num null matrix"); + + /* The two application paths agree numerically. Applying X on bit 1 + * swaps matrix rows r <-> r ^ 2, so the identity becomes the + * row-permutation matrix with values[4*r + c] = 1 iff c == r ^ 2. */ + Complex64 sym_values[16]; + Complex64 num_values[16]; + CHECK(eval_matrix(symbolic, sym_values, 16) == 0, "symbolic X evaluate"); + CHECK(eval_matrix(numeric, num_values, 16) == 0, "numeric X evaluate"); + int all_equal = 1; + for (int i = 0; i < 16; ++i) { + if (!close_enough(sym_values[i].re, num_values[i].re) || + !close_enough(sym_values[i].im, num_values[i].im)) { + all_equal = 0; + } + } + CHECK(all_equal, "apply gate X paths agree"); + CHECK(num_values[0 * 4 + 2].re == 1.0, "X permutation 0/2"); + CHECK(num_values[2 * 4 + 0].re == 1.0, "X permutation 2/0"); + CHECK(num_values[1 * 4 + 3].re == 1.0, "X permutation 1/3"); + CHECK(num_values[3 * 4 + 1].re == 1.0, "X permutation 3/1"); + CHECK(num_values[0 * 4 + 0].re == 0.0, "X double-swaps itself"); + + /* Applying X a second time restores the identity: X * X = I. */ + CHECK(symbolic_matrix_apply_gate(symbolic, x_gate, bit, 1) == 0, "apply gate X twice"); + CHECK(eval_matrix(symbolic, sym_values, 16) == 0, "symbolic X twice evaluate"); + int identity_after_twice = 1; + for (int i = 0; i < 16; ++i) { + int row = i / 4; + int col = i % 4; + double expect = (row == col) ? 1.0 : 0.0; + if (!close_enough(sym_values[i].re, expect) || !close_enough(sym_values[i].im, 0.0)) { + identity_after_twice = 0; + } + } + CHECK(identity_after_twice, "X twice restores identity"); + + /* single-qubit variants. A non-2x2 gate payload is rejected. */ + struct CSymbolicMatrix* swapped = symbolic_eye(2); + struct CSymbolicMatrix* cx_gate = standard_gate_symbolic_matrix("CX", NULL, 0); + CHECK(symbolic_matrix_apply_single_qubit_gate(swapped, cx_gate, 0) == -8, + "single_qubit rejects 4x4 gate"); + CHECK(symbolic_matrix_apply_single_qubit_gate(swapped, x_gate, 0) == 0, "single_qubit X"); + CHECK(symbolic_matrix_apply_single_qubit_gate(swapped, x_gate, 5) == -8, + "single_qubit bit out of range"); + + struct CSymbolicMatrix* swapped_num = symbolic_eye(2); + CHECK(symbolic_matrix_apply_single_qubit_gate_num(swapped_num, x_num, 0) == 0, + "single_qubit_num X"); + CHECK(symbolic_matrix_apply_single_qubit_gate_num(swapped_num, x_num, 2) == -8, + "single_qubit_num bit out of range"); + Complex64 swapped_values[4]; + Complex64 swapped_num_values[4]; + CHECK(eval_matrix(swapped, swapped_values, 4) == 0, "single_qubit evaluate"); + CHECK(eval_matrix(swapped_num, swapped_num_values, 4) == 0, "single_qubit_num evaluate"); + CHECK(close_enough(swapped_values[0].re, swapped_num_values[0].re) && + close_enough(swapped_values[1].re, swapped_num_values[1].re), + "single_qubit paths agree"); + + symbolic_matrix_free(swapped_num); + symbolic_matrix_free(swapped); + symbolic_matrix_free(cx_gate); + symbolic_matrix_free(numeric); + symbolic_matrix_free(symbolic); + symbolic_matrix_free(x_gate); +} + +static void test_apply_two_qubit_and_general(void) { + struct CSymbolicMatrix* cx_gate = standard_gate_symbolic_matrix("CX", NULL, 0); + CHECK(cx_gate != NULL, "two_qubit fixture CX"); + + /* A 2-qubit symbolic gate must be 4x4 (CX is) and the bits distinct. */ + struct CSymbolicMatrix* symbolic = symbolic_eye(4); + CHECK(symbolic_matrix_apply_two_qubit_gate(NULL, cx_gate, 0, 1) == -1, "two_qubit null matrix"); + CHECK(symbolic_matrix_apply_two_qubit_gate(symbolic, NULL, 0, 1) == -1, "two_qubit null gate"); + CHECK(symbolic_matrix_apply_two_qubit_gate(symbolic, cx_gate, 0, 0) == -8, + "two_qubit duplicated bits"); + CHECK(symbolic_matrix_apply_two_qubit_gate(symbolic, cx_gate, 0, 7) == -8, + "two_qubit bit out of range"); + CHECK(symbolic_matrix_apply_two_qubit_gate(symbolic, cx_gate, 0, 1) == 0, "two_qubit CX"); + + /* The numeric payload is read back from the standard CX gate so both + * paths use the exact same matrix. */ + Complex64 cx_num[16]; + CHECK(eval_matrix(cx_gate, cx_num, 16) == 0, "CX numeric payload"); + + struct CSymbolicMatrix* numeric = symbolic_eye(4); + CHECK(symbolic_matrix_apply_two_qubit_gate_num(numeric, NULL, 0, 1) == -1, + "two_qubit_num null gate"); + CHECK(symbolic_matrix_apply_two_qubit_gate_num(numeric, cx_num, 1, 1) == -8, + "two_qubit_num duplicated bits"); + CHECK(symbolic_matrix_apply_two_qubit_gate_num(NULL, cx_num, 0, 1) == -1, + "two_qubit_num null matrix"); + CHECK(symbolic_matrix_apply_two_qubit_gate_num(numeric, cx_num, 0, 1) == 0, "two_qubit_num CX"); + + /* Cross-check the two-qubit path against apply_standard_gate on a + * freshly built identity: both produce the same CX permutation. */ + struct CSymbolicMatrix* via_standard = symbolic_eye(4); + const uintptr_t std_bits[2] = {0, 1}; + CHECK(symbolic_matrix_apply_standard_gate(via_standard, "CX", std_bits, 2, NULL, 0) == 0, + "standard CX cross-check"); + + /* general-gate variants with a 2-bit target. */ + struct CSymbolicMatrix* general = symbolic_eye(4); + const uintptr_t bits[2] = {0, 1}; + CHECK(symbolic_matrix_apply_general_gate(general, cx_gate, bits, 2) == 0, "general CX"); + /* Shape mismatch on the general variant: a 2x2 gate with 2 bits. */ + struct CSymbolicMatrix* x_gate = standard_gate_symbolic_matrix("X", NULL, 0); + struct CSymbolicMatrix* bad_shape = symbolic_eye(4); + CHECK(symbolic_matrix_apply_general_gate(bad_shape, x_gate, bits, 2) == -8, + "general gate shape mismatch"); + + /* General numeric application equals the symbolic one. */ + struct CSymbolicMatrix* general_num = symbolic_eye(4); + CHECK(symbolic_matrix_apply_general_gate_num(general_num, cx_num, bits, 2) == 0, + "general_num CX"); + CHECK(symbolic_matrix_apply_general_gate_num(NULL, cx_num, bits, 2) == -1, + "general_num null matrix"); + + /* All four application paths produce the same matrix. */ + Complex64 sym_values[16]; + Complex64 num_values[16]; + Complex64 std_values[16]; + Complex64 gen_values[16]; + Complex64 gen_num_values[16]; + CHECK(eval_matrix(symbolic, sym_values, 16) == 0, "two_qubit evaluate"); + CHECK(eval_matrix(numeric, num_values, 16) == 0, "two_qubit_num evaluate"); + CHECK(eval_matrix(via_standard, std_values, 16) == 0, "standard CX evaluate"); + CHECK(eval_matrix(general, gen_values, 16) == 0, "general evaluate"); + CHECK(eval_matrix(general_num, gen_num_values, 16) == 0, "general_num evaluate"); + int all_equal = 1; + for (int i = 0; i < 16; ++i) { + if (!close_enough(sym_values[i].re, num_values[i].re) || + !close_enough(sym_values[i].re, std_values[i].re) || + !close_enough(sym_values[i].re, gen_values[i].re) || + !close_enough(sym_values[i].re, gen_num_values[i].re)) { + all_equal = 0; + } + } + CHECK(all_equal, "all CX paths agree"); + + symbolic_matrix_free(general_num); + symbolic_matrix_free(bad_shape); + symbolic_matrix_free(x_gate); + symbolic_matrix_free(general); + symbolic_matrix_free(via_standard); + symbolic_matrix_free(numeric); + symbolic_matrix_free(symbolic); + symbolic_matrix_free(cx_gate); +} + +static void test_apply_permutation_and_diagonal(void) { + /* X on bit 0 equals the permutation (i -> (1 - i, one)). */ + struct CSymbolicComplex* one_a = symbolic_complex_one(); + struct CSymbolicComplex* one_b = symbolic_complex_one(); + struct CSymbolicMatrix* symbolic = symbolic_eye(2); + const uintptr_t bit[1] = {0}; + const uintptr_t indices[2] = {1, 0}; + const struct CSymbolicComplex* values[2]; + values[0] = one_a; + values[1] = one_b; + CHECK(symbolic_matrix_apply_permutation_gate(NULL, indices, values, 2, bit, 1) == -1, + "permutation null matrix"); + CHECK(symbolic_matrix_apply_permutation_gate(symbolic, NULL, values, 2, bit, 1) == -8, + "permutation null indices"); + /* len must equal 1 << bits_len. */ + CHECK(symbolic_matrix_apply_permutation_gate(symbolic, indices, values, 3, bit, 1) == -8, + "permutation length mismatch"); + /* Source index beyond the gate dimension. */ + const uintptr_t bad_index[2] = {5, 0}; + CHECK(symbolic_matrix_apply_permutation_gate(symbolic, bad_index, values, 2, bit, 1) == -1, + "permutation index beyond dim"); + /* NULL value handle. */ + const struct CSymbolicComplex* null_values[2]; + null_values[0] = one_a; + null_values[1] = NULL; + CHECK(symbolic_matrix_apply_permutation_gate(symbolic, indices, null_values, 2, bit, 1) == -1, + "permutation null value"); + CHECK(symbolic_matrix_apply_permutation_gate(symbolic, indices, values, 2, bit, 1) == 0, + "permutation X"); + /* The permutation swaps rows 0 <-> 1 of the identity, i.e. it is X. */ + Complex64 permuted[4]; + CHECK(eval_matrix(symbolic, permuted, 4) == 0, "permutation evaluate"); + CHECK(permuted[0].re == 0.0 && permuted[1].re == 1.0, "permutation row 0"); + CHECK(permuted[2].re == 1.0 && permuted[3].re == 0.0, "permutation row 1"); + + /* Numeric permutation with factors (1, -1): output row 0 comes from + * input row 1 unchanged and output row 1 is the negated input row 0. */ + struct CSymbolicMatrix* perm_num = symbolic_eye(2); + const Complex64 factors[2] = {{1.0, 0.0}, {-1.0, 0.0}}; + CHECK(symbolic_matrix_apply_permutation_gate_num(perm_num, indices, factors, 2, bit, 1) == 0, + "permutation_num"); + CHECK(symbolic_matrix_apply_permutation_gate_num(NULL, indices, factors, 2, bit, 1) == -1, + "permutation_num null matrix"); + CHECK(symbolic_matrix_apply_permutation_gate_num(perm_num, bad_index, factors, 2, bit, 1) == -8, + "permutation_num index beyond dim"); + Complex64 flipped[4]; + CHECK(eval_matrix(perm_num, flipped, 4) == 0, "permutation_num evaluate"); + CHECK(flipped[0].re == 0.0 && flipped[1].re == 1.0, "permutation_num row 0"); + CHECK(flipped[2].re == -1.0 && flipped[3].re == 0.0, "permutation_num row 1"); + + /* Symbolic diagonal: diag(1, i) rotated as the S gate on bit 0. */ + struct CSymbolicComplex* eye_unit = symbolic_complex_i(); + struct CSymbolicComplex* diag_one = symbolic_complex_one(); + struct CSymbolicMatrix* diagonal = symbolic_eye(2); + const struct CSymbolicComplex* diag_values[2]; + diag_values[0] = diag_one; + diag_values[1] = eye_unit; + CHECK(symbolic_matrix_apply_diagonal_gate(NULL, diag_values, 2, bit, 1) == -1, + "diagonal null matrix"); + /* len/dim mismatch. */ + CHECK(symbolic_matrix_apply_diagonal_gate(diagonal, diag_values, 3, bit, 1) == -8, + "diagonal length mismatch"); + CHECK(symbolic_matrix_apply_diagonal_gate(diagonal, NULL, 2, bit, 1) == -8, + "diagonal null values"); + CHECK(symbolic_matrix_apply_diagonal_gate(diagonal, diag_values, 2, bit, 1) == 0, + "diagonal S gate"); + Complex64 rotated[4]; + CHECK(eval_matrix(diagonal, rotated, 4) == 0, "diagonal evaluate"); + CHECK(rotated[0].re == 1.0 && rotated[0].im == 0.0, "diagonal row 0"); + CHECK(rotated[3].re == 0.0 && rotated[3].im == 1.0, "diagonal row 1"); + + /* Numeric diagonal with the same payload. */ + struct CSymbolicMatrix* diagonal_num = symbolic_eye(2); + const Complex64 raw_diag[2] = {{1.0, 0.0}, {0.0, 1.0}}; + CHECK(symbolic_matrix_apply_diagonal_gate_num(diagonal_num, raw_diag, 2, bit, 1) == 0, + "diagonal_num"); + CHECK(symbolic_matrix_apply_diagonal_gate_num(NULL, raw_diag, 2, bit, 1) == -1, + "diagonal_num null matrix"); + CHECK(symbolic_matrix_apply_diagonal_gate_num(diagonal_num, raw_diag, 5, bit, 1) == -8, + "diagonal_num length mismatch"); + Complex64 rotated_num[4]; + CHECK(eval_matrix(diagonal_num, rotated_num, 4) == 0, "diagonal_num evaluate"); + CHECK(close_enough(rotated[3].re, rotated_num[3].re) && + close_enough(rotated[3].im, rotated_num[3].im), + "diagonal_num paths agree"); + + symbolic_matrix_free(diagonal_num); + symbolic_matrix_free(diagonal); + symbolic_complex_free(diag_one); + symbolic_complex_free(eye_unit); + symbolic_matrix_free(perm_num); + symbolic_matrix_free(symbolic); + symbolic_complex_free(one_b); + symbolic_complex_free(one_a); +} + +static void test_apply_keeps_shape(void) { + /* Sanity: applying gates never changes the matrix shape. */ + struct CSymbolicMatrix* matrix = symbolic_eye(4); + const uintptr_t bits[2] = {0, 1}; + CHECK(symbolic_matrix_apply_standard_gate(matrix, "CX", bits, 2, NULL, 0) == 0, + "shape check CX"); + CHECK(symbolic_matrix_rows(matrix) == 4, "shape rows unchanged"); + CHECK(symbolic_matrix_evaluate_len(matrix) == 16, "shape evaluate_len unchanged"); + symbolic_matrix_free(matrix); +} + +int main(void) { + test_symbolic_complex_construction(); + test_symbolic_complex_evaluate_simplify_replace(); + test_symbolic_matrix_new_and_element(); + test_apply_standard_gate_matches_circuit(); + test_apply_gate_and_single_qubit(); + test_apply_two_qubit_and_general(); + test_apply_permutation_and_diagonal(); + test_apply_keeps_shape(); + + printf("\n%d failure(s)\n", g_failures); + return (g_failures == 0) ? 0 : 1; +} diff --git a/crates/binding-c/tests/test_visualization.c b/crates/binding-c/tests/test_visualization.c new file mode 100644 index 00000000..843abd7f --- /dev/null +++ b/crates/binding-c/tests/test_visualization.c @@ -0,0 +1,103 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +#include +#include +#include +#include + +#include "cqlib_c.h" + +static CCircuit* build_bell_circuit(void) { + CCircuit* circuit = circuit_new(2); + assert(circuit != NULL); + assert(circuit_h(circuit, 0) == 0); + assert(circuit_cx(circuit, 0, 1) == 0); + return circuit; +} + +static void test_circuit_to_text(void) { + CCircuit* circuit = build_bell_circuit(); + + char* text = circuit_to_text(circuit, NULL); + assert(text != NULL); + assert(strchr(text, 'H') != NULL); + /* CX renders as a control dot with an "X" target label. */ + assert(strchr(text, 'X') != NULL); + cqlib_string_free(text); + + TextDrawerOptionsC options; + memset(&options, 0, sizeof(options)); + options.show_params = 1; + options.line_width = -1; + options.initial_state = 1; + text = circuit_to_text(circuit, &options); + assert(text != NULL); + cqlib_string_free(text); + + assert(circuit_to_text(NULL, NULL) == NULL); + circuit_free(circuit); +} + +static void test_circuit_to_figure(void) { + CCircuit* circuit = build_bell_circuit(); + + char* svg = circuit_to_figure(circuit, NULL); + assert(svg != NULL); + assert(strstr(svg, " +#include +#include +#include +#include + +#include "cqlib_c.h" + +static CStatevector* build_bell_state(void) { + double inv = 0.7071067811865476; + Complex64 amplitudes[4] = { + {inv, 0.0}, + {0.0, 0.0}, + {0.0, 0.0}, + {inv, 0.0}, + }; + CStatevector* state = statevector_from_state(2, amplitudes, 4); + assert(state != NULL); + return state; +} + +static CExecutionResult* build_bell_result(void) { + const char* task_id = "task-visualization"; + uint32_t qubits[2] = {0, 1}; + CExecutionResult* result = execution_result_new(task_id, qubits, 2, 7, NULL); + assert(result != NULL); + const char* bitstrings[2] = {"00", "11"}; + uint64_t counts[2] = {2, 5}; + assert(execution_result_finish(result, bitstrings, counts, 2) == 0); + return result; +} + +static void assert_is_svg(const char* svg) { + assert(svg != NULL); + assert(strstr(svg, " String { + if ptr.is_null() { + return String::new(); + } + unsafe { CStr::from_ptr(ptr) } + .to_string_lossy() + .into_owned() +} + +fn assert_svg(ptr: *mut c_char) { + assert!(!ptr.is_null(), "plot should return non-NULL SVG"); + let svg = cstr_to_string(ptr); + cqlib_string_free(ptr); + assert!(svg.starts_with(" Vec { + let inv_sqrt2 = 1.0 / 2.0_f64.sqrt(); + vec![ + Complex64::new(inv_sqrt2, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(inv_sqrt2, 0.0), + ] +} + +fn bell_statevector() -> *mut CStatevector { + let amplitudes = bell_amplitudes(); + let state = statevector_from_state(2, amplitudes.as_ptr(), amplitudes.len()); + assert!(!state.is_null()); + state +} + +fn bell_density_matrix() -> *mut CDensityMatrix { + let amplitudes = bell_amplitudes(); + let state = density_matrix_from_state(2, amplitudes.as_ptr(), amplitudes.len()); + assert!(!state.is_null()); + state +} + +fn bell_result() -> *mut CExecutionResult { + let task_id = CString::new("task-visualization").unwrap(); + let qubits = [0u32, 1]; + let result = execution_result_new(task_id.as_ptr(), qubits.as_ptr(), 2, 7, std::ptr::null()); + assert!(!result.is_null()); + let bits = [CString::new("00").unwrap(), CString::new("11").unwrap()]; + let bit_ptrs = [bits[0].as_ptr(), bits[1].as_ptr()]; + let counts = [2u64, 5]; + assert_eq!( + execution_result_finish(result, bit_ptrs.as_ptr(), counts.as_ptr(), 2), + 0 + ); + result +} + +fn bell_circuit() -> *mut CCircuit { + let circuit = circuit_new(2); + assert!(!circuit.is_null()); + assert_eq!(circuit_h(circuit, 0), 0); + assert_eq!(circuit_cx(circuit, 0, 1), 0); + circuit +} + +#[test] +fn test_plot_bloch_vector() { + assert_svg(plot_bloch_vector(0.0, 0.0, 1.0, std::ptr::null())); + // Vectors longer than the unit sphere are clamped, not rejected. + assert_svg(plot_bloch_vector(2.0, 0.0, 0.0, std::ptr::null())); +} + +#[test] +fn test_plot_state_functions_on_statevector() { + let state = bell_statevector(); + assert_svg(plot_bloch_multivector(state, std::ptr::null())); + assert_svg(plot_state_city(state, std::ptr::null())); + assert_svg(plot_state_paulivec(state, std::ptr::null())); + + let svg_ptr = plot_bloch_multivector(state, std::ptr::null()); + let svg = cstr_to_string(svg_ptr); + cqlib_string_free(svg_ptr); + assert!(svg.contains("q0"), "Bloch grid should label q0"); + assert!(svg.contains("q1"), "Bloch grid should label q1"); + statevector_free(state); +} + +#[test] +fn test_plot_state_functions_on_density_matrix() { + let state = bell_density_matrix(); + assert_svg(plot_bloch_multivector_density_matrix( + state, + std::ptr::null(), + )); + assert_svg(plot_state_city_density_matrix(state, std::ptr::null())); + assert_svg(plot_state_paulivec_density_matrix(state, std::ptr::null())); + density_matrix_free(state); +} + +#[test] +fn test_plot_state_options() { + let state = bell_statevector(); + let title = CString::new("Bell state").unwrap(); + let color = CString::new("#4569d4").unwrap(); + let colors = [color.as_ptr()]; + let options = CStatePlotOptions { + title: title.as_ptr(), + color: colors.as_ptr(), + color_len: 1, + alpha: 0.75, + reverse_bits: 1, + has_figsize: 1, + fig_width: 4.0, + fig_height: 3.0, + }; + let svg_ptr = plot_state_city(state, &options); + assert!(!svg_ptr.is_null()); + let svg = cstr_to_string(svg_ptr); + cqlib_string_free(svg_ptr); + assert!(svg.contains("Bell state"), "title should be rendered"); + statevector_free(state); +} + +#[test] +fn test_plot_state_null_handling() { + assert!(plot_bloch_multivector(std::ptr::null(), std::ptr::null()).is_null()); + assert!(plot_state_city(std::ptr::null(), std::ptr::null()).is_null()); + assert!(plot_state_paulivec(std::ptr::null(), std::ptr::null()).is_null()); + assert!(plot_bloch_multivector_density_matrix(std::ptr::null(), std::ptr::null()).is_null()); + assert!(plot_state_city_density_matrix(std::ptr::null(), std::ptr::null()).is_null()); + assert!(plot_state_paulivec_density_matrix(std::ptr::null(), std::ptr::null()).is_null()); +} + +#[test] +fn test_plot_histogram_and_distribution() { + let result = bell_result(); + assert_svg(plot_histogram(result, std::ptr::null())); + assert_svg(plot_distribution(result, std::ptr::null())); + execution_result_free(result); +} + +#[test] +fn test_plot_histogram_options() { + let result = bell_result(); + let title = CString::new("Measurement counts").unwrap(); + let sort = CString::new("value_desc").unwrap(); + let legend = CString::new("sim").unwrap(); + let legends = [legend.as_ptr()]; + let options = CResultPlotOptions { + has_figsize: 1, + fig_width: 3.2, + fig_height: 2.4, + color: std::ptr::null(), + color_len: 0, + number_to_keep: -1, + sort: sort.as_ptr(), + target_string: std::ptr::null(), + legend: legends.as_ptr(), + legend_len: 1, + bar_labels: 1, + title: title.as_ptr(), + }; + let svg_ptr = plot_distribution(result, &options); + assert!(!svg_ptr.is_null()); + let svg = cstr_to_string(svg_ptr); + cqlib_string_free(svg_ptr); + assert!(svg.contains("Measurement counts"), "title should render"); + assert!(svg.contains("sim"), "legend entry should render"); + execution_result_free(result); +} + +#[test] +fn test_plot_result_null_handling() { + assert!(plot_histogram(std::ptr::null(), std::ptr::null()).is_null()); + assert!(plot_distribution(std::ptr::null(), std::ptr::null()).is_null()); +} + +#[test] +fn test_render_state_plot_to_file() { + let svg_ptr = plot_bloch_vector(0.0, 0.0, 1.0, std::ptr::null()); + assert!(!svg_ptr.is_null()); + let path = std::env::temp_dir().join("cqlib_c_binding_test_state_plot.svg"); + let path_c = CString::new(path.to_string_lossy().into_owned()).unwrap(); + assert_eq!(render_state_plot_to_file(svg_ptr, path_c.as_ptr()), 0); + assert!(path.exists(), "rendered SVG file should exist"); + let _ = std::fs::remove_file(&path); + cqlib_string_free(svg_ptr); +} + +#[test] +fn test_render_result_plot_to_file() { + let result = bell_result(); + let svg_ptr = plot_histogram(result, std::ptr::null()); + assert!(!svg_ptr.is_null()); + let path = std::env::temp_dir().join("cqlib_c_binding_test_result_plot.svg"); + let path_c = CString::new(path.to_string_lossy().into_owned()).unwrap(); + assert_eq!(render_result_plot_to_file(svg_ptr, path_c.as_ptr()), 0); + assert!(path.exists(), "rendered SVG file should exist"); + let _ = std::fs::remove_file(&path); + cqlib_string_free(svg_ptr); + execution_result_free(result); +} + +#[test] +fn test_render_plot_null_handling() { + assert_eq!( + render_state_plot_to_file(std::ptr::null(), std::ptr::null()), + -1 + ); + assert_eq!( + render_result_plot_to_file(std::ptr::null(), std::ptr::null()), + -1 + ); +} + +#[test] +fn test_build_visual_circuit_and_accessors() { + let circuit = bell_circuit(); + let visual = build_visual_circuit(circuit, std::ptr::null()); + assert!(!visual.is_null()); + assert_eq!(visual_circuit_num_qubits(visual), 2); + assert_eq!(visual_circuit_num_operations(visual), 2); + assert_eq!(visual_circuit_num_columns(visual), 2); + + let mut qubits = [0u32; 2]; + assert_eq!(visual_circuit_qubits_len(visual), 2); + assert_eq!( + visual_circuit_qubits(visual, qubits.as_mut_ptr(), 2), + 0, + "qubit copy should succeed" + ); + assert_eq!(qubits, [0, 1]); + + let label_ptr = visual_circuit_operation_label(visual, 0); + assert!(!label_ptr.is_null()); + let label = cstr_to_string(label_ptr); + cqlib_string_free(label_ptr); + assert_eq!(label, "H"); + + assert_eq!(visual_circuit_operation_column(visual, 0), 0); + assert_eq!(visual_circuit_operation_column(visual, 1), 1); + assert_eq!( + visual_circuit_operation_style(visual, 0), + VISUAL_OP_STYLE_GATE + ); + assert_eq!( + visual_circuit_operation_style(visual, 1), + VISUAL_OP_STYLE_CONTROLLED + ); + assert_eq!(visual_circuit_operation_num_controls(visual, 1), 1); + assert_eq!(visual_circuit_operation_is_span_box(visual, 0), 0); + + assert_eq!(visual_circuit_operation_lanes_len(visual, 1), 2); + let mut lanes = [0usize; 2]; + assert_eq!( + visual_circuit_operation_lanes(visual, 1, lanes.as_mut_ptr(), 2), + 0 + ); + assert_eq!(lanes, [0, 1]); + assert_eq!( + visual_circuit_operation_covered_lanes_len(visual, 1), + 2, + "CX should cover both lanes" + ); + assert_eq!( + visual_circuit_operation_covered_lanes(visual, 1, lanes.as_mut_ptr(), 2), + 0 + ); + assert_eq!(lanes, [0, 1]); + + assert_eq!(visual_circuit_operation_params_len(visual, 0), 0); + assert_eq!( + visual_circuit_operation_params(visual, 0, std::ptr::null_mut(), 0), + 0, + "copying zero params should succeed" + ); + + visual_circuit_free(visual); + circuit_free(circuit); +} + +#[test] +fn test_draw_from_visual() { + let circuit = bell_circuit(); + let visual = build_visual_circuit(circuit, std::ptr::null()); + assert!(!visual.is_null()); + + let text_ptr = draw_text_from_visual(visual, std::ptr::null()); + assert!(!text_ptr.is_null()); + let text = cstr_to_string(text_ptr); + cqlib_string_free(text_ptr); + assert!(text.contains('H'), "text drawing should contain H gate"); + + let svg_ptr = draw_figure_from_visual(visual, std::ptr::null()); + assert_svg(svg_ptr); + + // Explicit drawer options also work. + let options = TextDrawerOptionsC { + show_params: 1, + decompose_circuit_gates: 0, + line_width: -1, + initial_state: 1, + reverse_bits: 0, + }; + let text_ptr = draw_text_from_visual(visual, &options); + assert!(!text_ptr.is_null()); + cqlib_string_free(text_ptr); + + let figure_options = FigureDrawerOptionsC { + show_params: 1, + decompose_circuit_gates: 0, + width_per_column: 1.5, + height_per_qubit: 1.0, + dpi: 160, + fold: -1, + initial_state: 0, + reverse_bits: 0, + }; + let svg_ptr = draw_figure_from_visual(visual, &figure_options); + assert_svg(svg_ptr); + + visual_circuit_free(visual); + circuit_free(circuit); +} + +#[test] +fn test_visual_ir_build_options() { + let circuit = bell_circuit(); + let options = CVisualBuildOptions { + decompose_circuit_gates: 0, + reserve_full_span_for_multi_qubit: 1, + parameter_format: CParameterFormatOptions { + mode: PARAM_MODE_PI_FRACTION_PREFERRED, + decimal_precision: 2, + scientific_lower_bound: 1e-3, + scientific_upper_bound: 1e4, + pi_tolerance: 1e-3, + pi_max_denominator: 16, + }, + }; + let visual = build_visual_circuit(circuit, &options); + assert!(!visual.is_null()); + assert_eq!(visual_circuit_num_operations(visual), 2); + visual_circuit_free(visual); + circuit_free(circuit); +} + +#[test] +fn test_visual_ir_null_handling() { + assert!(build_visual_circuit(std::ptr::null(), std::ptr::null()).is_null()); + assert_eq!(visual_circuit_num_qubits(std::ptr::null()), 0); + assert_eq!(visual_circuit_num_operations(std::ptr::null()), 0); + assert_eq!(visual_circuit_num_columns(std::ptr::null()), 0); + assert_eq!(visual_circuit_qubits_len(std::ptr::null()), 0); + assert_eq!( + visual_circuit_qubits(std::ptr::null(), std::ptr::null_mut(), 0), + -1 + ); + assert!(visual_circuit_operation_label(std::ptr::null(), 0).is_null()); + assert_eq!( + visual_circuit_operation_column(std::ptr::null(), 0), + usize::MAX + ); + assert_eq!( + visual_circuit_operation_style(std::ptr::null(), 0), + u32::MAX + ); + assert_eq!( + visual_circuit_operation_control_flow_kind(std::ptr::null(), 0), + u32::MAX + ); + assert!(draw_text_from_visual(std::ptr::null(), std::ptr::null()).is_null()); + assert!(draw_figure_from_visual(std::ptr::null(), std::ptr::null()).is_null()); + visual_circuit_free(std::ptr::null_mut()); + + // Out-of-bounds access on a valid handle must fail gracefully. + let circuit = bell_circuit(); + let visual = build_visual_circuit(circuit, std::ptr::null()); + assert!(!visual.is_null()); + assert!(visual_circuit_operation_label(visual, 2).is_null()); + assert_eq!(visual_circuit_operation_column(visual, 2), usize::MAX); + assert_eq!(visual_circuit_operation_lanes_len(visual, 2), 0); + visual_circuit_free(visual); + circuit_free(circuit); +} diff --git a/crates/binding-c/tests/visualization_test.rs b/crates/binding-c/tests/visualization_test.rs new file mode 100644 index 00000000..4429c538 --- /dev/null +++ b/crates/binding-c/tests/visualization_test.rs @@ -0,0 +1,112 @@ +//! Integration tests for visualization module C ABI. + +#![allow(dead_code)] + +use binding_c::circuit::{circuit_cx, circuit_free, circuit_h, circuit_new}; +use binding_c::cqlib_string_free; +use binding_c::visualization::*; +use std::ffi::{CStr, CString}; +use std::os::raw::c_char; + +fn cstr_to_string(ptr: *mut c_char) -> String { + if ptr.is_null() { + return String::new(); + } + unsafe { CStr::from_ptr(ptr) } + .to_string_lossy() + .into_owned() +} + +fn build_bell_circuit() -> *mut binding_c::circuit::CCircuit { + let circuit = circuit_new(2); + assert!(!circuit.is_null()); + assert_eq!(circuit_h(circuit, 0), 0); + assert_eq!(circuit_cx(circuit, 0, 1), 0); + circuit +} + +#[test] +fn test_circuit_to_text() { + let circuit = build_bell_circuit(); + let text_ptr = circuit_to_text(circuit, std::ptr::null()); + assert!(!text_ptr.is_null()); + let text = cstr_to_string(text_ptr); + cqlib_string_free(text_ptr); + assert!(text.contains("H"), "text drawing should contain H gate"); + // CX renders as a control dot with an "X" target label. + assert!( + text.contains('X'), + "text drawing should contain X target of CX" + ); + circuit_free(circuit); +} + +#[test] +fn test_circuit_to_text_with_options() { + let circuit = build_bell_circuit(); + let options = TextDrawerOptionsC { + show_params: 1, + decompose_circuit_gates: 0, + line_width: -1, + initial_state: 1, + reverse_bits: 0, + }; + let text_ptr = circuit_to_text(circuit, &options); + assert!(!text_ptr.is_null()); + cqlib_string_free(text_ptr); + circuit_free(circuit); +} + +#[test] +fn test_circuit_to_figure_svg() { + let circuit = build_bell_circuit(); + let svg_ptr = circuit_to_figure(circuit, std::ptr::null()); + assert!(!svg_ptr.is_null()); + let svg = cstr_to_string(svg_ptr); + cqlib_string_free(svg_ptr); + assert!(svg.contains(" None: ... + def __init__( + self, circuit_id: CircuitId, index: int, ty: ClassicalType + ) -> None: ... @property def id(self) -> int: """Stable identifier combining circuit id and index.""" @@ -126,7 +128,9 @@ class ClassicalValue: Produced by measurement operations. Once created the value never changes. """ - def __init__(self, circuit_id: CircuitId, index: int, ty: ClassicalType) -> None: ... + def __init__( + self, circuit_id: CircuitId, index: int, ty: ClassicalType + ) -> None: ... @property def index(self) -> int: """Position of this value within its owning circuit.""" diff --git a/crates/binding-python/cqlib/circuit/classical_expr.pyi b/crates/binding-python/cqlib/circuit/classical_expr.pyi index 3c3bc4d4..d1a02910 100644 --- a/crates/binding-python/cqlib/circuit/classical_expr.pyi +++ b/crates/binding-python/cqlib/circuit/classical_expr.pyi @@ -109,7 +109,9 @@ class ClassicalExpr: """Bool-typed greater-or-equal comparison.""" ... @staticmethod - def select(condition: ClassicalExpr, then_expr: ClassicalExpr, else_expr: ClassicalExpr) -> ClassicalExpr: + def select( + condition: ClassicalExpr, then_expr: ClassicalExpr, else_expr: ClassicalExpr + ) -> ClassicalExpr: """Ternary conditional: returns ``then_expr`` if condition is true, else ``else_expr``.""" ... def extract_bit(self, index: int) -> ClassicalExpr: diff --git a/crates/binding-python/cqlib/circuit/control_flow.pyi b/crates/binding-python/cqlib/circuit/control_flow.pyi index 5ef798e0..134987cd 100644 --- a/crates/binding-python/cqlib/circuit/control_flow.pyi +++ b/crates/binding-python/cqlib/circuit/control_flow.pyi @@ -79,7 +79,11 @@ class ClassicalControlOp: :meth:`~circuit.Circuit.append_control`. """ @staticmethod - def if_(condition: ClassicalExpr, then_body: ValueControlBody, else_body: ValueControlBody | None = ...) -> ClassicalControlOp: + def if_( + condition: ClassicalExpr, + then_body: ValueControlBody, + else_body: ValueControlBody | None = ..., + ) -> ClassicalControlOp: """Build an ``if`` / ``if-else`` operation. Args: @@ -98,7 +102,13 @@ class ClassicalControlOp: """ ... @staticmethod - def for_uint(var: ClassicalVar, start: ClassicalExpr, stop: ClassicalExpr, step: ClassicalExpr, body: ValueControlBody) -> ClassicalControlOp: + def for_uint( + var: ClassicalVar, + start: ClassicalExpr, + stop: ClassicalExpr, + step: ClassicalExpr, + body: ValueControlBody, + ) -> ClassicalControlOp: """Build an unsigned runtime range ``for`` loop. Args: @@ -114,7 +124,11 @@ class ClassicalControlOp: """ ... @staticmethod - def switch(target: ClassicalExpr, cases: list[ValueSwitchCase], default: ValueControlBody | None = ...) -> ClassicalControlOp: + def switch( + target: ClassicalExpr, + cases: list[ValueSwitchCase], + default: ValueControlBody | None = ..., + ) -> ClassicalControlOp: """Build a ``switch`` operation over a UInt expression. Args: diff --git a/crates/binding-python/cqlib/circuit/gates/unitary.pyi b/crates/binding-python/cqlib/circuit/gates/unitary.pyi index 0b823259..d2cad85d 100644 --- a/crates/binding-python/cqlib/circuit/gates/unitary.pyi +++ b/crates/binding-python/cqlib/circuit/gates/unitary.pyi @@ -44,7 +44,9 @@ class UnitaryGate: matrix: A 2D square NumPy array or array-like of shape (2ⁿ, 2ⁿ). """ ... - def with_symbolic_matrix(self, matrix: SymbolicMatrix, params: list[str]) -> UnitaryGate: + def with_symbolic_matrix( + self, matrix: SymbolicMatrix, params: list[str] + ) -> UnitaryGate: """Attach a symbolic matrix with positional parameter names. Args: @@ -97,7 +99,9 @@ class UnitaryGate: params: Numeric values for each positional parameter. """ ... - def __array__(self, dtype: Any | None = ..., copy: bool | None = ...) -> NDArray[np.complex128]: + def __array__( + self, dtype: Any | None = ..., copy: bool | None = ... + ) -> NDArray[np.complex128]: """NumPy array protocol — allows ``np.array(gate)``.""" ... def __copy__(self) -> UnitaryGate: ... diff --git a/crates/binding-python/cqlib/circuit/parameter.pyi b/crates/binding-python/cqlib/circuit/parameter.pyi index 8522102f..3e67a27d 100644 --- a/crates/binding-python/cqlib/circuit/parameter.pyi +++ b/crates/binding-python/cqlib/circuit/parameter.pyi @@ -130,7 +130,9 @@ class Parameter: """Conservative equality check within a numeric tolerance.""" ... - def provably_equal_modulo(self, other: Parameter, modulus: Parameter, tolerance: float = 1e-12) -> bool: + def provably_equal_modulo( + self, other: Parameter, modulus: Parameter, tolerance: float = 1e-12 + ) -> bool: """Conservative equality check modulo *modulus* within tolerance.""" ... @@ -187,6 +189,5 @@ class Parameter: def floor(self) -> Parameter: ... def ceil(self) -> Parameter: ... def round(self) -> Parameter: ... - def __str__(self) -> str: ... def __repr__(self) -> str: ... diff --git a/crates/binding-python/cqlib/circuit/symbolic_matrix.pyi b/crates/binding-python/cqlib/circuit/symbolic_matrix.pyi index 16b9e224..e75745f8 100644 --- a/crates/binding-python/cqlib/circuit/symbolic_matrix.pyi +++ b/crates/binding-python/cqlib/circuit/symbolic_matrix.pyi @@ -110,7 +110,9 @@ class SymbolicMatrix: def symbols(self) -> list[str]: """All free symbols in deterministic order.""" ... - def evaluate(self, bindings: Mapping[str, float] | None = ...) -> NDArray[np.complex128]: + def evaluate( + self, bindings: Mapping[str, float] | None = ... + ) -> NDArray[np.complex128]: """Evaluate as a NumPy complex128 array after binding symbols.""" ... def simplify(self) -> SymbolicMatrix: diff --git a/crates/binding-python/cqlib/compile/commutation/checker.pyi b/crates/binding-python/cqlib/compile/commutation/checker.pyi index 921192bb..34f0396a 100644 --- a/crates/binding-python/cqlib/compile/commutation/checker.pyi +++ b/crates/binding-python/cqlib/compile/commutation/checker.pyi @@ -193,9 +193,7 @@ class CommutationChecker: def config(self) -> CommutationConfig: """Return a copy of the active checker configuration.""" ... - def check( - self, lhs: ValueOperation, rhs: ValueOperation - ) -> Commutation | None: + def check(self, lhs: ValueOperation, rhs: ValueOperation) -> Commutation | None: """Prove whether two concrete operation applications commute. ``ValueOperation`` parameters are passed through without numeric @@ -213,9 +211,7 @@ class CommutationChecker: """Return an independent deep copy of this checker.""" ... -def check_commutation( - lhs: ValueOperation, rhs: ValueOperation -) -> Commutation | None: +def check_commutation(lhs: ValueOperation, rhs: ValueOperation) -> Commutation | None: """Check commutation using the shared builtin checker. Args: diff --git a/crates/binding-python/cqlib/compile/knowledge/library.pyi b/crates/binding-python/cqlib/compile/knowledge/library.pyi index a7f0e315..07b1d12f 100644 --- a/crates/binding-python/cqlib/compile/knowledge/library.pyi +++ b/crates/binding-python/cqlib/compile/knowledge/library.pyi @@ -137,7 +137,9 @@ class RuleLibrary: def metadata(self, id: RuleId) -> RuleMetadata | None: ... def id_by_name(self, name: str) -> RuleId | None: ... def get_by_name(self, name: str) -> Rule | None: ... - def candidates_for_first_instruction(self, instruction: Instruction) -> list[RuleId]: + def candidates_for_first_instruction( + self, instruction: Instruction + ) -> list[RuleId]: """Return rules indexed under an instruction's matcher key. Raises: @@ -145,7 +147,11 @@ class RuleLibrary: """ ... def rules_by_kind(self, kind: RuleKind) -> list[RuleId]: ... - def filter_rule_ids_by_instruction_keys(self, op_instructions: Sequence[Instruction], target_instructions: Sequence[Instruction]) -> list[RuleId]: + def filter_rule_ids_by_instruction_keys( + self, + op_instructions: Sequence[Instruction], + target_instructions: Sequence[Instruction], + ) -> list[RuleId]: """Select rules whose complete match and target instruction sets fit the supplied bases.""" ... def __contains__(self, name: str) -> bool: ... @@ -165,6 +171,7 @@ def loads(source: str) -> list[Rule]: ValueError: For syntax, lowering, arity, or duplicate-name errors. """ ... + def load(path: str | PathLike[str]) -> list[Rule]: """Read and parse a DSL file. @@ -173,9 +180,11 @@ def load(path: str | PathLike[str]) -> list[Rule]: ValueError: If DSL parsing or lowering fails. """ ... + def dumps(rule: Rule) -> str: """Serialize one runtime rule to canonical DSL text.""" ... + def dump(rule_or_rules: Rule | Sequence[Rule], path: str | PathLike[str]) -> None: """Write one or more rules; multiple rules are separated by a blank line. diff --git a/crates/binding-python/cqlib/compile/knowledge/matcher.pyi b/crates/binding-python/cqlib/compile/knowledge/matcher.pyi index 6b14e788..d3e5ebc9 100644 --- a/crates/binding-python/cqlib/compile/knowledge/matcher.pyi +++ b/crates/binding-python/cqlib/compile/knowledge/matcher.pyi @@ -31,7 +31,9 @@ class MatchBindings: def __copy__(self) -> MatchBindings: ... def __deepcopy__(self, memo: dict) -> MatchBindings: ... -def match_rule_item(item: RuleItem, operation: ValueOperation, bindings: MatchBindings) -> bool: +def match_rule_item( + item: RuleItem, operation: ValueOperation, bindings: MatchBindings +) -> bool: """Match one item and transactionally update ``bindings``. Instruction identity, arity, one-to-one qubit mapping, and symbolic @@ -48,10 +50,14 @@ def match_rule_item(item: RuleItem, operation: ValueOperation, bindings: MatchBi print(bindings.qubits, bindings.params) """ ... + def conditions_hold(conditions: Sequence[Condition], bindings: MatchBindings) -> bool: """Return whether every condition is fully bound and provably satisfied.""" ... -def instantiate_target(target: Sequence[RuleItem], bindings: MatchBindings) -> list[ValueOperation]: + +def instantiate_target( + target: Sequence[RuleItem], bindings: MatchBindings +) -> list[ValueOperation]: """Create self-contained replacement operations from target items. Output labels are ``None``. Fixed parameters remain numeric and symbolic @@ -61,7 +67,10 @@ def instantiate_target(target: Sequence[RuleItem], bindings: MatchBindings) -> l ValueError: For unsupported instructions or unbound labels/symbols. """ ... -def rule_matches_operations(rule: Rule, operations: Sequence[ValueOperation]) -> MatchBindings | None: + +def rule_matches_operations( + rule: Rule, operations: Sequence[ValueOperation] +) -> MatchBindings | None: """Match a complete rule against one adjacent operation sequence. This helper performs no circuit search, commutation, cost comparison, or diff --git a/crates/binding-python/cqlib/compile/resource/__init__.pyi b/crates/binding-python/cqlib/compile/resource/__init__.pyi index 20dcc038..f94696a9 100644 --- a/crates/binding-python/cqlib/compile/resource/__init__.pyi +++ b/crates/binding-python/cqlib/compile/resource/__init__.pyi @@ -19,6 +19,7 @@ from .policy import ResourcePolicy as ResourcePolicy class ResourceError(CqlibError): """Base exception for ancillary-resource management failures.""" + ... class ResourceUnavailableError(ResourceError): @@ -28,6 +29,7 @@ class ResourceUnavailableError(ResourceError): candidate and try another. Other ``ResourceError`` instances indicate an invalid request or inconsistent planner state and should not be ignored. """ + ... __all__ = [ diff --git a/crates/binding-python/cqlib/device/device_impl.pyi b/crates/binding-python/cqlib/device/device_impl.pyi index 84308b0c..55b90fde 100644 --- a/crates/binding-python/cqlib/device/device_impl.pyi +++ b/crates/binding-python/cqlib/device/device_impl.pyi @@ -11,7 +11,7 @@ # that they have been altered from the originals. from datetime import datetime -from cqlib.circuit import Circuit, Instruction, Qubit, StandardGate, ValueOperation +from cqlib.circuit import Circuit, Instruction, Qubit, ValueOperation from .qubit import PhysicalQubit from .topology import Topology @@ -98,7 +98,6 @@ class InstructionProp: def __copy__(self) -> "InstructionProp": ... def __deepcopy__(self, memo: dict) -> "InstructionProp": ... - class QubitProp: """Physical properties of an individual qubit. @@ -234,7 +233,6 @@ class QubitProp: def __copy__(self) -> "QubitProp": ... def __deepcopy__(self, memo: dict) -> "QubitProp": ... - class EdgeProp: """Properties of a coupling edge between two qubits. @@ -275,7 +273,6 @@ class EdgeProp: def __copy__(self) -> "EdgeProp": ... def __deepcopy__(self, memo: dict) -> "EdgeProp": ... - class Device: """Complete hardware description of a quantum device. @@ -567,9 +564,7 @@ class Device: # ---- Per-qubit / per-edge property management ---- - def add_qubit_properties( - self, qubit: _PhysicalQubitLike, props: QubitProp - ) -> None: + def add_qubit_properties(self, qubit: _PhysicalQubitLike, props: QubitProp) -> None: """Add or update properties for a specific qubit. Args: diff --git a/crates/binding-python/cqlib/device/layout.pyi b/crates/binding-python/cqlib/device/layout.pyi index 5070034b..eb55f80a 100644 --- a/crates/binding-python/cqlib/device/layout.pyi +++ b/crates/binding-python/cqlib/device/layout.pyi @@ -16,7 +16,6 @@ from .qubit import LogicalQubit, PhysicalQubit _LogicalQubitLike = int | Qubit | LogicalQubit _PhysicalQubitLike = int | Qubit | PhysicalQubit - class Layout: """Maps circuit logical qubits to physical qubits on a quantum device. @@ -89,9 +88,7 @@ class Layout: """ @staticmethod - def from_pairs( - pairs: list[tuple[int, int]], physical_count: int - ) -> "Layout": + def from_pairs(pairs: list[tuple[int, int]], physical_count: int) -> "Layout": """Create a layout from ``(logical_id, physical_id)`` pairs. Logical qubits are the logical IDs in ``pairs``. Physical @@ -185,7 +182,9 @@ class Layout: # ---- Mutation (routing operations) ---- - def bind(self, logical_id: _LogicalQubitLike, physical_id: _PhysicalQubitLike) -> None: + def bind( + self, logical_id: _LogicalQubitLike, physical_id: _PhysicalQubitLike + ) -> None: """Bind an unmapped logical qubit to a vacant physical qubit. May introduce a new logical qubit to the layout. The caller must diff --git a/crates/binding-python/cqlib/device/topology.pyi b/crates/binding-python/cqlib/device/topology.pyi index 089c02fb..89c98f89 100644 --- a/crates/binding-python/cqlib/device/topology.pyi +++ b/crates/binding-python/cqlib/device/topology.pyi @@ -15,7 +15,6 @@ from .qubit import PhysicalQubit _PhysicalQubitLike = int | Qubit | PhysicalQubit - class Topology: """A directed coupling graph representing quantum hardware connectivity. diff --git a/crates/binding-python/cqlib/error_mitigation/unified.pyi b/crates/binding-python/cqlib/error_mitigation/unified.pyi index cfd718db..e4a181f1 100644 --- a/crates/binding-python/cqlib/error_mitigation/unified.pyi +++ b/crates/binding-python/cqlib/error_mitigation/unified.pyi @@ -1,4 +1,5 @@ # This code is part of Cqlib. +# Modified to document repeated post-processing of collected estimates. # # (C) Copyright China Telecom Quantum Group 2026 # @@ -24,6 +25,7 @@ Estimator = Callable[[Circuit, Hamiltonian | None, int | None], tuple[float, flo class ErrorMitigationError(CqlibError): """Base exception for error-mitigation API failures.""" + ... class MitigationMethod: @@ -109,8 +111,8 @@ class ErrorMitigation: """Unified sequential mitigation pipeline. The workflow is ``run(...)`` followed by ``get_mitigated(...)``. Each - instance can be run and post-processed once, matching the core state - machine. + instance can be run once and post-processed repeatedly with different + processing arguments, without rerunning the estimator. """ def __init__(self, circuit: Circuit, method: MitigationMethod) -> None: @@ -125,7 +127,7 @@ class ErrorMitigation: """Execute the method-specific circuits with ``estimator``.""" ... def get_mitigated(self, process_args: ProcessArgs) -> MitigatedResult: - """Post-process stored run outputs and return the final estimate.""" + """Read stored run outputs without consuming them and return an estimate.""" ... def __repr__(self) -> str: ... def __copy__(self) -> ErrorMitigation: ... diff --git a/crates/binding-python/cqlib/qis/__init__.py b/crates/binding-python/cqlib/qis/__init__.py index bf9e3137..37eff524 100644 --- a/crates/binding-python/cqlib/qis/__init__.py +++ b/crates/binding-python/cqlib/qis/__init__.py @@ -55,6 +55,7 @@ from typing import Protocol, List, Tuple, Dict from . import state +from .._native import qis as _qis_module # Pauli module from .pauli import Phase as Phase @@ -67,8 +68,13 @@ # Evolution module from .evolution import TrotterMode as TrotterMode +# Entropy and metrics modules +from . import entropy as entropy +from . import metrics as metrics + # Expose key state classes at qis level for convenience +sample = _qis_module.sample DensityMatrix = state.DensityMatrix Statevector = state.Statevector DensityMatrixNoise = state.DensityMatrixNoise @@ -91,11 +97,8 @@ def variance_statevector(self, sv: Statevector) -> float: ... def num_qubits(self) -> int: ... -# Entropy and metrics modules -from . import entropy as entropy -from . import metrics as metrics - __all__ = [ + "sample", "Phase", "Pauli", "PauliString", diff --git a/crates/binding-python/cqlib/qis/__init__.pyi b/crates/binding-python/cqlib/qis/__init__.pyi index ff622f01..fb4dbe49 100644 --- a/crates/binding-python/cqlib/qis/__init__.pyi +++ b/crates/binding-python/cqlib/qis/__init__.pyi @@ -20,7 +20,10 @@ This module provides quantum information tools including: - Quantum metrics and entanglement measures """ -from typing import Protocol, List, Tuple, Dict +from typing import Protocol, List, Tuple, Dict, Literal, Sequence + +from ..circuit import Circuit, Qubit +from ..device import ExecutionResult # Pauli module from .pauli import Phase as Phase @@ -47,6 +50,22 @@ from . import entropy as entropy from . import metrics as metrics from . import state as state +def sample( + circuit: Circuit, + *, + shots: int = 1000, + seed: int | None = None, + simulator: Literal["statevector", "density_matrix", "stabilizer"] = "statevector", + qubits: Sequence[int] | Sequence[Qubit] | None = None, +) -> ExecutionResult: + """Sample final Z-basis outputs; default to terminal declarations or all qubits. + + The first selected qubit is the rightmost counts bit. Parameters must be + bound, classical control flow and mid-circuit measurements are rejected, + and reset requires density_matrix. The input circuit is not modified. + """ + ... + class Observable(Protocol): """Protocol for quantum observables.""" def expectation_statevector(self, sv: Statevector) -> float: ... @@ -59,6 +78,7 @@ class Observable(Protocol): def num_qubits(self) -> int: ... __all__ = [ + "sample", "Phase", "Pauli", "PauliString", diff --git a/crates/binding-python/cqlib/qis/hamiltonian.pyi b/crates/binding-python/cqlib/qis/hamiltonian.pyi index 60bd6055..3210e3a8 100644 --- a/crates/binding-python/cqlib/qis/hamiltonian.pyi +++ b/crates/binding-python/cqlib/qis/hamiltonian.pyi @@ -310,7 +310,9 @@ class Hamiltonian: """ ... - def to_evolution_circuit(self, time: float, steps: int, mode: TrotterMode) -> Circuit: + def to_evolution_circuit( + self, time: float, steps: int, mode: TrotterMode + ) -> Circuit: """Converts the Hamiltonian evolution e^(-iHt) into a quantum circuit. Commuting terms use an exact single-pass decomposition. Non-commuting diff --git a/crates/binding-python/cqlib/qis/state/density_matrix.pyi b/crates/binding-python/cqlib/qis/state/density_matrix.pyi index 1a2ee6fa..61515eff 100644 --- a/crates/binding-python/cqlib/qis/state/density_matrix.pyi +++ b/crates/binding-python/cqlib/qis/state/density_matrix.pyi @@ -1,4 +1,5 @@ # This code is part of Cqlib. +# Modified to support reproducible state sampling. # # (C) Copyright China Telecom Quantum Group 2026 # @@ -653,7 +654,7 @@ class DensityMatrix: ... def reset(self, qubit: int) -> None: - """Resets the specified qubit to the |0⟩ state by measuring and flipping if 1. + """Resets the specified qubit to |0⟩ using a deterministic quantum channel. Raises: IndexError: If qubit index is out of bounds. @@ -668,11 +669,13 @@ class DensityMatrix: """Measures all qubits and collapses the state.""" ... - def sample_shots(self, shots: int) -> List[Outcome]: - """Samples measurement outcomes without mutating this state.""" + def sample_shots(self, shots: int, *, seed: int | None = None) -> List[Outcome]: + """Sample without mutation; an explicit seed is independent of sampling thread count.""" ... - def sample(self, measurement: Measurement, shots: int) -> ExecutionResult: + def sample( + self, measurement: Measurement, shots: int, *, seed: int | None = None + ) -> ExecutionResult: """Samples measurement outcomes according to a circuit measurement receipt.""" ... diff --git a/crates/binding-python/cqlib/qis/state/density_matrix_noise.pyi b/crates/binding-python/cqlib/qis/state/density_matrix_noise.pyi index 034e852b..fc37fcd2 100644 --- a/crates/binding-python/cqlib/qis/state/density_matrix_noise.pyi +++ b/crates/binding-python/cqlib/qis/state/density_matrix_noise.pyi @@ -1,4 +1,5 @@ # This code is part of Cqlib. +# Modified to support reproducible state sampling. # # (C) Copyright China Telecom Quantum Group 2026 # @@ -323,11 +324,13 @@ class DensityMatrixNoise: """Measures all qubits and collapses the state.""" ... - def sample_shots(self, shots: int) -> List[Outcome]: - """Samples measurement outcomes with readout noise, without mutating this state.""" + def sample_shots(self, shots: int, *, seed: int | None = None) -> List[Outcome]: + """Samples the prepared state without mutation or additional readout noise.""" ... - def sample(self, measurement: Measurement, shots: int) -> ExecutionResult: + def sample( + self, measurement: Measurement, shots: int, *, seed: int | None = None + ) -> ExecutionResult: """Samples measurement outcomes according to a circuit measurement receipt.""" ... diff --git a/crates/binding-python/cqlib/qis/state/stabilizer.pyi b/crates/binding-python/cqlib/qis/state/stabilizer.pyi index 1a04bc1c..82e37fd6 100644 --- a/crates/binding-python/cqlib/qis/state/stabilizer.pyi +++ b/crates/binding-python/cqlib/qis/state/stabilizer.pyi @@ -1,4 +1,5 @@ # This code is part of Cqlib. +# Modified to support reproducible state sampling. # # (C) Copyright China Telecom Quantum Group 2026 # @@ -138,11 +139,13 @@ class StabilizerState: """Returns the full computational-basis probability distribution.""" ... - def sample_shots(self, shots: int) -> List[Outcome]: - """Samples measurement outcomes without mutating this state.""" + def sample_shots(self, shots: int, *, seed: int | None = None) -> List[Outcome]: + """Sample without mutation; an explicit seed is independent of sampling thread count.""" ... - def sample(self, measurement: Measurement, shots: int) -> ExecutionResult: + def sample( + self, measurement: Measurement, shots: int, *, seed: int | None = None + ) -> ExecutionResult: """Samples measurement outcomes according to a circuit measurement receipt.""" ... diff --git a/crates/binding-python/cqlib/qis/state/statevector.pyi b/crates/binding-python/cqlib/qis/state/statevector.pyi index bbee6ab8..bd997414 100644 --- a/crates/binding-python/cqlib/qis/state/statevector.pyi +++ b/crates/binding-python/cqlib/qis/state/statevector.pyi @@ -1,4 +1,5 @@ # This code is part of Cqlib. +# Modified to support reproducible state sampling. # # (C) Copyright China Telecom Quantum Group 2026 # @@ -578,11 +579,13 @@ class Statevector: """Measures all qubits and collapses the state.""" ... - def sample_shots(self, shots: int) -> List[Outcome]: - """Samples measurement outcomes without mutating this state.""" + def sample_shots(self, shots: int, *, seed: int | None = None) -> List[Outcome]: + """Sample without mutation; an explicit seed is independent of sampling thread count.""" ... - def sample(self, measurement: Measurement, shots: int) -> ExecutionResult: + def sample( + self, measurement: Measurement, shots: int, *, seed: int | None = None + ) -> ExecutionResult: """Samples measurement outcomes according to a circuit measurement receipt.""" ... diff --git a/crates/binding-python/pyproject.toml b/crates/binding-python/pyproject.toml index afd8a543..1d92fce9 100644 --- a/crates/binding-python/pyproject.toml +++ b/crates/binding-python/pyproject.toml @@ -36,7 +36,7 @@ dependencies = ["numpy>=2.1.0"] dynamic = ["version"] [dependency-groups] -test = ["pytest>=8.0"] +test = ["pytest>=8.0", "resvg-py==0.5.0"] [tool.pytest.ini_options] testpaths = ["tests"] diff --git a/crates/binding-python/src/error_mitigation/mod.rs b/crates/binding-python/src/error_mitigation/mod.rs index f40a7d74..1c038f77 100644 --- a/crates/binding-python/src/error_mitigation/mod.rs +++ b/crates/binding-python/src/error_mitigation/mod.rs @@ -1,4 +1,5 @@ // This code is part of Cqlib. +// Modified to allow repeated post-processing of collected estimates. // // (C) Copyright China Telecom Quantum Group 2026 // @@ -747,7 +748,7 @@ impl PyErrorMitigation { } fn get_mitigated( - &mut self, + &self, py: Python<'_>, process_args: PyProcessArgs, ) -> PyResult { diff --git a/crates/binding-python/src/qis/mod.rs b/crates/binding-python/src/qis/mod.rs index 75324faf..305ef130 100644 --- a/crates/binding-python/src/qis/mod.rs +++ b/crates/binding-python/src/qis/mod.rs @@ -21,6 +21,7 @@ pub mod evolution; pub mod hamiltonian; pub mod metrics; pub mod pauli; +mod sampling; pub mod state; /// Converts a QisError to the appropriate Python exception. @@ -34,6 +35,7 @@ pub(crate) fn qis_error_to_py_err(err: QisError) -> PyErr { /// Register the qis submodule. pub fn register_qis_module(parent: &Bound<'_, PyModule>) -> PyResult<()> { let qis_module = PyModule::new(parent.py(), "qis")?; + qis_module.add_function(wrap_pyfunction!(sampling::sample, &qis_module)?)?; // Register evolution types qis_module.add_class::()?; diff --git a/crates/binding-python/src/qis/sampling.rs b/crates/binding-python/src/qis/sampling.rs new file mode 100644 index 00000000..09ac535c --- /dev/null +++ b/crates/binding-python/src/qis/sampling.rs @@ -0,0 +1,82 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +use crate::circuit::{bit::PyIntListOrQubitList, circuit_impl::PyCircuit}; +use crate::device::result::PyExecutionResult; +use crate::qis::qis_error_to_py_err; +use cqlib_core::circuit::Qubit; +use cqlib_core::qis::Simulator; +use pyo3::exceptions::PyValueError; +use pyo3::prelude::*; + +/// Sample a circuit from the all-zero state and return an ExecutionResult. +/// +/// Args: +/// circuit: Circuit to simulate. The input is not modified. +/// shots: Positive number of samples, default 1000. +/// seed: Optional unsigned 64-bit seed. Counts are reproducible for the +/// same circuit, options, simulator and library version. +/// simulator: "statevector" (default), "density_matrix", or "stabilizer". +/// qubits: Optional ordered list of qubit IDs or Qubit objects to sample. +/// By default, terminal measurement declarations select the outputs +/// in first-occurrence order, with duplicates removed. If there are +/// no measurements, all circuit qubits are sampled in circuit order. +/// +/// Measurement is in the Z basis. The first output qubit corresponds to the +/// rightmost bit of each counts key; result.qubits records the output order. +/// The result is completed, and result.probabilities contains shot frequencies. +/// +/// Parameters must be bound. Only terminal measurements are supported; +/// classical control flow is rejected. Barrier and Delay are no-ops. Store +/// does not affect the quantum measurement counts. Reset requires +/// simulator="density_matrix". The stabilizer simulator only accepts its +/// supported Clifford gates. These simulators do not apply a noise model. +/// +/// Raises: +/// ValueError: For invalid options, empty output selection, or unsupported +/// circuit operations. +/// TypeError: For invalid argument types. +/// OverflowError: For negative or out-of-range shots or seed. +/// +/// Example: +/// >>> from cqlib import Circuit, sample +/// >>> circuit = Circuit(2) +/// >>> circuit.h(0) +/// >>> circuit.cx(0, 1) +/// >>> result = sample(circuit, shots=1000, seed=42) +/// >>> sum(result.counts.values()) +/// 1000 +#[pyfunction] +#[pyo3(signature = (circuit, *, shots=1000, seed=None, simulator="statevector", qubits=None))] +pub fn sample( + py: Python<'_>, + circuit: &PyCircuit, + shots: usize, + seed: Option, + simulator: &str, + qubits: Option, +) -> PyResult { + let simulator = match simulator { + "statevector" => Simulator::Statevector, + "density_matrix" => Simulator::DensityMatrix, + "stabilizer" => Simulator::Stabilizer, + _ => { + return Err(PyValueError::new_err( + "simulator must be 'statevector', 'density_matrix', or 'stabilizer'", + )); + } + }; + let qubits: Option> = qubits.map(Into::into); + py.detach(|| cqlib_core::qis::sample(&circuit.inner, shots, seed, simulator, qubits.as_deref())) + .map(PyExecutionResult::from) + .map_err(qis_error_to_py_err) +} diff --git a/crates/binding-python/src/qis/state/density_matrix.rs b/crates/binding-python/src/qis/state/density_matrix.rs index 8fc589b5..44e1b174 100644 --- a/crates/binding-python/src/qis/state/density_matrix.rs +++ b/crates/binding-python/src/qis/state/density_matrix.rs @@ -1,4 +1,5 @@ // This code is part of Cqlib. +// Modified to support reproducible state sampling. // // (C) Copyright China Telecom Quantum Group 2026 // @@ -946,7 +947,7 @@ impl PyDensityMatrix { .map_err(qis_error_to_py_err) } - /// Resets the specified qubit to the |0⟩ state by measuring and flipping if 1. + /// Resets the specified qubit to |0⟩ using a deterministic quantum channel. /// /// Args: /// qubit: Target qubit index @@ -970,21 +971,24 @@ impl PyDensityMatrix { } /// Samples measurement outcomes without mutating this state. - fn sample_shots(&self, py: Python<'_>, shots: usize) -> Vec { - py.detach(|| self.inner.sample_shots(shots)) + #[pyo3(signature = (shots, *, seed=None))] + fn sample_shots(&self, py: Python<'_>, shots: usize, seed: Option) -> Vec { + py.detach(|| self.inner.sample_shots_with_seed(shots, seed)) .into_iter() .map(PyOutcome::from) .collect() } /// Samples measurement outcomes according to a circuit measurement receipt. + #[pyo3(signature = (measurement, shots, *, seed=None))] fn sample( &self, py: Python<'_>, measurement: &PyMeasurement, shots: usize, + seed: Option, ) -> PyResult { - py.detach(|| self.inner.sample(&measurement.inner, shots)) + py.detach(|| self.inner.sample_with_seed(&measurement.inner, shots, seed)) .map(PyExecutionResult::from) .map_err(qis_error_to_py_err) } diff --git a/crates/binding-python/src/qis/state/density_matrix_noise.rs b/crates/binding-python/src/qis/state/density_matrix_noise.rs index 3d2cc5a4..84bfe638 100644 --- a/crates/binding-python/src/qis/state/density_matrix_noise.rs +++ b/crates/binding-python/src/qis/state/density_matrix_noise.rs @@ -1,4 +1,5 @@ // This code is part of Cqlib. +// Modified to support reproducible state sampling. // // (C) Copyright China Telecom Quantum Group 2026 // @@ -474,22 +475,25 @@ impl PyDensityMatrixNoise { PyOutcome::from(py.detach(|| self.inner.measure_all())) } - /// Samples measurement outcomes with readout noise, without mutating this state. - fn sample_shots(&self, py: Python<'_>, shots: usize) -> Vec { - py.detach(|| self.inner.sample_shots(shots)) + /// Samples the prepared state without mutation or additional readout noise. + #[pyo3(signature = (shots, *, seed=None))] + fn sample_shots(&self, py: Python<'_>, shots: usize, seed: Option) -> Vec { + py.detach(|| self.inner.sample_shots_with_seed(shots, seed)) .into_iter() .map(PyOutcome::from) .collect() } /// Samples measurement outcomes according to a circuit measurement receipt. + #[pyo3(signature = (measurement, shots, *, seed=None))] fn sample( &self, py: Python<'_>, measurement: &PyMeasurement, shots: usize, + seed: Option, ) -> PyResult { - py.detach(|| self.inner.sample(&measurement.inner, shots)) + py.detach(|| self.inner.sample_with_seed(&measurement.inner, shots, seed)) .map(PyExecutionResult::from) .map_err(qis_error_to_py_err) } diff --git a/crates/binding-python/src/qis/state/stabilizer.rs b/crates/binding-python/src/qis/state/stabilizer.rs index 67dc7007..97433332 100644 --- a/crates/binding-python/src/qis/state/stabilizer.rs +++ b/crates/binding-python/src/qis/state/stabilizer.rs @@ -1,4 +1,5 @@ // This code is part of Cqlib. +// Modified to support reproducible state sampling. // // (C) Copyright China Telecom Quantum Group 2026 // @@ -220,21 +221,24 @@ impl PyStabilizerState { } /// Samples measurement outcomes without mutating this state. - fn sample_shots(&self, py: Python<'_>, shots: usize) -> Vec { - py.detach(|| self.inner.sample_shots(shots)) + #[pyo3(signature = (shots, *, seed=None))] + fn sample_shots(&self, py: Python<'_>, shots: usize, seed: Option) -> Vec { + py.detach(|| self.inner.sample_shots_with_seed(shots, seed)) .into_iter() .map(PyOutcome::from) .collect() } /// Samples measurement outcomes according to a circuit measurement receipt. + #[pyo3(signature = (measurement, shots, *, seed=None))] fn sample( &self, py: Python<'_>, measurement: &PyMeasurement, shots: usize, + seed: Option, ) -> PyResult { - py.detach(|| self.inner.sample(&measurement.inner, shots)) + py.detach(|| self.inner.sample_with_seed(&measurement.inner, shots, seed)) .map(PyExecutionResult::from) .map_err(qis_error_to_py_err) } diff --git a/crates/binding-python/src/qis/state/statevector.rs b/crates/binding-python/src/qis/state/statevector.rs index 7cbe9843..506bb3e7 100644 --- a/crates/binding-python/src/qis/state/statevector.rs +++ b/crates/binding-python/src/qis/state/statevector.rs @@ -1,4 +1,5 @@ // This code is part of Cqlib. +// Modified to support reproducible state sampling. // // (C) Copyright China Telecom Quantum Group 2026 // @@ -660,21 +661,24 @@ impl PyStatevector { } /// Samples measurement outcomes without mutating this state. - fn sample_shots(&self, py: Python<'_>, shots: usize) -> Vec { - py.detach(|| self.inner.sample_shots(shots)) + #[pyo3(signature = (shots, *, seed=None))] + fn sample_shots(&self, py: Python<'_>, shots: usize, seed: Option) -> Vec { + py.detach(|| self.inner.sample_shots_with_seed(shots, seed)) .into_iter() .map(PyOutcome::from) .collect() } /// Samples measurement outcomes according to a circuit measurement receipt. + #[pyo3(signature = (measurement, shots, *, seed=None))] fn sample( &self, py: Python<'_>, measurement: &PyMeasurement, shots: usize, + seed: Option, ) -> PyResult { - py.detach(|| self.inner.sample(&measurement.inner, shots)) + py.detach(|| self.inner.sample_with_seed(&measurement.inner, shots, seed)) .map(PyExecutionResult::from) .map_err(qis_error_to_py_err) } diff --git a/crates/binding-python/tests/binding/test_gil_release.py b/crates/binding-python/tests/binding/test_gil_release.py index 1cf5fa64..806448dc 100644 --- a/crates/binding-python/tests/binding/test_gil_release.py +++ b/crates/binding-python/tests/binding/test_gil_release.py @@ -10,7 +10,9 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -import threading +import sys + +import pytest import numpy as np @@ -23,30 +25,14 @@ ) -def _assert_python_thread_progresses(action): - ready = threading.Event() - stop = threading.Event() - progress = [0] - - def worker(): - ready.set() - while not stop.is_set(): - progress[0] += 1 - - thread = threading.Thread(target=worker) - thread.start() - ready.wait() - before = progress[0] - try: - action() - finally: - stop.set() - thread.join() - - assert progress[0] - before > 100 +def test_gil_probe_rejects_an_action_that_holds_the_gil(assert_releases_gil) -> None: + switch_interval = sys.getswitchinterval() + with pytest.raises(pytest.fail.Exception, match="No Python thread progress"): + assert_releases_gil(lambda: None, timeout=0.05) + assert sys.getswitchinterval() == switch_interval -def test_circuit_to_matrix_releases_gil(): +def test_circuit_to_matrix_releases_gil(assert_releases_gil): circuit = Circuit(10) for _ in range(4): for qubit in range(10): @@ -54,10 +40,10 @@ def test_circuit_to_matrix_releases_gil(): for qubit in range(9): circuit.cx(qubit, qubit + 1) - _assert_python_thread_progresses(circuit.to_matrix) + assert_releases_gil(circuit.to_matrix) -def test_statevector_apply_circuit_releases_gil(): +def test_statevector_apply_circuit_releases_gil(assert_releases_gil): circuit = Circuit(20) for _ in range(6): for qubit in range(20): @@ -66,30 +52,30 @@ def test_statevector_apply_circuit_releases_gil(): circuit.cx(qubit, qubit + 1) state = Statevector(20) - _assert_python_thread_progresses(lambda: state.apply_circuit(circuit)) + assert_releases_gil(lambda: state.apply_circuit(circuit)) -def test_statevector_probabilities_releases_gil(): +def test_statevector_probabilities_releases_gil(assert_releases_gil): state = Statevector(20) - _assert_python_thread_progresses(state.probabilities) + assert_releases_gil(state.probabilities) -def test_density_matrix_unitary_releases_gil(): +def test_density_matrix_unitary_releases_gil(assert_releases_gil): state = DensityMatrix(9) matrix = np.array([[0.0, 1.0], [1.0, 0.0]], dtype=complex) - _assert_python_thread_progresses(lambda: state.apply_unitary_gate([0], matrix)) + assert_releases_gil(lambda: state.apply_unitary_gate([0], matrix)) -def test_noise_simulator_unitary_releases_gil(): +def test_noise_simulator_unitary_releases_gil(assert_releases_gil): state = DensityMatrixNoise(9) matrix = np.array([[0.0, 1.0], [1.0, 0.0]], dtype=complex) - _assert_python_thread_progresses(lambda: state.apply_unitary_gate([0], matrix)) + assert_releases_gil(lambda: state.apply_unitary_gate([0], matrix)) -def test_stabilizer_probabilities_releases_gil(): +def test_stabilizer_probabilities_releases_gil(assert_releases_gil): state = StabilizerState(18) - _assert_python_thread_progresses(state.probabilities) + assert_releases_gil(state.probabilities) diff --git a/crates/binding-python/tests/compile/test_workflow.py b/crates/binding-python/tests/compile/test_workflow.py index 9cb64515..ec011f1b 100644 --- a/crates/binding-python/tests/compile/test_workflow.py +++ b/crates/binding-python/tests/compile/test_workflow.py @@ -11,9 +11,8 @@ # that they have been altered from the originals. import copy -import threading -import time +import numpy as np import pytest import cqlib.compile as compile_module @@ -247,29 +246,12 @@ def test_compile_rejects_conflicting_or_orphaned_target_arguments( "run", [compile, lambda circuit: CompilerWorkflow().run(circuit)], ) -def test_compiler_entry_points_release_gil(run) -> None: +def test_compiler_entry_points_release_gil(run, assert_releases_gil) -> None: circuit = Circuit(1) for _ in range(20_000): circuit.h(0) - started = threading.Event() - finished = threading.Event() - progressed = threading.Event() - - def worker() -> None: - started.wait() - time.sleep(0.01) - if not finished.is_set(): - progressed.set() - - thread = threading.Thread(target=worker) - thread.start() - started.set() - run(circuit) - finished.set() - thread.join() - - assert progressed.is_set() + assert_releases_gil(lambda: run(circuit)) def test_device_compile_returns_layout_metadata() -> None: @@ -334,3 +316,24 @@ def test_workflow_rejects_non_standard_target_instruction_when_run() -> None: CompilerConfigError, match="unsupported workflow target instruction" ): CompilerWorkflow(config).run(Circuit(1)) + + +@pytest.mark.parametrize("mode", ["normal", "enhanced"]) +@pytest.mark.parametrize("phase", [0.0, 0.371]) +@pytest.mark.parametrize("qubits", [(0, 1), (1, 0)]) +def test_two_qubit_local_cleanup_cost_reversal(mode, phase, qubits) -> None: + source = Circuit(2) + control, target = qubits + source.cx(control, target) + source.h(target) + source.h(control) + source.set_global_phase(phase) + + output = compile(source, target_basis=["RZ", "X2P", "CZ"], mode=mode).circuit + + assert sum(len(op.qubits) == 2 for op in output.operations) == 1 + assert len(output.operations) <= 7 + assert output.depth() <= 6 + np.testing.assert_allclose( + output.to_matrix(), source.to_matrix(), atol=1e-8, rtol=0 + ) diff --git a/crates/binding-python/tests/compile/transform/test_transform.py b/crates/binding-python/tests/compile/transform/test_transform.py index 6119b77d..7faf14e1 100644 --- a/crates/binding-python/tests/compile/transform/test_transform.py +++ b/crates/binding-python/tests/compile/transform/test_transform.py @@ -12,8 +12,6 @@ import copy import sys -import threading -import time import numpy as np import pytest @@ -174,29 +172,14 @@ def test_transform_results_have_value_equality() -> None: "run", [canonicalize_circuit, lambda circuit: Canonicalizer().run(circuit)], ) -def test_canonicalization_releases_gil(run) -> None: +def test_canonicalization_releases_gil(run, assert_releases_gil) -> None: circuit = Circuit(1) for _ in range(20_000): circuit.h(0) - started = threading.Event() - finished = threading.Event() - progressed = threading.Event() - - def worker() -> None: - started.wait() - time.sleep(0.01) - if not finished.is_set(): - progressed.set() - - thread = threading.Thread(target=worker) - thread.start() - started.set() - run(circuit) - finished.set() - thread.join() - - assert progressed.is_set() + # Fast implementations may finish a call before the worker is scheduled. + # Retry within a bounded window instead of requiring any minimum call time. + assert_releases_gil(lambda: run(circuit)) def test_zero_round_limit_is_rejected_when_run() -> None: diff --git a/crates/binding-python/tests/conftest.py b/crates/binding-python/tests/conftest.py index d739f4c6..0fdebaba 100644 --- a/crates/binding-python/tests/conftest.py +++ b/crates/binding-python/tests/conftest.py @@ -14,6 +14,10 @@ Pytest fixtures and utilities for cqlib tests. """ +import sys +import threading +import time + import pytest import numpy as np from cqlib.circuit import Circuit, Parameter @@ -228,3 +232,55 @@ def qcis_ghz_state(): CNOT Q0 Q1 CNOT Q1 Q2 """ + + +def _assert_action_releases_gil(action, *, timeout=5.0) -> None: + if not getattr(sys, "_is_gil_enabled", lambda: True)(): + pytest.skip("GIL release is only observable when the GIL is enabled") + + ready = threading.Event() + stop = threading.Event() + progress = [0] + + def worker() -> None: + ready.set() + while not stop.is_set(): + progress[0] += 1 + # Yield promptly once we acquire the GIL; the main thread disables + # automatic bytecode switching during the observation window. + time.sleep(0) + + thread = threading.Thread(target=worker, daemon=True) + switch_interval = sys.getswitchinterval() + try: + # Python execution between calls must not count as evidence of release. + # Set this before starting the worker so it cannot request a handoff + # using the old interval just before the observation window starts. + sys.setswitchinterval(max(switch_interval, 60.0, timeout * 10)) + thread.start() + assert ready.wait(timeout=5.0), "GIL probe thread did not become ready" + before = progress[0] + deadline = time.monotonic() + timeout + attempts = 0 + while time.monotonic() < deadline: + action() + attempts += 1 + if progress[0] > before: + break + else: + pytest.fail( + f"No Python thread progress during {attempts} calls in " + f"{timeout}s; the action may be holding the GIL" + ) + finally: + stop.set() + sys.setswitchinterval(switch_interval) + if thread.ident is not None: + thread.join(timeout=5.0) + assert not thread.is_alive(), "GIL probe thread did not stop" + + +@pytest.fixture +def assert_releases_gil(): + """Observe explicit GIL release without requiring a minimum call duration.""" + return _assert_action_releases_gil diff --git a/crates/binding-python/tests/error_mitigation/test_pipeline.py b/crates/binding-python/tests/error_mitigation/test_pipeline.py index fed08236..d8fb499b 100644 --- a/crates/binding-python/tests/error_mitigation/test_pipeline.py +++ b/crates/binding-python/tests/error_mitigation/test_pipeline.py @@ -1,4 +1,5 @@ # This code is part of Cqlib. +# Modified to verify repeated post-processing of collected estimates. # # (C) Copyright China Telecom Quantum Group 2026 # @@ -186,10 +187,12 @@ def estimator( assert result.variance is None assert result == copy.copy(result) - with pytest.raises(em.ErrorMitigationError, match="already been completed"): + assert ( mitigation.get_mitigated( em.ProcessArgs.zne(em.ExtrapolateMethod.polynomial(), degree=1) ) + == result + ) def test_unified_error_mitigation_virtual_distillation_pipeline() -> None: @@ -217,6 +220,48 @@ def estimator( assert result.expectation == pytest.approx(0.75) assert result.variance == pytest.approx(0.203125) + assert mitigation.get_mitigated(em.ProcessArgs.virtual_distillation()) == result + + +@pytest.mark.parametrize("variance", [0.04, float("nan"), -1e-16]) +def test_unified_zne_reprocesses_without_rerunning_estimator(variance): + mitigation = em.ErrorMitigation( + single_x_circuit(), em.MitigationMethod.zne(em.ZneConfig([0, 1, 2])) + ) + calls = [] + + def estimator(circuit, observable, shots): + factor = len(circuit.operations) + calls.append(factor) + assert observable is not None and shots == 128 + # Exact quadratic data: linear intercept = 41/300; quadratic = .2. + # ZNE keeps its existing behavior of ignoring estimator variance. + return 0.2 + 0.05 * factor + 0.01 * factor**2, variance + + args = em.RunArgs.zne(shots=128) + hamiltonian = single_qubit_z_hamiltonian() + mitigation.run(hamiltonian, args, estimator) + linear = em.ProcessArgs.zne(em.ExtrapolateMethod.polynomial(), 1) + quadratic = em.ProcessArgs.zne(em.ExtrapolateMethod.polynomial(), 2) + + assert mitigation.get_mitigated(linear).expectation == pytest.approx(41 / 300) + assert mitigation.get_mitigated(quadratic).expectation == pytest.approx(0.2) + with pytest.raises(em.ErrorMitigationError): + mitigation.get_mitigated( + em.ProcessArgs.zne(em.ExtrapolateMethod.polynomial(), 3) + ) + with pytest.raises(em.ErrorMitigationError): + mitigation.get_mitigated(em.ProcessArgs.virtual_distillation()) + assert mitigation.get_mitigated(linear).expectation == pytest.approx(41 / 300) + assert mitigation.get_mitigated(quadratic).expectation == pytest.approx(0.2) + + exponential = em.ProcessArgs.zne(em.ExtrapolateMethod.exponential()) + assert mitigation.get_mitigated(exponential) == mitigation.get_mitigated( + exponential + ) + with pytest.raises(em.ErrorMitigationError, match="already been completed"): + mitigation.run(hamiltonian, args, estimator) + assert calls == [1, 3, 5] def test_error_mitigation_rejects_invalid_inputs_and_propagates_estimator_errors() -> ( diff --git a/crates/binding-python/tests/qis/test_circuit_semantics.py b/crates/binding-python/tests/qis/test_circuit_semantics.py new file mode 100644 index 00000000..592060a6 --- /dev/null +++ b/crates/binding-python/tests/qis/test_circuit_semantics.py @@ -0,0 +1,154 @@ +# This code is part of Cqlib. +# +# (C) Copyright China Telecom Quantum Group 2026 +# +# Licensed under the Apache License, Version 2.0. See LICENSE.txt or +# http://www.apache.org/licenses/LICENSE-2.0. + +"""Regression tests for measurement declarations, execution, and reset.""" + +import numpy as np +import pytest + +from cqlib.circuit import Circuit, ClassicalExpr, ClassicalType +from cqlib.qis import DensityMatrix, DensityMatrixNoise, StabilizerState, Statevector + + +SIMULATORS = [Statevector, DensityMatrix, DensityMatrixNoise, StabilizerState] + + +def state_data(state): + if isinstance(state, StabilizerState): + return state.to_stim_format() + if isinstance(state, DensityMatrixNoise): + return state.state + return state.data + + +@pytest.mark.parametrize("simulator", SIMULATORS) +@pytest.mark.parametrize("measurement", ["measure", "measure_bits", "measure_into"]) +@pytest.mark.parametrize("following", ["h", "cx", "reset", "nested"]) +def test_mid_circuit_measurement_rejected_before_evolution( + simulator, measurement, following +): + circuit = Circuit(2) + circuit.h(0) + if measurement == "measure_bits": + circuit.measure_bits([1, 0]) + elif measurement == "measure_into": + circuit.measure_into(0, circuit.var(ClassicalType.bit())) + else: + circuit.measure(0) + circuit.barrier([0, 1]) + circuit.delay(0, 1.0) + if following == "nested": + child = Circuit(1) + child.h(0) + circuit.append_circuit_gate(child.to_gate("Hadamard"), [0]) + elif following == "cx": + circuit.cx(1, 0) + else: + getattr(circuit, following)(0) + + with pytest.raises(ValueError, match="mid-circuit measurement on qubit"): + simulator.from_circuit(circuit) + + state = simulator(2) + state.apply_h(1) + before = state_data(state) + with pytest.raises(ValueError, match="mid-circuit measurement"): + state.apply_circuit(circuit) + np.testing.assert_equal(state_data(state), before) + + +@pytest.mark.parametrize("simulator", SIMULATORS) +def test_terminal_declarations_allow_independent_gates_stores_and_repeated_measurements( + simulator, +): + circuit = Circuit(2) + circuit.h(0) + circuit.measure_into(0, circuit.var(ClassicalType.bit())) + circuit.barrier([0, 1]) + circuit.delay(0, 1.0) + circuit.h(1) + circuit.measure_bits([1, 0]) + flag = circuit.var(ClassicalType.bool()) + circuit.store(flag, ClassicalExpr.bool_literal(True)) + + state = simulator.from_circuit(circuit) + applied = simulator(2) + applied.apply_circuit(circuit) + for result in [state, applied]: + np.testing.assert_allclose(result.probabilities(), [0.25] * 4, atol=1e-12) + # The declarations did not collapse either |+> state. + result.apply_h(0) + result.apply_h(1) + np.testing.assert_allclose(result.probabilities(), [1, 0, 0, 0], atol=1e-12) + + +@pytest.mark.parametrize("simulator", SIMULATORS) +def test_classical_control_flow_remains_unsupported(simulator): + circuit = Circuit(1) + circuit.if_(ClassicalExpr.bool_literal(True), lambda body: body.x(0)) + with pytest.raises(ValueError, match="classical control flow"): + simulator.from_circuit(circuit) + + +def test_stabilizer_execution_keeps_mid_circuit_result_and_conditional_state(): + circuit = Circuit(1) + circuit.h(0) + measured = circuit.measure(0) + circuit.h(0) + + result = StabilizerState.run_circuit(circuit) + bit = result.classical.value(measured.value).as_bit() + state = result.state + np.testing.assert_allclose(state.probabilities(), [0.5, 0.5], atol=1e-12) + state.apply_h(0) + np.testing.assert_allclose(state.probabilities(), [not bit, bit], atol=1e-12) + + +@pytest.mark.parametrize("simulator", [DensityMatrix, DensityMatrixNoise]) +@pytest.mark.parametrize( + "qubit, expected", [(0, [0.5, 0, 0.5, 0]), (1, [0.5, 0.5, 0, 0])] +) +def test_circuit_reset_keeps_bell_partner_mixed(simulator, qubit, expected): + circuit = Circuit(2) + circuit.h(0) + circuit.cx(0, 1) + circuit.reset(qubit) + circuit.measure_bits([0, 1]) + + state = simulator.from_circuit(circuit) + applied = simulator(2) + applied.apply_circuit(circuit) + for result in [state, applied]: + np.testing.assert_allclose(state_data(result), np.diag(expected), atol=1e-12) + + +def test_density_matrix_reset_preserves_complex_coherence_of_other_qubit(): + state = DensityMatrix(2) + state.apply_h(0) + state.apply_h(1) + state.apply_s(1) + before = state.partial_trace([1]).data + + state.reset(0) + + np.testing.assert_allclose(state.partial_trace([1]).data, before, atol=1e-12) + np.testing.assert_allclose( + state.data, np.kron(before, [[1, 0], [0, 0]]), atol=1e-12 + ) + + +@pytest.mark.parametrize("simulator", [Statevector, StabilizerState]) +def test_pure_state_reset_remains_a_single_trajectory(simulator): + circuit = Circuit(2) + circuit.h(0) + circuit.cx(0, 1) + circuit.reset(0) + + state = simulator.from_circuit(circuit) + probs = state.probabilities() + assert np.isclose(probs[0], 1.0) or np.isclose(probs[2], 1.0) + np.testing.assert_allclose([probs[1], probs[3]], [0, 0], atol=1e-12) diff --git a/crates/binding-python/tests/qis/test_observable.py b/crates/binding-python/tests/qis/test_observable.py index e03b3513..781fe467 100644 --- a/crates/binding-python/tests/qis/test_observable.py +++ b/crates/binding-python/tests/qis/test_observable.py @@ -1,4 +1,5 @@ # This code is part of Cqlib. +# Modified to cover Pauli source phases in expectation and variance calculations. # # (C) Copyright China Telecom Quantum Group 2026 # @@ -12,9 +13,137 @@ import pytest import math +from itertools import product + +import numpy as np +from cqlib import Circuit from cqlib.qis import PauliString, Hamiltonian, Statevector, DensityMatrix +_PAULI_MATRICES = { + "I": np.eye(2, dtype=complex), + "X": np.array([[0, 1], [1, 0]], dtype=complex), + "Y": np.array([[0, -1j], [1j, 0]], dtype=complex), + "Z": np.diag([1, -1]).astype(complex), +} +_PAULI_LABELS = [ + "".join(chars) for n in range(1, 4) for chars in product("IXYZ", repeat=n) +] + ["YYYY"] + + +def _pauli_matrix(label): + matrix = np.ones((1, 1), dtype=complex) + for char in label: + matrix = np.kron(matrix, _PAULI_MATRICES[char]) + return matrix + + +@pytest.mark.parametrize("sign", [1, -1]) +def test_y_expectation_from_circuit(sign): + circuit = Circuit(1) + circuit.h(0) + circuit.rz(0, sign * math.pi / 2) + sv = Statevector.from_circuit(circuit) + pauli = PauliString.from_str("Y") + h = Hamiltonian.from_pauli(pauli) + amplitudes = sv.data + reference = np.vdot(amplitudes, _PAULI_MATRICES["Y"] @ amplitudes) + assert reference == pytest.approx(sign, abs=1e-12) + for actual in ( + sv.expectation(pauli), + pauli.expectation_statevector(sv), + sv.expectation(h), + h.expectation_statevector(sv), + ): + assert actual == pytest.approx(reference.real, abs=1e-12) + + +@pytest.mark.parametrize("label", _PAULI_LABELS) +@pytest.mark.parametrize("prefix,phase", [("", 1), ("-", -1), ("i", 1j), ("-i", -1j)]) +def test_pauli_expectation_phases_match_independent_matrix(label, prefix, phase): + n = len(label) + rng = np.random.default_rng(23923 + n) + amplitudes = rng.normal(size=2**n) + 1j * rng.normal(size=2**n) + amplitudes /= np.linalg.norm(amplitudes) + sv = Statevector.from_state(n, amplitudes) + dm = DensityMatrix.from_state(n, amplitudes) + pauli = PauliString.from_str(prefix + label) + reference = np.vdot(amplitudes, phase * _pauli_matrix(label) @ amplitudes) + # Ensure odd-Y sign errors cannot hide behind a zero expectation. + assert abs(reference) > 1e-10 + + if phase in (1, -1): + assert abs(reference.imag) < 1e-12 + for actual in ( + sv.expectation(pauli), + pauli.expectation_statevector(sv), + pauli.expectation_density_matrix(dm), + ): + assert actual == pytest.approx(reference.real, abs=1e-12) + + coefficient = -0.37 / phase + h = Hamiltonian.from_list([(pauli, coefficient)]) + expected = coefficient * reference + assert abs(expected.imag) < 1e-12 + for actual in ( + sv.expectation(h), + h.expectation_statevector(sv), + h.expectation_density_matrix(dm), + ): + assert actual == pytest.approx(expected.real, abs=1e-12) + + +@pytest.mark.parametrize("other,angle", [("X", math.pi / 4), ("I", math.pi / 2)]) +def test_hamiltonian_y_variance_matches_independent_matrix(other, angle): + amplitudes = np.array([1, np.exp(1j * angle)]) / np.sqrt(2) + sv = Statevector.from_state(1, amplitudes) + h = Hamiltonian.from_list( + [(PauliString.from_str(other), 1), (PauliString.from_str("-iY"), 1j)] + ) + matrix = _PAULI_MATRICES[other] + _PAULI_MATRICES["Y"] + applied = matrix @ amplitudes + mean = np.vdot(amplitudes, applied) + variance = np.vdot(applied, applied) - mean**2 + assert abs(mean.imag) < 1e-12 + assert variance == pytest.approx(0, abs=1e-12) + assert h.expectation_statevector(sv) == pytest.approx(mean.real, abs=1e-12) + assert h.variance_statevector(sv) == pytest.approx(variance.real, abs=1e-12) + + +def test_mixed_hamiltonian_moments_match_independent_matrix(): + rng = np.random.default_rng(23923) + amplitudes = rng.normal(size=8) + 1j * rng.normal(size=8) + amplitudes /= np.linalg.norm(amplitudes) + sv = Statevector.from_state(3, amplitudes) + terms = [ + ("XYZ", "", 1, 0.73), + ("YYY", "-i", -1j, 0.2j), + ("YIY", "-", -1, -0.41), + ("ZXI", "i", 1j, -0.19j), + ("III", "", 1, -0.3), + ("XYZ", "", 1, -0.11), + ] + h = Hamiltonian.from_list( + [ + (PauliString.from_str(prefix + label), coeff) + for label, prefix, _, coeff in terms + ] + ) + matrix = sum( + phase * coeff * _pauli_matrix(label) for label, _, phase, coeff in terms + ) + applied = matrix @ amplitudes + mean = np.vdot(amplitudes, applied) + variance = np.vdot(applied, applied) - mean**2 + assert abs(mean.imag) < 1e-12 + assert abs(variance.imag) < 1e-12 + assert h.expectation_statevector(sv) == pytest.approx(mean.real, abs=1e-12) + assert h.variance_statevector(sv) == pytest.approx(variance.real, abs=1e-12) + h.simplify() + assert h.expectation_statevector(sv) == pytest.approx(mean.real, abs=1e-12) + assert h.variance_statevector(sv) == pytest.approx(variance.real, abs=1e-12) + + def test_pauli_expectation_statevector(): # |0⟩ state sv = Statevector(1) diff --git a/crates/binding-python/tests/qis/test_qis_bindings.py b/crates/binding-python/tests/qis/test_qis_bindings.py index efbc5b1f..35fc0b8c 100644 --- a/crates/binding-python/tests/qis/test_qis_bindings.py +++ b/crates/binding-python/tests/qis/test_qis_bindings.py @@ -16,7 +16,7 @@ import pytest -from cqlib.circuit import Circuit, ClassicalExpr, ClassicalType +from cqlib.circuit import Circuit, ClassicalExpr, ClassicalType, Parameter from cqlib.device import NoiseModel, Outcome, ReadoutError from cqlib.qis import ( DensityMatrix, @@ -62,6 +62,166 @@ def test_state_measurement_probs_and_sample_bindings(): assert result.counts == {"00": 8} +@pytest.mark.parametrize( + "state_type", [Statevector, DensityMatrix, StabilizerState, DensityMatrixNoise] +) +@pytest.mark.parametrize("qubits", [[0, 1], [1, 0], [5, 9], [9, 5]]) +@pytest.mark.parametrize("apply_in_place", [False, True]) +def test_state_measurement_resolves_circuit_qubit_positions( + state_type, qubits, apply_in_place +): + circuit = Circuit(qubits) + circuit.x(qubits[0]) + measurements = [ + (circuit.measure_bits(qubits), "01"), + (circuit.measure_bits(qubits[::-1]), "10"), + (circuit.measure(qubits[0]), "1"), + (circuit.measure(qubits[1]), "0"), + ] + if apply_in_place: + state = state_type(2) + state.apply_circuit(circuit) + else: + state = state_type.from_circuit(circuit) + + assert state.probabilities() == [0.0, 1.0, 0.0, 0.0] + for candidate in [state, state.copy()]: + for measurement, expected in measurements: + assert candidate.sample(measurement, 4).counts == {expected: 4} + assert candidate.probs(measurement) == {Outcome(expected): 1.0} + + +@pytest.mark.parametrize( + "state_type", [Statevector, DensityMatrix, StabilizerState, DensityMatrixNoise] +) +def test_state_measurement_rejects_ids_absent_from_circuit(state_type): + state = state_type.from_circuit(Circuit([5, 9])) + measurement = Circuit(2).measure(0) + with pytest.raises(IndexError): + state.sample(measurement, 4) + with pytest.raises(IndexError): + state.probs(measurement) + + +@pytest.mark.parametrize( + "state_type", [Statevector, DensityMatrix, StabilizerState, DensityMatrixNoise] +) +@pytest.mark.parametrize("incoming_qubits", [[9, 5], [5, 7]]) +@pytest.mark.parametrize("with_gate", [False, True]) +def test_apply_circuit_rejects_incompatible_mapping( + state_type, incoming_qubits, with_gate +): + circuit = Circuit([5, 9]) + circuit.x(5) + measurement = circuit.measure_bits([5, 9]) + state = state_type.from_circuit(circuit) + incoming = Circuit(incoming_qubits) + if with_gate: + incoming.x(incoming_qubits[0]) + + with pytest.raises(ValueError, match="qubit IDs and order"): + state.apply_circuit(incoming) + + assert state.probabilities() == [0.0, 1.0, 0.0, 0.0] + assert state.sample(measurement, 4).counts == {"01": 4} + assert state.probs(measurement) == {Outcome("01"): 1.0} + + compatible = Circuit([5, 9]) + compatible.x(9) + state.apply_circuit(compatible) + assert state.probs(measurement) == {Outcome("11"): 1.0} + + +@pytest.mark.parametrize( + "state_type", [Statevector, DensityMatrix, StabilizerState, DensityMatrixNoise] +) +@pytest.mark.parametrize("qubits", [[1, 0], [5, 9]]) +@pytest.mark.parametrize("bound", [False, True]) +def test_failed_apply_circuit_preserves_measurement_positions( + state_type, qubits, bound +): + state = state_type.from_circuit(Circuit(qubits)) if bound else state_type(2) + state.apply_x(0) + measurement_qubits = qubits if bound else [0, 1] + measurement = Circuit(measurement_qubits).measure_bits(measurement_qubits) + invalid = Circuit(qubits) + invalid.rx(qubits[0], Parameter("theta")) + + with pytest.raises(ValueError): + state.apply_circuit(invalid) + + assert state.probabilities() == [0.0, 1.0, 0.0, 0.0] + assert state.sample(measurement, 4).counts == {"01": 4} + assert state.probs(measurement) == {Outcome("01"): 1.0} + + +@pytest.mark.parametrize( + "state_type", [Statevector, DensityMatrix, StabilizerState, DensityMatrixNoise] +) +@pytest.mark.parametrize("bound", [False, True]) +def test_failed_apply_circuit_keeps_completed_gates(state_type, bound): + state = state_type.from_circuit(Circuit([5, 9])) if bound else state_type(2) + measurement_qubits = [5, 9] if bound else [0, 1] + measurement = Circuit(measurement_qubits).measure_bits(measurement_qubits) + invalid = Circuit([5, 9]) + invalid.x(5) + invalid.rx(9, Parameter("theta")) + + with pytest.raises(ValueError): + state.apply_circuit(invalid) + + assert state.sample(measurement, 4).counts == {"01": 4} + assert state.probs(measurement) == {Outcome("01"): 1.0} + + +def test_partial_trace_retains_circuit_qubit_identifiers(): + circuit = Circuit([5, 9]) + circuit.x(9) + measurement = circuit.measure(9) + reduced = DensityMatrix.from_circuit(circuit).partial_trace([1]) + assert reduced.sample(measurement, 4).counts == {"1": 4} + assert reduced.probs(measurement) == {Outcome("1"): 1.0} + + +def test_stabilizer_run_retains_circuit_qubit_identifiers(): + circuit = Circuit([5, 9]) + circuit.x(5) + measurement = circuit.measure_bits([5, 9]) + state = StabilizerState.run_circuit(circuit).state + assert state.sample(measurement, 4).counts == {"01": 4} + assert state.probs(measurement) == {Outcome("01"): 1.0} + + +@pytest.mark.parametrize( + "state_type", [Statevector, DensityMatrix, StabilizerState, DensityMatrixNoise] +) +def test_state_measurement_on_routed_sparse_physical_qubits(state_type): + from cqlib.compile.sabre import SabreConfig + from cqlib.compile.transform import route_with_layout + from cqlib.device import Device, Layout, Topology + + logical_qubits = [1, 0, 2] + circuit = Circuit(logical_qubits) + circuit.x(1) + circuit.cx(1, 2) + device = Device("sparse", [5, 7, 9], Topology.line([5, 7, 9])) + routed = route_with_layout( + circuit, + device, + Layout(logical_qubits, [5, 7, 9], {1: 5, 0: 7, 2: 9}), + SabreConfig.deterministic_seeded(7), + ) + assert routed.swap_count > 0 + assert routed.final_layout.l2p_map != routed.initial_layout.l2p_map + physical = routed.circuit + measurement = physical.measure_bits( + [routed.final_layout.get_physical(q).index for q in logical_qubits] + ) + state = state_type.from_circuit(physical) + assert state.sample(measurement, 4).counts == {"101": 4} + assert state.probs(measurement) == {Outcome("101"): 1.0} + + def test_density_matrix_noise_readout_errors_raise_index_error(): noise_model = NoiseModel() noise_model.add_readout_error(0, ReadoutError(0.1, 0.2)) @@ -83,6 +243,27 @@ def test_density_matrix_noise_probs_with_readout_binding(): assert sim.probs_with_readout(measurement) == {Outcome("1"): 1.0} +@pytest.mark.parametrize("qubits", [[0, 1], [1, 0], [5, 9]]) +@pytest.mark.parametrize("target", [0, 1]) +def test_density_matrix_readout_uses_ids_for_noise_and_positions_for_probabilities( + qubits, target +): + circuit = Circuit(qubits) + measurements = [ + (circuit.measure(qubits[target]), "1"), + (circuit.measure(qubits[1 - target]), "0"), + (circuit.measure_bits(qubits), "01" if target == 0 else "10"), + (circuit.measure_bits(qubits[::-1]), "10" if target == 0 else "01"), + ] + noise = NoiseModel() + noise.add_readout_error(qubits[target], ReadoutError(1.0, 1.0)) + state = DensityMatrixNoise.from_circuit(circuit, noise) + + for measurement, expected in measurements: + assert state.probs_with_readout(measurement) == {Outcome(expected): 1.0} + assert state.probs(measurement) == {Outcome("0" * len(expected)): 1.0} + + def test_runtime_value_equality_and_hash(): """Runtime values compare by value and deduplicate in sets.""" diff --git a/crates/binding-python/tests/qis/test_sampling.py b/crates/binding-python/tests/qis/test_sampling.py new file mode 100644 index 00000000..9524258a --- /dev/null +++ b/crates/binding-python/tests/qis/test_sampling.py @@ -0,0 +1,346 @@ +# This code is part of Cqlib. +# Modified to verify seeded sampling across state backends. +# +# (C) Copyright China Telecom Quantum Group 2026 +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Direct circuit sampling, output selection, and supported execution semantics.""" + +from collections import Counter +import copy +import os +import subprocess +import sys + +import numpy as np +import pytest + +from cqlib import Circuit, ExecutionResult, Parameter, Qubit, sample +from cqlib.circuit import ClassicalExpr, ClassicalType +from cqlib.qis import ( + DensityMatrix, + DensityMatrixNoise, + StabilizerState, + Statevector, + sample as qis_sample, +) + + +SIMULATORS = ["statevector", "density_matrix", "stabilizer"] +STATE_TYPES = [Statevector, DensityMatrix, DensityMatrixNoise, StabilizerState] + + +@pytest.mark.parametrize("simulator", SIMULATORS) +def test_bell_sampling_returns_completed_result(simulator): + circuit = Circuit(2) + circuit.h(0) + circuit.cx(0, 1) + before = copy.copy(circuit) + + result = sample(circuit, shots=1000, seed=42, simulator=simulator) + + assert qis_sample is sample + assert isinstance(result, ExecutionResult) + assert result.status.kind == "completed" + assert result.shots == sum(result.counts.values()) == 1000 + assert result.backend == simulator + assert result.qubits == [Qubit(0), Qubit(1)] + assert set(result.counts) == {"00", "11"} + assert 400 < result.counts["00"] < 600 + assert result.probabilities == pytest.approx( + {key: count / 1000 for key, count in result.counts.items()} + ) + assert ( + sample(circuit, shots=1000, seed=42, simulator=simulator).counts + == result.counts + ) + assert circuit == before + + +def test_defaults_include_idle_qubits_in_circuit_order(): + circuit = Circuit([9, 5, 2]) + circuit.x(9) + + result = sample(circuit) + + assert result.backend == "statevector" + assert result.counts == {"001": 1000} + assert result.qubits == circuit.qubits + + +@pytest.mark.parametrize("simulator", SIMULATORS) +def test_terminal_declarations_and_explicit_output_order(simulator): + circuit = Circuit([9, 5, 2]) + circuit.x(9) + circuit.measure_into(9, circuit.var(ClassicalType.bit())) + circuit.barrier([9, 5, 2]) + circuit.delay(9, 1.0) + circuit.x(2) # Independent gates after a measurement are supported. + circuit.measure_bits([5, 9]) + before = copy.copy(circuit) + + result = sample(circuit, shots=8, simulator=simulator) + assert result.counts == {"01": 8} + assert result.qubits == [Qubit(9), Qubit(5)] + assert result.num_qubits == 2 + + reverse = sample(circuit, shots=8, simulator=simulator, qubits=[5, 9]) + assert reverse.counts == {"10": 8} + assert reverse.qubits == [Qubit(5), Qubit(9)] + subset = sample(circuit, shots=8, simulator=simulator, qubits=[Qubit(2)]) + assert subset.counts == {"1": 8} + assert circuit == before + + +@pytest.mark.parametrize("simulator", SIMULATORS) +def test_mid_circuit_measurement_cannot_be_bypassed_by_output_selection(simulator): + circuit = Circuit(2) + circuit.measure(0) + child = Circuit(1) + child.h(0) + circuit.append_circuit_gate(child.to_gate("Hadamard"), [0]) + + with pytest.raises(ValueError, match="mid-circuit measurement"): + sample(circuit, simulator=simulator, qubits=[1]) + + +@pytest.mark.parametrize("simulator", SIMULATORS) +def test_control_flow_is_rejected(simulator): + circuit = Circuit(1) + circuit.if_(ClassicalExpr.bool_literal(True), lambda body: body.x(0)) + with pytest.raises(ValueError, match="classical control flow"): + sample(circuit, simulator=simulator) + + +def test_reset_samples_the_mixed_ensemble(): + circuit = Circuit(2) + circuit.h(0) + circuit.cx(0, 1) + circuit.reset(0) + + result = sample(circuit, shots=1000, seed=42, simulator="density_matrix") + assert set(result.counts) == {"00", "10"} + assert 400 < result.counts["00"] < 600 + for simulator in ["statevector", "stabilizer"]: + with pytest.raises(ValueError, match="reset requires the density_matrix"): + sample(circuit, simulator=simulator) + + +def test_stabilizer_samples_large_outputs_without_probability_enumeration(): + circuit = Circuit(128) + circuit.h(0) + for qubit in range(1, 128): + circuit.cx(0, qubit) + + result = sample(circuit, shots=32, seed=42, simulator="stabilizer") + assert set(result.counts) == {"0" * 128, "1" * 128} + assert sum(result.counts.values()) == 32 + + +@pytest.mark.parametrize("simulator", SIMULATORS) +def test_unbound_parameters_are_rejected(simulator): + circuit = Circuit(1) + circuit.ry(0, Parameter("theta")) + with pytest.raises(ValueError): + sample(circuit, simulator=simulator) + + +def test_non_clifford_gates_require_a_general_simulator(): + circuit = Circuit(1) + circuit.ry(0, 0.3) + for simulator in ["statevector", "density_matrix"]: + assert ( + sum(sample(circuit, shots=16, seed=42, simulator=simulator).counts.values()) + == 16 + ) + with pytest.raises(ValueError): + sample(circuit, simulator="stabilizer") + + +@pytest.mark.parametrize("angle", [0.2, 2.0]) +def test_density_matrix_sampling_tolerates_roundoff(angle): + circuit = Circuit(1) + circuit.ry(0, angle) + circuit.ry(0, -angle) + assert sample(circuit, simulator="density_matrix", seed=42).counts == {"0": 1000} + + +@pytest.mark.parametrize( + "options, error", + [ + ({"shots": 0}, ValueError), + ({"shots": -1}, OverflowError), + ({"shots": 1.5}, TypeError), + ({"seed": -1}, OverflowError), + ({"seed": 2**64}, OverflowError), + ({"simulator": "unknown"}, ValueError), + ({"qubits": []}, ValueError), + ({"qubits": [0, 0]}, ValueError), + ({"qubits": [2]}, ValueError), + ], +) +def test_invalid_sampling_options(options, error): + with pytest.raises(error): + sample(Circuit(1), **options) + + +def test_zero_qubit_circuit_is_rejected(): + with pytest.raises(ValueError, match="at least one output qubit"): + sample(Circuit(0)) + + +def test_seed_is_independent_of_worker_count(): + script = """ +import json +from cqlib import Circuit, sample +circuit = Circuit(4) +for qubit in range(4): + circuit.h(qubit) +print(json.dumps({ + simulator: sample(circuit, shots=256, seed=42, simulator=simulator).counts + for simulator in ['statevector', 'density_matrix', 'stabilizer'] +}, sort_keys=True)) +""" + results = [ + subprocess.check_output( + [sys.executable, "-c", script], + env={**os.environ, "RAYON_NUM_THREADS": str(workers)}, + text=True, + ) + for workers in [1, 4] + ] + assert results[0] == results[1] + + +def test_state_sampling_seed_is_independent_of_workers_and_processes(): + script = """ +import json +from cqlib import Circuit +from cqlib.qis import Statevector, DensityMatrix, DensityMatrixNoise, StabilizerState +circuit = Circuit(4) +for qubit in range(4): + circuit.h(qubit) +measurement = circuit.measure_bits([3, 0, 2]) +results = {} +for state_type in [Statevector, DensityMatrix, DensityMatrixNoise, StabilizerState]: + state = state_type.from_circuit(circuit) + results[state_type.__name__] = { + "shots": [outcome.to_bitstring(4) for outcome in state.sample_shots(256, seed=42)], + "counts": state.sample(measurement, 256, seed=42).counts, + } +print(json.dumps(results, sort_keys=True)) +""" + results = [ + subprocess.check_output( + [sys.executable, "-c", script], + env={**os.environ, "RAYON_NUM_THREADS": str(workers)}, + text=True, + timeout=45, + ) + for workers in [1, 2, 4, 1] + ] + assert all(result == results[0] for result in results[1:]) + + +@pytest.mark.parametrize("state_type", STATE_TYPES) +def test_seeded_shot_sequence_replays_and_keeps_prefix(state_type, bell_state_circuit): + state = state_type.from_circuit(bell_state_circuit) + before = state.probabilities() + + def draw(shots, **kwargs): + return [o.to_bitstring(2) for o in state.sample_shots(shots, **kwargs)] + + first = draw(512, seed=42) + assert len(draw(17)) == 17 + assert len(draw(17, seed=None)) == 17 + assert draw(512, seed=42) == first + assert draw(128, seed=42) == first[:128] + assert len(first) == 512 and set(first) <= {"00", "11"} + np.testing.assert_array_equal(state.probabilities(), before) + + +@pytest.mark.parametrize("state_type", STATE_TYPES) +def test_seeded_measurement_counts_replay_with_sparse_qubit_order(state_type): + circuit = Circuit([9, 5, 12]) + circuit.h(9) + circuit.cx(9, 5) + measurement = circuit.measure_bits([12, 9]) + state = state_type.from_circuit(circuit) + before = state.probabilities() + result = state.sample(measurement, 512, seed=42) + assert result.counts == state.sample(measurement, 512, seed=42).counts + assert sum(result.counts.values()) == 512 + assert set(result.counts) <= {"00", "10"} + # Full shots and measurement counts use the same seeded stream. + full = state.sample_shots(512, seed=42) + projected = Counter( + "10" if shot.to_bitstring(3)[-1] == "1" else "00" for shot in full + ) + assert result.counts == projected + np.testing.assert_array_equal(state.probabilities(), before) + + +@pytest.mark.parametrize("state_type", STATE_TYPES) +@pytest.mark.parametrize("seed", [0, 42, 2**64 - 1]) +def test_seed_boundaries_and_concurrent_calls(state_type, seed, bell_state_circuit): + from concurrent.futures import ThreadPoolExecutor + + state = state_type.from_circuit(bell_state_circuit) + + def draw(value): + return [o.to_bitstring(2) for o in state.sample_shots(128, seed=value)] + + seeds = [seed, 17, seed, 23] + expected = [draw(s) for s in seeds] + with ThreadPoolExecutor(4) as executor: + assert list(executor.map(draw, seeds)) == expected + assert state.sample_shots(0, seed=seed) == [] + + +@pytest.mark.parametrize("state_type", STATE_TYPES) +@pytest.mark.parametrize( + "seed,error", [(-1, OverflowError), (2**64, OverflowError), (1.5, TypeError)] +) +def test_invalid_sampling_seed_is_rejected(state_type, seed, error, bell_state_circuit): + state = state_type.from_circuit(bell_state_circuit) + measurement = bell_state_circuit.measure_bits([0, 1]) + with pytest.raises(error): + state.sample_shots(8, seed=seed) + with pytest.raises(error): + state.sample(measurement, 8, seed=seed) + + +@pytest.mark.parametrize("state_type", STATE_TYPES) +@pytest.mark.parametrize("seed", [0, 42]) +def test_seed_controls_the_random_sequence(state_type, seed, bell_state_circuit): + if sys.maxsize <= 2**32: + pytest.skip("SmallRng uses a different algorithm on 32-bit targets") + expected = { + 0: "00011111100000000000011011110011", + 42: "11010011100010000010111011100010", + } + stabilizer_expected = { + 0: "11100000011111111111100100001100", + 42: "00101100011101111101000100011101", + } + state = state_type.from_circuit(bell_state_circuit) + sequence = "".join( + str(int(o.to_bitstring(2) == "11")) for o in state.sample_shots(32, seed=seed) + ) + # Golden sequences for the current seeded sampler, not a promise across + # package versions. Detects a binding that accepts but ignores the seed. + assert ( + sequence + == ( + stabilizer_expected + if state_type.__name__ == "StabilizerState" + else expected + )[seed] + ) diff --git a/crates/binding-python/tests/visualization/test_visualization.py b/crates/binding-python/tests/visualization/test_visualization.py index 9b48c650..42b1098d 100644 --- a/crates/binding-python/tests/visualization/test_visualization.py +++ b/crates/binding-python/tests/visualization/test_visualization.py @@ -15,12 +15,17 @@ from __future__ import annotations from dataclasses import dataclass +from functools import cache +import hashlib import os from pathlib import Path import struct +import subprocess +import tempfile import zlib import pytest +import resvg_py import cqlib import cqlib._native as cqlib_native @@ -31,6 +36,54 @@ _PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n" _FIGURE_ROOT = Path(__file__).resolve().parent / "figure" +_FONT_URL = "https://cdn.jsdelivr.net/npm/pdfjs-dist@5.4.624/standard_fonts/LiberationSans-Regular.ttf" +_FONT_SHA256 = "f8ace1f892b2bd9dc1792ba7f097fa7588f84fed48321480e04de5390828221f" + + +@cache +def _visual_font() -> Path: + """Use the same verified font as the Rust tests, including installed-wheel tests.""" + override = os.environ.get("CQLIB_VISUAL_FONT") + cache_root = Path(os.environ.get("CARGO_TARGET_DIR", tempfile.gettempdir())) + path = ( + Path(override) + if override + else cache_root / "cqlib-visual-fonts" / "LiberationSans-Regular.ttf" + ) + if path.is_file() and hashlib.sha256(path.read_bytes()).hexdigest() == _FONT_SHA256: + return path + assert not override, f"{path} is missing or has the wrong SHA-256" + result = subprocess.run( + [ + "curl", + "--fail", + "--location", + "--silent", + "--show-error", + "--proto", + "=https", + "--proto-redir", + "=https", + "--max-time", + "45", + "--retry", + "1", + _FONT_URL, + ], + capture_output=True, + check=False, + timeout=100, + ) + assert ( + result.returncode == 0 + and hashlib.sha256(result.stdout).hexdigest() == _FONT_SHA256 + ), ( + f"visual-test font download failed or SHA-256 mismatch: {result.stderr.decode(errors='replace')}; " + "set CQLIB_VISUAL_FONT to a verified font for offline tests" + ) + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(result.stdout) + return path @dataclass(frozen=True) @@ -44,6 +97,7 @@ class _RgbImage: class _VisualCasePaths: actual_svg: Path actual_png: Path + exported_png: Path reference_png: Path diff_png: Path @@ -150,6 +204,7 @@ def _visual_case_paths(filename: str) -> _VisualCasePaths: return _VisualCasePaths( actual_svg=_FIGURE_ROOT / filename.replace(".png", ".svg"), actual_png=_FIGURE_ROOT / filename, + exported_png=_FIGURE_ROOT / filename.replace(".png", "_export.png"), reference_png=references_dir / filename, diff_png=diffs_dir / f"diff_{filename}", ) @@ -316,10 +371,6 @@ def _save_diff_and_similarity( actual_png: Path, reference_png: Path, diff_png: Path ) -> float: actual = _load_png_rgb(actual_png) - if not reference_png.exists(): - reference_png.write_bytes(actual_png.read_bytes()) - return 1.0 - reference = _load_png_rgb(reference_png) width = max(actual.width, reference.width) height = max(actual.height, reference.height) @@ -327,6 +378,9 @@ def _save_diff_and_similarity( reference_padded = _pad_rgb_to_canvas(reference, width, height) ratio = _similarity_ratio(actual_padded, reference_padded) _save_diff_png(actual_padded, reference_padded, width, height, diff_png) + assert (actual.width, actual.height) == (reference.width, reference.height), ( + f"image dimensions changed: actual {actual_png}, reference {reference_png}, diff {diff_png}" + ) return ratio @@ -334,18 +388,38 @@ def _assert_visual_match(filename: str, render): paths = _visual_case_paths(filename) svg = render(str(paths.actual_svg)) - render(str(paths.actual_png)) + # Exercise the public PNG exporter; compare with a fixed font independently + # of OS font discovery, just as the Rust visual tests do. + render(str(paths.exported_png)) + exported = _load_png_rgb(paths.exported_png) + assert exported.width > 0 and exported.height > 0 assert isinstance(svg, str) assert paths.actual_svg.read_text(encoding="utf-8").startswith("= threshold, ( - f"Similarity ratio {ratio:.4f} < {threshold:.4f} for {filename}; " - f"diff written to {paths.diff_png}" + f"Similarity ratio {ratio:.6f} < {threshold:.6f} for {filename}; " + f"actual: {paths.actual_png}; reference: {paths.reference_png}; " + f"diff: {paths.diff_png}; SVG: {paths.actual_svg}; font: Liberation Sans" ) return svg diff --git a/crates/cqlib-core/Cargo.toml b/crates/cqlib-core/Cargo.toml index 5638668e..93c46666 100644 --- a/crates/cqlib-core/Cargo.toml +++ b/crates/cqlib-core/Cargo.toml @@ -29,6 +29,7 @@ include = [ "src/compile/knowledge/rules/*.rule", "src/ir/qasm2/parser.lalrpop", "src/visualization/circuit/styles/*.json", + "src/visualization/**/figure/references/*.png", "build.rs", "Cargo.toml", "README.md", @@ -65,6 +66,7 @@ lalrpop.workspace = true [dev-dependencies] criterion.workspace = true proptest.workspace = true +sha2 = "0.10.9" [[bench]] name = "two_qubit_synthesis_cache" diff --git a/crates/cqlib-core/src/circuit/ansatz/qaoa.rs b/crates/cqlib-core/src/circuit/ansatz/qaoa.rs index 192504a7..f808c061 100644 --- a/crates/cqlib-core/src/circuit/ansatz/qaoa.rs +++ b/crates/cqlib-core/src/circuit/ansatz/qaoa.rs @@ -33,8 +33,7 @@ use crate::circuit::circuit_impl::Circuit; use crate::circuit::circuit_param::ParameterValue; use crate::circuit::error::CircuitError; use crate::qis::evolution::{ - PauliEvolution, TrotterMode, multiply_angle_by_factor, trotter_first_order_core, - trotter_second_order_core, + PauliEvolution, TrotterMode, trotter_first_order_core, trotter_second_order_core, }; use crate::qis::hamiltonian::Hamiltonian; use crate::qis::pauli::{Pauli, PauliString}; @@ -293,7 +292,13 @@ fn append_exact_hamiltonian_evolution( qubits: &[Qubit], ) -> Result<(), CircuitError> { for (pauli_str, coeff) in &hamiltonian.terms { - let term_angle = multiply_angle_by_factor(time.clone(), 2.0 * coeff.re); + let factor = 2.0 * coeff.re; + if !factor.is_finite() { + return Err(CircuitError::InvalidOperation(format!( + "Hamiltonian evolution scale factor must be finite, got {factor}" + ))); + } + let term_angle = time.scaled(factor); circuit.pauli_evolution(pauli_str, term_angle, qubits)?; } Ok(()) diff --git a/crates/cqlib-core/src/circuit/ansatz/qaoa_test.rs b/crates/cqlib-core/src/circuit/ansatz/qaoa_test.rs index fb0af458..8a21bb40 100644 --- a/crates/cqlib-core/src/circuit/ansatz/qaoa_test.rs +++ b/crates/cqlib-core/src/circuit/ansatz/qaoa_test.rs @@ -49,6 +49,40 @@ fn test_qaoa_ansatz_default_mixer() { assert_eq!(ops.len(), 20); } +#[test] +fn test_qaoa_non_finite_scale_factor_returns_error() { + let mut h = Hamiltonian::new(1); + for _ in 0..2 { + h.add_term("Z".parse().unwrap(), f64::MAX.into()).unwrap(); + } + + for strategy in [ + EvolutionStrategy::Exact, + EvolutionStrategy::Auto { steps: 2 }, + EvolutionStrategy::Trotter { + mode: TrotterMode::FirstOrder, + steps: 2, + }, + EvolutionStrategy::Trotter { + mode: TrotterMode::SecondOrder, + steps: 2, + }, + EvolutionStrategy::Trotter { + mode: TrotterMode::Randomized(42), + steps: 2, + }, + ] { + let result = QAOAAnsatz::new(h.clone()) + .unwrap() + .evolution_strategy(strategy) + .build_circuit("p"); + assert!(matches!( + result, + Err(CircuitError::InvalidOperation(message)) if message.contains("scale factor") + )); + } +} + #[test] fn test_qaoa_custom_mixer() { let mut h_c = Hamiltonian::new(2); diff --git a/crates/cqlib-core/src/circuit/circuit_param.rs b/crates/cqlib-core/src/circuit/circuit_param.rs index 168727a0..15721c06 100644 --- a/crates/cqlib-core/src/circuit/circuit_param.rs +++ b/crates/cqlib-core/src/circuit/circuit_param.rs @@ -97,6 +97,32 @@ pub enum ParameterValue { Fixed(f64), } +impl ParameterValue { + /// Returns a scaled value, preserving the `Fixed` or `Param` variant. + /// + /// Fixed values use floating-point multiplication. Symbolic values use + /// expression multiplication and remain `Param`, even if the resulting + /// expression is constant (including multiplication by zero). This method + /// does not explicitly evaluate, simplify, or canonicalize expressions. + /// Unlike converting a `Parameter` with `Into`, it does not fold constants + /// into `Fixed` values. + /// + /// Fixed multiplication follows IEEE 754, including signed zero and + /// overflow; validation of the resulting value remains the caller's + /// responsibility. + /// + /// # Panics + /// + /// Panics if `factor` is not finite, for either variant. + pub fn scaled(&self, factor: f64) -> Self { + assert!(factor.is_finite(), "parameter scale factor must be finite"); + match self { + Self::Fixed(value) => Self::Fixed(value * factor), + Self::Param(parameter) => Self::Param(parameter.clone() * factor), + } + } +} + impl PartialEq for ParameterValue { /// Compares the construction form: two values are equal only when both /// are symbolic with structurally equal expressions, or both are fixed @@ -177,3 +203,7 @@ impl From<&str> for ParameterValue { Parameter::symbol(para).into() } } + +#[cfg(test)] +#[path = "circuit_param_test.rs"] +mod circuit_param_test; diff --git a/crates/cqlib-core/src/circuit/circuit_param_test.rs b/crates/cqlib-core/src/circuit/circuit_param_test.rs new file mode 100644 index 00000000..45690540 --- /dev/null +++ b/crates/cqlib-core/src/circuit/circuit_param_test.rs @@ -0,0 +1,72 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +use super::{Parameter, ParameterValue}; +use std::collections::HashMap; + +#[test] +fn scaled_fixed_values_preserve_floating_point_results() { + for (value, factor, expected) in [ + (8.0, -0.25, -2.0_f64), + (8.0, 1.0, 8.0), + (8.0, 0.0, 0.0), + (0.0, -1.0, -0.0), + (f64::MAX, 2.0, f64::INFINITY), + ] { + let ParameterValue::Fixed(actual) = ParameterValue::Fixed(value).scaled(factor) else { + panic!("fixed input must remain fixed"); + }; + assert_eq!(actual.to_bits(), expected.to_bits()); + } +} + +#[test] +fn scaled_symbolic_values_preserve_variant_and_binding_semantics() { + let input = ParameterValue::Param(Parameter::symbol("theta")); + for factor in [-0.25, 0.0, 1.0, 2.0] { + let ParameterValue::Param(actual) = input.scaled(factor) else { + panic!("symbolic input must remain Param, even when scaled by zero"); + }; + for theta in [-4.0, 0.0, 8.0] { + let bindings = Some(HashMap::from([("theta", theta)])); + assert_eq!(actual.evaluate(&bindings).unwrap(), theta * factor); + } + } + assert_eq!(input, ParameterValue::Param(Parameter::symbol("theta"))); +} + +#[test] +fn scaled_constant_expressions_remain_param() { + for parameter in [Parameter::from(2.0), Parameter::pi()] { + let value = parameter.evaluate(&None).unwrap(); + for factor in [-0.5, 0.0, 1.0] { + let ParameterValue::Param(actual) = + ParameterValue::Param(parameter.clone()).scaled(factor) + else { + panic!("constant expressions must not be converted to Fixed"); + }; + assert_eq!(actual.evaluate(&None).unwrap(), value * factor); + } + } +} + +#[test] +fn scaled_rejects_non_finite_factors_for_both_variants() { + for input in [ + ParameterValue::Fixed(2.0), + ParameterValue::Param(Parameter::symbol("theta")), + ] { + for factor in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] { + assert!(std::panic::catch_unwind(|| input.scaled(factor)).is_err()); + } + } +} diff --git a/crates/cqlib-core/src/circuit/control_flow/control_op.rs b/crates/cqlib-core/src/circuit/control_flow/control_op.rs index 2e8b4642..95761a2f 100644 --- a/crates/cqlib-core/src/circuit/control_flow/control_op.rs +++ b/crates/cqlib-core/src/circuit/control_flow/control_op.rs @@ -10,7 +10,7 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. -use super::{ForOp, IfOp, SwitchOp, WhileOp}; +use super::{ControlBody, ForOp, IfOp, SwitchCase, SwitchOp, WhileOp}; use crate::circuit::{ClassicalValue, ClassicalVar, Qubit}; use std::collections::BTreeSet; use std::fmt; @@ -58,6 +58,27 @@ pub enum ClassicalControlOp { } impl ClassicalControlOp { + /// Borrows direct child bodies without allocating or recursing. + /// + /// Visits then/else for `if`, the single body for loops, and cases in source + /// order followed by default for `switch`. Absent bodies are skipped, but + /// present empty bodies are retained. Break and continue have no bodies. + /// Conditions and iteration counts do not affect this structural order. + pub(crate) fn bodies(&self) -> impl Iterator + '_ { + let (first, cases, last): (Option<&ControlBody>, &[SwitchCase], Option<&ControlBody>) = + match self { + Self::If(op) => (Some(op.then_body()), &[], op.else_body()), + Self::While(op) => (Some(op.body()), &[], None), + Self::For(op) => (Some(op.body()), &[], None), + Self::Switch(op) => (None, op.cases(), op.default()), + Self::Break | Self::Continue => (None, &[], None), + }; + first + .into_iter() + .chain(cases.iter().map(SwitchCase::body)) + .chain(last) + } + /// Returns the stable category of this control-flow operation. pub const fn kind(&self) -> ClassicalControlKind { match self { @@ -120,19 +141,7 @@ impl ClassicalControlOp { /// Returns true when any structured body contains a measurement operation. pub fn has_measurement(&self) -> bool { - match self { - Self::If(op) => { - op.then_body().has_measurement() - || op.else_body().is_some_and(|body| body.has_measurement()) - } - Self::While(op) => op.body().has_measurement(), - Self::For(op) => op.body().has_measurement(), - Self::Switch(op) => { - op.cases().iter().any(|case| case.body().has_measurement()) - || op.default().is_some_and(|body| body.has_measurement()) - } - Self::Break | Self::Continue => false, - } + self.bodies().any(ControlBody::has_measurement) } /// Returns true when this operation's controlling expressions or structured @@ -142,19 +151,7 @@ impl ClassicalControlOp { return true; } - match self { - Self::If(op) => { - op.then_body().reads_value(value) - || op.else_body().is_some_and(|body| body.reads_value(value)) - } - Self::While(op) => op.body().reads_value(value), - Self::For(op) => op.body().reads_value(value), - Self::Switch(op) => { - op.cases().iter().any(|case| case.body().reads_value(value)) - || op.default().is_some_and(|body| body.reads_value(value)) - } - Self::Break | Self::Continue => false, - } + self.bodies().any(|body| body.reads_value(value)) } } @@ -168,10 +165,116 @@ impl fmt::Display for ClassicalControlOp { mod tests { use super::ClassicalControlOp; use crate::circuit::{ - CircuitId, ClassicalExpr, ClassicalType, ClassicalValue, ClassicalVar, ControlBody, ForOp, - IfOp, SwitchCase, SwitchOp, WhileOp, + Circuit, CircuitId, ClassicalExpr, ClassicalType, ClassicalValue, ClassicalVar, + ControlBody, ForOp, IfOp, Instruction, Qubit, SwitchCase, SwitchOp, WhileOp, }; + fn assert_bodies(control: &ClassicalControlOp, expected: &[&ControlBody]) { + let actual = control.bodies().collect::>(); + assert_eq!(actual.len(), expected.len()); + for (actual, expected) in actual.into_iter().zip(expected) { + assert!(std::ptr::eq(actual, *expected)); + } + } + + #[test] + fn bodies_preserve_optional_empty_branches_without_recursing() { + let mut nested = Circuit::new(1); + nested + .while_(ClassicalExpr::bool_literal(false), |body| { + body.x(Qubit::new(0)) + }) + .unwrap(); + for else_body in [None, Some(ControlBody::new(vec![]))] { + let control = ClassicalControlOp::If( + IfOp::new( + ClassicalExpr::bool_literal(false), + ControlBody::new(nested.operations().to_vec()), + else_body, + ) + .unwrap(), + ); + let ClassicalControlOp::If(op) = &control else { + unreachable!() + }; + if let Some(else_body) = op.else_body() { + assert_bodies(&control, &[op.then_body(), else_body]); + } else { + assert_bodies(&control, &[op.then_body()]); + } + assert!(matches!( + control.bodies().next().unwrap().operations()[0].instruction, + Instruction::ClassicalControl(ClassicalControlOp::While(_)) + )); + } + } + + #[test] + fn bodies_preserve_switch_source_order_and_optional_default() { + let empty = ControlBody::new(vec![]); + for cases in [ + vec![], + vec![ + SwitchCase::new(2, empty.clone()), + SwitchCase::new(0, empty.clone()), + ], + ] { + for default in [None, Some(empty.clone())] { + let control = ClassicalControlOp::Switch( + SwitchOp::new( + ClassicalExpr::uint_literal(2, 0).unwrap(), + cases.clone(), + default, + ) + .unwrap(), + ); + let ClassicalControlOp::Switch(op) = &control else { + unreachable!() + }; + let mut expected = Vec::new(); + if !op.cases().is_empty() { + // The source order is 2, 0, even though the target is 0. + expected.push(op.cases()[0].body()); + expected.push(op.cases()[1].body()); + } + if let Some(default) = op.default() { + expected.push(default); + } + assert_bodies(&control, &expected); + } + } + } + + #[test] + fn bodies_enumerate_loop_structure_independently_of_iteration_count() { + let var = ClassicalVar::new(CircuitId::new(), 0, ClassicalType::uint(8).unwrap()); + for stop in [0, 3] { + let control = ClassicalControlOp::For( + ForOp::new( + var, + ClassicalExpr::uint_literal(8, 0).unwrap(), + ClassicalExpr::uint_literal(8, stop).unwrap(), + ClassicalExpr::uint_literal(8, 1).unwrap(), + ControlBody::new(vec![]), + ) + .unwrap(), + ); + let ClassicalControlOp::For(op) = &control else { + unreachable!() + }; + assert_bodies(&control, &[op.body()]); + } + let control = ClassicalControlOp::While( + WhileOp::new(ClassicalExpr::bool_literal(false), ControlBody::new(vec![])).unwrap(), + ); + let ClassicalControlOp::While(op) = &control else { + unreachable!() + }; + assert_bodies(&control, &[op.body()]); + assert_bodies(&ClassicalControlOp::Break, &[]); + assert_bodies(&ClassicalControlOp::Continue, &[]); + } + #[test] fn break_and_continue_have_no_resource_dependencies() { assert!(ClassicalControlOp::Break.classical_var_reads().is_empty()); diff --git a/crates/cqlib-core/src/circuit/depth.rs b/crates/cqlib-core/src/circuit/depth.rs index c3a12f45..78a6cfe5 100644 --- a/crates/cqlib-core/src/circuit/depth.rs +++ b/crates/cqlib-core/src/circuit/depth.rs @@ -230,7 +230,7 @@ fn used_qubits_recursive(operations: &[Operation]) -> BTreeSet { for operation in operations { qubits.extend(operation.qubits.iter().copied()); if let Instruction::ClassicalControl(control) = &operation.instruction { - for body in control_bodies(control) { + for body in control.bodies() { qubits.extend(used_qubits_recursive(body.operations())); } } @@ -238,29 +238,6 @@ fn used_qubits_recursive(operations: &[Operation]) -> BTreeSet { qubits } -/// Borrows the structured bodies of a control-flow op for recursion. -fn control_bodies(control: &ClassicalControlOp) -> Vec<&ControlBody> { - match control { - ClassicalControlOp::If(op) => { - let mut bodies = vec![op.then_body()]; - if let Some(else_body) = op.else_body() { - bodies.push(else_body); - } - bodies - } - ClassicalControlOp::While(op) => vec![op.body()], - ClassicalControlOp::For(op) => vec![op.body()], - ClassicalControlOp::Switch(op) => { - let mut bodies: Vec<&ControlBody> = op.cases().iter().map(|case| case.body()).collect(); - if let Some(default) = op.default() { - bodies.push(default); - } - bodies - } - ClassicalControlOp::Break | ClassicalControlOp::Continue => Vec::new(), - } -} - /// Returns true iff `operations` contains any `ClassicalControl` instruction. /// `CircuitGate` interiors are not scanned (they are opaque sub-circuits, not /// control flow at this level); control flow nested inside another control diff --git a/crates/cqlib-core/src/compile/device_planning/planner.rs b/crates/cqlib-core/src/compile/device_planning/planner.rs index 8d830c99..b8367795 100644 --- a/crates/cqlib-core/src/compile/device_planning/planner.rs +++ b/crates/cqlib-core/src/compile/device_planning/planner.rs @@ -422,7 +422,7 @@ impl<'a> DevicePlanner<'a> { .filter(|child| self.plans[*child].is_none()) .filter(|child| seen.insert(*child)) .map(|child| DeviceLoweringDependency { - instruction: instruction_from_key(&self.states[child].instruction), + instruction: self.states[child].instruction.to_instruction(), qargs: self.states[child].ordered_qargs.to_vec(), }) .collect(); @@ -434,7 +434,7 @@ impl<'a> DevicePlanner<'a> { .collect(); DeviceLoweringFailure { - instruction: instruction_from_key(&state.instruction), + instruction: state.instruction.to_instruction(), qargs: state.ordered_qargs.to_vec(), attempted_candidates, } @@ -443,7 +443,7 @@ impl<'a> DevicePlanner<'a> { fn solve(&mut self) -> Result<(), DevicePlannerError> { for state_id in 0..self.states.len() { let state = self.states[state_id].clone(); - let instruction = instruction_from_key(&state.instruction); + let instruction = state.instruction.to_instruction(); if self .device .supports_native_instruction(&instruction, &state.ordered_qargs) @@ -786,7 +786,7 @@ impl<'a> GraphBuilder<'a> { &self, parent: &DeviceGateState, ) -> Result)>, String> { - let instruction = instruction_from_key(&parent.instruction); + let instruction = parent.instruction.to_instruction(); let rule_ids = self .library .candidates_for_first_instruction(&instruction) @@ -875,13 +875,6 @@ fn source_params_are_generic_and_distinct(params: &Option Instruction { - match key { - KnowledgeInstructionKey::Standard(gate) => Instruction::Standard(*gate), - KnowledgeInstructionKey::McGate(gate) => Instruction::McGate(Box::new(gate.clone())), - } -} - #[cfg(test)] #[path = "planner_test.rs"] mod planner_test; diff --git a/crates/cqlib-core/src/compile/knowledge/matcher.rs b/crates/cqlib-core/src/compile/knowledge/matcher.rs index a688b006..570d24bf 100644 --- a/crates/cqlib-core/src/compile/knowledge/matcher.rs +++ b/crates/cqlib-core/src/compile/knowledge/matcher.rs @@ -42,6 +42,14 @@ impl KnowledgeInstructionKey { } } + /// Reconstructs the instruction represented by this key. + pub(crate) fn to_instruction(&self) -> Instruction { + match self { + Self::Standard(gate) => Instruction::Standard(*gate), + Self::McGate(gate) => Instruction::McGate(Box::new(gate.clone())), + } + } + pub(crate) fn is_implicit(&self) -> bool { matches!(self, Self::Standard(StandardGate::GPhase)) } diff --git a/crates/cqlib-core/src/compile/knowledge/matcher_test.rs b/crates/cqlib-core/src/compile/knowledge/matcher_test.rs index cd4464f7..ead1817e 100644 --- a/crates/cqlib-core/src/compile/knowledge/matcher_test.rs +++ b/crates/cqlib-core/src/compile/knowledge/matcher_test.rs @@ -33,7 +33,15 @@ fn instruction_keys_support_standard_and_multi_controlled_gates() { StandardGate::X ))) ); - // assert!(!is_supported_instruction(&barrier)); + for instruction in [&x, &mcx] { + let key = KnowledgeInstructionKey::from_instruction(instruction).unwrap(); + let restored = key.to_instruction(); + assert_eq!(&restored, instruction); + assert_eq!( + KnowledgeInstructionKey::from_instruction(&restored), + Some(key) + ); + } assert!(KnowledgeInstructionKey::from_instruction(&barrier).is_none()); } diff --git a/crates/cqlib-core/src/compile/knowledge/rule_dsl/lower.rs b/crates/cqlib-core/src/compile/knowledge/rule_dsl/lower.rs index 6c976dc6..f3364e21 100644 --- a/crates/cqlib-core/src/compile/knowledge/rule_dsl/lower.rs +++ b/crates/cqlib-core/src/compile/knowledge/rule_dsl/lower.rs @@ -207,7 +207,7 @@ fn lower_gate_pattern(pattern: GatePattern) -> Result { let params = pattern .params .into_iter() - .map(lower_param_pattern) + .map(ParameterValue::from) .collect(); Ok(LoweredGate { @@ -233,13 +233,6 @@ fn lower_gate_spec(gate: GateSpec) -> Result { } } -fn lower_param_pattern(param: crate::circuit::Parameter) -> ParameterValue { - if let Ok(value) = param.evaluate(&None) { - return ParameterValue::Fixed(value); - } - ParameterValue::Param(param) -} - fn validate_unique_qubits(gate: &str, qubits: &[u32]) -> Result<(), LowerError> { let mut seen = SmallVec::<[u32; 3]>::new(); for &qubit in qubits { diff --git a/crates/cqlib-core/src/compile/knowledge/rule_equivalence_test.rs b/crates/cqlib-core/src/compile/knowledge/rule_equivalence_test.rs index 68b51902..2f5e7333 100644 --- a/crates/cqlib-core/src/compile/knowledge/rule_equivalence_test.rs +++ b/crates/cqlib-core/src/compile/knowledge/rule_equivalence_test.rs @@ -288,6 +288,77 @@ fn max_diff_strict_detects_global_phase_difference() { assert!(max_diff_strict(&lhs, &rhs) > 1.0); } +/// Builtin rewrites must preserve the entire unitary, even though the public +/// Rule::verify API deliberately also accepts equivalence up to global phase. +/// Evaluate the actual rule circuits, not copied decomposition formulas. +#[test] +fn verify_all_builtin_rules_strict_numeric() { + use crate::circuit::circuit_to_matrix::circuit_to_matrix; + + let probes = [ + 0.0, + PI / 2.0, + -PI / 2.0, + PI, + -PI, + 2.0 * PI, + -2.0 * PI, + 4.0 * PI, + -4.0 * PI, + 0.173, + -1.239, + 7.19, + ]; + for rule in RuleLibrary::builtin_rules().unwrap().rules() { + let lhs = rule_items_to_circuit(&rule.operations, rule.num_qubits()).unwrap(); + let rhs = rule_items_to_circuit(&rule.target, rule.num_qubits()).unwrap(); + let symbols = rule.collect_free_symbols(); + let names = symbols.iter().map(String::as_str).collect::>(); + let count = if names.is_empty() { 1 } else { probes.len() }; + for sample in 0..count { + let mut binding = names + .iter() + .enumerate() + .map(|(index, &name)| { + (name.to_owned(), probes[(sample + 3 * index) % probes.len()]) + }) + .collect::>(); + // Conditions can share symbols. Reapply them to settle the affine + // constraints, then explicitly check that this is a valid sample. + for _ in 0..4 { + for condition in rule.conditions.iter().flatten() { + adjust_binding_for_condition(&mut binding, condition, &names); + } + } + let binding_ref = Some(binding.iter().map(|(k, &v)| (k.as_str(), v)).collect()); + for condition in rule.conditions.iter().flatten() { + let (Condition::Eq(a, b) | Condition::EqMod(a, b, _)) = condition; + let delta = a.evaluate(&binding_ref).unwrap() - b.evaluate(&binding_ref).unwrap(); + let residual = match condition { + Condition::Eq(_, _) => delta, + Condition::EqMod(_, _, modulus) => { + let period = modulus.evaluate(&binding_ref).unwrap(); + delta - (delta / period).round() * period + } + }; + assert!( + residual.abs() < 1e-8, + "{}: invalid probe {binding:?}", + rule.name + ); + } + let a = circuit_to_matrix(&lhs.assign_parameters(&binding_ref).unwrap(), None).unwrap(); + let b = circuit_to_matrix(&rhs.assign_parameters(&binding_ref).unwrap(), None).unwrap(); + let error = max_diff_strict(&a, &b); + assert!( + error < 1e-9, + "{}: strict error {error}, probe {binding:?}", + rule.name + ); + } + } +} + /// Layered equivalence check for every rule in the builtin [`RuleLibrary`]. fn verify_all_builtin_rules_layered_equivalence() { let library = RuleLibrary::builtin_rules().unwrap(); diff --git a/crates/cqlib-core/src/compile/knowledge/rules/cancel.rule b/crates/cqlib-core/src/compile/knowledge/rules/cancel.rule index c9029b9b..98704f27 100644 --- a/crates/cqlib-core/src/compile/knowledge/rules/cancel.rule +++ b/crates/cqlib-core/src/compile/knowledge/rules/cancel.rule @@ -442,3 +442,16 @@ rule cancel_mcswap1_targets_swapped { match { MCSWAP[1] 0 1 2, MCSWAP[1] 0 2 1 } rewrite {} } + +// Equal-sign half turns about opposite axes cancel exactly. +rule cancel_xy2p_opposite_axis { + match { XY2P(a) 0, XY2P(b) 0 } + require { b - a == π mod 2*π } + rewrite { } +} + +rule cancel_xy2m_opposite_axis { + match { XY2M(a) 0, XY2M(b) 0 } + require { b - a == π mod 2*π } + rewrite { } +} diff --git a/crates/cqlib-core/src/compile/knowledge/rules/decompose_ccx.rule b/crates/cqlib-core/src/compile/knowledge/rules/decompose_ccx.rule index 6903f426..fddaa78d 100644 --- a/crates/cqlib-core/src/compile/knowledge/rules/decompose_ccx.rule +++ b/crates/cqlib-core/src/compile/knowledge/rules/decompose_ccx.rule @@ -121,7 +121,7 @@ rule decompose_ccx_to_rz_x2p_x_cz { CZ 0 1, X2P 1, RZ(π/2) 1, X2P 2, RZ(5*π/4) 2, X2P 2, RZ(π/2) 2, - GPhase(13*π/8) + GPhase(21*π/8) } } @@ -249,7 +249,7 @@ rule decompose_h_ccx_h_to_rz_x2p_cz { CZ 0 1, X2P 1, RZ(π/2) 1, X2P 2, RZ(3*π/4) 2, - GPhase(13*π/8) + GPhase(21*π/8) } } diff --git a/crates/cqlib-core/src/compile/knowledge/rules/decompose_controlled_pauli.rule b/crates/cqlib-core/src/compile/knowledge/rules/decompose_controlled_pauli.rule index dabefb13..03053273 100644 --- a/crates/cqlib-core/src/compile/knowledge/rules/decompose_controlled_pauli.rule +++ b/crates/cqlib-core/src/compile/knowledge/rules/decompose_controlled_pauli.rule @@ -51,7 +51,7 @@ rule decompose_cy_to_cry { rule decompose_cy_to_rx_ry_rzz { match { CY 0 1 } - rewrite { RX(-π/2) 1, RY(-π/2) 1, RX(π/2) 1, RY(π/2) 1, RX(π) 1, RX(-π/2) 0, RY(π/2) 0, RX(π/2) 0, RX(-π/2) 1, RY(π/2) 1, RX(π/2) 1, RZZ(-π/2) 0 1, RY(π/2) 1, RX(π) 1, RX(-π/2) 1, RY(π/2) 1, RX(π/2) 1, GPhase(π) } + rewrite { RX(-π/2) 1, RY(-π/2) 1, RX(π/2) 1, RY(π/2) 1, RX(π) 1, RX(-π/2) 0, RY(π/2) 0, RX(π/2) 0, RX(-π/2) 1, RY(π/2) 1, RX(π/2) 1, RZZ(-π/2) 0 1, RY(π/2) 1, RX(π) 1, RX(-π/2) 1, RY(π/2) 1, RX(π/2) 1, GPhase(5π/4) } } rule decompose_cy_direction_flip { diff --git a/crates/cqlib-core/src/compile/knowledge/rules/decompose_ising.rule b/crates/cqlib-core/src/compile/knowledge/rules/decompose_ising.rule index 1fd453ed..6b05f984 100644 --- a/crates/cqlib-core/src/compile/knowledge/rules/decompose_ising.rule +++ b/crates/cqlib-core/src/compile/knowledge/rules/decompose_ising.rule @@ -17,13 +17,13 @@ // RZZ-native lowering: CZ = RZ(π/2)·RZ(π/2)·RZZ(-π/2) up to global phase. rule decompose_cz_to_rzz { match { CZ 0 1 } - rewrite { RZ(π/2) 0, RZ(π/2) 1, RZZ(-π/2) 0 1 } + rewrite { RZ(π/2) 0, RZ(π/2) 1, RZZ(-π/2) 0 1, GPhase(π/4) } } // CX = H·CZ·H; suitable for {H, RZ, RZZ} or intermediate lowering (not pure {RZ, RZZ}). rule decompose_cx_to_rzz { match { CX 0 1 } - rewrite { H 1, RZ(π/2) 0, RZ(π/2) 1, RZZ(-π/2) 0 1, H 1 } + rewrite { H 1, RZ(π/2) 0, RZ(π/2) 1, RZZ(-π/2) 0 1, H 1, GPhase(π/4) } } // CY = SDG_target · CX · S_target; CX lowered via CZ/RZZ as above. @@ -36,7 +36,8 @@ rule decompose_cy_to_rzz { RZ(π/2) 1, RZZ(-π/2) 0 1, H 1, - S 1 + S 1, + GPhase(π/4) } } @@ -142,7 +143,7 @@ rule decompose_rzx_to_cz { rule decompose_cz_to_rx_ry_rzz { match { CZ 0 1 } - rewrite { RX(-π/2) 0, RY(π/2) 0, RX(π/2) 0, RX(-π/2) 1, RY(π/2) 1, RX(π/2) 1, RZZ(-π/2) 0 1 } + rewrite { RX(-π/2) 0, RY(π/2) 0, RX(π/2) 0, RX(-π/2) 1, RY(π/2) 1, RX(π/2) 1, RZZ(-π/2) 0 1, GPhase(π/4) } } rule decompose_cx_to_rx_ry_rzz { diff --git a/crates/cqlib-core/src/compile/knowledge/rules/decompose_mc_gate.rule b/crates/cqlib-core/src/compile/knowledge/rules/decompose_mc_gate.rule index 27da42f0..4461b35a 100644 --- a/crates/cqlib-core/src/compile/knowledge/rules/decompose_mc_gate.rule +++ b/crates/cqlib-core/src/compile/knowledge/rules/decompose_mc_gate.rule @@ -267,7 +267,7 @@ rule decompose_mccx0_to_cx { rule decompose_mccx0_to_rx_ry_rzz { match { MCCX[0] 0 1 } - rewrite { RY(π/2) 1, RX(π) 1, RX(-π/2) 0, RY(π/2) 0, RX(π/2) 0, RX(-π/2) 1, RY(π/2) 1, RX(π/2) 1, RZZ(-π/2) 0 1, RY(π/2) 1, RX(π) 1, GPhase(π) } + rewrite { RY(π/2) 1, RX(π) 1, RX(-π/2) 0, RY(π/2) 0, RX(π/2) 0, RX(-π/2) 1, RY(π/2) 1, RX(π/2) 1, RZZ(-π/2) 0 1, RY(π/2) 1, RX(π) 1, GPhase(5*π/4) } } rule decompose_mcccx0_to_ccx { @@ -292,7 +292,7 @@ rule decompose_mcccx0_to_rx_ry_rzz { RY(π/2) 1, RX(π) 1, RX(-π/2) 0, RY(π/2) 0, RX(π/2) 0, RX(-π/2) 1, RY(π/2) 1, RX(π/2) 1, RZZ(-π/2) 0 1, RY(π/2) 1, RX(π) 1, GPhase(π), RX(-π/2) 0, RY(π/4) 0, RX(π/2) 0, GPhase(π/8), RX(-π/2) 1, RY(-π/4) 1, RX(π/2) 1, GPhase(-π/8), - RY(π/2) 1, RX(π) 1, RX(-π/2) 0, RY(π/2) 0, RX(π/2) 0, RX(-π/2) 1, RY(π/2) 1, RX(π/2) 1, RZZ(-π/2) 0 1, RY(π/2) 1, RX(π) 1, GPhase(π) + RY(π/2) 1, RX(π) 1, RX(-π/2) 0, RY(π/2) 0, RX(π/2) 0, RX(-π/2) 1, RY(π/2) 1, RX(π/2) 1, RZZ(-π/2) 0 1, RY(π/2) 1, RX(π) 1, GPhase(π/2) } } @@ -303,7 +303,7 @@ rule decompose_mccy0_to_cy { rule decompose_mccy0_to_rx_ry_rzz { match { MCCY[0] 0 1 } - rewrite { RX(-π/2) 1, RY(-π/2) 1, RX(π/2) 1, RY(π/2) 1, RX(π) 1, RX(-π/2) 0, RY(π/2) 0, RX(π/2) 0, RX(-π/2) 1, RY(π/2) 1, RX(π/2) 1, RZZ(-π/2) 0 1, RY(π/2) 1, RX(π) 1, RX(-π/2) 1, RY(π/2) 1, RX(π/2) 1, GPhase(π) } + rewrite { RX(-π/2) 1, RY(-π/2) 1, RX(π/2) 1, RY(π/2) 1, RX(π) 1, RX(-π/2) 0, RY(π/2) 0, RX(π/2) 0, RX(-π/2) 1, RY(π/2) 1, RX(π/2) 1, RZZ(-π/2) 0 1, RY(π/2) 1, RX(π) 1, RX(-π/2) 1, RY(π/2) 1, RX(π/2) 1, GPhase(5*π/4) } } rule decompose_mccz0_to_cz { @@ -313,7 +313,7 @@ rule decompose_mccz0_to_cz { rule decompose_mccz0_to_rx_ry_rzz { match { MCCZ[0] 0 1 } - rewrite { RX(-π/2) 0, RY(π/2) 0, RX(π/2) 0, RX(-π/2) 1, RY(π/2) 1, RX(π/2) 1, RZZ(-π/2) 0 1 } + rewrite { RX(-π/2) 0, RY(π/2) 0, RX(π/2) 0, RX(-π/2) 1, RY(π/2) 1, RX(π/2) 1, RZZ(-π/2) 0 1, GPhase(π/4) } } rule decompose_mcfsim0_to_fsim { @@ -333,7 +333,7 @@ rule decompose_mcx1_to_cx { rule decompose_mcx1_to_rx_ry_rzz { match { MCX[1] 0 1 } - rewrite { RY(π/2) 1, RX(π) 1, RX(-π/2) 0, RY(π/2) 0, RX(π/2) 0, RX(-π/2) 1, RY(π/2) 1, RX(π/2) 1, RZZ(-π/2) 0 1, RY(π/2) 1, RX(π) 1, GPhase(π) } + rewrite { RY(π/2) 1, RX(π) 1, RX(-π/2) 0, RY(π/2) 0, RX(π/2) 0, RX(-π/2) 1, RY(π/2) 1, RX(π/2) 1, RZZ(-π/2) 0 1, RY(π/2) 1, RX(π) 1, GPhase(5*π/4) } } rule decompose_mcx2_to_ccx { @@ -358,7 +358,7 @@ rule decompose_mcx2_to_rx_ry_rzz { RY(π/2) 1, RX(π) 1, RX(-π/2) 0, RY(π/2) 0, RX(π/2) 0, RX(-π/2) 1, RY(π/2) 1, RX(π/2) 1, RZZ(-π/2) 0 1, RY(π/2) 1, RX(π) 1, GPhase(π), RX(-π/2) 0, RY(π/4) 0, RX(π/2) 0, GPhase(π/8), RX(-π/2) 1, RY(-π/4) 1, RX(π/2) 1, GPhase(-π/8), - RY(π/2) 1, RX(π) 1, RX(-π/2) 0, RY(π/2) 0, RX(π/2) 0, RX(-π/2) 1, RY(π/2) 1, RX(π/2) 1, RZZ(-π/2) 0 1, RY(π/2) 1, RX(π) 1, GPhase(π) + RY(π/2) 1, RX(π) 1, RX(-π/2) 0, RY(π/2) 0, RX(π/2) 0, RX(-π/2) 1, RY(π/2) 1, RX(π/2) 1, RZZ(-π/2) 0 1, RY(π/2) 1, RX(π) 1, GPhase(π/2) } } @@ -369,7 +369,7 @@ rule decompose_mcy1_to_cy { rule decompose_mcy1_to_rx_ry_rzz { match { MCY[1] 0 1 } - rewrite { RX(-π/2) 1, RY(-π/2) 1, RX(π/2) 1, RY(π/2) 1, RX(π) 1, RX(-π/2) 0, RY(π/2) 0, RX(π/2) 0, RX(-π/2) 1, RY(π/2) 1, RX(π/2) 1, RZZ(-π/2) 0 1, RY(π/2) 1, RX(π) 1, RX(-π/2) 1, RY(π/2) 1, RX(π/2) 1, GPhase(π) } + rewrite { RX(-π/2) 1, RY(-π/2) 1, RX(π/2) 1, RY(π/2) 1, RX(π) 1, RX(-π/2) 0, RY(π/2) 0, RX(π/2) 0, RX(-π/2) 1, RY(π/2) 1, RX(π/2) 1, RZZ(-π/2) 0 1, RY(π/2) 1, RX(π) 1, RX(-π/2) 1, RY(π/2) 1, RX(π/2) 1, GPhase(5*π/4) } } rule decompose_mcz1_to_cz { @@ -379,7 +379,7 @@ rule decompose_mcz1_to_cz { rule decompose_mcz1_to_rx_ry_rzz { match { MCZ[1] 0 1 } - rewrite { RX(-π/2) 0, RY(π/2) 0, RX(π/2) 0, RX(-π/2) 1, RY(π/2) 1, RX(π/2) 1, RZZ(-π/2) 0 1 } + rewrite { RX(-π/2) 0, RY(π/2) 0, RX(π/2) 0, RX(-π/2) 1, RY(π/2) 1, RX(π/2) 1, RZZ(-π/2) 0 1, GPhase(π/4) } } rule decompose_mcrx1_to_crx { @@ -436,7 +436,7 @@ rule decompose_mcy2_to_rx_ry_rzz { RX(-π/2) 0, RY(π/4) 0, RX(π/2) 0, GPhase(π/8), RX(-π/2) 1, RY(-π/4) 1, RX(π/2) 1, GPhase(-π/8), RY(π/2) 1, RX(π) 1, RX(-π/2) 0, RY(π/2) 0, RX(π/2) 0, RX(-π/2) 1, RY(π/2) 1, RX(π/2) 1, RZZ(-π/2) 0 1, RY(π/2) 1, RX(π) 1, GPhase(π), - RX(-π/2) 2, RY(π/2) 2, RX(π/2) 2, GPhase(π/4) + RX(-π/2) 2, RY(π/2) 2, RX(π/2) 2, GPhase(-π/4) } } @@ -464,7 +464,7 @@ rule decompose_mcz2_to_rx_ry_rzz { RX(-π/2) 0, RY(π/4) 0, RX(π/2) 0, GPhase(π/8), RX(-π/2) 1, RY(-π/4) 1, RX(π/2) 1, GPhase(-π/8), RY(π/2) 1, RX(π) 1, RX(-π/2) 0, RY(π/2) 0, RX(π/2) 0, RX(-π/2) 1, RY(π/2) 1, RX(π/2) 1, RZZ(-π/2) 0 1, RY(π/2) 1, RX(π) 1, GPhase(π), - RY(π/2) 2, RX(π) 2, GPhase(π/2) + RY(π/2) 2, RX(π) 2 } } @@ -475,7 +475,7 @@ rule decompose_mcs1_to_phase_cx { rule decompose_mcs1_to_rx_ry_rzz { match { MCS[1] 0 1 } - rewrite { RX(-π/2) 0, RY(π/4) 0, RX(π/2) 0, GPhase(π/8), RX(-π/2) 1, RY(π/4) 1, RX(π/2) 1, GPhase(π/8), RY(π/2) 1, RX(π) 1, RX(-π/2) 0, RY(π/2) 0, RX(π/2) 0, RX(-π/2) 1, RY(π/2) 1, RX(π/2) 1, RZZ(-π/2) 0 1, RY(π/2) 1, RX(π) 1, GPhase(π), RX(-π/2) 1, RY(-π/4) 1, RX(π/2) 1, GPhase(-π/8), RY(π/2) 1, RX(π) 1, RX(-π/2) 0, RY(π/2) 0, RX(π/2) 0, RX(-π/2) 1, RY(π/2) 1, RX(π/2) 1, RZZ(-π/2) 0 1, RY(π/2) 1, RX(π) 1, GPhase(π) } + rewrite { RX(-π/2) 0, RY(π/4) 0, RX(π/2) 0, GPhase(π/8), RX(-π/2) 1, RY(π/4) 1, RX(π/2) 1, GPhase(π/8), RY(π/2) 1, RX(π) 1, RX(-π/2) 0, RY(π/2) 0, RX(π/2) 0, RX(-π/2) 1, RY(π/2) 1, RX(π/2) 1, RZZ(-π/2) 0 1, RY(π/2) 1, RX(π) 1, GPhase(π), RX(-π/2) 1, RY(-π/4) 1, RX(π/2) 1, GPhase(-π/8), RY(π/2) 1, RX(π) 1, RX(-π/2) 0, RY(π/2) 0, RX(π/2) 0, RX(-π/2) 1, RY(π/2) 1, RX(π/2) 1, RZZ(-π/2) 0 1, RY(π/2) 1, RX(π) 1, GPhase(3*π/2) } } rule decompose_mcsdg1_to_phase_cx { @@ -485,7 +485,7 @@ rule decompose_mcsdg1_to_phase_cx { rule decompose_mcsdg1_to_rx_ry_rzz { match { MCSDG[1] 0 1 } - rewrite { RX(-π/2) 0, RY(-π/4) 0, RX(π/2) 0, GPhase(-π/8), RX(-π/2) 1, RY(-π/4) 1, RX(π/2) 1, GPhase(-π/8), RY(π/2) 1, RX(π) 1, RX(-π/2) 0, RY(π/2) 0, RX(π/2) 0, RX(-π/2) 1, RY(π/2) 1, RX(π/2) 1, RZZ(-π/2) 0 1, RY(π/2) 1, RX(π) 1, GPhase(π), RX(-π/2) 1, RY(π/4) 1, RX(π/2) 1, GPhase(π/8), RY(π/2) 1, RX(π) 1, RX(-π/2) 0, RY(π/2) 0, RX(π/2) 0, RX(-π/2) 1, RY(π/2) 1, RX(π/2) 1, RZZ(-π/2) 0 1, RY(π/2) 1, RX(π) 1, GPhase(π) } + rewrite { RX(-π/2) 0, RY(-π/4) 0, RX(π/2) 0, GPhase(-π/8), RX(-π/2) 1, RY(-π/4) 1, RX(π/2) 1, GPhase(-π/8), RY(π/2) 1, RX(π) 1, RX(-π/2) 0, RY(π/2) 0, RX(π/2) 0, RX(-π/2) 1, RY(π/2) 1, RX(π/2) 1, RZZ(-π/2) 0 1, RY(π/2) 1, RX(π) 1, GPhase(π), RX(-π/2) 1, RY(π/4) 1, RX(π/2) 1, GPhase(π/8), RY(π/2) 1, RX(π) 1, RX(-π/2) 0, RY(π/2) 0, RX(π/2) 0, RX(-π/2) 1, RY(π/2) 1, RX(π/2) 1, RZZ(-π/2) 0 1, RY(π/2) 1, RX(π) 1, GPhase(3*π/2) } } rule decompose_mct1_to_phase_cx { @@ -495,7 +495,7 @@ rule decompose_mct1_to_phase_cx { rule decompose_mct1_to_rx_ry_rzz { match { MCT[1] 0 1 } - rewrite { RX(-π/2) 0, RY(π/8) 0, RX(π/2) 0, GPhase(π/16), RX(-π/2) 1, RY(π/8) 1, RX(π/2) 1, GPhase(π/16), RY(π/2) 1, RX(π) 1, RX(-π/2) 0, RY(π/2) 0, RX(π/2) 0, RX(-π/2) 1, RY(π/2) 1, RX(π/2) 1, RZZ(-π/2) 0 1, RY(π/2) 1, RX(π) 1, GPhase(π), RX(-π/2) 1, RY(-π/8) 1, RX(π/2) 1, GPhase(-π/16), RY(π/2) 1, RX(π) 1, RX(-π/2) 0, RY(π/2) 0, RX(π/2) 0, RX(-π/2) 1, RY(π/2) 1, RX(π/2) 1, RZZ(-π/2) 0 1, RY(π/2) 1, RX(π) 1, GPhase(π) } + rewrite { RX(-π/2) 0, RY(π/8) 0, RX(π/2) 0, GPhase(π/16), RX(-π/2) 1, RY(π/8) 1, RX(π/2) 1, GPhase(π/16), RY(π/2) 1, RX(π) 1, RX(-π/2) 0, RY(π/2) 0, RX(π/2) 0, RX(-π/2) 1, RY(π/2) 1, RX(π/2) 1, RZZ(-π/2) 0 1, RY(π/2) 1, RX(π) 1, GPhase(π), RX(-π/2) 1, RY(-π/8) 1, RX(π/2) 1, GPhase(-π/16), RY(π/2) 1, RX(π) 1, RX(-π/2) 0, RY(π/2) 0, RX(π/2) 0, RX(-π/2) 1, RY(π/2) 1, RX(π/2) 1, RZZ(-π/2) 0 1, RY(π/2) 1, RX(π) 1, GPhase(3*π/2) } } rule decompose_mctdg1_to_phase_cx { @@ -505,7 +505,7 @@ rule decompose_mctdg1_to_phase_cx { rule decompose_mctdg1_to_rx_ry_rzz { match { MCTDG[1] 0 1 } - rewrite { RX(-π/2) 0, RY(-π/8) 0, RX(π/2) 0, GPhase(-π/16), RX(-π/2) 1, RY(-π/8) 1, RX(π/2) 1, GPhase(-π/16), RY(π/2) 1, RX(π) 1, RX(-π/2) 0, RY(π/2) 0, RX(π/2) 0, RX(-π/2) 1, RY(π/2) 1, RX(π/2) 1, RZZ(-π/2) 0 1, RY(π/2) 1, RX(π) 1, GPhase(π), RX(-π/2) 1, RY(π/8) 1, RX(π/2) 1, GPhase(π/16), RY(π/2) 1, RX(π) 1, RX(-π/2) 0, RY(π/2) 0, RX(π/2) 0, RX(-π/2) 1, RY(π/2) 1, RX(π/2) 1, RZZ(-π/2) 0 1, RY(π/2) 1, RX(π) 1, GPhase(π) } + rewrite { RX(-π/2) 0, RY(-π/8) 0, RX(π/2) 0, GPhase(-π/16), RX(-π/2) 1, RY(-π/8) 1, RX(π/2) 1, GPhase(-π/16), RY(π/2) 1, RX(π) 1, RX(-π/2) 0, RY(π/2) 0, RX(π/2) 0, RX(-π/2) 1, RY(π/2) 1, RX(π/2) 1, RZZ(-π/2) 0 1, RY(π/2) 1, RX(π) 1, GPhase(π), RX(-π/2) 1, RY(π/8) 1, RX(π/2) 1, GPhase(π/16), RY(π/2) 1, RX(π) 1, RX(-π/2) 0, RY(π/2) 0, RX(π/2) 0, RX(-π/2) 1, RY(π/2) 1, RX(π/2) 1, RZZ(-π/2) 0 1, RY(π/2) 1, RX(π) 1, GPhase(3*π/2) } } rule decompose_mcphase1_to_phase_cx { @@ -533,7 +533,7 @@ rule decompose_mcphase1_to_rz_x2p_cz { X2P 1, RZ(π) 1, CZ 0 1, X2P 1, RZ(π/2) 1, RZ(theta/2) 1, - GPhase(π + theta/4) + GPhase(theta/4) } } @@ -575,7 +575,7 @@ rule decompose_mcswap1_to_rz_x2p_cz { X2P 2, RZ(5*π/4) 2, X2P 2, RZ(π/2) 2, CZ 2 1, X2P 1, RZ(π/2) 1, - GPhase(15*π/8) + GPhase(11*π/8) } } @@ -594,7 +594,7 @@ rule decompose_mcswap1_to_rz_x2p_cx { CX 0 1, RZ(3*π/4) 2, X2P 2, RZ(π/2) 2, CX 2 1, - GPhase(7*π/8) + GPhase(3*π/8) } } @@ -666,6 +666,5 @@ rule decompose_mcx3_to_parity_phase { CX 0 3, CX 2 3, RZ(π/8) 3, CX 2 3, CX 0 3, CX 1 3, CX 2 3, RZ(π/8) 3, CX 2 3, CX 1 3, CX 0 3, CX 1 3, CX 2 3, RZ(-π/8) 3, CX 2 3, CX 1 3, CX 0 3, - H 3 - } + H 3, GPhase(π/16) } } diff --git a/crates/cqlib-core/src/compile/knowledge/rules/decompose_phase.rule b/crates/cqlib-core/src/compile/knowledge/rules/decompose_phase.rule index 68a0d67f..12b94b2c 100644 --- a/crates/cqlib-core/src/compile/knowledge/rules/decompose_phase.rule +++ b/crates/cqlib-core/src/compile/knowledge/rules/decompose_phase.rule @@ -19,6 +19,12 @@ rule decompose_phase_to_rz { rewrite { RZ(theta) 0, GPhase(theta/2) } } +// Keep phase-specialized U gates lowerable to a U-native target basis. +rule decompose_phase_to_u { + match { Phase(theta) 0 } + rewrite { U(0, 0, theta) 0 } +} + rule decompose_phase_to_rx_ry { match { Phase(theta) 0 } rewrite { RX(-π/2) 0, RY(theta) 0, RX(π/2) 0, GPhase(theta/2) } diff --git a/crates/cqlib-core/src/compile/knowledge/rules/decompose_qcis.rule b/crates/cqlib-core/src/compile/knowledge/rules/decompose_qcis.rule index 15c404d1..37f3516f 100644 --- a/crates/cqlib-core/src/compile/knowledge/rules/decompose_qcis.rule +++ b/crates/cqlib-core/src/compile/knowledge/rules/decompose_qcis.rule @@ -209,7 +209,7 @@ rule qcis_fsim_to_cz { // CCX → CZ + RZ + single-qubit QCIS gates rule qcis_ccx_to_cz { match { CCX 0 1 2 } - rewrite { RZ(π) 2, Y2P 2, RZ(π) 2, Y2P 2, CZ 1 2, RZ(π) 2, Y2P 2, RZ(-π/4) 2, RZ(π) 2, Y2P 2, CZ 0 2, RZ(π) 2, Y2P 2, RZ(π/4) 2, RZ(π) 2, Y2P 2, CZ 1 2, RZ(π) 2, Y2P 2, RZ(-π/4) 2, RZ(π) 2, Y2P 2, CZ 0 2, RZ(π) 2, Y2P 2, RZ(π/4) 1, RZ(π/4) 2, RZ(π) 2, Y2P 2, RZ(π) 1, Y2P 1, CZ 0 1, RZ(π) 1, Y2P 1, RZ(π/4) 0, RZ(-π/4) 1, RZ(π) 1, Y2P 1, CZ 0 1, RZ(π) 1, Y2P 1, GPhase(53π/8) } + rewrite { RZ(π) 2, Y2P 2, RZ(π) 2, Y2P 2, CZ 1 2, RZ(π) 2, Y2P 2, RZ(-π/4) 2, RZ(π) 2, Y2P 2, CZ 0 2, RZ(π) 2, Y2P 2, RZ(π/4) 2, RZ(π) 2, Y2P 2, CZ 1 2, RZ(π) 2, Y2P 2, RZ(-π/4) 2, RZ(π) 2, Y2P 2, CZ 0 2, RZ(π) 2, Y2P 2, RZ(π/4) 1, RZ(π/4) 2, RZ(π) 2, Y2P 2, RZ(π) 1, Y2P 1, CZ 0 1, RZ(π) 1, Y2P 1, RZ(π/4) 0, RZ(-π/4) 1, RZ(π) 1, Y2P 1, CZ 0 1, RZ(π) 1, Y2P 1, GPhase(57*π/8) } } // 5. QCIS native gate interconversion @@ -285,3 +285,34 @@ rule qcis_xy2m_to_y2m { match { XY2M(phi) 0 } rewrite { RZ(-phi + π/2) 0, Y2M 0, RZ(phi - π/2) 0 } } + +// Compact native decompositions; retain alternatives for restricted target bases. +rule qcis_cx_to_cz_y2m_y2p { + match { CX 0 1 } + rewrite { Y2M 1, CZ 0 1, Y2P 1 } +} + +rule qcis_cy_to_cz_x2p_x2m { + match { CY 0 1 } + rewrite { X2P 1, CZ 0 1, X2M 1 } +} + +rule qcis_rxy_to_phased_xy2p_rz { + match { RXY(theta, phi) 0 } + rewrite { XY2M(phi + π/2) 0, RZ(theta) 0, XY2P(phi + π/2) 0 } +} + +rule qcis_u_to_phased_xy2p_rz { + match { U(theta, phi, lambda) 0 } + rewrite { XY2P(-lambda) 0, XY2M(-lambda - theta) 0, RZ(lambda + theta + phi) 0, GPhase((phi + lambda)/2) } +} + +rule qcis_xy2m_to_xy2p { + match { XY2M(phi) 0 } + rewrite { XY2P(phi + π) 0 } +} + +rule qcis_rzz_to_cz_y2m_y2p { + match { RZZ(theta) 0 1 } + rewrite { Y2M 1, CZ 0 1, Y2P 1, RZ(theta) 1, Y2M 1, CZ 0 1, Y2P 1 } +} diff --git a/crates/cqlib-core/src/compile/knowledge/rules/identity.rule b/crates/cqlib-core/src/compile/knowledge/rules/identity.rule index c98a6dd0..35fc14ec 100644 --- a/crates/cqlib-core/src/compile/knowledge/rules/identity.rule +++ b/crates/cqlib-core/src/compile/knowledge/rules/identity.rule @@ -471,3 +471,233 @@ rule move_x1_cz { match { X 1, CZ 0 1 } rewrite { CZ 0 1, Z 0, X 1 } } + +// Shortening identities, in circuit execution order; preserve scalar phases. +rule identity_rz_xy_absorb { + match { RZ(a) 0, XY(b) 0 } + rewrite { XY(b - a/2) 0 } +} + +rule identity_xy_rz_absorb { + match { XY(b) 0, RZ(a) 0 } + rewrite { XY(b + a/2) 0 } +} + +rule compose_rz_xy2p_rz { + match { RZ(a) 0, XY2P(b) 0, RZ(c) 0 } + rewrite { XY2P(b - a) 0, RZ(a + c) 0 } +} + +rule compose_rz_xy2p_rz_cancel { + match { RZ(a) 0, XY2P(b) 0, RZ(c) 0 } + require { a + c == 0 mod 4*π } + rewrite { XY2P(b - a) 0 } +} + +rule compose_rz_xy2p_rz_global_phase { + match { RZ(a) 0, XY2P(b) 0, RZ(c) 0 } + require { a + c == 2*π mod 4*π } + rewrite { XY2P(b - a) 0, GPhase(π) } +} + +rule compose_rz_xy2m_rz { + match { RZ(a) 0, XY2M(b) 0, RZ(c) 0 } + rewrite { XY2M(b - a) 0, RZ(a + c) 0 } +} + +rule compose_rz_xy2m_rz_cancel { + match { RZ(a) 0, XY2M(b) 0, RZ(c) 0 } + require { a + c == 0 mod 4*π } + rewrite { XY2M(b - a) 0 } +} + +rule compose_rz_xy2m_rz_global_phase { + match { RZ(a) 0, XY2M(b) 0, RZ(c) 0 } + require { a + c == 2*π mod 4*π } + rewrite { XY2M(b - a) 0, GPhase(π) } +} + +rule compose_rz_rxy_rz { + match { RZ(a) 0, RXY(theta, b) 0, RZ(c) 0 } + rewrite { RXY(theta, b - a) 0, RZ(a + c) 0 } +} + +rule compose_rz_rxy_rz_cancel { + match { RZ(a) 0, RXY(theta, b) 0, RZ(c) 0 } + require { a + c == 0 mod 4*π } + rewrite { RXY(theta, b - a) 0 } +} + +rule compose_rz_rxy_rz_global_phase { + match { RZ(a) 0, RXY(theta, b) 0, RZ(c) 0 } + require { a + c == 2*π mod 4*π } + rewrite { RXY(theta, b - a) 0, GPhase(π) } +} + +rule compose_x2p_rz_x2m { + match { X2P 0, RZ(a) 0, X2M 0 } + rewrite { RY(a) 0 } +} + +rule compose_x2m_rz_x2p { + match { X2M 0, RZ(a) 0, X2P 0 } + rewrite { RY(-a) 0 } +} + +rule compose_y2m_rz_y2p { + match { Y2M 0, RZ(a) 0, Y2P 0 } + rewrite { RX(a) 0 } +} + +rule compose_y2p_rz_y2m { + match { Y2P 0, RZ(a) 0, Y2M 0 } + rewrite { RX(-a) 0 } +} + +rule identity_x2p_rz_pi_echo { + match { X2P 0, RZ(a) 0, X2P 0 } + require { a == π mod 2*π } + rewrite { RZ(a) 0 } +} + +rule identity_x2m_rz_pi_echo { + match { X2M 0, RZ(a) 0, X2M 0 } + require { a == π mod 2*π } + rewrite { RZ(a) 0 } +} + +rule identity_y2p_rz_pi_echo { + match { Y2P 0, RZ(a) 0, Y2P 0 } + require { a == π mod 2*π } + rewrite { RZ(a) 0 } +} + +rule identity_y2m_rz_pi_echo { + match { Y2M 0, RZ(a) 0, Y2M 0 } + require { a == π mod 2*π } + rewrite { RZ(a) 0 } +} + +rule identity_xy2p_rz_pi_echo { + match { XY2P(phi) 0, RZ(a) 0, XY2P(phi) 0 } + require { a == π mod 2*π } + rewrite { RZ(a) 0 } +} + +rule identity_xy2m_rz_pi_echo { + match { XY2M(phi) 0, RZ(a) 0, XY2M(phi) 0 } + require { a == π mod 2*π } + rewrite { RZ(a) 0 } +} + +// CZ conjugation of pi pulses removes both entanglers; include native RZ targets. +rule identity_cz_y0_cz { + match { CZ 0 1, Y 0, CZ 0 1 } + rewrite { Y 0, Z 1 } +} + +rule identity_cz_y0_cz_to_rz { + match { CZ 0 1, Y 0, CZ 0 1 } + rewrite { Y 0, RZ(π) 1, GPhase(π/2) } +} + +rule identity_cz_y0_cz_swapped { + match { CZ 0 1, Y 0, CZ 1 0 } + rewrite { Y 0, Z 1 } +} + +rule identity_cz_y0_cz_to_rz_swapped { + match { CZ 0 1, Y 0, CZ 1 0 } + rewrite { Y 0, RZ(π) 1, GPhase(π/2) } +} + +rule identity_cz_y1_cz { + match { CZ 0 1, Y 1, CZ 0 1 } + rewrite { Y 1, Z 0 } +} + +rule identity_cz_y1_cz_to_rz { + match { CZ 0 1, Y 1, CZ 0 1 } + rewrite { Y 1, RZ(π) 0, GPhase(π/2) } +} + +rule identity_cz_y1_cz_swapped { + match { CZ 0 1, Y 1, CZ 1 0 } + rewrite { Y 1, Z 0 } +} + +rule identity_cz_y1_cz_to_rz_swapped { + match { CZ 0 1, Y 1, CZ 1 0 } + rewrite { Y 1, RZ(π) 0, GPhase(π/2) } +} + +rule identity_cz_xy0_cz { + match { CZ 0 1, XY(phi) 0, CZ 0 1 } + rewrite { XY(phi) 0, Z 1 } +} + +rule identity_cz_xy0_cz_to_rz { + match { CZ 0 1, XY(phi) 0, CZ 0 1 } + rewrite { XY(phi) 0, RZ(π) 1, GPhase(π/2) } +} + +rule identity_cz_xy0_cz_swapped { + match { CZ 0 1, XY(phi) 0, CZ 1 0 } + rewrite { XY(phi) 0, Z 1 } +} + +rule identity_cz_xy0_cz_to_rz_swapped { + match { CZ 0 1, XY(phi) 0, CZ 1 0 } + rewrite { XY(phi) 0, RZ(π) 1, GPhase(π/2) } +} + +rule identity_cz_xy1_cz { + match { CZ 0 1, XY(phi) 1, CZ 0 1 } + rewrite { XY(phi) 1, Z 0 } +} + +rule identity_cz_xy1_cz_to_rz { + match { CZ 0 1, XY(phi) 1, CZ 0 1 } + rewrite { XY(phi) 1, RZ(π) 0, GPhase(π/2) } +} + +rule identity_cz_xy1_cz_swapped { + match { CZ 0 1, XY(phi) 1, CZ 1 0 } + rewrite { XY(phi) 1, Z 0 } +} + +rule identity_cz_xy1_cz_to_rz_swapped { + match { CZ 0 1, XY(phi) 1, CZ 1 0 } + rewrite { XY(phi) 1, RZ(π) 0, GPhase(π/2) } +} + +// Also accept Z after the central RZ(pi) has been specialized. +rule identity_x2p_z_echo { + match { X2P 0, Z 0, X2P 0 } + rewrite { Z 0 } +} + +rule identity_x2m_z_echo { + match { X2M 0, Z 0, X2M 0 } + rewrite { Z 0 } +} + +rule identity_y2p_z_echo { + match { Y2P 0, Z 0, Y2P 0 } + rewrite { Z 0 } +} + +rule identity_y2m_z_echo { + match { Y2M 0, Z 0, Y2M 0 } + rewrite { Z 0 } +} + +rule identity_xy2p_z_echo { + match { XY2P(phi) 0, Z 0, XY2P(phi) 0 } + rewrite { Z 0 } +} + +rule identity_xy2m_z_echo { + match { XY2M(phi) 0, Z 0, XY2M(phi) 0 } + rewrite { Z 0 } +} diff --git a/crates/cqlib-core/src/compile/knowledge/rules/merge.rule b/crates/cqlib-core/src/compile/knowledge/rules/merge.rule index 188f9961..2e6ae525 100644 --- a/crates/cqlib-core/src/compile/knowledge/rules/merge.rule +++ b/crates/cqlib-core/src/compile/knowledge/rules/merge.rule @@ -263,36 +263,21 @@ rule merge_y2m_to_y { } rule merge_rxy { - match { - RXY(a, phi) 0, - RXY(b, phi) 0 - } - - rewrite { - RXY(a + b, phi) 0 - } + match { RXY(a, phi_a) 0, RXY(b, phi_b) 0 } + require { phi_b - phi_a == 0 mod 2*π } + rewrite { RXY(a + b, phi_a) 0 } } rule merge_xy2p { - match { - XY2P(a) 0, - XY2P(a) 0 - } - - rewrite { - XY(a) 0 - } + match { XY2P(a) 0, XY2P(b) 0 } + require { b - a == 0 mod 2*π } + rewrite { XY(a) 0 } } rule merge_xy2m { - match { - XY2M(a) 0, - XY2M(a) 0 - } - - rewrite { - XY(π + a) 0 - } + match { XY2M(a) 0, XY2M(b) 0 } + require { b - a == 0 mod 2*π } + rewrite { XY(π + a) 0 } } rule merge_fsim { @@ -311,3 +296,15 @@ rule merge_xy2m_xy2p_to_xy { require { b - a == π mod 2*π } rewrite { XY(π + a) 0 } } + +// General equatorial pi-pulse products and opposite-axis rotations. +rule merge_xy_pair_to_rz { + match { XY(a) 0, XY(b) 0 } + rewrite { RZ(2*(b - a)) 0, GPhase(π) } +} + +rule merge_rxy_opposite_axis { + match { RXY(a, phi_a) 0, RXY(b, phi_b) 0 } + require { phi_b - phi_a == π mod 2*π } + rewrite { RXY(a - b, phi_a) 0 } +} diff --git a/crates/cqlib-core/src/compile/knowledge/rules/normalize.rule b/crates/cqlib-core/src/compile/knowledge/rules/normalize.rule index 088bd2b3..8fd65e2b 100644 --- a/crates/cqlib-core/src/compile/knowledge/rules/normalize.rule +++ b/crates/cqlib-core/src/compile/knowledge/rules/normalize.rule @@ -183,3 +183,10 @@ rule normalize_crz_2pi { require { a == 2*π mod 4*π } rewrite { Phase(π) 0 } } + +// A full RXY turn is minus identity, independently of its axis. +rule normalize_rxy_2pi { + match { RXY(a, phi) 0 } + require { a == 2*π mod 4*π } + rewrite { GPhase(π) } +} diff --git a/crates/cqlib-core/src/compile/knowledge/rules/specialize.rule b/crates/cqlib-core/src/compile/knowledge/rules/specialize.rule index 9bb16539..95c00abb 100644 --- a/crates/cqlib-core/src/compile/knowledge/rules/specialize.rule +++ b/crates/cqlib-core/src/compile/knowledge/rules/specialize.rule @@ -371,3 +371,58 @@ rule specialize_fsim_to_cz { require { a == 0 mod 2*π, b == π mod 2*π } rewrite { CZ 0 1 } } + +// Arbitrary-axis pulse specialization; rotation and axis periods differ. +rule specialize_rxy_pi_2_to_xy2p { + match { RXY(a, phi) 0 } + require { a == π/2 mod 4*π } + rewrite { XY2P(phi) 0 } +} + +rule specialize_rxy_minus_pi_2_to_xy2m { + match { RXY(a, phi) 0 } + require { a == -π/2 mod 4*π } + rewrite { XY2M(phi) 0 } +} + +rule specialize_rxy_pi_to_xy { + match { RXY(a, phi) 0 } + require { a == π mod 4*π } + rewrite { XY(phi) 0 } +} + +rule specialize_rxy_axis_x { + match { RXY(a, phi) 0 } + require { phi == 0 mod 2*π } + rewrite { RX(a) 0 } +} + +rule specialize_rxy_axis_minus_x { + match { RXY(a, phi) 0 } + require { phi == π mod 2*π } + rewrite { RX(-a) 0 } +} + +rule specialize_rxy_axis_y { + match { RXY(a, phi) 0 } + require { phi == π/2 mod 2*π } + rewrite { RY(a) 0 } +} + +rule specialize_rxy_axis_minus_y { + match { RXY(a, phi) 0 } + require { phi == -π/2 mod 2*π } + rewrite { RY(-a) 0 } +} + +rule specialize_xy2p_pi_to_x2m { + match { XY2P(a) 0 } + require { a == π mod 2*π } + rewrite { X2M 0 } +} + +rule specialize_xy2m_pi_to_x2p { + match { XY2M(a) 0 } + require { a == π mod 2*π } + rewrite { X2P 0 } +} diff --git a/crates/cqlib-core/src/compile/mod.rs b/crates/cqlib-core/src/compile/mod.rs index d44533f3..57388b1d 100644 --- a/crates/cqlib-core/src/compile/mod.rs +++ b/crates/cqlib-core/src/compile/mod.rs @@ -264,6 +264,7 @@ pub mod compiler; pub(crate) mod device_planning; pub mod error; pub mod knowledge; +pub(crate) mod numeric_matrix; pub(crate) mod parallelism; pub mod physical_target; pub mod resource; diff --git a/crates/cqlib-core/src/compile/numeric_matrix.rs b/crates/cqlib-core/src/compile/numeric_matrix.rs new file mode 100644 index 00000000..8abcf56a --- /dev/null +++ b/crates/cqlib-core/src/compile/numeric_matrix.rs @@ -0,0 +1,65 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! Target-independent numeric matrix reconstruction for compiler passes. + +use crate::circuit::{Instruction, ParameterValue, ValueInstruction, ValueOperation}; +use ndarray::Array2; +use num_complex::Complex64; + +/// Reconstructs a one-qubit run in circuit execution order, including its +/// global phase: the result is `U_last * ... * U_first`. An empty run is `I₂`. +/// +/// Returns `None` unless every operation is a standard one-qubit gate with +/// exactly one qarg, all qargs agree, and each gate has exactly the required +/// number of finite [`ParameterValue::Fixed`] parameters. Matrix construction +/// failures also return `None`; symbolic parameters are never evaluated. +/// +/// The caller must collect the run within a single logical/control-flow scope +/// and establish that any intervening operations may be crossed. Value +/// operations do not record their owning scope, so this cannot be checked here. +/// Labels, run boundaries, device feasibility, fusion costs, and the destination +/// of any subsequently extracted phase remain the caller's responsibility. +pub(crate) fn one_qubit_run_matrix(operations: &[ValueOperation]) -> Option> { + let mut matrix = Array2::::eye(2); + let mut run_qubit = None; + for operation in operations { + let ValueInstruction::Instruction(Instruction::Standard(gate)) = &operation.instruction + else { + return None; + }; + if gate.num_qubits() != 1 || operation.params.len() != gate.num_params() { + return None; + } + let [qubit] = operation.qubits.as_slice() else { + return None; + }; + if run_qubit.is_some_and(|previous| previous != *qubit) { + return None; + } + run_qubit = Some(*qubit); + let params = operation + .params + .iter() + .map(|param| match param { + ParameterValue::Fixed(value) if value.is_finite() => Some(*value), + ParameterValue::Fixed(_) | ParameterValue::Param(_) => None, + }) + .collect::>>()?; + matrix = gate.matrix(¶ms).ok()?.dot(&matrix); + } + Some(matrix) +} + +#[cfg(test)] +#[path = "numeric_matrix_test.rs"] +mod numeric_matrix_test; diff --git a/crates/cqlib-core/src/compile/numeric_matrix_test.rs b/crates/cqlib-core/src/compile/numeric_matrix_test.rs new file mode 100644 index 00000000..4a915eb0 --- /dev/null +++ b/crates/cqlib-core/src/compile/numeric_matrix_test.rs @@ -0,0 +1,148 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +use super::one_qubit_run_matrix; +use crate::circuit::{ + Directive, Instruction, Parameter, ParameterValue, Qubit, StandardGate, + ValueClassicalControlOp, ValueInstruction, ValueOperation, +}; +use ndarray::{Array2, array}; +use num_complex::Complex64; +use std::f64::consts::{FRAC_1_SQRT_2, PI}; + +fn operation(gate: StandardGate, params: &[f64]) -> ValueOperation { + ValueOperation::from_standard( + gate, + [Qubit::new(0)], + params.iter().copied().map(ParameterValue::Fixed), + ) +} + +fn assert_matrix(operations: &[ValueOperation], expected: Array2) { + let actual = one_qubit_run_matrix(operations).expect("valid one-qubit run"); + assert_eq!(actual.dim(), expected.dim()); + for (actual, expected) in actual.iter().zip(expected.iter()) { + assert!((actual - expected).norm() < 1e-12, "{actual} != {expected}"); + } +} + +#[test] +fn empty_run_is_identity() { + assert_matrix(&[], Array2::eye(2)); +} + +#[test] +fn noncommuting_gates_follow_circuit_execution_order() { + // H followed by Z negates the second row, not the second column. + assert_matrix( + &[ + operation(StandardGate::H, &[]), + operation(StandardGate::Z, &[]), + ], + array![[1.0, 1.0], [-1.0, 1.0]].mapv(|value| Complex64::new(value * FRAC_1_SQRT_2, 0.0)), + ); +} + +#[test] +fn scalar_phase_is_preserved() { + assert_matrix( + &[ + operation(StandardGate::X, &[]), + operation(StandardGate::Y, &[]), + operation(StandardGate::X, &[]), + operation(StandardGate::Y, &[]), + ], + -Array2::eye(2), + ); +} + +#[test] +fn fixed_rotation_is_reconstructed_without_interpreting_labels() { + let mut rz = operation(StandardGate::RZ, &[PI]); + rz.label = Some("preserve-at-pass-boundary".into()); + assert_matrix( + &[rz], + array![ + [Complex64::new(0.0, -1.0), Complex64::new(0.0, 0.0)], + [Complex64::new(0.0, 0.0), Complex64::new(0.0, 1.0)] + ], + ); +} + +#[test] +fn rejects_wrong_parameter_counts_for_all_standard_one_qubit_gates() { + for gate in StandardGate::all() + .iter() + .copied() + .filter(|gate| gate.num_qubits() == 1) + { + let params = vec![0.25; gate.num_params()]; + assert!( + one_qubit_run_matrix(&[operation(gate, ¶ms)]).is_some(), + "{gate:?}" + ); + if !params.is_empty() { + assert!( + one_qubit_run_matrix(&[operation(gate, ¶ms[1..])]).is_none(), + "{gate:?}" + ); + } + assert!( + one_qubit_run_matrix(&[operation(gate, &vec![0.25; params.len() + 1])]).is_none(), + "{gate:?}" + ); + } +} + +#[test] +fn rejects_symbolic_and_nonfinite_parameters() { + for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] { + assert!(one_qubit_run_matrix(&[operation(StandardGate::RZ, &[value])]).is_none()); + } + let mut rz = operation(StandardGate::RZ, &[0.0]); + rz.params[0] = ParameterValue::Param(Parameter::symbol("theta")); + assert!(one_qubit_run_matrix(&[rz]).is_none()); +} + +#[test] +fn rejects_incompatible_qubit_support() { + let h = operation(StandardGate::H, &[]); + let mut other_qubit = h.clone(); + other_qubit.qubits[0] = Qubit::new(1); + assert!(one_qubit_run_matrix(&[h.clone(), other_qubit]).is_none()); + + let mut no_qubits = h.clone(); + no_qubits.qubits.clear(); + assert!(one_qubit_run_matrix(&[no_qubits]).is_none()); + let mut extra_qubit = h; + extra_qubit.qubits.push(Qubit::new(1)); + assert!(one_qubit_run_matrix(&[extra_qubit]).is_none()); + + let cx = ValueOperation::from_standard(StandardGate::CX, [Qubit::new(0), Qubit::new(1)], []); + assert!(one_qubit_run_matrix(&[cx]).is_none()); + let phase = + ValueOperation::from_standard(StandardGate::GPhase, [], [ParameterValue::Fixed(0.2)]); + assert!(one_qubit_run_matrix(&[phase]).is_none()); +} + +#[test] +fn rejects_nonstandard_and_control_flow_instructions() { + for instruction in [ + ValueInstruction::Instruction(Instruction::Directive(Directive::Barrier)), + ValueInstruction::Instruction(Instruction::Delay), + ValueInstruction::ClassicalControl(ValueClassicalControlOp::Break), + ] { + let mut invalid = operation(StandardGate::H, &[]); + invalid.instruction = instruction; + assert!(one_qubit_run_matrix(&[invalid]).is_none()); + } +} diff --git a/crates/cqlib-core/src/compile/sabre/dag.rs b/crates/cqlib-core/src/compile/sabre/dag.rs index 9238cdff..cd2c2b1b 100644 --- a/crates/cqlib-core/src/compile/sabre/dag.rs +++ b/crates/cqlib-core/src/compile/sabre/dag.rs @@ -446,40 +446,16 @@ impl WorkloadBuilder { state: &mut WorkloadState, ) -> Result, CompilerError> { let fork = self.add_global_synchronize(state); - let branches = match flow { - ClassicalControlOp::If(op) => { - let mut branches = vec![op.then_body().operations()]; - if let Some(else_body) = op.else_body() { - branches.push(else_body.operations()); - } - branches - } - ClassicalControlOp::While(op) => vec![op.body().operations()], - ClassicalControlOp::For(op) => vec![op.body().operations()], - ClassicalControlOp::Switch(op) => { - let mut branches = op - .cases() - .iter() - .map(|case| case.body().operations()) - .collect::>(); - if let Some(default) = op.default() { - branches.push(default.operations()); - } - branches - } - ClassicalControlOp::Break | ClassicalControlOp::Continue => Vec::new(), - }; - let base_frontier = state.wire_frontier.clone(); let mut cross_branch_frontier = base_frontier.clone(); let mut all_touched = BTreeSet::new(); let mut join_parents = BTreeSet::from([fork]); - for branch in branches { + for body in flow.bodies() { let mut branch_state = WorkloadState { wire_frontier: cross_branch_frontier.clone(), global_barrier: Some(fork), }; - let branch_touched = self.add_operations(branch, &mut branch_state)?; + let branch_touched = self.add_operations(body.operations(), &mut branch_state)?; for &logical in &branch_touched { if let Some(frontier) = branch_state.wire_frontier.get(&logical).copied() { cross_branch_frontier.insert(logical, frontier); diff --git a/crates/cqlib-core/src/compile/sabre/routing.rs b/crates/cqlib-core/src/compile/sabre/routing.rs index 7e0926ba..30d861cb 100644 --- a/crates/cqlib-core/src/compile/sabre/routing.rs +++ b/crates/cqlib-core/src/compile/sabre/routing.rs @@ -5178,27 +5178,13 @@ fn two_qubit_operation_count(operations: &[Operation]) -> usize { operations .iter() .map(|operation| match &operation.instruction { - Instruction::ClassicalControl(ClassicalControlOp::If(op)) => { - two_qubit_operation_count(op.then_body().operations()) - + op.else_body() - .map(|body| two_qubit_operation_count(body.operations())) - .unwrap_or(0) - } - Instruction::ClassicalControl(ClassicalControlOp::While(op)) => { - two_qubit_operation_count(op.body().operations()) - } - Instruction::ClassicalControl(ClassicalControlOp::For(op)) => { - two_qubit_operation_count(op.body().operations()) - } - Instruction::ClassicalControl(ClassicalControlOp::Switch(op)) => { - op.cases() - .iter() - .map(|case| two_qubit_operation_count(case.body().operations())) - .sum::() - + op.default() - .map(|body| two_qubit_operation_count(body.operations())) - .unwrap_or(0) - } + Instruction::ClassicalControl( + ClassicalControlOp::Break | ClassicalControlOp::Continue, + ) => usize::from(operation.qubits.len() == 2), + Instruction::ClassicalControl(control) => control + .bodies() + .map(|body| two_qubit_operation_count(body.operations())) + .sum(), _ => usize::from(operation.qubits.len() == 2), }) .sum() @@ -5276,27 +5262,10 @@ fn operation_count(operations: &[Operation]) -> usize { .iter() .map(|operation| { 1 + match &operation.instruction { - Instruction::ClassicalControl(ClassicalControlOp::If(op)) => { - operation_count(op.then_body().operations()) - + op.else_body() - .map(|body| operation_count(body.operations())) - .unwrap_or(0) - } - Instruction::ClassicalControl(ClassicalControlOp::While(op)) => { - operation_count(op.body().operations()) - } - Instruction::ClassicalControl(ClassicalControlOp::For(op)) => { - operation_count(op.body().operations()) - } - Instruction::ClassicalControl(ClassicalControlOp::Switch(op)) => { - op.cases() - .iter() - .map(|case| operation_count(case.body().operations())) - .sum::() - + op.default() - .map(|body| operation_count(body.operations())) - .unwrap_or(0) - } + Instruction::ClassicalControl(control) => control + .bodies() + .map(|body| operation_count(body.operations())) + .sum::(), _ => 0, } }) diff --git a/crates/cqlib-core/src/compile/sabre/score_test.rs b/crates/cqlib-core/src/compile/sabre/score_test.rs index 1018fec8..c8ca162c 100644 --- a/crates/cqlib-core/src/compile/sabre/score_test.rs +++ b/crates/cqlib-core/src/compile/sabre/score_test.rs @@ -1185,6 +1185,36 @@ fn fixed_for_loop_multiplies_execution_path_two_qubit_depth() { .unwrap(); assert_eq!(two_qubit_depth(circuit.operations()), 3); + assert_eq!(two_qubit_operation_count(circuit.operations()), 1); + assert_eq!(operation_count(circuit.operations()), 2); +} + +#[test] +fn structural_counts_include_all_branches_and_nested_default_bodies() { + let mut circuit = Circuit::new(2); + let q0 = Qubit::new(0); + let q1 = Qubit::new(1); + circuit + .switch(ClassicalExpr::uint_literal(2, 0).unwrap(), |cases| { + cases.value(0, |body| body.cx(q0, q1))?; + cases.value(1, |body| body.h(q0))?; + cases.default(|body| { + body.if_else( + ClassicalExpr::bool_literal(true), + |body| body.cx(q0, q1), + |body| { + body.cx(q0, q1)?; + body.cx(q0, q1) + }, + ) + }) + }) + .unwrap(); + + // Five gates and two control nodes are stored, regardless of reachability. + assert_eq!(operation_count(circuit.operations()), 7); + assert_eq!(two_qubit_operation_count(circuit.operations()), 4); + assert_eq!(two_qubit_depth(circuit.operations()), 2); } #[test] diff --git a/crates/cqlib-core/src/compile/transform/analysis.rs b/crates/cqlib-core/src/compile/transform/analysis.rs index e1d0f965..c1440dce 100644 --- a/crates/cqlib-core/src/compile/transform/analysis.rs +++ b/crates/cqlib-core/src/compile/transform/analysis.rs @@ -179,24 +179,8 @@ impl WorkflowCircuitAnalysis { } fn scan_control_flow(&mut self, op: &ClassicalControlOp) { - match op { - ClassicalControlOp::If(op) => { - self.scan_operations(op.then_body().operations()); - if let Some(body) = op.else_body() { - self.scan_operations(body.operations()); - } - } - ClassicalControlOp::While(op) => self.scan_operations(op.body().operations()), - ClassicalControlOp::For(op) => self.scan_operations(op.body().operations()), - ClassicalControlOp::Switch(op) => { - for case in op.cases() { - self.scan_operations(case.body().operations()); - } - if let Some(body) = op.default() { - self.scan_operations(body.operations()); - } - } - ClassicalControlOp::Break | ClassicalControlOp::Continue => {} + for body in op.bodies() { + self.scan_operations(body.operations()); } } } diff --git a/crates/cqlib-core/src/compile/transform/analysis_test.rs b/crates/cqlib-core/src/compile/transform/analysis_test.rs index aba4545c..7c19a5f7 100644 --- a/crates/cqlib-core/src/compile/transform/analysis_test.rs +++ b/crates/cqlib-core/src/compile/transform/analysis_test.rs @@ -15,6 +15,37 @@ use crate::circuit::{ Circuit, ClassicalExpr, Directive, Instruction, ParameterValue, Qubit, StandardGate, }; +#[test] +fn analysis_reaches_measurements_and_gates_in_nested_default_and_else() { + let mut circuit = Circuit::new(2); + circuit + .switch(ClassicalExpr::uint_literal(2, 0).unwrap(), |cases| { + cases.value(0, |_| Ok(()))?; + cases.default(|body| { + body.if_else( + ClassicalExpr::bool_literal(true), + |_| Ok(()), + |body| { + body.cx(Qubit::new(0), Qubit::new(1))?; + body.measure(Qubit::new(0))?; + Ok(()) + }, + ) + }) + }) + .unwrap(); + + let analysis = WorkflowCircuitAnalysis::analyze(&circuit); + assert!(analysis.public().has_measurement); + assert!(analysis.public().has_classical_data); + assert!(analysis.has_direction_sensitive_two_qubit_operation); + assert_eq!( + analysis.standard_gates().collect::>(), + vec![StandardGate::CX] + ); + assert!(circuit.operations()[0].instruction.has_measurement()); +} + #[test] fn analysis_detects_runtime_classical_and_definitions_recursively() { let mut inner = Circuit::new(1); diff --git a/crates/cqlib-core/src/compile/transform/canonicalize/verify.rs b/crates/cqlib-core/src/compile/transform/canonicalize/verify.rs index c59445c4..337b3cf4 100644 --- a/crates/cqlib-core/src/compile/transform/canonicalize/verify.rs +++ b/crates/cqlib-core/src/compile/transform/canonicalize/verify.rs @@ -564,23 +564,7 @@ fn collect_used_parameters(operations: &[Operation], used: &mut BTreeSet) { } fn collect_control_parameters(control: &ClassicalControlOp, used: &mut BTreeSet) { - match control { - ClassicalControlOp::If(op) => { - collect_used_parameters(op.then_body().operations(), used); - if let Some(body) = op.else_body() { - collect_used_parameters(body.operations(), used); - } - } - ClassicalControlOp::While(op) => collect_used_parameters(op.body().operations(), used), - ClassicalControlOp::For(op) => collect_used_parameters(op.body().operations(), used), - ClassicalControlOp::Switch(op) => { - for case in op.cases() { - collect_used_parameters(case.body().operations(), used); - } - if let Some(body) = op.default() { - collect_used_parameters(body.operations(), used); - } - } - ClassicalControlOp::Break | ClassicalControlOp::Continue => {} + for body in control.bodies() { + collect_used_parameters(body.operations(), used); } } diff --git a/crates/cqlib-core/src/compile/transform/decompose/mc_gate/fsim.rs b/crates/cqlib-core/src/compile/transform/decompose/mc_gate/fsim.rs index a25abe22..2bbbb67f 100644 --- a/crates/cqlib-core/src/compile/transform/decompose/mc_gate/fsim.rs +++ b/crates/cqlib-core/src/compile/transform/decompose/mc_gate/fsim.rs @@ -18,7 +18,7 @@ use super::{ phase::{decompose_phase_n_clean, decompose_phase_no_aux}, rotation::{decompose_rotation_n_clean, decompose_rotation_no_aux}, }; -use crate::circuit::{Parameter, ParameterValue, Qubit, StandardGate, operation::ValueOperation}; +use crate::circuit::{ParameterValue, Qubit, StandardGate, operation::ValueOperation}; use crate::compile::error::CompilerError; const DECOMPOSE_FSIM_NAME: &str = "decompose.fsim"; @@ -143,9 +143,9 @@ fn decompose_fsim_with( } let theta = ¶ms[0]; - let phi = Parameter::from(¶ms[1]); - let negative_half_phi = ParameterValue::from(phi.clone() * -0.5); - let negative_phi = ParameterValue::from(phi * -1.0); + let phi = ¶ms[1]; + let negative_half_phi = phi.scaled(-0.5); + let negative_phi = phi.scaled(-1.0); let mut flattened_controls = controls.to_vec(); flattened_controls.push(first); let mut operations = decompose_pauli_rotation(StandardGate::RXX, theta)?; diff --git a/crates/cqlib-core/src/compile/transform/decompose/mc_gate/fsim_test.rs b/crates/cqlib-core/src/compile/transform/decompose/mc_gate/fsim_test.rs index 52265d83..9fc0d539 100644 --- a/crates/cqlib-core/src/compile/transform/decompose/mc_gate/fsim_test.rs +++ b/crates/cqlib-core/src/compile/transform/decompose/mc_gate/fsim_test.rs @@ -91,6 +91,59 @@ fn symbolic_decomposition_matches_interaction_phase_and_rotation_formula() { assert_value_operations_equal(&operations, &expected); } +#[test] +fn constant_phi_expression_is_preserved_without_changing_numeric_semantics() { + let controls = [Qubit::new(0)]; + let first = Qubit::new(1); + let second = Qubit::new(2); + let params = [ + ParameterValue::Fixed(0.731), + ParameterValue::Param(Parameter::from(0.418)), + ]; + let operations = decompose_fsim_no_aux(¶ms, &controls, first, second).unwrap(); + let phase = operations + .iter() + .find(|operation| { + matches!( + operation.instruction, + ValueInstruction::Instruction(Instruction::Standard(StandardGate::Phase)) + ) + }) + .expect("controlled FSIM must emit its conditional phase"); + assert!(matches!( + phase.params.as_slice(), + [ParameterValue::Param(parameter)] + if parameter.evaluate(&None).unwrap() == -0.1045 + )); + let rotations: Vec<_> = operations + .iter() + .filter(|operation| { + matches!( + operation.instruction, + ValueInstruction::Instruction(Instruction::Standard(StandardGate::RZ)) + ) && operation.qubits.as_slice() == [second] + && matches!(operation.params.as_slice(), [ParameterValue::Param(_)]) + }) + .collect(); + // The twice-controlled RZ(-phi) emits four quarter-angle rotations. + // Other RZ gates from the numeric theta interaction remain Fixed. + assert_eq!(rotations.len(), 4); + + // Matrix evaluation requires explicit constant resolution after preserving Param. + let circuit = circuit_from_value_operations(3, operations) + .assign_parameters(&None) + .unwrap(); + let actual = circuit_to_matrix(&circuit, None).unwrap(); + let expected = mc_gate_matrix( + 3, + 1, + StandardGate::FSIM, + vec![controls[0], first, second], + [ParameterValue::Fixed(0.731), ParameterValue::Fixed(0.418)], + ); + assert_selected_matrix_columns_approx_eq(&actual, &expected, 0..8, EPSILON); +} + #[test] fn no_aux_decompositions_match_mcgate_semantics_exactly() { let params = [ParameterValue::Fixed(0.731), ParameterValue::Fixed(-0.418)]; diff --git a/crates/cqlib-core/src/compile/transform/decompose/mc_gate/mc_su2/no_auxiliary.rs b/crates/cqlib-core/src/compile/transform/decompose/mc_gate/mc_su2/no_auxiliary.rs index 7a7ef675..28572464 100644 --- a/crates/cqlib-core/src/compile/transform/decompose/mc_gate/mc_su2/no_auxiliary.rs +++ b/crates/cqlib-core/src/compile/transform/decompose/mc_gate/mc_su2/no_auxiliary.rs @@ -19,10 +19,7 @@ //! `RY` and `RZ` use the Vale sequence directly. `RX` is reduced to `RZ` by //! Hadamard conjugation. -use super::{ - Su2RotationAxis, - utils::{scale_parameter, validate_distinct_qubits}, -}; +use super::{Su2RotationAxis, utils::validate_distinct_qubits}; use crate::circuit::{ParameterValue, Qubit, StandardGate, operation::ValueOperation}; use crate::compile::error::CompilerError; use crate::compile::transform::decompose::mc_gate::mcx::decompose_mcx_n_dirty; @@ -73,8 +70,8 @@ pub fn decompose_mc_su2_no_aux( let (first_group, second_group) = controls.split_at(first_group_len); let first_mcx = decompose_mcx_n_dirty(first_group, target, second_group)?; let second_mcx = decompose_mcx_n_dirty(second_group, target, first_group)?; - let negative_quarter_theta = scale_parameter(theta, -0.25); - let positive_quarter_theta = scale_parameter(theta, 0.25); + let negative_quarter_theta = theta.scaled(-0.25); + let positive_quarter_theta = theta.scaled(0.25); let inner_rotation = match axis { Su2RotationAxis::X => StandardGate::RZ, Su2RotationAxis::Y | Su2RotationAxis::Z => axis.rotation_gate(), diff --git a/crates/cqlib-core/src/compile/transform/decompose/mc_gate/mc_su2/utils.rs b/crates/cqlib-core/src/compile/transform/decompose/mc_gate/mc_su2/utils.rs index 171371c0..5553fd25 100644 --- a/crates/cqlib-core/src/compile/transform/decompose/mc_gate/mc_su2/utils.rs +++ b/crates/cqlib-core/src/compile/transform/decompose/mc_gate/mc_su2/utils.rs @@ -13,18 +13,10 @@ //! Shared MC-SU(2) synthesis helpers. use super::DECOMPOSE_MC_SU2_NAME; -use crate::circuit::{ParameterValue, Qubit}; +use crate::circuit::Qubit; use crate::compile::error::CompilerError; use crate::compile::transform::decompose::mc_gate::validation::find_duplicate_qubit; -/// Returns a scaled clone while preserving symbolic expressions. -pub(super) fn scale_parameter(theta: &ParameterValue, factor: f64) -> ParameterValue { - match theta { - ParameterValue::Fixed(value) => ParameterValue::Fixed(value * factor), - ParameterValue::Param(parameter) => ParameterValue::Param(parameter.clone() * factor), - } -} - /// Rejects a repeated qubit across the consumed input groups. pub(super) fn validate_distinct_qubits(qubit_groups: &[&[Qubit]]) -> Result<(), CompilerError> { if let Some(qubit) = find_duplicate_qubit(qubit_groups) { diff --git a/crates/cqlib-core/src/compile/transform/decompose/mc_gate/phase.rs b/crates/cqlib-core/src/compile/transform/decompose/mc_gate/phase.rs index 14d83943..cd466fd3 100644 --- a/crates/cqlib-core/src/compile/transform/decompose/mc_gate/phase.rs +++ b/crates/cqlib-core/src/compile/transform/decompose/mc_gate/phase.rs @@ -123,10 +123,7 @@ fn decompose_normalized_phase_with( )]); }; - let half_theta = match theta { - ParameterValue::Fixed(value) => ParameterValue::Fixed(value * 0.5), - ParameterValue::Param(parameter) => ParameterValue::Param(parameter.clone() * 0.5), - }; + let half_theta = theta.scaled(0.5); let mut operations = decompose_normalized_phase_with( &half_theta, phase_controls, diff --git a/crates/cqlib-core/src/compile/transform/decompose/mc_gate/qcis.rs b/crates/cqlib-core/src/compile/transform/decompose/mc_gate/qcis.rs index 60c0343e..0de9eaa3 100644 --- a/crates/cqlib-core/src/compile/transform/decompose/mc_gate/qcis.rs +++ b/crates/cqlib-core/src/compile/transform/decompose/mc_gate/qcis.rs @@ -25,7 +25,7 @@ //! changes. Only the central rotation receives the caller-provided controls. use super::rotation::{decompose_rotation_n_clean, decompose_rotation_no_aux}; -use crate::circuit::{Parameter, ParameterValue, Qubit, StandardGate, operation::ValueOperation}; +use crate::circuit::{ParameterValue, Qubit, StandardGate, operation::ValueOperation}; use crate::compile::error::CompilerError; use std::f64::consts::PI; @@ -120,11 +120,10 @@ fn decompose_xy_with( &ParameterValue, ) -> Result, CompilerError>, ) -> Result, CompilerError> { - let phi = Parameter::from(phi); let mut operations = vec![ValueOperation::from_standard( StandardGate::RZ, [target], - [ParameterValue::from(-phi.clone())], + [phi.scaled(-1.0)], )]; operations.extend(decompose_rotation( StandardGate::RX, @@ -133,7 +132,7 @@ fn decompose_xy_with( operations.push(ValueOperation::from_standard( StandardGate::RZ, [target], - [ParameterValue::from(phi)], + [phi.clone()], )); Ok(operations) } diff --git a/crates/cqlib-core/src/compile/transform/decompose/mc_gate/qcis_test.rs b/crates/cqlib-core/src/compile/transform/decompose/mc_gate/qcis_test.rs index 6e6913f1..1e605bb4 100644 --- a/crates/cqlib-core/src/compile/transform/decompose/mc_gate/qcis_test.rs +++ b/crates/cqlib-core/src/compile/transform/decompose/mc_gate/qcis_test.rs @@ -106,6 +106,29 @@ fn symbolic_xy_angle_is_preserved() { )); } +#[test] +fn constant_xy_angle_expression_is_preserved() { + let phi = Parameter::from(0.731); + for gate in [StandardGate::XY2P, StandardGate::XY2M] { + let operations = decompose_qcis_no_aux( + gate, + &[ParameterValue::Param(phi.clone())], + &[Qubit::new(0)], + Qubit::new(1), + ) + .unwrap(); + + assert!(matches!( + operations[0].params.as_slice(), + [ParameterValue::Param(actual)] if actual.evaluate(&None).unwrap() == -0.731 + )); + assert!(matches!( + operations[2].params.as_slice(), + [ParameterValue::Param(actual)] if actual == &phi + )); + } +} + #[test] fn no_aux_decompositions_match_mcgate_semantics_exactly() { for num_controls in 1..=3 { diff --git a/crates/cqlib-core/src/compile/transform/decompose/unitary/device_synthesis.rs b/crates/cqlib-core/src/compile/transform/decompose/unitary/device_synthesis.rs index fb8b9ffc..894ec55a 100644 --- a/crates/cqlib-core/src/compile/transform/decompose/unitary/device_synthesis.rs +++ b/crates/cqlib-core/src/compile/transform/decompose/unitary/device_synthesis.rs @@ -661,8 +661,6 @@ fn ordered_topology_pairs( fn collect_exact_physical_gate_roots( operations: &[crate::circuit::Operation], ) -> Vec { - use crate::circuit::ClassicalControlOp; - let mut roots = Vec::new(); for operation in operations { match &operation.instruction { @@ -680,31 +678,11 @@ fn collect_exact_physical_gate_roots( roots.push(state); } } - Instruction::ClassicalControl(control) => match control { - ClassicalControlOp::If(op) => { - roots.extend(collect_exact_physical_gate_roots( - op.then_body().operations(), - )); - if let Some(body) = op.else_body() { - roots.extend(collect_exact_physical_gate_roots(body.operations())); - } - } - ClassicalControlOp::While(op) => { - roots.extend(collect_exact_physical_gate_roots(op.body().operations())); - } - ClassicalControlOp::For(op) => { - roots.extend(collect_exact_physical_gate_roots(op.body().operations())); + Instruction::ClassicalControl(control) => { + for body in control.bodies() { + roots.extend(collect_exact_physical_gate_roots(body.operations())); } - ClassicalControlOp::Switch(op) => { - for case in op.cases() { - roots.extend(collect_exact_physical_gate_roots(case.body().operations())); - } - if let Some(body) = op.default() { - roots.extend(collect_exact_physical_gate_roots(body.operations())); - } - } - ClassicalControlOp::Break | ClassicalControlOp::Continue => {} - }, + } _ => {} } } @@ -714,8 +692,6 @@ fn collect_exact_physical_gate_roots( fn collect_exact_physical_pairs( operations: &[crate::circuit::Operation], ) -> Vec<[PhysicalQubit; 2]> { - use crate::circuit::ClassicalControlOp; - let mut pairs = BTreeSet::new(); for operation in operations { match &operation.instruction { @@ -731,29 +707,11 @@ fn collect_exact_physical_pairs( pairs.insert([pair[0], pair[1]]); pairs.insert([pair[1], pair[0]]); } - Instruction::ClassicalControl(control) => match control { - ClassicalControlOp::If(op) => { - pairs.extend(collect_exact_physical_pairs(op.then_body().operations())); - if let Some(body) = op.else_body() { - pairs.extend(collect_exact_physical_pairs(body.operations())); - } - } - ClassicalControlOp::While(op) => { - pairs.extend(collect_exact_physical_pairs(op.body().operations())); - } - ClassicalControlOp::For(op) => { - pairs.extend(collect_exact_physical_pairs(op.body().operations())); + Instruction::ClassicalControl(control) => { + for body in control.bodies() { + pairs.extend(collect_exact_physical_pairs(body.operations())); } - ClassicalControlOp::Switch(op) => { - for case in op.cases() { - pairs.extend(collect_exact_physical_pairs(case.body().operations())); - } - if let Some(body) = op.default() { - pairs.extend(collect_exact_physical_pairs(body.operations())); - } - } - ClassicalControlOp::Break | ClassicalControlOp::Continue => {} - }, + } _ => {} } } diff --git a/crates/cqlib-core/src/compile/transform/decompose/unitary/device_synthesis_test.rs b/crates/cqlib-core/src/compile/transform/decompose/unitary/device_synthesis_test.rs index 0edf26d1..2237904a 100644 --- a/crates/cqlib-core/src/compile/transform/decompose/unitary/device_synthesis_test.rs +++ b/crates/cqlib-core/src/compile/transform/decompose/unitary/device_synthesis_test.rs @@ -14,14 +14,13 @@ use super::*; use crate::circuit::{MCGate, ParameterValue, UnitaryGate}; use crate::compile::device_planning::cost::MetricAvailability; use crate::compile::test_utils::build_device_synthesis_context; -use crate::compile::transform::decompose::unitary::TwoQubitUnitaryDecomposeBasis; use crate::compile::transform::decompose::unitary::unitary_2q::{ plan_numeric_2q_unitary_for_device, take_best_device_unitary_candidate, }; use crate::device::{EdgeProp, InstructionProp}; #[test] -fn pre_layout_prefers_broad_family_over_single_calibrated_edge() { +fn pre_layout_preserves_full_pair_coverage_with_one_calibrated_edge() { let p1 = PhysicalQubit::new(1); let p2 = PhysicalQubit::new(2); let mut device = Device::bidirectional_line("coverage", 4) @@ -65,13 +64,17 @@ fn pre_layout_prefers_broad_family_over_single_calibrated_edge() { .unwrap() .candidate; - assert_eq!(selected.backend, TwoQubitUnitaryDecomposeBasis::Cx); - assert!(selected.operations.iter().all(|operation| { - !matches!( - operation.instruction, - ValueInstruction::Instruction(Instruction::Standard(StandardGate::CZ)) - ) - })); + // Phase -> U also makes CZ decompositions executable on the U/CX edges. + // Either backend can now have full coverage; the calibrated CZ edge must + // not entice selection of a sequence that only works on that single pair. + let evaluation = context + .evaluate_pre_layout(&selected.operations, qubits) + .unwrap(); + assert_eq!(evaluation.coverage, DeviceCoverageKey::default()); + assert_eq!( + evaluation.domain, + context.data.eligible_pairs.iter().copied().collect() + ); assert!(selected.operations.iter().all(|operation| { operation .params diff --git a/crates/cqlib-core/src/compile/transform/decompose/unitary/euler_1q.rs b/crates/cqlib-core/src/compile/transform/decompose/unitary/euler_1q.rs index 89627a24..7882da55 100644 --- a/crates/cqlib-core/src/compile/transform/decompose/unitary/euler_1q.rs +++ b/crates/cqlib-core/src/compile/transform/decompose/unitary/euler_1q.rs @@ -13,7 +13,7 @@ //! Parameter-aware numeric one-qubit Euler synthesis. //! //! Given one numeric `U(theta, phi, lambda)` decomposition, this module emits -//! candidate gate sequences for the half-rotation target families. Every +//! candidate gate sequences for continuous-axis and half-rotation families. Every //! template below is derived from Cqlib's own gate matrices: //! //! ```text @@ -34,14 +34,14 @@ use smallvec::SmallVec; use crate::circuit::StandardGate; use crate::compile::{CompilerError, PARAMETER_EQ_TOLERANCE}; -use super::unitary_1q::OneQubitUnitaryDecomposition; +use super::unitary_1q::{OneQubitUnitaryDecomposition, synthesize_numeric_1q_unitary}; const TWO_PI: f64 = 2.0 * PI; -/// Tolerance for the degenerate-angle branches and near-zero RZ elision. +/// Tolerance for the degenerate-angle branches and near-zero rotation elision. const EULER_ANGLE_EPS: f64 = PARAMETER_EQ_TOLERANCE; -/// Half-rotation gate families a target basis can support. +/// Euler gate families a target basis can support. /// /// Declaration order is the deterministic tie-break order between equally /// costly candidates. @@ -55,10 +55,12 @@ pub(crate) enum Euler1qFamily { Zxpm, /// `{RZ, X2P, X2M, X}`: `Zxpm` plus a native Pauli X. Zxpmx, + /// `{RX, RY}`: continuous rotations about the X and Y axes. + Xyx, } -/// One synthesized gate: `RZ(param)` when `param` is set, otherwise a fixed -/// half rotation or Pauli gate. +/// One synthesized gate: an axis rotation when `param` is set, otherwise a +/// fixed half rotation or Pauli gate. #[derive(Clone, Copy, Debug, PartialEq)] pub(crate) struct Euler1qGate { pub gate: StandardGate, @@ -101,6 +103,7 @@ impl Euler1qFamily { StandardGate::X2M, StandardGate::X, ], + Self::Xyx => &[StandardGate::RX, StandardGate::RY], } } @@ -129,19 +132,34 @@ fn push_normalized_rz( gates: &mut SmallVec<[Euler1qGate; 5]>, phase: &mut f64, ) -> Result<(), CompilerError> { + push_normalized_rotation(StandardGate::RZ, angle, gates, phase) +} + +/// All axis rotations obey `R(a + 2*pi*k) = (-1)^k * R(a)`. +fn normalize_rotation_angle(angle: f64, phase: &mut f64) -> Result { if !angle.is_finite() { return Err(CompilerError::InvalidInput(format!( - "euler 1q synthesis received a non-finite RZ angle: {angle}" + "euler 1q synthesis received a non-finite rotation angle: {angle}" ))); } let k = ((angle - PI) / TWO_PI).ceil(); let normalized = angle - TWO_PI * k; *phase += k * PI; + Ok(normalized) +} + +fn push_normalized_rotation( + gate: StandardGate, + angle: f64, + gates: &mut SmallVec<[Euler1qGate; 5]>, + phase: &mut f64, +) -> Result<(), CompilerError> { + let normalized = normalize_rotation_angle(angle, phase)?; if normalized.abs() <= EULER_ANGLE_EPS { return Ok(()); } gates.push(Euler1qGate { - gate: StandardGate::RZ, + gate, param: Some(normalized), }); Ok(()) @@ -151,6 +169,66 @@ fn push_fixed(gates: &mut SmallVec<[Euler1qGate; 5]>, gate: StandardGate) { gates.push(Euler1qGate { gate, param: None }); } +/// Change coordinates with H, synthesize ZYZ, and change back using +/// H*RZ(a)*H = RX(a) and H*RY(a)*H = RY(-a). H is only used to compute +/// angles; the emitted sequence contains RX/RY alone. +fn synthesize_xyx( + decomposition: OneQubitUnitaryDecomposition, +) -> Result { + let matrix = StandardGate::U + .matrix(&[decomposition.theta, decomposition.phi, decomposition.lambda]) + .map_err(CompilerError::Circuit)?; + let h = StandardGate::H + .matrix(&[]) + .map_err(CompilerError::Circuit)?; + let rotated = h.dot(matrix.as_ref()).dot(h.as_ref()); + let zyz = synthesize_numeric_1q_unitary(&rotated)?; + // The U convention contributes exp(i*(phi+lambda)/2) in addition to + // both the input decomposition's phase and the rotated matrix's phase. + let mut phase = decomposition.global_phase + zyz.global_phase + (zyz.phi + zyz.lambda) / 2.0; + let mut gates = SmallVec::new(); + + if zyz.theta.abs() <= EULER_ANGLE_EPS { + // RX(phi) * RX(lambda), including scalar identities such as -I. + push_normalized_rotation( + StandardGate::RX, + zyz.lambda + zyz.phi, + &mut gates, + &mut phase, + )?; + } else if (zyz.theta - PI).abs() <= EULER_ANGLE_EPS { + // RX(phi) * RY(-pi) = RY(-pi) * RX(-phi). + push_normalized_rotation( + StandardGate::RX, + zyz.lambda - zyz.phi, + &mut gates, + &mut phase, + )?; + push_normalized_rotation(StandardGate::RY, -zyz.theta, &mut gates, &mut phase)?; + } else { + let mut first = normalize_rotation_angle(zyz.lambda, &mut phase)?; + let mut last = normalize_rotation_angle(zyz.phi, &mut phase)?; + let mut middle = -zyz.theta; + if (first.abs() - PI).abs() <= EULER_ANGLE_EPS || (last.abs() - PI).abs() <= EULER_ANGLE_EPS + { + // Shifting both X angles by pi and reversing the Y angle changes + // the sequence's sign. Compensate it and eliminate the pi endpoint. + first += PI; + last += PI; + middle = -middle; + phase += PI; + } + push_normalized_rotation(StandardGate::RX, first, &mut gates, &mut phase)?; + push_normalized_rotation(StandardGate::RY, middle, &mut gates, &mut phase)?; + push_normalized_rotation(StandardGate::RX, last, &mut gates, &mut phase)?; + } + Ok(Euler1qCandidate { + gates, + global_phase: phase, + family: Euler1qFamily::Xyx, + }) +} + /// Enumerates one synthesis candidate per family supported by the available /// gate set, cheapest-shortlist style: degenerate angles take dedicated short /// templates, generic angles take the family template. @@ -162,7 +240,7 @@ fn push_fixed(gates: &mut SmallVec<[Euler1qGate; 5]>, gate: StandardGate) { /// # Errors /// /// Returns [`CompilerError::InvalidInput`] when any decomposition angle or -/// derived RZ angle is not finite. +/// derived rotation angle is not finite. pub(crate) fn synthesize_euler_1q_candidates( decomposition: OneQubitUnitaryDecomposition, is_available: &dyn Fn(StandardGate) -> bool, @@ -192,10 +270,15 @@ pub(crate) fn synthesize_euler_1q_candidates( Euler1qFamily::Zsxx, Euler1qFamily::Zxpm, Euler1qFamily::Zxpmx, + Euler1qFamily::Xyx, ] { if !family.is_supported_by(is_available) { continue; } + if family == Euler1qFamily::Xyx { + candidates.push(synthesize_xyx(decomposition)?); + continue; + } let mut gates = SmallVec::new(); // Every template carries the `exp(i(phi+lambda)/2)` convention factor // of Cqlib's `U` plus the decomposition's own scalar phase. diff --git a/crates/cqlib-core/src/compile/transform/decompose/unitary/euler_1q_test.rs b/crates/cqlib-core/src/compile/transform/decompose/unitary/euler_1q_test.rs index 22d1564f..416764a7 100644 --- a/crates/cqlib-core/src/compile/transform/decompose/unitary/euler_1q_test.rs +++ b/crates/cqlib-core/src/compile/transform/decompose/unitary/euler_1q_test.rs @@ -21,7 +21,10 @@ use super::{ synthesize_euler_1q_candidates, }; use crate::circuit::StandardGate; -use crate::compile::transform::decompose::unitary::unitary_1q::OneQubitUnitaryDecomposition; +use crate::circuit::test_utils::assert_matrix_approx_eq; +use crate::compile::transform::decompose::unitary::unitary_1q::{ + OneQubitUnitaryDecomposition, synthesize_numeric_1q_unitary, +}; const MATRIX_EQ_EPS: f64 = 1e-12; @@ -354,6 +357,12 @@ fn family_selection_follows_available_gates() { families_of(&[StandardGate::RZ, StandardGate::X2M]).is_empty(), "no complete family means an empty candidate list" ); + assert_eq!( + families_of(&[StandardGate::RX, StandardGate::RY]), + vec![Euler1qFamily::Xyx] + ); + assert!(families_of(&[StandardGate::RX]).is_empty()); + assert!(families_of(&[StandardGate::RY]).is_empty()); } #[test] @@ -364,6 +373,8 @@ fn synthesis_is_deterministic() { StandardGate::X2P, StandardGate::X2M, StandardGate::X, + StandardGate::RX, + StandardGate::RY, ]); let first = synthesize_euler_1q_candidates(decomposition, &gates).unwrap(); let second = synthesize_euler_1q_candidates(decomposition, &gates).unwrap(); @@ -393,6 +404,8 @@ fn random_unitaries_match_source_matrix_including_global_phase() { StandardGate::X2P, StandardGate::X2M, StandardGate::X, + StandardGate::RX, + StandardGate::RY, ]); for sample in 0..200 { let decomposition = OneQubitUnitaryDecomposition { @@ -402,13 +415,97 @@ fn random_unitaries_match_source_matrix_including_global_phase() { global_phase: rng.range(-PI, PI), }; let candidates = synthesize_euler_1q_candidates(decomposition, &gates).unwrap(); - assert_eq!(candidates.len(), 4, "sample {sample}"); + assert_eq!(candidates.len(), 5, "sample {sample}"); for candidate in &candidates { assert_candidate_equivalent(candidate, &decomposition); } } } +#[test] +fn generic_u_has_three_gate_xyx_candidate() { + let source = decomposition(0.37, 0.23, 0.29); + let candidates = + synthesize_euler_1q_candidates(source, &available(&[StandardGate::RX, StandardGate::RY])) + .unwrap(); + let candidate = &candidates[0]; + assert_eq!(candidate.physical_cost(), (3, 3)); + assert_eq!( + candidate + .gates + .iter() + .map(|gate| gate.gate) + .collect::>(), + [StandardGate::RX, StandardGate::RY, StandardGate::RX] + ); + assert_candidate_equivalent(candidate, &source); +} + +fn assert_xyx_synthesis(first: f64, middle: f64, last: f64, expected_ops: usize) { + let source = StandardGate::RX + .matrix(&[last]) + .unwrap() + .dot(StandardGate::RY.matrix(&[middle]).unwrap().as_ref()) + .dot(StandardGate::RX.matrix(&[first]).unwrap().as_ref()) + * Complex64::new(0.0, 0.37).exp(); + let candidates = synthesize_euler_1q_candidates( + synthesize_numeric_1q_unitary(&source).unwrap(), + &available(&[StandardGate::RX, StandardGate::RY]), + ) + .unwrap(); + let candidate = &candidates[0]; + assert_eq!( + candidate.gates.len(), + expected_ops, + "XYX({first}, {middle}, {last}): {candidate:?}" + ); + assert_matrix_approx_eq(&candidate_matrix(candidate), &source, MATRIX_EQ_EPS); + for gate in &candidate.gates { + let angle = gate.param.unwrap(); + assert!(angle > -PI && angle <= PI); + assert!(angle.abs() > EULER_ANGLE_EPS); + } +} + +#[test] +fn xyx_simplifies_degenerate_angles_including_scalar_phases() { + for (first, middle, last, count) in [ + (0.0, 0.0, 0.0, 0), + (0.0, 0.0, 2.0 * PI, 0), + (0.0, 0.0, -2.0 * PI, 0), + (0.23, 0.0, 0.29, 1), + (0.23, 2.0 * PI, -0.23, 0), + (0.0, 0.37, 0.0, 1), + (0.0, -0.37, 0.0, 1), + (0.23, PI, 0.23, 1), + (0.23, -PI, 0.29, 2), + (0.23, 0.37, 0.0, 2), + (0.0, 0.37, 0.29, 2), + (PI, 0.37, PI, 1), + (PI, 0.37, 0.29, 2), + (0.23, 0.37, -PI, 2), + ] { + assert_xyx_synthesis(first, middle, last, count); + } +} + +#[test] +fn xyx_preserves_angles_near_degenerate_values() { + for delta in [-5e-9, 5e-9] { + for (first, middle, last) in [ + (0.23, delta, 0.29), + (0.23, PI + delta, 0.29), + (0.23, -PI + delta, 0.29), + (delta, 0.37, 0.29), + (0.23, 0.37, delta), + (PI + delta, 0.37, 0.29), + (0.23, 0.37, -PI + delta), + ] { + assert_xyx_synthesis(first, middle, last, 3); + } + } +} + #[test] fn non_finite_decomposition_is_rejected() { for theta in [f64::NAN, f64::INFINITY] { diff --git a/crates/cqlib-core/src/compile/transform/decompose/unitary/unitary_2q.rs b/crates/cqlib-core/src/compile/transform/decompose/unitary/unitary_2q.rs index 9ccd1fb2..21f50016 100644 --- a/crates/cqlib-core/src/compile/transform/decompose/unitary/unitary_2q.rs +++ b/crates/cqlib-core/src/compile/transform/decompose/unitary/unitary_2q.rs @@ -242,7 +242,8 @@ pub struct TwoQubitSynthesisRequest<'a> { /// minimize final two-qubit count, final depth, final operation count, /// remaining parameterized operations, and finally a deterministic backend /// tie-breaker. When a target basis is configured these values are measured -/// after applying the same lowering rules used by final translation. +/// after target lowering and one canonicalization/one-qubit cleanup round. +/// This local estimate does not predict full compiler output quality. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] pub struct TargetAwareSynthesisCost { /// Two-qubit operations after target-basis lowering. @@ -1167,6 +1168,8 @@ fn push_scored_candidate( } /// Computes target-aware cost for value operations emitted by synthesis. +/// Explicit bases include one existing local cleanup round. The same function +/// scores source blocks, primary candidates and numerical fallbacks. pub fn target_aware_cost_of_value_operations( operations: &[ValueOperation], target: &TwoQubitSynthesisTarget, @@ -1179,7 +1182,7 @@ pub fn target_aware_cost_of_value_operations( depth, total_ops, parameterized_ops, - } = model.cost_of_fixed_operations(qubits, operations.to_vec())?; + } = model.cost_after_local_cleanup(qubits, operations.to_vec())?; return Ok(TargetAwareSynthesisCost { lowered_two_qubit_ops: two_qubit_ops, lowered_depth: depth, diff --git a/crates/cqlib-core/src/compile/transform/decompose/unitary/unitary_2q_test.rs b/crates/cqlib-core/src/compile/transform/decompose/unitary/unitary_2q_test.rs index 9c741b2f..bebe41fe 100644 --- a/crates/cqlib-core/src/compile/transform/decompose/unitary/unitary_2q_test.rs +++ b/crates/cqlib-core/src/compile/transform/decompose/unitary/unitary_2q_test.rs @@ -76,3 +76,53 @@ fn selected_boundary_inexact_candidate_falls_back_to_higher_cx_template() { assert_eq!(count_cx(&selected), 3); validate_numeric_2q_candidate(&matrix, qubits, &selected).unwrap(); } + +#[test] +fn local_cleanup_scoring_agrees_for_primary_cx_and_pauli_fallbacks() { + use super::unitary_2q::{ + plan_cx_family_fallbacks_from_kak, plan_pauli_fallback_from_kak, + target_aware_cost_of_value_operations, + }; + let matrix = StandardGate::RXX.matrix(&[0.23]).unwrap().into_owned(); + let qubits = [Qubit::new(1), Qubit::new(0)]; + let target = TwoQubitSynthesisTarget::from_standard_gates( + vec![StandardGate::RZ, StandardGate::X2P], + vec![StandardGate::CX], + true, + ) + .unwrap(); + let decomp = kak_decompose(&matrix).unwrap(); + let request = || TwoQubitSynthesisRequest { + matrix: &matrix, + qubits, + target: target.clone(), + }; + for candidates in [ + plan_numeric_2q_unitary_from_kak(request(), &decomp).unwrap(), + plan_cx_family_fallbacks_from_kak(request(), &decomp, TwoQubitUnitaryDecomposeBasis::Cx) + .unwrap(), + plan_pauli_fallback_from_kak(request(), &decomp).unwrap(), + ] { + assert!(!candidates.is_empty()); + for candidate in candidates { + validate_numeric_2q_candidate(&matrix, qubits, &candidate).unwrap(); + let model = target.lowering_cost_model().unwrap(); + let cost = model + .cost_after_local_cleanup(qubits.to_vec(), candidate.operations.clone()) + .unwrap(); + assert_eq!(candidate.cost.lowered_two_qubit_ops, cost.two_qubit_ops); + assert_eq!(candidate.cost.lowered_depth, cost.depth); + assert_eq!(candidate.cost.lowered_total_ops, cost.total_ops); + assert_eq!(candidate.cost.parameterized_ops, cost.parameterized_ops); + assert_eq!( + candidate.cost, + target_aware_cost_of_value_operations( + &candidate.operations, + &target, + candidate.backend, + ) + .unwrap() + ); + } + } +} diff --git a/crates/cqlib-core/src/compile/transform/device_lowering.rs b/crates/cqlib-core/src/compile/transform/device_lowering.rs index cbb28cbe..14c821f6 100644 --- a/crates/cqlib-core/src/compile/transform/device_lowering.rs +++ b/crates/cqlib-core/src/compile/transform/device_lowering.rs @@ -43,13 +43,14 @@ use crate::compile::knowledge::{ ConcreteOperationView, KnowledgeInstructionKey, RuleLibrary, instantiate_target, rule_matches_operations, }; +use crate::compile::numeric_matrix::one_qubit_run_matrix; use crate::compile::transform::decompose::unitary::synthesize_numeric_1q_unitary; -use crate::compile::transform::lowering_support::{LoweringTarget, OperationSequenceLowerer}; +use crate::compile::transform::lowering_support::{ + LowerableOperation, LoweringTarget, OperationSequenceLowerer, +}; use crate::compile::transform::rebuild::{CircuitRebuildContext, ClassicalRemap}; use crate::compile::transform::{CircuitAnalysis, TransformOutcome, Transformer}; use crate::device::{Device, PhysicalQubit}; -use ndarray::Array2; -use num_complex::Complex64; use smallvec::{SmallVec, smallvec}; use std::collections::{BTreeMap, HashSet}; use std::f64::consts::{FRAC_PI_2, PI}; @@ -57,14 +58,6 @@ use std::sync::Arc; const PHASE_EPS: f64 = 1e-12; -#[derive(Debug, Clone)] -pub(super) struct LowerableOperation { - pub(super) instruction: Instruction, - pub(super) qubits: SmallVec<[Qubit; 3]>, - pub(super) params: SmallVec<[ParameterValue; 1]>, - pub(super) label: Option>, -} - /// Lowers a routed physical circuit to one device's exact native instruction /// capabilities, including ordered qargs and local capability overrides. #[derive(Debug, Clone, Copy)] @@ -282,7 +275,7 @@ impl<'a> DeviceCircuitLowerer<'a> { Instruction::Standard(StandardGate::GPhase) ) { self.changed = true; - target.accumulate_phase(gphase_param(&operation)?); + target.accumulate_phase(operation.gphase_param()?); return Ok(()); } @@ -938,27 +931,6 @@ fn bufferable_one_qubit( .then_some(qubits[0]) } -/// Recomposes the exact 2x2 unitary of one buffered one-qubit run. -fn one_qubit_run_matrix(operations: &[ValueOperation]) -> Option> { - let mut matrix = Array2::::eye(2); - for operation in operations { - let ValueInstruction::Instruction(Instruction::Standard(gate)) = &operation.instruction - else { - return None; - }; - let params = operation - .params - .iter() - .map(|param| match param { - ParameterValue::Fixed(value) if value.is_finite() => Some(*value), - ParameterValue::Fixed(_) | ParameterValue::Param(_) => None, - }) - .collect::>>()?; - matrix = gate.matrix(¶ms).ok()?.dot(&matrix); - } - Some(matrix) -} - /// Returns the fixed angle of a one-parameter `RZ` operation. fn fixed_rz_angle(operation: &ValueOperation) -> Option { if !matches!( @@ -1040,25 +1012,11 @@ fn collect_root_states(circuit: &Circuit) -> Result match control { - ClassicalControlOp::If(op) => { - collect(op.then_body().operations(), scan); - if let Some(body) = op.else_body() { - collect(body.operations(), scan); - } - } - ClassicalControlOp::While(op) => collect(op.body().operations(), scan), - ClassicalControlOp::For(op) => collect(op.body().operations(), scan), - ClassicalControlOp::Switch(op) => { - for case in op.cases() { - collect(case.body().operations(), scan); - } - if let Some(body) = op.default() { - collect(body.operations(), scan); - } + Instruction::ClassicalControl(control) => { + for body in control.bodies() { + collect(body.operations(), scan); } - ClassicalControlOp::Break | ClassicalControlOp::Continue => {} - }, + } _ => {} } } @@ -1121,19 +1079,7 @@ fn has_fusible_one_qubit_run(circuit: &Circuit) -> bool { } Instruction::ClassicalControl(control) => { pending.clear(); - let body_has_run = match control { - ClassicalControlOp::If(op) => { - scan(op.then_body().operations()) - || op.else_body().is_some_and(|body| scan(body.operations())) - } - ClassicalControlOp::While(op) => scan(op.body().operations()), - ClassicalControlOp::For(op) => scan(op.body().operations()), - ClassicalControlOp::Switch(op) => { - op.cases().iter().any(|case| scan(case.body().operations())) - || op.default().is_some_and(|body| scan(body.operations())) - } - ClassicalControlOp::Break | ClassicalControlOp::Continue => false, - }; + let body_has_run = control.bodies().any(|body| scan(body.operations())); if body_has_run { return true; } @@ -1205,18 +1151,6 @@ fn instantiate_direction_template( } } -fn gphase_param(operation: &LowerableOperation) -> Result { - operation - .params - .first() - .map(Parameter::from) - .ok_or_else(|| { - CompilerError::InvariantViolation( - "GPhase operation must contain one parameter".to_string(), - ) - }) -} - #[cfg(test)] #[path = "device_lowering_test.rs"] mod device_lowering_test; diff --git a/crates/cqlib-core/src/compile/transform/layout/analysis.rs b/crates/cqlib-core/src/compile/transform/layout/analysis.rs index f83ce7ab..af437084 100644 --- a/crates/cqlib-core/src/compile/transform/layout/analysis.rs +++ b/crates/cqlib-core/src/compile/transform/layout/analysis.rs @@ -255,28 +255,10 @@ fn count_layout_order_operations(operations: &[Operation]) -> usize { operations .iter() .map(|operation| match &operation.instruction { - Instruction::ClassicalControl(control) => match control { - ClassicalControlOp::If(op) => { - count_layout_order_operations(op.then_body().operations()) - + op.else_body() - .map_or(0, |body| count_layout_order_operations(body.operations())) - } - ClassicalControlOp::While(op) => { - count_layout_order_operations(op.body().operations()) - } - ClassicalControlOp::For(op) => { - count_layout_order_operations(op.body().operations()) - } - ClassicalControlOp::Switch(op) => { - op.cases() - .iter() - .map(|case| count_layout_order_operations(case.body().operations())) - .sum::() - + op.default() - .map_or(0, |body| count_layout_order_operations(body.operations())) - } - ClassicalControlOp::Break | ClassicalControlOp::Continue => 0, - }, + Instruction::ClassicalControl(control) => control + .bodies() + .map(|body| count_layout_order_operations(body.operations())) + .sum(), Instruction::ClassicalData(_) | Instruction::Directive(_) | Instruction::Delay => 0, _ => 1, }) @@ -365,27 +347,9 @@ impl InteractionAnalyzer { /// This is structural analysis: each branch, loop body, or switch case is /// included once, without runtime probability or loop-count weighting. fn scan_control_flow(&mut self, control: &ClassicalControlOp) -> Result<(), CompilerError> { - // Control flow is scanned as a static operation tree. We include every - // body once and leave runtime path weighting to future profile-guided - // compilation work. - match control { - ClassicalControlOp::If(op) => { - self.scan_operations(op.then_body().operations())?; - if let Some(body) = op.else_body() { - self.scan_operations(body.operations())?; - } - } - ClassicalControlOp::While(op) => self.scan_operations(op.body().operations())?, - ClassicalControlOp::For(op) => self.scan_operations(op.body().operations())?, - ClassicalControlOp::Switch(op) => { - for case in op.cases() { - self.scan_operations(case.body().operations())?; - } - if let Some(body) = op.default() { - self.scan_operations(body.operations())?; - } - } - ClassicalControlOp::Break | ClassicalControlOp::Continue => {} + // Structural scan: visit each body once without execution weighting. + for body in control.bodies() { + self.scan_operations(body.operations())?; } Ok(()) } diff --git a/crates/cqlib-core/src/compile/transform/lowering_support.rs b/crates/cqlib-core/src/compile/transform/lowering_support.rs index a1ec4def..b37b2e79 100644 --- a/crates/cqlib-core/src/compile/transform/lowering_support.rs +++ b/crates/cqlib-core/src/compile/transform/lowering_support.rs @@ -11,11 +11,36 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. -//! Shared output and traversal support for circuit lowering passes. +//! Shared operation, output, and traversal support for circuit lowering passes. -use crate::circuit::{Operation, Parameter, ValueOperation}; +use crate::circuit::{Instruction, Operation, Parameter, ParameterValue, Qubit, ValueOperation}; use crate::compile::CompilerError; use crate::compile::transform::rebuild::ClassicalRemap; +use smallvec::SmallVec; + +/// A gate-like operation with resolved parameters awaiting lowering. +#[derive(Debug, Clone)] +pub(super) struct LowerableOperation { + pub(super) instruction: Instruction, + pub(super) qubits: SmallVec<[Qubit; 3]>, + pub(super) params: SmallVec<[ParameterValue; 1]>, + pub(super) label: Option>, +} + +impl LowerableOperation { + /// Reads the first parameter as an exact global-phase contribution. + /// + /// The caller must have identified this operation as `GPhase`. This checks + /// for a missing parameter; instruction and parameter arity validation + /// remain the responsibility of the caller's input pipeline. + pub(super) fn gphase_param(&self) -> Result { + self.params.first().map(Parameter::from).ok_or_else(|| { + CompilerError::InvariantViolation( + "GPhase operation must contain one parameter".to_string(), + ) + }) + } +} #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum LoweringScope { diff --git a/crates/cqlib-core/src/compile/transform/native_optimization.rs b/crates/cqlib-core/src/compile/transform/native_optimization.rs index 76467c60..d5fe9590 100644 --- a/crates/cqlib-core/src/compile/transform/native_optimization.rs +++ b/crates/cqlib-core/src/compile/transform/native_optimization.rs @@ -43,6 +43,7 @@ use crate::circuit::{ use crate::compile::CompilerError; use crate::compile::device_planning::DevicePlanningSession; use crate::compile::device_planning::cost::{RobustDurationKey, RobustErrorKey}; +use crate::compile::numeric_matrix::one_qubit_run_matrix; use crate::compile::sabre::MetricAvailability; use crate::compile::transform::decompose::unitary::{ DeviceContextCostFailure, DeviceSynthesisPlacement, DeviceTwoQubitSynthesisContext, @@ -61,8 +62,6 @@ use crate::compile::transform::{ Canonicalizer, CircuitAnalysis, DeviceLowerer, RewriteEdits, TransformOutcome, Transformer, }; use crate::device::Device; -use ndarray::Array2; -use num_complex::Complex64; use smallvec::{SmallVec, smallvec}; use std::borrow::Cow; use std::cmp::Ordering; @@ -655,18 +654,16 @@ impl<'a> NativeOptimizer<'a> { if let TransformOutcome::Changed(cleaned) = OptimizeNativeLocalGates::with_quality_policy(cleanup_context, self.quality_policy) .transform(&raw_phase_b, None)? - { - if let NativeStageOutcome::Candidate(candidate) = self.evaluate_stage_candidate( + && let NativeStageOutcome::Candidate(candidate) = self.evaluate_stage_candidate( branch_start, Arc::new(cleaned), best.costs, entry_costs, branch_context, session, - )? { - improved |= - install_best_checkpoint(&candidate, best.circuit, best.costs, best.context); - } + )? + { + improved |= install_best_checkpoint(&candidate, best.circuit, best.costs, best.context); } Ok(improved) } @@ -838,29 +835,11 @@ fn collect_scope_costs( Instruction::Standard(_) | Instruction::McGate(_) => accumulator .add_gate(&operation.instruction, &operation.qubits) .map_err(ScopeCostError::Context)?, - Instruction::ClassicalControl(control) => match control { - ClassicalControlOp::If(op) => { - collect_scope_costs(op.then_body().operations(), context, output)?; - if let Some(body) = op.else_body() { - collect_scope_costs(body.operations(), context, output)?; - } - } - ClassicalControlOp::While(op) => { - collect_scope_costs(op.body().operations(), context, output)?; - } - ClassicalControlOp::For(op) => { - collect_scope_costs(op.body().operations(), context, output)?; - } - ClassicalControlOp::Switch(op) => { - for case in op.cases() { - collect_scope_costs(case.body().operations(), context, output)?; - } - if let Some(body) = op.default() { - collect_scope_costs(body.operations(), context, output)?; - } + Instruction::ClassicalControl(control) => { + for body in control.bodies() { + collect_scope_costs(body.operations(), context, output)?; } - ClassicalControlOp::Break | ClassicalControlOp::Continue => {} - }, + } Instruction::UnitaryGate(_) | Instruction::CircuitGate(_) | Instruction::ClassicalData(_) @@ -1573,19 +1552,6 @@ fn fixed_params(operation: &ValueOperation) -> Option> { .collect() } -fn one_qubit_run_matrix(operations: &[ValueOperation]) -> Option> { - let mut matrix = Array2::::eye(2); - for operation in operations { - let ValueInstruction::Instruction(Instruction::Standard(gate)) = &operation.instruction - else { - return None; - }; - let gate_matrix = gate.matrix(&fixed_params(operation)?).ok()?; - matrix = gate_matrix.dot(&matrix); - } - Some(matrix) -} - fn u_operation(qubit: Qubit, decomposition: OneQubitUnitaryDecomposition) -> ValueOperation { ValueOperation { instruction: ValueInstruction::from_instruction(Instruction::Standard(StandardGate::U)), diff --git a/crates/cqlib-core/src/compile/transform/one_qubit_optimization_test.rs b/crates/cqlib-core/src/compile/transform/one_qubit_optimization_test.rs index f596332d..b5fe6759 100644 --- a/crates/cqlib-core/src/compile/transform/one_qubit_optimization_test.rs +++ b/crates/cqlib-core/src/compile/transform/one_qubit_optimization_test.rs @@ -11,9 +11,13 @@ // that they have been altered from the originals. use super::*; +use crate::circuit::test_utils::assert_matrix_approx_eq; use crate::circuit::{ClassicalControlOp, ClassicalExpr, Instruction, Qubit, StandardGate}; +use crate::circuit::{Parameter, circuit_to_matrix}; use crate::compile::test_utils::assert_compiled_circuit_equivalent; +use crate::compile::transform::TargetBasisLowerer; use crate::compile::transform::{OperationReplacement, RewriteEdits, TransformerTestExt}; +use std::f64::consts::{FRAC_PI_2, PI}; use std::sync::Arc; #[test] @@ -154,6 +158,118 @@ fn basis_uses_lowered_cost_and_keeps_exact_semantics() { assert_compiled_circuit_equivalent(&result.circuit, &circuit); } +fn xy_basis() -> Vec { + [StandardGate::RX, StandardGate::RY, StandardGate::CZ] + .into_iter() + .map(Instruction::Standard) + .collect() +} + +fn lower_xy_and_assert_exact(source: &Circuit, optimized: &Circuit) -> Circuit { + let lowered = TargetBasisLowerer::new(xy_basis()) + .unwrap() + .transform_resolved(optimized, None) + .unwrap() + .circuit; + assert_matrix_approx_eq( + &circuit_to_matrix(&lowered, None).unwrap(), + &circuit_to_matrix(source, None).unwrap(), + 1e-12, + ); + lowered +} + +#[test] +fn xy_basis_fuses_four_and_five_gate_runs_but_keeps_short_sequences() { + let optimizer = OptimizeOneQubitRuns::basis(xy_basis()).unwrap(); + let q0 = Qubit::new(0); + for count in 1..=6 { + let mut circuit = Circuit::new(1); + for index in 0..count { + let angle = 0.13 + 0.17 * index as f64; + if index % 2 == 0 { + circuit.rx(q0, angle).unwrap(); + } else { + circuit.ry(q0, angle).unwrap(); + } + } + let result = optimizer.transform_resolved(&circuit, None).unwrap(); + assert_eq!(result.changed, count > 3, "source length {count}"); + if count <= 3 { + assert_eq!(result.circuit, circuit); + } + let lowered = lower_xy_and_assert_exact(&circuit, &result.circuit); + assert_eq!(lowered.operations().len(), count.min(3)); + assert!( + !optimizer + .transform_resolved(&lowered, None) + .unwrap() + .changed + ); + } +} + +#[test] +fn xy_basis_collapses_four_gate_runs_to_rotation_or_scalar_phase() { + let optimizer = OptimizeOneQubitRuns::basis(xy_basis()).unwrap(); + let q0 = Qubit::new(0); + for (last, count) in [(PI, 0), (7.0 * PI / 8.0, 1)] { + let mut circuit = Circuit::new(1); + circuit.set_global_phase(Parameter::from(0.2)); + circuit.ry(q0, FRAC_PI_2).unwrap(); + circuit.rx(q0, PI).unwrap(); + circuit.ry(q0, FRAC_PI_2).unwrap(); + circuit.rx(q0, last).unwrap(); + let result = optimizer.transform_resolved(&circuit, None).unwrap(); + assert!(result.changed); + let lowered = lower_xy_and_assert_exact(&circuit, &result.circuit); + assert_eq!(lowered.operations().len(), count); + } +} + +#[test] +fn xy_basis_cleanup_preserves_entanglers_and_crosses_disjoint_operations() { + let optimizer = OptimizeOneQubitRuns::basis(xy_basis()).unwrap(); + let mut circuit = Circuit::new(2); + for round in 0..3 { + for index in 0..5 { + for qubit in 0..2 { + let angle = 0.13 + 0.17 * index as f64 + 0.23 * (qubit + round) as f64; + if index % 2 == 0 { + circuit.rx(Qubit::new(qubit), angle).unwrap(); + } else { + circuit.ry(Qubit::new(qubit), angle).unwrap(); + } + } + } + if round < 2 { + circuit + .cz(Qubit::new(round), Qubit::new(1 - round)) + .unwrap(); + } + } + let result = optimizer.transform_resolved(&circuit, None).unwrap(); + assert!(result.changed); + let lowered = lower_xy_and_assert_exact(&circuit, &result.circuit); + let entanglers = |circuit: &Circuit| { + circuit + .operations() + .iter() + .filter(|op| op.qubits.len() == 2) + .cloned() + .collect::>() + }; + assert_eq!(entanglers(&lowered), entanglers(&circuit)); + assert_eq!( + lowered + .operations() + .iter() + .filter(|op| op.qubits.len() == 1) + .count(), + 18 + ); +} + #[test] fn labeled_gate_is_a_boundary() { let q0 = Qubit::new(0); diff --git a/crates/cqlib-core/src/compile/transform/rebuild.rs b/crates/cqlib-core/src/compile/transform/rebuild.rs index edef479a..d7fede00 100644 --- a/crates/cqlib-core/src/compile/transform/rebuild.rs +++ b/crates/cqlib-core/src/compile/transform/rebuild.rs @@ -302,7 +302,7 @@ impl CircuitRebuildContext { /// parameter suitable for a rebuilt operation. /// /// [`CircuitParam::Index`] is resolved against `source`'s parameter table. - /// [`CircuitParam::Fixed`] values are constant-folded when possible. + /// Resolved parameters are converted to fixed values when constant evaluation succeeds. pub fn resolve_source_param( source: &Circuit, param: &CircuitParam, @@ -502,6 +502,39 @@ mod tests { use super::*; use crate::circuit::StandardGate; + #[test] + fn source_resolution_folds_constants_but_parameter_value_preserves_representation() { + use crate::circuit::{CircuitError, Parameter}; + + let mut source = Circuit::new(1); + let (index, _) = source.add_parameter(Parameter::pi()); + let param = CircuitParam::Index(index as u32); + assert!(matches!(source.parameter_value(¶m).unwrap(), + ParameterValue::Param(value) if value == Parameter::pi())); + assert!( + matches!(CircuitRebuildContext::resolve_source_param(&source, ¶m).unwrap(), + ParameterValue::Fixed(value) if value == std::f64::consts::PI) + ); + let (index, _) = source.add_parameter(Parameter::symbol("theta")); + assert!( + matches!(CircuitRebuildContext::resolve_source_param(&source, &CircuitParam::Index(index as u32)).unwrap(), + ParameterValue::Param(value) if value == Parameter::symbol("theta")) + ); + for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] { + let param = CircuitParam::Fixed(value); + assert!( + matches!(source.parameter_value(¶m).unwrap(), ParameterValue::Fixed(stored) if stored.to_bits() == value.to_bits()) + ); + assert!(matches!( + CircuitRebuildContext::resolve_source_param(&source, ¶m), + Err(CompilerError::Circuit(CircuitError::InvalidParameterValue( + 0, + _ + ))) + )); + } + } + #[test] fn preserved_rebuild_remaps_runtime_classical_handles_recursively() { let mut source = Circuit::new(3); diff --git a/crates/cqlib-core/src/compile/transform/resynthesis/cost_test.rs b/crates/cqlib-core/src/compile/transform/resynthesis/cost_test.rs index 4664b5f1..7f2e9f86 100644 --- a/crates/cqlib-core/src/compile/transform/resynthesis/cost_test.rs +++ b/crates/cqlib-core/src/compile/transform/resynthesis/cost_test.rs @@ -102,7 +102,7 @@ fn depth_estimate_keeps_disjoint_single_qubit_ops_parallel() { } #[test] -fn depth_estimate_serializes_ops_on_shared_qubit() { +fn local_cleanup_fuses_ops_on_shared_qubit() { let q0 = Qubit::new(0); let ops = [ ValueOperation::from_standard(StandardGate::H, [q0], []), @@ -111,6 +111,119 @@ fn depth_estimate_serializes_ops_on_shared_qubit() { let cost = replacement_cost(&ops); - assert_eq!(cost.lowered_total_ops, 2); - assert_eq!(cost.lowered_depth, 2); + assert_eq!(cost.lowered_total_ops, 1); + assert_eq!(cost.lowered_depth, 1); +} + +#[test] +fn explicit_basis_scoring_reuses_xyx_and_removes_zero_rotation() { + let target = TwoQubitSynthesisTarget::from_standard_gates( + vec![StandardGate::RX, StandardGate::RY], + vec![StandardGate::CZ], + true, + ) + .unwrap(); + let q = [Qubit::new(0), Qubit::new(1)]; + let operations = [ + ValueOperation::from_standard( + StandardGate::U, + [q[0]], + [0.37.into(), 0.23.into(), (-0.41).into()], + ), + ValueOperation::from_standard(StandardGate::CZ, q, []), + ValueOperation::from_standard(StandardGate::RX, [q[1]], [0.0.into()]), + ]; + let candidate = target_aware_cost_of_value_operations( + &operations, + &target, + TwoQubitUnitaryDecomposeBasis::Cz, + ) + .unwrap(); + // Generic U uses the completed three-rotation XYX candidate. The one + // cleanup round removes the trailing native zero rotation. + assert_eq!(candidate.lowered_two_qubit_ops, 1); + assert_eq!(candidate.lowered_depth, 4); + assert_eq!(candidate.lowered_total_ops, 4); + assert_eq!(candidate.parameterized_ops, 3); + let source = cost_of_source_value_operations(&operations, &target).unwrap(); + assert_eq!( + source, + ResynthesisCost { + backend_order: 0, + ..candidate + } + ); +} + +#[test] +fn local_cleanup_corrects_original_cost_reversal() { + use crate::circuit::{Circuit, circuit_to_matrix}; + use crate::compile::transform::decompose::unitary::{ + TwoQubitSynthesisRequest, plan_numeric_2q_unitary, + }; + + let q = [Qubit::new(0), Qubit::new(1)]; + let operations = vec![ + ValueOperation::from_standard(StandardGate::CX, q, []), + ValueOperation::from_standard(StandardGate::H, [q[1]], []), + ValueOperation::from_standard(StandardGate::H, [q[0]], []), + ]; + let target = TwoQubitSynthesisTarget::from_standard_gates( + vec![StandardGate::RZ, StandardGate::X2P], + vec![StandardGate::CZ], + true, + ) + .unwrap(); + let model = target.lowering_cost_model().unwrap(); + let raw_source = model + .cost_of_fixed_operations(q.to_vec(), operations.clone()) + .unwrap(); + let source_cost = cost_of_source_value_operations(&operations, &target).unwrap(); + assert_eq!( + ( + raw_source.two_qubit_ops, + raw_source.depth, + raw_source.total_ops, + raw_source.parameterized_ops + ), + (1, 10, 13, 8), + ); + assert_eq!( + source_cost, + ResynthesisCost { + lowered_two_qubit_ops: 1, + lowered_depth: 6, + lowered_total_ops: 7, + parameterized_ops: 4, + backend_order: 0, + } + ); + let circuit = Circuit::from_operations(q.to_vec(), operations, None, None).unwrap(); + let matrix = circuit_to_matrix(&circuit, None).unwrap(); + let candidates = plan_numeric_2q_unitary(TwoQubitSynthesisRequest { + matrix: &matrix, + qubits: q, + target: target.clone(), + }) + .unwrap(); + assert!(!candidates.is_empty()); + for candidate in candidates { + let raw_candidate = model + .cost_of_fixed_operations(q.to_vec(), candidate.operations.clone()) + .unwrap(); + // Lowering alone favors the candidate; local cleanup reverses that + // ordering because lowering exposes two adjacent H gates that cancel. + assert_eq!( + ( + raw_candidate.two_qubit_ops, + raw_candidate.depth, + raw_candidate.total_ops, + raw_candidate.parameterized_ops + ), + (1, 7, 11, 6), + ); + assert_eq!(candidate.cost.lowered_total_ops, 9); + assert_eq!(candidate.cost.lowered_depth, 6); + assert!(candidate.cost >= source_cost); + } } diff --git a/crates/cqlib-core/src/compile/transform/resynthesis/resynthesizer.rs b/crates/cqlib-core/src/compile/transform/resynthesis/resynthesizer.rs index c331fe2c..e5158f88 100644 --- a/crates/cqlib-core/src/compile/transform/resynthesis/resynthesizer.rs +++ b/crates/cqlib-core/src/compile/transform/resynthesis/resynthesizer.rs @@ -773,29 +773,9 @@ fn has_fixed_numeric_two_qubit_standard(operations: &[Operation], circuit: &Circ } fn control_has_fixed_numeric_2q(control: &ClassicalControlOp, circuit: &Circuit) -> bool { - match control { - ClassicalControlOp::If(op) => { - has_fixed_numeric_two_qubit_standard(op.then_body().operations(), circuit) - || op.else_body().is_some_and(|body| { - has_fixed_numeric_two_qubit_standard(body.operations(), circuit) - }) - } - ClassicalControlOp::While(op) => { - has_fixed_numeric_two_qubit_standard(op.body().operations(), circuit) - } - ClassicalControlOp::For(op) => { - has_fixed_numeric_two_qubit_standard(op.body().operations(), circuit) - } - ClassicalControlOp::Switch(op) => { - op.cases() - .iter() - .any(|case| has_fixed_numeric_two_qubit_standard(case.body().operations(), circuit)) - || op.default().is_some_and(|body| { - has_fixed_numeric_two_qubit_standard(body.operations(), circuit) - }) - } - ClassicalControlOp::Break | ClassicalControlOp::Continue => false, - } + control + .bodies() + .any(|body| has_fixed_numeric_two_qubit_standard(body.operations(), circuit)) } #[cfg(test)] diff --git a/crates/cqlib-core/src/compile/transform/resynthesis/selector/planning_bound.rs b/crates/cqlib-core/src/compile/transform/resynthesis/selector/planning_bound.rs index be56df81..c8a1f2bb 100644 --- a/crates/cqlib-core/src/compile/transform/resynthesis/selector/planning_bound.rs +++ b/crates/cqlib-core/src/compile/transform/resynthesis/selector/planning_bound.rs @@ -99,6 +99,19 @@ pub(super) fn estimate_block_priority_bound( .collect::>(); native_two_qubit_gates.sort_by_key(|gate| *gate as u8); minimum_constructive_entanglers(&decomp, &native_two_qubit_gates, true) + } else if config.two_qubit_target.lowering_cost_model().is_some() + && config.two_qubit_target.native_2q().iter().any(|gate| { + !matches!( + gate, + crate::circuit::StandardGate::CX + | crate::circuit::StandardGate::CY + | crate::circuit::StandardGate::CZ + ) + }) + { + // Cleanup leaves CX/CY/CZ gates in place, but may remove a zero-angle + // native interaction. A template count is not a lower bound then. + Some(0) } else { minimum_constructive_entanglers( &decomp, diff --git a/crates/cqlib-core/src/compile/transform/resynthesis/selector_test.rs b/crates/cqlib-core/src/compile/transform/resynthesis/selector_test.rs index 24e3771b..9679bd43 100644 --- a/crates/cqlib-core/src/compile/transform/resynthesis/selector_test.rs +++ b/crates/cqlib-core/src/compile/transform/resynthesis/selector_test.rs @@ -751,3 +751,131 @@ fn patch_priority_uses_the_native_quality_policy() { Ordering::Greater ); } + +#[test] +fn local_cleanup_cost_reversal_keeps_original() { + let q = [Qubit::new(0), Qubit::new(1)]; + let operations = [ + standard_op(StandardGate::CX, &q), + standard_op(StandardGate::H, &[q[1]]), + standard_op(StandardGate::H, &[q[0]]), + ]; + let ops = operations + .iter() + .enumerate() + .map(|(i, op)| view(i, op)) + .collect::>(); + let config = TwoQubitBlockResynthesisConfig::normal( + TwoQubitSynthesisTarget::from_standard_gates( + vec![StandardGate::RZ, StandardGate::X2P], + vec![StandardGate::CZ], + true, + ) + .unwrap(), + ); + let commutation = CachedCommutation::new(config.commutation.clone()); + let patches = select_patches( + vec![block(q, vec![0, 1, 2], 2, 1)], + &ops, + &commutation, + &config, + ) + .unwrap(); + assert!(patches.is_empty(), "{patches:?}"); +} + +#[test] +fn local_cleanup_pruning_matches_eager_selection_with_cold_warm_and_full_caches() { + for entangler in [ + StandardGate::CZ, + StandardGate::CX, + StandardGate::RXX, + StandardGate::RZZ, + ] { + for seed in 0..4 { + let q = if seed % 2 == 0 { + [Qubit::new(0), Qubit::new(1)] + } else { + [Qubit::new(1), Qubit::new(0)] + }; + let mut operations = Vec::new(); + for i in 0..10 { + operations.push(rotation_op( + StandardGate::RX, + &[q[i % 2]], + 0.13 * (seed + i) as f64, + )); + operations.push( + if matches!(entangler, StandardGate::RXX | StandardGate::RZZ) { + rotation_op(entangler, &q, if i % 3 == 0 { 0.0 } else { 0.27 }) + } else { + standard_op(entangler, &q) + }, + ); + } + let ops = operations + .iter() + .enumerate() + .map(|(i, op)| view(i, op)) + .collect::>(); + let target = TwoQubitSynthesisTarget::from_standard_gates( + if matches!(entangler, StandardGate::RXX | StandardGate::RZZ) { + vec![StandardGate::RX, StandardGate::RY] + } else { + vec![StandardGate::RZ, StandardGate::X2P] + }, + vec![entangler], + true, + ) + .unwrap(); + let config = TwoQubitBlockResynthesisConfig::enhanced(target); + let commutation = CachedCommutation::new(config.commutation.clone()); + let blocks = (0..7) + .map(|i| { + TwoQubitNumericBlock::dag_dependency_closed( + q, + (i * 2..i * 2 + 8).collect(), + vec![], + 4, + 4, + false, + ) + }) + .collect::>(); + let eager = select_patches( + (0..7) + .map(|i| block(q, (i * 2..i * 2 + 8).collect(), 4, 4)) + .collect(), + &ops, + &commutation, + &config, + ) + .unwrap(); + assert!(!eager.is_empty(), "entangler={entangler:?}, seed={seed}"); + for budget in [0, 1, 4096] { + let mut cache = TwoQubitSynthesisCache::new(budget); + cache.ensure_namespace(&config, None); + for _ in 0..2 { + let actual = select_patches_with_device_policy( + blocks.clone(), + &ops, + &commutation, + &config, + None, + &mut cache, + NativeQualityPolicy::EntanglerFirst, + ) + .unwrap(); + assert_eq!(actual.len(), eager.len()); + for (a, b) in actual.iter().zip(&eager) { + assert_eq!(a.matched_orders, b.matched_orders); + assert_eq!(a.replacement, b.replacement); + assert_eq!(a.before_cost, b.before_cost); + assert_eq!(a.after_cost, b.after_cost); + assert_eq!(a.synthesis_phase.to_bits(), b.synthesis_phase.to_bits()); + } + } + } + } + } +} diff --git a/crates/cqlib-core/src/compile/transform/rewrite/basis.rs b/crates/cqlib-core/src/compile/transform/rewrite/basis.rs index 932c67c8..5a67f723 100644 --- a/crates/cqlib-core/src/compile/transform/rewrite/basis.rs +++ b/crates/cqlib-core/src/compile/transform/rewrite/basis.rs @@ -12,7 +12,7 @@ //! Target-basis policy for knowledge-based rewrite. -use crate::circuit::{Circuit, ClassicalControlOp, Instruction, Operation, StandardGate}; +use crate::circuit::{Circuit, Instruction, Operation, StandardGate}; use crate::compile::error::CompilerError; use crate::compile::knowledge::library::RuleKind; use crate::compile::knowledge::matcher::KnowledgeInstructionKey as RewriteInstructionKey; @@ -192,58 +192,13 @@ fn scan_operations( } Instruction::ClassicalControl(cc) => { if recurse_control_flow { - match cc { - ClassicalControlOp::If(op) => { - scan_operations( - op.then_body().operations(), - physical_keys, - recurse_control_flow, - scan, - ); - if let Some(else_body) = op.else_body() { - scan_operations( - else_body.operations(), - physical_keys, - recurse_control_flow, - scan, - ); - } - } - ClassicalControlOp::While(op) => { - scan_operations( - op.body().operations(), - physical_keys, - recurse_control_flow, - scan, - ); - } - ClassicalControlOp::For(op) => { - scan_operations( - op.body().operations(), - physical_keys, - recurse_control_flow, - scan, - ); - } - ClassicalControlOp::Switch(op) => { - for case in op.cases() { - scan_operations( - case.body().operations(), - physical_keys, - recurse_control_flow, - scan, - ); - } - if let Some(default) = op.default() { - scan_operations( - default.operations(), - physical_keys, - recurse_control_flow, - scan, - ); - } - } - ClassicalControlOp::Break | ClassicalControlOp::Continue => {} + for body in cc.bodies() { + scan_operations( + body.operations(), + physical_keys, + recurse_control_flow, + scan, + ); } } else { scan.control_flow_ops += 1; diff --git a/crates/cqlib-core/src/compile/transform/rewrite/config.rs b/crates/cqlib-core/src/compile/transform/rewrite/config.rs index d7955be0..c1d00c2f 100644 --- a/crates/cqlib-core/src/compile/transform/rewrite/config.rs +++ b/crates/cqlib-core/src/compile/transform/rewrite/config.rs @@ -285,6 +285,9 @@ pub(super) struct LocalRewriteCost { pub(super) depth_estimate: usize, pub(super) total_ops: usize, pub(super) parameterized_ops: usize, + // Break otherwise equal costs by expression count, so RXY -> XY2P/RX + // specializations strictly decrease cost without permitting neutral cycles. + pub(super) parameter_count: usize, } /// Policy for counting `GPhase` operations in local rewrite cost. @@ -321,6 +324,7 @@ impl LocalRewriteCost { if param_count > 0 { self.parameterized_ops += 1; } + self.parameter_count += param_count; true } } diff --git a/crates/cqlib-core/src/compile/transform/rewrite/edit.rs b/crates/cqlib-core/src/compile/transform/rewrite/edit.rs index 2f8bd45f..251ba634 100644 --- a/crates/cqlib-core/src/compile/transform/rewrite/edit.rs +++ b/crates/cqlib-core/src/compile/transform/rewrite/edit.rs @@ -12,7 +12,7 @@ //! Rewrite-local edit scripts and routing equivalence metadata. -use crate::circuit::{Circuit, CircuitParam, Operation, Parameter, Qubit}; +use crate::circuit::{Circuit, CircuitParam, Operation, Qubit}; use std::ops::Range; /// One exact replacement in a flat operation sequence. @@ -257,22 +257,27 @@ pub(crate) fn operations_are_equivalent( source.instruction == target.instruction && source.label == target.label && source.qubits == target.qubits - && source.params.len() == target.params.len() - && source - .params - .iter() - .zip(&target.params) - .all(|(source_parameter, target_parameter)| { - resolve_operation_param(before, source_parameter) - == resolve_operation_param(after, target_parameter) - }) + && resolved_params_are_equal(before, &source.params, after, &target.params) } -fn resolve_operation_param(circuit: &Circuit, parameter: &CircuitParam) -> Option { - match parameter { - CircuitParam::Fixed(value) => Some(Parameter::from(*value)), - CircuitParam::Index(index) => circuit.parameters().get_index(*index as usize).cloned(), - } +/// Compares resolved expression structure across circuit-local parameter tables. +/// Resolution failures cannot establish equality for edit/proof reuse. +pub(crate) fn resolved_params_are_equal( + before: &Circuit, + source: &[CircuitParam], + after: &Circuit, + target: &[CircuitParam], +) -> bool { + source.len() == target.len() + && source.iter().zip(target).all(|(left, right)| { + match ( + before.resolve_parameter(left), + after.resolve_parameter(right), + ) { + (Ok(left), Ok(right)) => left == right, + _ => false, + } + }) } fn longest_increasing_source_subsequence(candidates: &[(usize, usize)]) -> Vec<(usize, usize)> { diff --git a/crates/cqlib-core/src/compile/transform/rewrite/edit_test.rs b/crates/cqlib-core/src/compile/transform/rewrite/edit_test.rs index 9efa9a03..99b3b758 100644 --- a/crates/cqlib-core/src/compile/transform/rewrite/edit_test.rs +++ b/crates/cqlib-core/src/compile/transform/rewrite/edit_test.rs @@ -117,3 +117,83 @@ fn linear_derivation_handles_insertions_and_deletions() { ) ); } + +#[test] +fn resolved_comparison_uses_values_across_parameter_tables() { + use super::resolved_params_are_equal; + use crate::circuit::{CircuitParam, Parameter}; + + let mut before = Circuit::new(1); + let mut after = Circuit::new(1); + before.add_parameter(Parameter::symbol("theta")); + after.add_parameter(Parameter::symbol("phi")); + after.add_parameter(Parameter::symbol("theta")); + assert!(!resolved_params_are_equal( + &before, + &[CircuitParam::Index(0)], + &after, + &[CircuitParam::Index(0)], + )); + assert!(resolved_params_are_equal( + &before, + &[CircuitParam::Index(0)], + &after, + &[CircuitParam::Index(1)], + )); + assert!(!resolved_params_are_equal( + &before, + &[], + &after, + &[CircuitParam::Fixed(0.0)] + )); +} + +#[test] +fn resolved_comparison_preserves_structure_without_constant_folding() { + use super::resolved_params_are_equal; + use crate::circuit::{CircuitParam, Parameter}; + + let mut circuit = Circuit::new(1); + circuit.add_parameter(Parameter::from(0.5)); + circuit.add_parameter(Parameter::pi()); + assert!(resolved_params_are_equal( + &circuit, + &[CircuitParam::Fixed(0.5)], + &circuit, + &[CircuitParam::Index(0)], + )); + assert!(!resolved_params_are_equal( + &circuit, + &[CircuitParam::Fixed(std::f64::consts::PI)], + &circuit, + &[CircuitParam::Index(1)], + )); +} + +#[test] +fn invalid_parameters_cannot_establish_operation_equivalence() { + use super::operations_are_equivalent; + use crate::circuit::CircuitParam; + + let mut circuit = Circuit::new(1); + circuit.rx(Qubit::new(0), 0.5).unwrap(); + let valid = &circuit.operations()[0]; + for param in [ + CircuitParam::Index(99), + CircuitParam::Fixed(f64::NAN), + CircuitParam::Fixed(f64::INFINITY), + CircuitParam::Fixed(f64::NEG_INFINITY), + ] { + let mut invalid = valid.clone(); + invalid.params[0] = param; + assert!(!operations_are_equivalent( + &circuit, &invalid, &circuit, &invalid + )); + assert!(!operations_are_equivalent( + &circuit, valid, &circuit, &invalid + )); + assert!(!operations_are_equivalent( + &circuit, &invalid, &circuit, valid + )); + } +} diff --git a/crates/cqlib-core/src/compile/transform/rewrite/edit_validation.rs b/crates/cqlib-core/src/compile/transform/rewrite/edit_validation.rs index 11329c9f..ffc50c41 100644 --- a/crates/cqlib-core/src/compile/transform/rewrite/edit_validation.rs +++ b/crates/cqlib-core/src/compile/transform/rewrite/edit_validation.rs @@ -13,8 +13,10 @@ //! Validation and coordinate mapping for rewrite edit scripts. -use super::edit::{OperationReplacement, QubitBijection, operations_are_equivalent}; -use crate::circuit::{Circuit, CircuitParam, Operation, Parameter}; +use super::edit::{ + OperationReplacement, QubitBijection, operations_are_equivalent, resolved_params_are_equal, +}; +use crate::circuit::Circuit; use std::ops::Range; pub(super) fn valid_exact_clean_gaps( @@ -234,7 +236,7 @@ fn valid_clean_gap_bijection( if source.instruction != target.instruction || source.label != target.label || source.qubits.len() != target.qubits.len() - || !resolved_params_are_equal(before, source, after, target) + || !resolved_params_are_equal(before, &source.params, after, &target.params) { return false; } @@ -253,27 +255,3 @@ fn valid_clean_gap_bijection( } used.into_iter().all(|entry| entry) } - -fn resolved_params_are_equal( - before: &Circuit, - source: &Operation, - after: &Circuit, - target: &Operation, -) -> bool { - source.params.len() == target.params.len() - && source - .params - .iter() - .zip(&target.params) - .all(|(source_parameter, target_parameter)| { - resolve_operation_param(before, source_parameter) - == resolve_operation_param(after, target_parameter) - }) -} - -fn resolve_operation_param(circuit: &Circuit, parameter: &CircuitParam) -> Option { - match parameter { - CircuitParam::Fixed(value) => Some(Parameter::from(*value)), - CircuitParam::Index(index) => circuit.parameters().get_index(*index as usize).cloned(), - } -} diff --git a/crates/cqlib-core/src/compile/transform/rewrite/matcher.rs b/crates/cqlib-core/src/compile/transform/rewrite/matcher.rs index cf3439a1..e95456da 100644 --- a/crates/cqlib-core/src/compile/transform/rewrite/matcher.rs +++ b/crates/cqlib-core/src/compile/transform/rewrite/matcher.rs @@ -734,7 +734,8 @@ impl BlockMatchCache { let params = operation .params .iter() - .map(|param| resolve_operation_param(circuit, param)) + .enumerate() + .map(|(index, param)| resolve_operation_param(circuit, param, index)) .collect::, _>>()?; touched_qubits.extend(operation.qubits.iter().copied()); @@ -853,7 +854,8 @@ impl BlockMatchCache { let params = operation .params .iter() - .map(|parameter| resolve_operation_param(after, parameter)) + .enumerate() + .map(|(index, parameter)| resolve_operation_param(after, parameter, index)) .collect::, _>>() .ok()?; instruction_positions @@ -2259,17 +2261,19 @@ fn match_item( pub(super) fn resolve_operation_param( circuit: &Circuit, param: &CircuitParam, + param_index: usize, ) -> Result { - match param { - CircuitParam::Fixed(value) => Ok(Parameter::from(*value)), - CircuitParam::Index(index) => circuit - .parameters() - .get_index(*index as usize) - .cloned() - .ok_or_else(|| { - CompilerError::InvalidInput(format!("invalid rewrite parameter index {}", index)) - }), - } + circuit.resolve_parameter(param).map_err(|error| { + CompilerError::InvalidInput(match error { + crate::circuit::CircuitError::InvalidParameterIndex(index) => { + format!("invalid rewrite parameter index {}", index) + } + crate::circuit::CircuitError::InvalidParameterValue(_, value) => { + format!("invalid rewrite parameter {param_index}: non-finite fixed value {value}") + } + _ => format!("invalid rewrite parameter {param_index}: {error}"), + }) + }) } /// Computes the local rewrite cost for matched source operations. diff --git a/crates/cqlib-core/src/compile/transform/rewrite/matcher_test.rs b/crates/cqlib-core/src/compile/transform/rewrite/matcher_test.rs index 8e5d439a..b81ef8de 100644 --- a/crates/cqlib-core/src/compile/transform/rewrite/matcher_test.rs +++ b/crates/cqlib-core/src/compile/transform/rewrite/matcher_test.rs @@ -628,3 +628,32 @@ fn occurrence_index_is_ordered_and_only_used_for_sparse_keys() { CandidatePositions::Linear(_) )); } + +#[test] +fn match_cache_reports_invalid_parameters_without_panicking() { + use crate::circuit::CircuitParam; + use crate::compile::CompilerError; + + let mut circuit = Circuit::new(1); + circuit.rx(Qubit::new(0), 0.5).unwrap(); + for param in [ + CircuitParam::Index(99), + CircuitParam::Fixed(f64::NAN), + CircuitParam::Fixed(f64::INFINITY), + CircuitParam::Fixed(f64::NEG_INFINITY), + ] { + let missing_index = matches!(param, CircuitParam::Index(_)); + let mut operation = circuit.operations()[0].clone(); + operation.params[0] = param; + let error = + BlockMatchCache::new_with_diagnostics(&circuit, &[operation], false).unwrap_err(); + let CompilerError::InvalidInput(message) = error else { + panic!("unexpected error: {error}"); + }; + if missing_index { + assert_eq!(message, "invalid rewrite parameter index 99"); + } else { + assert!(message.contains("invalid rewrite parameter"), "{message}"); + } + } +} diff --git a/crates/cqlib-core/src/compile/transform/rewrite/mod.rs b/crates/cqlib-core/src/compile/transform/rewrite/mod.rs index 968aad67..32c50eaf 100644 --- a/crates/cqlib-core/src/compile/transform/rewrite/mod.rs +++ b/crates/cqlib-core/src/compile/transform/rewrite/mod.rs @@ -45,6 +45,7 @@ mod session; pub use config::{RewriteConfig, RewriteMode}; pub use diagnostics::KnowledgeRewriteDiagnostics; +pub(crate) use edit::resolved_params_are_equal; pub(crate) use edit::{OperationReplacement, QubitBijection, RewriteEdits}; pub use rewriter::{ KnowledgeRewriteResult, KnowledgeRewriteStats, KnowledgeRewriter, rewrite_circuit, @@ -54,3 +55,6 @@ pub(crate) use session::{KnowledgeRewriteSession, RewriteExecutionRecord}; #[cfg(test)] #[path = "./rewrite_test.rs"] mod rewrite_test; + +#[cfg(test)] +mod qcis_rules_test; diff --git a/crates/cqlib-core/src/compile/transform/rewrite/proof.rs b/crates/cqlib-core/src/compile/transform/rewrite/proof.rs index 5d2be60a..7dc7abb6 100644 --- a/crates/cqlib-core/src/compile/transform/rewrite/proof.rs +++ b/crates/cqlib-core/src/compile/transform/rewrite/proof.rs @@ -103,28 +103,8 @@ fn collect_control_flow_gate_counts( control: &ClassicalControlOp, counts: &mut Vec<(Instruction, usize)>, ) { - match control { - ClassicalControlOp::If(operation) => { - collect_gate_instruction_counts(operation.then_body().operations(), true, counts); - if let Some(body) = operation.else_body() { - collect_gate_instruction_counts(body.operations(), true, counts); - } - } - ClassicalControlOp::While(operation) => { - collect_gate_instruction_counts(operation.body().operations(), true, counts); - } - ClassicalControlOp::For(operation) => { - collect_gate_instruction_counts(operation.body().operations(), true, counts); - } - ClassicalControlOp::Switch(operation) => { - for case in operation.cases() { - collect_gate_instruction_counts(case.body().operations(), true, counts); - } - if let Some(body) = operation.default() { - collect_gate_instruction_counts(body.operations(), true, counts); - } - } - ClassicalControlOp::Break | ClassicalControlOp::Continue => {} + for body in control.bodies() { + collect_gate_instruction_counts(body.operations(), true, counts); } } diff --git a/crates/cqlib-core/src/compile/transform/rewrite/qcis_rules_test.rs b/crates/cqlib-core/src/compile/transform/rewrite/qcis_rules_test.rs new file mode 100644 index 00000000..642591f8 --- /dev/null +++ b/crates/cqlib-core/src/compile/transform/rewrite/qcis_rules_test.rs @@ -0,0 +1,193 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// Licensed under the Apache License, Version 2.0; see LICENSE.txt. + +//! Regression tests for symbolic QCIS rewriting. + +use super::{KnowledgeRewriter, RewriteConfig}; +use crate::circuit::circuit_to_matrix::circuit_to_matrix; +use crate::circuit::test_utils::assert_matrix_approx_eq; +use crate::circuit::{Circuit, Instruction, Parameter, Qubit, StandardGate}; +use crate::compile::knowledge::rule_dsl::load::load_rules_from_str; +use crate::compile::test_utils::standard_ops; +use std::collections::HashMap; +use std::f64::consts::PI; + +fn source_circuit(source: &str) -> Circuit { + let rule = load_rules_from_str(&format!( + "rule input {{ match {{ {source} }} rewrite {{}} }}" + )) + .unwrap() + .remove(0); + let mut circuit = Circuit::new(rule.num_qubits()); + for item in rule.operations { + circuit + .append( + item.instruction, + item.qubits.into_iter().map(Qubit::new), + item.params.unwrap_or_default(), + None, + ) + .unwrap(); + } + circuit +} + +fn assert_strict_equivalence(source: &Circuit, actual: &Circuit) { + for angle in [0.0, 0.317, -1.239, PI / 2.0, PI, 2.0 * PI, -4.0 * PI] { + let bindings = Some(HashMap::from([ + ("a", angle), + ("b", 0.73 - angle), + ("c", -0.91), + ("theta", angle), + ("phi", 0.19 + angle), + ("lambda", -0.39), + ])); + let lhs = circuit_to_matrix(&source.assign_parameters(&bindings).unwrap(), None).unwrap(); + let rhs = circuit_to_matrix(&actual.assign_parameters(&bindings).unwrap(), None).unwrap(); + assert_matrix_approx_eq(&lhs, &rhs, 1e-9); + } +} + +#[test] +fn symbolic_qcis_rules_shorten_and_reach_a_fixed_point() { + use StandardGate::*; + for (source, expected) in [ + ("XY(a) 0, XY(b) 0", vec![RZ]), + ("RZ(a) 0, XY(b) 0", vec![XY]), + ("XY(b) 0, RZ(a) 0", vec![XY]), + ("RZ(a) 0, XY2P(b) 0, RZ(c) 0", vec![XY2P, RZ]), + ("RZ(a) 0, XY2P(b) 0, RZ(-a) 0", vec![XY2P]), + ("RZ(a) 0, XY2M(b) 0, RZ(2*π-a) 0", vec![XY2M]), + ("RZ(a) 0, RXY(theta,b) 0, RZ(-a) 0", vec![RXY]), + ("X2P 0, RZ(a) 0, X2M 0", vec![RY]), + ("X2M 0, RZ(a) 0, X2P 0", vec![RY]), + ("Y2M 0, RZ(a) 0, Y2P 0", vec![RX]), + ("Y2P 0, RZ(a) 0, Y2M 0", vec![RX]), + ("XY2P(phi) 0, XY2P(phi+π) 0", vec![]), + ("XY2M(phi) 0, XY2M(phi-3*π) 0", vec![]), + ("XY2P(phi) 0, XY2P(phi+2*π) 0", vec![XY]), + ("XY2M(phi) 0, XY2M(phi-2*π) 0", vec![XY]), + ("RXY(a,phi) 0, RXY(b,phi+2*π) 0", vec![RXY]), + ("RXY(a,phi) 0, RXY(b,phi+π) 0", vec![RXY]), + ("RXY(2*π,phi) 0", vec![]), + ("RXY(-2*π,phi) 0", vec![]), + ] { + let input = source_circuit(source); + let result = KnowledgeRewriter::production().run(&input).unwrap(); + assert_eq!(standard_ops(&result.circuit), expected, "{source}"); + assert!(result.stats.reached_fixpoint, "{source}"); + assert!( + !KnowledgeRewriter::production() + .run(&result.circuit) + .unwrap() + .changed, + "{source}" + ); + assert_strict_equivalence(&input, &result.circuit); + } +} + +#[test] +fn parameter_reducing_specializations_are_not_rejected_as_equal_cost() { + use StandardGate::*; + for (source, expected) in [ + ("RXY(π/2,phi) 0", XY2P), + ("RXY(-π/2,phi) 0", XY2M), + ("RXY(9*π,phi) 0", XY), + ("RXY(theta,2*π) 0", RX), + ("RXY(theta,π) 0", RX), + ("RXY(theta,π/2) 0", RY), + ("RXY(theta,-π/2) 0", RY), + ("XY2P(3*π) 0", X2M), + ("XY2M(-π) 0", X2P), + ] { + let input = source_circuit(source); + let result = KnowledgeRewriter::production().run(&input).unwrap(); + assert_eq!(standard_ops(&result.circuit), vec![expected], "{source}"); + assert!(result.stats.reached_fixpoint, "{source}"); + assert_strict_equivalence(&input, &result.circuit); + } +} + +#[test] +fn cz_pi_pulse_echo_removes_both_entanglers_for_either_wire_order() { + for pulse in ["Y", "XY(phi)"] { + for active in [0, 1] { + for reversed in [false, true] { + let last = if reversed { "1 0" } else { "0 1" }; + let input = source_circuit(&format!("CZ 0 1, {pulse} {active}, CZ {last}")); + let result = KnowledgeRewriter::production().run(&input).unwrap(); + assert_eq!(result.circuit.operations().len(), 2); + assert!(!standard_ops(&result.circuit).contains(&StandardGate::CZ)); + assert_strict_equivalence(&input, &result.circuit); + } + } + } +} + +#[test] +fn cz_echo_stays_in_the_requested_native_basis() { + let input = source_circuit("CZ 0 1, XY(phi) 1, CZ 1 0"); + let basis = [StandardGate::CZ, StandardGate::XY, StandardGate::RZ]; + let config = RewriteConfig::production() + .with_target_instructions(basis.map(Instruction::Standard).to_vec()) + .unwrap(); + let result = KnowledgeRewriter::new(config).run(&input).unwrap(); + assert_eq!(result.circuit.operations().len(), 2); + assert!(!standard_ops(&result.circuit).contains(&StandardGate::CZ)); + assert!( + standard_ops(&result.circuit) + .iter() + .all(|gate| basis.contains(gate)) + ); + assert_strict_equivalence(&input, &result.circuit); +} + +#[test] +fn half_turn_echo_keeps_the_phase_of_the_original_rz_angle() { + for pulse in ["X2P", "X2M", "Y2P", "Y2M", "XY2P(phi)", "XY2M(phi)"] { + for angle in ["π", "3*π", "-π"] { + let input = source_circuit(&format!("{pulse} 0, RZ({angle}) 0, {pulse} 0")); + let result = KnowledgeRewriter::production().run(&input).unwrap(); + assert_eq!(result.circuit.operations().len(), 1, "{pulse}, {angle}"); + assert_strict_equivalence(&input, &result.circuit); + } + } +} + +#[test] +fn qcis_rules_respect_angles_barriers_and_labels() { + for source in [ + "XY2P(phi) 0, XY2P(phi+π+0.001) 0", + "RXY(5*π/2,phi) 0", // differs from XY2P by a minus sign + "CZ 0 1, XY2P(phi) 0, CZ 0 1", // half pulse is not a Pauli echo + ] { + let input = source_circuit(source); + assert!( + !KnowledgeRewriter::production().run(&input).unwrap().changed, + "{source}" + ); + } + for barrier in [false, true] { + let mut input = Circuit::new(2); + input.cz(Qubit::new(0), Qubit::new(1)).unwrap(); + if barrier { + input.barrier(vec![Qubit::new(0)]).unwrap(); + input.xy(Qubit::new(0), Parameter::symbol("phi")).unwrap(); + } else { + input + .append( + Instruction::Standard(StandardGate::XY), + [Qubit::new(0)], + [Parameter::symbol("phi").into()], + Some("keep pulse"), + ) + .unwrap(); + } + input.cz(Qubit::new(0), Qubit::new(1)).unwrap(); + assert!(!KnowledgeRewriter::production().run(&input).unwrap().changed); + } +} diff --git a/crates/cqlib-core/src/compile/transform/rewrite/rewriter.rs b/crates/cqlib-core/src/compile/transform/rewrite/rewriter.rs index 8fe2b1de..24713297 100644 --- a/crates/cqlib-core/src/compile/transform/rewrite/rewriter.rs +++ b/crates/cqlib-core/src/compile/transform/rewrite/rewriter.rs @@ -1182,7 +1182,7 @@ impl<'a> RoundRewriter<'a> { "GPhase operation must contain one parameter".to_string(), )); } - resolve_operation_param(self.source, ¶ms[0]) + resolve_operation_param(self.source, ¶ms[0], 0) } fn replacement_gphase_param(replacement: &ReplacementItem) -> Result { @@ -1354,28 +1354,11 @@ fn merge_ranges(ranges: &mut Vec>) { fn recursive_operation_count(operations: &[Operation]) -> usize { let mut count = operations.len(); for operation in operations { - let Instruction::ClassicalControl(control) = &operation.instruction else { - continue; - }; - count = count.saturating_add(match control { - ClassicalControlOp::If(op) => recursive_operation_count(op.then_body().operations()) - .saturating_add( - op.else_body() - .map_or(0, |body| recursive_operation_count(body.operations())), - ), - ClassicalControlOp::While(op) => recursive_operation_count(op.body().operations()), - ClassicalControlOp::For(op) => recursive_operation_count(op.body().operations()), - ClassicalControlOp::Switch(op) => op - .cases() - .iter() - .map(|case| recursive_operation_count(case.body().operations())) - .fold(0usize, usize::saturating_add) - .saturating_add( - op.default() - .map_or(0, |body| recursive_operation_count(body.operations())), - ), - ClassicalControlOp::Break | ClassicalControlOp::Continue => 0, - }); + if let Instruction::ClassicalControl(control) = &operation.instruction { + for body in control.bodies() { + count = count.saturating_add(recursive_operation_count(body.operations())); + } + } } count } diff --git a/crates/cqlib-core/src/compile/transform/routing/sabre.rs b/crates/cqlib-core/src/compile/transform/routing/sabre.rs index c3bc6796..eaabceed 100644 --- a/crates/cqlib-core/src/compile/transform/routing/sabre.rs +++ b/crates/cqlib-core/src/compile/transform/routing/sabre.rs @@ -25,7 +25,7 @@ //! Returns [`SabreRouteResult`], which wraps a [`RoutedCircuit`] and adds //! the layout score. -use crate::circuit::{Circuit, CircuitParam, Parameter, Qubit}; +use crate::circuit::{Circuit, Qubit}; use crate::compile::CompilerError; use crate::compile::device_planning::DevicePlanningSession; use crate::compile::sabre::{ @@ -39,6 +39,7 @@ use crate::compile::transform::layout::{ prepare_sabre_device_target_with_session_and_physical, prepare_sabre_topology_target, prepare_sabre_topology_target_with_prepared, sabre_route_selection_prepared, }; +use crate::compile::transform::rewrite::resolved_params_are_equal; use crate::compile::transform::{QubitBijection, RewriteEdits}; use crate::device::{Device, Layout, LogicalQubit, PhysicalQubit}; use std::collections::HashMap; @@ -304,12 +305,7 @@ fn routed_gap_bijection( if source.instruction != target.instruction || source.label != target.label || source.qubits.len() != target.qubits.len() - || source.params.len() != target.params.len() - || !source - .params - .iter() - .zip(&target.params) - .all(|(left, right)| resolve_param(original, left) == resolve_param(routed, right)) + || !resolved_params_are_equal(original, &source.params, routed, &target.params) { return None; } @@ -330,13 +326,6 @@ fn routed_gap_bijection( Some(QubitBijection { pairs }) } -fn resolve_param(circuit: &Circuit, parameter: &CircuitParam) -> Option { - match parameter { - CircuitParam::Fixed(value) => Some(Parameter::from(*value)), - CircuitParam::Index(index) => circuit.parameters().get_index(*index as usize).cloned(), - } -} - /// Full SABRE pipeline result: layout selection + routing. /// /// Returned by [`route_sabre`]. Wraps a [`RoutedCircuit`] and adds the layout diff --git a/crates/cqlib-core/src/compile/transform/routing_basis.rs b/crates/cqlib-core/src/compile/transform/routing_basis.rs index 51ea0761..80a56398 100644 --- a/crates/cqlib-core/src/compile/transform/routing_basis.rs +++ b/crates/cqlib-core/src/compile/transform/routing_basis.rs @@ -18,9 +18,7 @@ //! on more than two qubits, while preserving existing 2-qubit standard gates //! that are already routable. -use crate::circuit::{ - Circuit, ClassicalControlOp, Instruction, Operation, StandardGate, ValueOperation, -}; +use crate::circuit::{Circuit, Instruction, Operation, StandardGate, ValueOperation}; use crate::compile::CompilerError; use crate::compile::transform::analysis::WorkflowCircuitAnalysis; use crate::compile::transform::rebuild::CircuitRebuildContext; @@ -165,14 +163,12 @@ fn can_directly_lower_top_level_ccx(operations: &[Operation]) -> bool { { return false; } - Instruction::ClassicalControl(op) => { - let mut nested_unroutable = false; - for_each_control_body(op, |body| { - nested_unroutable |= has_unroutable_gate_like_operation(body.operations()); - }); - if nested_unroutable { - return false; - } + Instruction::ClassicalControl(op) + if op + .bodies() + .any(|body| has_unroutable_gate_like_operation(body.operations())) => + { + return false; } _ => {} } @@ -300,16 +296,12 @@ fn has_unroutable_gate_like_operation(operations: &[Operation]) -> bool { return true; } } - Instruction::ClassicalControl(op) => { - let mut found = false; - for_each_control_body(op, |body| { - if !found { - found = has_unroutable_gate_like_operation(body.operations()); - } - }); - if found { - return true; - } + Instruction::ClassicalControl(op) + if op + .bodies() + .any(|body| has_unroutable_gate_like_operation(body.operations())) => + { + return true; } _ => {} } @@ -327,9 +319,9 @@ fn collect_routable_two_qubit_gates(operations: &[Operation], basis: &mut Vec { - for_each_control_body(op, |body| { + for body in op.bodies() { collect_routable_two_qubit_gates(body.operations(), basis); - }); + } } _ => {} } @@ -352,13 +344,9 @@ fn validate_routing_basis_contract(operations: &[Operation]) -> Result<(), Compi ))); } Instruction::ClassicalControl(op) => { - let mut result = Ok(()); - for_each_control_body(op, |body| { - if result.is_ok() { - result = validate_routing_basis_contract(body.operations()); - } - }); - result?; + for body in op.bodies() { + validate_routing_basis_contract(body.operations())?; + } } _ => {} } @@ -367,35 +355,6 @@ fn validate_routing_basis_contract(operations: &[Operation]) -> Result<(), Compi Ok(()) } -fn for_each_control_body( - op: &ClassicalControlOp, - mut visit: impl FnMut(&crate::circuit::ControlBody), -) { - match op { - ClassicalControlOp::If(op) => { - visit(op.then_body()); - if let Some(body) = op.else_body() { - visit(body); - } - } - ClassicalControlOp::While(op) => { - visit(op.body()); - } - ClassicalControlOp::For(op) => { - visit(op.body()); - } - ClassicalControlOp::Switch(op) => { - for case in op.cases() { - visit(case.body()); - } - if let Some(body) = op.default() { - visit(body); - } - } - ClassicalControlOp::Break | ClassicalControlOp::Continue => {} - } -} - #[cfg(test)] #[path = "./routing_basis_test.rs"] mod routing_basis_test; diff --git a/crates/cqlib-core/src/compile/transform/target_basis.rs b/crates/cqlib-core/src/compile/transform/target_basis.rs index 5508292f..05da0062 100644 --- a/crates/cqlib-core/src/compile/transform/target_basis.rs +++ b/crates/cqlib-core/src/compile/transform/target_basis.rs @@ -20,9 +20,9 @@ //! //! One parameter-aware specialization sits on top of the static rule graph: //! fixed-parameter `U` gates are re-synthesized numerically by the `euler_1q` -//! module so degenerate angles collapse to the shortest half-rotation -//! sequence. The dynamic candidate is used only when its physical output cost -//! is strictly below the static plan's; ties keep the static path. +//! module using continuous-axis or half-rotation candidates with degenerate +//! angles simplified. The dynamic candidate is used only when its physical +//! output cost is strictly below the static plan's; ties keep the static path. use crate::circuit::{ Circuit, ClassicalControlOp, Instruction, Operation, Parameter, ParameterValue, Qubit, @@ -39,10 +39,14 @@ use crate::compile::transform::decompose::unitary::euler_1q::{ Euler1qCandidate, synthesize_euler_1q_candidates, }; use crate::compile::transform::decompose::unitary::synthesize_numeric_1q_unitary; -use crate::compile::transform::lowering_support::{LoweringTarget, OperationSequenceLowerer}; +use crate::compile::transform::lowering_support::{ + LowerableOperation, LoweringTarget, OperationSequenceLowerer, +}; use crate::compile::transform::rebuild::{CircuitRebuildContext, ClassicalRemap}; use crate::compile::transform::transformer::{PassApplicability, WorkflowPass}; -use crate::compile::transform::{CircuitAnalysis, TransformOutcome, Transformer}; +use crate::compile::transform::{ + Canonicalizer, CircuitAnalysis, OptimizeOneQubitRuns, TransformOutcome, Transformer, +}; use smallvec::{SmallVec, smallvec}; use std::collections::{HashMap, HashSet}; use std::sync::Arc; @@ -120,14 +124,6 @@ struct PlanCost { rule_id: usize, } -#[derive(Debug, Clone)] -struct LowerableOperation { - instruction: Instruction, - qubits: SmallVec<[Qubit; 3]>, - params: SmallVec<[ParameterValue; 1]>, - label: Option>, -} - struct CircuitLowerer<'a> { source: &'a Circuit, plans: &'a LoweringPlans, @@ -219,25 +215,9 @@ fn instruction_requires_lowering(instruction: &Instruction, plans: &LoweringPlan } fn control_flow_requires_lowering(control: &ClassicalControlOp, plans: &LoweringPlans) -> bool { - match control { - ClassicalControlOp::If(op) => { - operations_require_lowering(op.then_body().operations(), plans) - || op - .else_body() - .is_some_and(|body| operations_require_lowering(body.operations(), plans)) - } - ClassicalControlOp::While(op) => operations_require_lowering(op.body().operations(), plans), - ClassicalControlOp::For(op) => operations_require_lowering(op.body().operations(), plans), - ClassicalControlOp::Switch(op) => { - op.cases() - .iter() - .any(|case| operations_require_lowering(case.body().operations(), plans)) - || op - .default() - .is_some_and(|body| operations_require_lowering(body.operations(), plans)) - } - ClassicalControlOp::Break | ClassicalControlOp::Continue => false, - } + control + .bodies() + .any(|body| operations_require_lowering(body.operations(), plans)) } impl TargetBasisSignature { @@ -300,12 +280,54 @@ impl TargetBasisCostModel { qubits: Vec, operations: Vec, ) -> Result { + Self::count_lowered(&self.lower_fixed_operations(qubits, operations)?) + } + + /// Two-qubit scoring includes one existing local cleanup pass. The 1Q + /// optimizer calls `cost_of_fixed_operations`, so scoring never recurses. + pub(crate) fn cost_after_local_cleanup( + &self, + qubits: Vec, + operations: Vec, + ) -> Result { + let lowered = self.lower_fixed_operations(qubits, operations)?; + Self::count_lowered(&self.cleanup_lowered(lowered)?) + } + + fn lower_fixed_operations( + &self, + qubits: Vec, + operations: Vec, + ) -> Result { let source = Circuit::from_operations(qubits, operations, None, None) .map_err(CompilerError::Circuit)?; let lowered = match self.lowerer.transform(&source, None)? { TransformOutcome::Unchanged => source, TransformOutcome::Changed(lowered) => lowered, }; + Ok(lowered) + } + + fn cleanup_lowered(&self, mut circuit: Circuit) -> Result { + let canonicalizer = Canonicalizer::production(); + let optimizer = OptimizeOneQubitRuns::basis_with_cost_model(Arc::new(self.clone())); + for pass in [&canonicalizer as &dyn Transformer, &optimizer] { + if let TransformOutcome::Changed(next) = pass.transform(&circuit, None)? { + circuit = next; + } + } + if self.lowerer.requires_lowering(&circuit) + && let TransformOutcome::Changed(next) = self.lowerer.transform(&circuit, None)? + { + circuit = next; + } + if let TransformOutcome::Changed(next) = canonicalizer.transform(&circuit, None)? { + circuit = next; + } + Ok(circuit) + } + + fn count_lowered(lowered: &Circuit) -> Result { let mut cost = TargetBasisCost::default(); let mut depths = HashMap::new(); for operation in lowered.operations() { @@ -609,7 +631,7 @@ impl<'a> CircuitLowerer<'a> { })?; if key.is_implicit() { self.changed = true; - target.accumulate_phase(gphase_param(&operation)?); + target.accumulate_phase(operation.gphase_param()?); return Ok(()); } if self.plans.is_physical(&key) { @@ -1070,13 +1092,6 @@ fn instantiate_rule_param( Ok(ParameterValue::from(parameter)) } -fn gphase_param(operation: &LowerableOperation) -> Result { - let phase = operation.params.first().ok_or_else(|| { - CompilerError::InvariantViolation("GPhase operation must contain one parameter".to_string()) - })?; - Ok(Parameter::from(phase)) -} - fn key_rule_sort_value(key: &KnowledgeInstructionKey) -> usize { match key { KnowledgeInstructionKey::Standard(gate) => *gate as usize, diff --git a/crates/cqlib-core/src/compile/transform/target_basis_test.rs b/crates/cqlib-core/src/compile/transform/target_basis_test.rs index 9092e547..bb940e9b 100644 --- a/crates/cqlib-core/src/compile/transform/target_basis_test.rs +++ b/crates/cqlib-core/src/compile/transform/target_basis_test.rs @@ -12,7 +12,9 @@ // that they have been altered from the originals. use super::{TargetBasisCostModel, TargetBasisLowerer}; +use crate::circuit::circuit_to_matrix; use crate::circuit::gate::FrozenCircuit; +use crate::circuit::test_utils::assert_matrix_approx_eq; use crate::circuit::{ Circuit, CircuitGate, ClassicalControlOp, ClassicalExpr, ClassicalType, Instruction, MCGate, Operation, Parameter, ParameterValue, Qubit, StandardGate, UnitaryGate, ValueOperation, @@ -471,6 +473,65 @@ fn cost_model_sees_degenerate_u_as_one_gate() { assert_eq!(cost.depth, 1); } +#[test] +fn xy_basis_lowering_and_cost_use_three_gate_numeric_u() { + let q0 = Qubit::new(0); + for entangler in [StandardGate::CX, StandardGate::CZ, StandardGate::RXX] { + let basis = [StandardGate::RX, StandardGate::RY, entangler]; + let mut circuit = u_circuit(0.37, 0.23, 0.29); + circuit.set_global_phase(Parameter::from(-0.41)); + let result = run_target_lowering(&circuit, &basis); + assert_only_target_standard_gates(&result, &basis); + assert_eq!( + standard_ops(&result), + [StandardGate::RX, StandardGate::RY, StandardGate::RX] + ); + assert_matrix_approx_eq( + &circuit_to_matrix(&result, None).unwrap(), + &circuit_to_matrix(&circuit, None).unwrap(), + 1e-12, + ); + + let cost = TargetBasisCostModel::new(target_basis(&basis)) + .unwrap() + .cost_of_fixed_operations( + vec![q0], + vec![ValueOperation::from_standard( + StandardGate::U, + [q0], + [0.37.into(), 0.23.into(), 0.29.into()], + )], + ) + .unwrap(); + assert_eq!(cost.total_ops, 3); + assert_eq!(cost.depth, 3); + assert_eq!(cost.parameterized_ops, 3); + assert_eq!(cost.two_qubit_ops, 0); + } +} + +#[test] +fn xy_basis_symbolic_u_retains_exact_five_gate_fallback() { + let basis = [StandardGate::RX, StandardGate::RY]; + let mut circuit = Circuit::new(1); + circuit + .u(Qubit::new(0), Parameter::symbol("theta"), 0.23, 0.29) + .unwrap(); + let result = run_target_lowering(&circuit, &basis); + assert_only_target_standard_gates(&result, &basis); + assert_eq!(non_gphase_ops(&result).len(), 5); + for theta in [0.0, 0.37, std::f64::consts::PI, -std::f64::consts::PI] { + let bindings = Some(std::collections::HashMap::from([("theta", theta)])); + let source = circuit.assign_parameters(&bindings).unwrap(); + let lowered = result.assign_parameters(&bindings).unwrap(); + assert_matrix_approx_eq( + &circuit_to_matrix(&lowered, None).unwrap(), + &circuit_to_matrix(&source, None).unwrap(), + 1e-12, + ); + } +} + #[test] fn stored_plan_cost_matches_static_lowering_output() { let basis = [StandardGate::RZ, StandardGate::X2P, StandardGate::X2M]; @@ -776,3 +837,152 @@ fn swap_fails_for_rz_cz_basis_without_half_rotation() { &["cannot lower", "SWAP", "RZ", "CZ"], ); } + +#[test] +fn controlled_pauli_rzz_lowering_preserves_scalar_phase() { + for basis in [ + vec![StandardGate::RZ, StandardGate::RX, StandardGate::RZZ], + vec![StandardGate::RX, StandardGate::RY, StandardGate::RZZ], + vec![StandardGate::H, StandardGate::RZ, StandardGate::RZZ], + ] { + for gate in [StandardGate::CX, StandardGate::CY, StandardGate::CZ] { + for reversed in [false, true] { + let qubits = if reversed { + [Qubit::new(1), Qubit::new(0)] + } else { + [Qubit::new(0), Qubit::new(1)] + }; + let mut source = Circuit::from_operations( + vec![Qubit::new(0), Qubit::new(1)], + vec![ValueOperation::from_standard(gate, qubits, [])], + None, + None, + ) + .unwrap(); + source.set_global_phase(0.371.into()); + let actual = run_target_lowering(&source, &basis); + let expected = circuit_to_matrix(&source, None).unwrap(); + let actual = circuit_to_matrix(&actual, None).unwrap(); + assert!( + expected + .iter() + .zip(actual.iter()) + .all(|(a, b)| (*a - *b).norm() <= 1e-8), + "gate={gate:?}, basis={basis:?}, reversed={reversed}" + ); + } + } + } +} + +#[test] +fn local_cleanup_preserves_matrix_phase_and_target_basis() { + use std::f64::consts::{PI, TAU}; + for basis in [ + vec![StandardGate::RZ, StandardGate::X2P, StandardGate::CZ], + vec![StandardGate::RX, StandardGate::RY, StandardGate::CZ], + vec![StandardGate::RX, StandardGate::RZ, StandardGate::CX], + vec![StandardGate::RX, StandardGate::RY, StandardGate::RXX], + vec![StandardGate::RX, StandardGate::RY, StandardGate::RZZ], + vec![StandardGate::H, StandardGate::RZ, StandardGate::RZZ], + ] { + let model = TargetBasisCostModel::new(target_basis(&basis)).unwrap(); + for reversed in [false, true] { + let q = if reversed { + [Qubit::new(1), Qubit::new(0)] + } else { + [Qubit::new(0), Qubit::new(1)] + }; + for angle in [0.0, PI, -PI, TAU, -TAU, 0.37] { + let operations = vec![ + ValueOperation::from_standard(StandardGate::CX, q, []), + ValueOperation::from_standard(StandardGate::H, [q[1]], []), + ValueOperation::from_standard(StandardGate::H, [q[0]], []), + ValueOperation::from_standard(StandardGate::RX, [q[0]], [PI.into()]), + ValueOperation::from_standard(StandardGate::RZZ, q, [angle.into()]), + ValueOperation::from_standard(StandardGate::RX, [q[0]], [(-PI).into()]), + ]; + let mut source = + Circuit::from_operations(q.to_vec(), operations.clone(), None, None).unwrap(); + source.set_global_phase(0.371.into()); + let mut lowered = model + .lower_fixed_operations(q.to_vec(), operations.clone()) + .unwrap(); + lowered.set_global_phase(lowered.global_phase() + 0.371); + let actual = model.cleanup_lowered(lowered).unwrap(); + assert_only_target_standard_gates(&actual, &basis); + let expected_matrix = circuit_to_matrix(&source, None).unwrap(); + let actual_matrix = circuit_to_matrix(&actual, None).unwrap(); + assert!( + expected_matrix + .iter() + .zip(actual_matrix.iter()) + .all(|(a, b)| (*a - *b).norm() <= 1e-8), + "basis={basis:?}, reversed={reversed}, angle={angle}" + ); + assert_eq!( + model + .cost_after_local_cleanup(q.to_vec(), operations) + .unwrap(), + TargetBasisCostModel::count_lowered(&actual).unwrap(), + ); + } + } + } +} + +#[test] +fn target_lowering_uses_compact_qcis_decompositions() { + use StandardGate::*; + use std::collections::HashMap; + use std::f64::consts::PI; + + let fixed = vec![RZ, X2P, X2M, Y2P, Y2M, CZ]; + let phased = vec![RZ, XY2P, XY2M, CZ]; + for (gate, parameters, basis, max_ops, entanglers) in [ + (CX, vec![], fixed.clone(), 3, 1), + (CY, vec![], fixed.clone(), 3, 1), + (RZZ, vec!["theta"], fixed, 7, 2), + (RXY, vec!["theta", "phi"], phased.clone(), 3, 0), + (U, vec!["theta", "phi", "lambda"], phased, 3, 0), + (XY2M, vec!["phi"], vec![XY2P], 1, 0), + (U, vec!["theta", "phi", "lambda"], vec![XY2P, RZ], 3, 0), + (Phase, vec!["theta"], vec![U, CZ], 1, 0), + ] { + let num_qubits = if matches!(gate, CX | CY | RZZ) { 2 } else { 1 }; + let mut input = Circuit::new(num_qubits); + input + .append( + Instruction::Standard(gate), + (0..num_qubits as u32).map(Qubit::new), + parameters + .into_iter() + .map(|name| ParameterValue::Param(Parameter::symbol(name))), + None, + ) + .unwrap(); + let result = run_target_lowering(&input, &basis); + assert!( + result.operations().len() <= max_ops, + "{gate:?}: {} operations", + result.operations().len() + ); + let gates = standard_ops(&result); + assert_eq!(gates.iter().filter(|&&gate| gate == CZ).count(), entanglers); + assert_only_target_standard_gates(&result, &basis); + for angle in [0.0, 0.317, -1.239, PI / 2.0, PI, 2.0 * PI, -4.0 * PI] { + let bindings = Some(HashMap::from([ + ("theta", angle), + ("phi", 0.19 + angle), + ("lambda", -0.39), + ])); + let source = input.assign_parameters(&bindings).unwrap(); + let lowered = result.assign_parameters(&bindings).unwrap(); + assert_matrix_approx_eq( + &circuit_to_matrix(&source, None).unwrap(), + &circuit_to_matrix(&lowered, None).unwrap(), + 1e-9, + ); + } + } +} diff --git a/crates/cqlib-core/src/compile/workflow.rs b/crates/cqlib-core/src/compile/workflow.rs index 1022d035..57695351 100644 --- a/crates/cqlib-core/src/compile/workflow.rs +++ b/crates/cqlib-core/src/compile/workflow.rs @@ -52,7 +52,7 @@ //! orchestration and reporting step; it applies no transform after validation //! and can select only an already validated candidate. -use crate::circuit::{Circuit, ClassicalControlOp, Instruction, Operation, StandardGate}; +use crate::circuit::{Circuit, Instruction, Operation, StandardGate}; use crate::compile::CompilerError; use crate::compile::device_planning::DevicePlanningSession; use crate::compile::resource::ResourceLimits; @@ -1991,29 +1991,11 @@ fn validate_operations_in_target_basis( "target-basis one-qubit cleanup left gate {gate:?} outside the configured basis" ))); } - Instruction::ClassicalControl(control) => match control { - ClassicalControlOp::If(op) => { - validate_operations_in_target_basis(op.then_body().operations(), allowed)?; - if let Some(body) = op.else_body() { - validate_operations_in_target_basis(body.operations(), allowed)?; - } + Instruction::ClassicalControl(control) => { + for body in control.bodies() { + validate_operations_in_target_basis(body.operations(), allowed)?; } - ClassicalControlOp::While(op) => { - validate_operations_in_target_basis(op.body().operations(), allowed)?; - } - ClassicalControlOp::For(op) => { - validate_operations_in_target_basis(op.body().operations(), allowed)?; - } - ClassicalControlOp::Switch(op) => { - for case in op.cases() { - validate_operations_in_target_basis(case.body().operations(), allowed)?; - } - if let Some(body) = op.default() { - validate_operations_in_target_basis(body.operations(), allowed)?; - } - } - ClassicalControlOp::Break | ClassicalControlOp::Continue => {} - }, + } Instruction::McGate(_) | Instruction::UnitaryGate(_) | Instruction::CircuitGate(_) => { return Err(CompilerError::InvariantViolation(format!( "target-basis one-qubit cleanup left gate-like instruction {} outside the configured basis", @@ -2065,29 +2047,11 @@ fn validate_operations_on_topology( "topology-basis output contains undecomposed gate {gate:?}" ))); } - Instruction::ClassicalControl(control) => match control { - ClassicalControlOp::If(op) => { - validate_operations_on_topology(op.then_body().operations(), device)?; - if let Some(body) = op.else_body() { - validate_operations_on_topology(body.operations(), device)?; - } + Instruction::ClassicalControl(control) => { + for body in control.bodies() { + validate_operations_on_topology(body.operations(), device)?; } - ClassicalControlOp::While(op) => { - validate_operations_on_topology(op.body().operations(), device)?; - } - ClassicalControlOp::For(op) => { - validate_operations_on_topology(op.body().operations(), device)?; - } - ClassicalControlOp::Switch(op) => { - for case in op.cases() { - validate_operations_on_topology(case.body().operations(), device)?; - } - if let Some(body) = op.default() { - validate_operations_on_topology(body.operations(), device)?; - } - } - ClassicalControlOp::Break | ClassicalControlOp::Continue => {} - }, + } Instruction::McGate(_) | Instruction::UnitaryGate(_) | Instruction::CircuitGate(_) => { return Err(CompilerError::InvariantViolation(format!( "topology-basis output contains undecomposed instruction {}", diff --git a/crates/cqlib-core/src/compile/workflow_test.rs b/crates/cqlib-core/src/compile/workflow_test.rs index db46a5a2..689dd5b6 100644 --- a/crates/cqlib-core/src/compile/workflow_test.rs +++ b/crates/cqlib-core/src/compile/workflow_test.rs @@ -54,6 +54,36 @@ fn compile_config(mode: CompileMode) -> CompileConfig { } } +#[test] +fn xy_basis_compiles_generic_u_to_three_rotations_in_both_modes() { + let mut circuit = Circuit::new(1); + circuit.u(Qubit::new(0), 0.37, 0.23, 0.29).unwrap(); + circuit.set_global_phase(Parameter::from(-0.41)); + for mode in [CompileMode::Normal, CompileMode::Enhanced] { + for entangler in [StandardGate::CX, StandardGate::CZ, StandardGate::RXX] { + let config = CompileConfig { + target: CompileTarget::Basis( + [StandardGate::RX, StandardGate::RY, entangler] + .into_iter() + .map(Instruction::Standard) + .collect(), + ), + ..compile_config(mode) + }; + let result = compile(&circuit, config).unwrap(); + assert_eq!( + standard_ops(&result.circuit), + [StandardGate::RX, StandardGate::RY, StandardGate::RX] + ); + crate::circuit::test_utils::assert_matrix_approx_eq( + &crate::circuit::circuit_to_matrix(&result.circuit, None).unwrap(), + &crate::circuit::circuit_to_matrix(&circuit, None).unwrap(), + 1e-12, + ); + } + } +} + fn workflow_analysis(circuit: &Circuit) -> Option { Some(WorkflowCircuitAnalysis::analyze(circuit)) } @@ -1639,12 +1669,12 @@ fn workflow_uses_three_native_cz_for_device_targeted_swap_unitary() { } #[test] -fn native_fixed_point_recovers_exact_pair_lowering_in_both_modes() { +fn workflow_lowers_to_exact_pair_and_runs_native_fixed_point_in_both_modes() { // Pre-layout prefers the broad CX family when CZ is only native on one edge // (see device_synthesis pre_layout coverage test). Force the routed pair onto - // that CZ-only edge. Enhanced can recover the exact-pair implementation in - // its post-routing step, while Normal recovers it in the shared native - // fixed-point closure. + // that CZ-only edge. Both modes must emit its exact native basis and run + // the shared native fixed-point closure, even when earlier device lowering + // has already produced a stable circuit. let p1 = PhysicalQubit::new(1); let p2 = PhysicalQubit::new(2); let mut device = Device::bidirectional_line("heterogeneous-exact-pair", 4) @@ -1722,27 +1752,27 @@ fn native_fixed_point_recovers_exact_pair_lowering_in_both_modes() { .iter() .all(|gate| matches!(gate, StandardGate::U | StandardGate::CZ)) ); - assert!(!normal.step_changed("resynthesize.two_qubit_blocks.post_routing")); - assert!(enhanced.step_changed("resynthesize.two_qubit_blocks.post_routing")); - assert!(normal.step_changed("optimize.native_fixed_point")); - assert!( - normal - .step("optimize.native_fixed_point") - .unwrap() - .reason - .as_deref() - .unwrap() - .contains("quality_policy=balanced_depth") - ); - assert!( - normal - .step("optimize.native_fixed_point") - .unwrap() - .reason - .as_deref() - .unwrap() - .contains("quality_rejections=") - ); + let post_routing = "resynthesize.two_qubit_blocks.post_routing"; + assert!(normal.step(post_routing).is_none()); + let resynthesis = enhanced.step(post_routing).unwrap(); + assert!(!resynthesis.skipped, "{resynthesis:?}"); + // A resynthesis candidate need not improve on an earlier decomposition. + // Floating-point tie-breaking can affect acceptance on different platforms; + // the contract is that enhanced mode runs this stage and the final circuit + // uses the exact physical basis, not that this particular stage must change it. + // `changed` can reflect only canonicalization of a tiny global-phase + // residual, which varies across architectures. Require execution and the + // configured quality policy; correctness is checked on the final circuits. + for result in [&normal, &enhanced] { + let native = result.step("optimize.native_fixed_point").unwrap(); + assert!(!native.skipped, "{native:?}"); + let reason = native.reason.as_deref().unwrap(); + assert!( + reason.contains("quality_policy=balanced_depth"), + "{native:?}" + ); + assert!(reason.contains("quality_rejections="), "{native:?}"); + } let mut physical_expected = Circuit::new(4); physical_expected .swap(Qubit::new(1), Qubit::new(2)) diff --git a/crates/cqlib-core/src/error_mitigation/error_mitigation_test.rs b/crates/cqlib-core/src/error_mitigation/error_mitigation_test.rs index e45c93c7..55114d3a 100644 --- a/crates/cqlib-core/src/error_mitigation/error_mitigation_test.rs +++ b/crates/cqlib-core/src/error_mitigation/error_mitigation_test.rs @@ -1,4 +1,5 @@ // This code is part of Cqlib. +// Modified to verify repeated post-processing of collected estimates. // // (C) Copyright China Telecom Quantum Group 2026 // @@ -114,7 +115,7 @@ fn test_error_mitigation_new_validates_supported_methods() { #[test] fn test_error_mitigation_requires_run_before_get_mitigated() { - let mut mitigation = ErrorMitigation::new( + let mitigation = ErrorMitigation::new( Circuit::new(1), MitigationMethod::Zne(ZneConfig { fold_levels: vec![0, 1], @@ -242,13 +243,13 @@ fn test_error_mitigation_zne_run_then_get_mitigated() { } ); - let second_err = mitigation + let second = mitigation .get_mitigated(ProcessArgs::Zne { method: ExtrapolateMethod::Polynomial, degree: Some(1), }) - .unwrap_err(); - assert!(matches!(second_err, ErrorMitigationError::AlreadyMitigated)); + .unwrap(); + assert_eq!(second, mitigated); } #[test] @@ -293,4 +294,10 @@ fn test_error_mitigation_vd_run_then_get_mitigated() { assert!((mitigated.expectation - 0.75).abs() < 1e-12); assert!((mitigated.variance.unwrap() - 0.203125).abs() < 1e-12); + assert_eq!( + mitigation + .get_mitigated(ProcessArgs::VirtualDistillation) + .unwrap(), + mitigated + ); } diff --git a/crates/cqlib-core/src/error_mitigation/unified.rs b/crates/cqlib-core/src/error_mitigation/unified.rs index 31f05e45..f6ba05a3 100644 --- a/crates/cqlib-core/src/error_mitigation/unified.rs +++ b/crates/cqlib-core/src/error_mitigation/unified.rs @@ -1,4 +1,5 @@ // This code is part of Cqlib. +// Modified to allow repeated post-processing of collected estimates. // // (C) Copyright China Telecom Quantum Group 2026 // @@ -113,7 +114,6 @@ pub struct MitigatedResult { enum ErrorMitigationState { Initialized, RunCompleted(Box), - Mitigated(MitigatedResult), } #[derive(Debug, Clone)] @@ -124,20 +124,14 @@ enum RunRecord { #[derive(Debug, Clone)] struct ZneRunRecord { - folded_circuits: Vec, noisy_expectations: Vec, noise_factors: Vec, - gate_set: Option>, - shots: Option, } #[derive(Debug, Clone)] struct VirtualDistillationRunRecord { - copy_swap_circuit: Circuit, numerator: (f64, f64), denominator: (f64, f64), - shots_numerator: usize, - shots_denominator: usize, } /// Unified facade over supported error-mitigation methods. @@ -340,9 +334,6 @@ impl ErrorMitigation { match self.state { ErrorMitigationState::Initialized => {} ErrorMitigationState::RunCompleted(_) => return Err(ErrorMitigationError::AlreadyRun), - ErrorMitigationState::Mitigated(_) => { - return Err(ErrorMitigationError::AlreadyMitigated); - } } let run_record = match (&self.method, run_args) { @@ -374,7 +365,8 @@ impl ErrorMitigation { /// Produces the final mitigated result from the stored raw execution outputs. /// /// `run()` must be called first. The provided [`ProcessArgs`] must match the - /// configured mitigation method. + /// configured mitigation method. This method can be called repeatedly with + /// different processing arguments; it does not consume data or rerun the estimator. /// /// # Example /// @@ -424,7 +416,7 @@ impl ErrorMitigation { /// assert!(mitigated.expectation.is_finite()); /// ``` pub fn get_mitigated( - &mut self, + &self, process_args: ProcessArgs, ) -> Result { let mitigated = match (&self.method, &self.state, process_args) { @@ -434,10 +426,6 @@ impl ErrorMitigation { (MitigationMethod::VirtualDistillation(_), ErrorMitigationState::Initialized, _) => { return Err(ErrorMitigationError::RunRequiredBeforeMitigation); } - (_, ErrorMitigationState::Mitigated(cached), _) => { - let _ = cached; - return Err(ErrorMitigationError::AlreadyMitigated); - } ( MitigationMethod::Zne(config), ErrorMitigationState::RunCompleted(record), @@ -472,7 +460,6 @@ impl ErrorMitigation { } }; - self.state = ErrorMitigationState::Mitigated(mitigated.clone()); Ok(mitigated) } @@ -518,11 +505,8 @@ impl ErrorMitigation { .collect(); Ok(ZneRunRecord { - folded_circuits, noisy_expectations, noise_factors: zne.noise_factors().to_vec(), - gate_set, - shots, }) } @@ -554,11 +538,8 @@ impl ErrorMitigation { let denominator = estimator(©_swap_circuit, None, Some(shots_denominator)); Ok(VirtualDistillationRunRecord { - copy_swap_circuit, numerator, denominator, - shots_numerator, - shots_denominator, }) } @@ -571,13 +552,8 @@ impl ErrorMitigation { ) -> Result { let zne = ZNEMitigation::new(self.circuit.clone(), config.fold_levels.clone()); - debug_assert_eq!( - record.folded_circuits.len(), - record.noisy_expectations.len() - ); debug_assert_eq!(record.noise_factors, zne.noise_factors().to_vec()); debug_assert_eq!(record.noise_factors.len(), record.noisy_expectations.len()); - let _ = (&record.gate_set, record.shots); let degree = match method { ExtrapolateMethod::Polynomial => { degree.unwrap_or_else(|| record.noisy_expectations.len().saturating_sub(1).min(1)) @@ -596,8 +572,6 @@ impl ErrorMitigation { &self, record: &VirtualDistillationRunRecord, ) -> Result { - debug_assert!(record.copy_swap_circuit.width() >= self.circuit.width()); - let _ = (record.shots_numerator, record.shots_denominator); let (expectation, variance) = VirtualDistillation::mitigate_from_statistics( record.numerator.0, record.numerator.1, diff --git a/crates/cqlib-core/src/ir/qasm2/dump.rs b/crates/cqlib-core/src/ir/qasm2/dump.rs index 30f19ebd..03af805a 100644 --- a/crates/cqlib-core/src/ir/qasm2/dump.rs +++ b/crates/cqlib-core/src/ir/qasm2/dump.rs @@ -68,7 +68,6 @@ //! - Non-zero global phase and `GPhase` instructions are emitted only as comments; //! loading the generated OpenQASM 2.0 does not restore that phase. -use crate::circuit::circuit_param::CircuitParam; use crate::circuit::gate::{ CircuitGate, ClassicalDataOp, Directive, Instruction, StandardGate, UnitaryGate, }; @@ -703,7 +702,7 @@ fn process_circuit_operations( // Output gate call: name(params) qubits; // Resolve arguments - let params_str = format_params(op, circuit, param_map); + let params_str = format_params(op, circuit, param_map)?; // Map qubits let mapped_qs = map_qubits(op, qubit_map); @@ -713,7 +712,7 @@ fn process_circuit_operations( Instruction::UnitaryGate(unitary_gate) => { // Output gate call: label(params) qubits; // (The gate definition or opaque declaration was already output in Pass 1) - let params_str = format_params(op, circuit, param_map); + let params_str = format_params(op, circuit, param_map)?; let mapped_qs = map_qubits(op, qubit_map); writeln!( output, @@ -767,7 +766,7 @@ fn process_circuit_operations( } Instruction::Delay => { // Output delay gate call: delay(value) q[i]; - let params_str = format_params(op, circuit, param_map); + let params_str = format_params(op, circuit, param_map)?; let mapped_qs = map_qubits(op, qubit_map); writeln!(output, "delay({}) {};", params_str, mapped_qs[0])?; @@ -963,43 +962,46 @@ fn measurement_qasm_lines( } fn resolve_param( - circuit_param: &CircuitParam, + op: &Operation, + param_index: usize, circuit: &Circuit, param_map: &HashMap, -) -> Parameter { - let mut param = match circuit_param { - CircuitParam::Fixed(val) => Parameter::from(*val), - CircuitParam::Index(idx) => circuit.parameters()[*idx as usize].clone(), - }; - - if !param_map.is_empty() { - for (sym, repl) in param_map { - param = param.replace(sym, repl.clone()); - } +) -> Result { + let mut param = circuit + .resolve_parameter(&op.params[param_index]) + .map_err(|error| { + let error = match error { + crate::circuit::CircuitError::InvalidParameterValue(_, value) => { + crate::circuit::CircuitError::InvalidParameterValue(param_index, value) + } + error => error, + }; + QasmDumpError::FormatError(format!( + "invalid parameter {param_index} for {:?}: {error}", + op.instruction + )) + })?; + for (symbol, replacement) in param_map { + param = param.replace(symbol, replacement.clone()); } - - // param = param.simplify(); - param + Ok(param) } fn format_params( op: &Operation, circuit: &Circuit, param_map: &HashMap, -) -> String { +) -> Result { if op.params.is_empty() { - return String::new(); + return Ok(String::new()); } - let params_str: Vec = op - .params - .iter() - .map(|p| { - resolve_param(p, circuit, param_map) - .to_string() - .replace("π", "pi") + let params = (0..op.params.len()) + .map(|index| { + resolve_param(op, index, circuit, param_map) + .map(|param| param.to_string().replace("π", "pi")) }) - .collect(); - format!("({})", params_str.join(",")) + .collect::, _>>()?; + Ok(format!("({})", params.join(","))) } fn map_qubits(op: &Operation, qubit_map: &HashMap) -> Vec { @@ -1079,7 +1081,7 @@ fn dump_standard_gate( return Ok(()); } StandardGate::XY2P => { - let theta = resolve_param(&op.params[0], circuit, param_map); + let theta = resolve_param(op, 0, circuit, param_map)?; let p1 = Parameter::pi() / 2.0 - theta.clone(); let p3 = theta.clone() - Parameter::pi() / 2.0; let q_str = &mapped_qs[0]; @@ -1091,7 +1093,7 @@ fn dump_standard_gate( return Ok(()); } StandardGate::XY2M => { - let theta = resolve_param(&op.params[0], circuit, param_map); + let theta = resolve_param(op, 0, circuit, param_map)?; let m1 = Parameter::from(0.0) - (Parameter::pi() / 2.0 + theta.clone()); let m3 = theta.clone() + Parameter::pi() / 2.0; let q_str = &mapped_qs[0]; @@ -1103,7 +1105,7 @@ fn dump_standard_gate( return Ok(()); } StandardGate::XY => { - let theta = resolve_param(&op.params[0], circuit, param_map); + let theta = resolve_param(op, 0, circuit, param_map)?; let phase: Parameter = theta.clone() - Parameter::pi() / 2.0; let lambda: Parameter = Parameter::pi() / 2.0 - theta.clone(); let q_str = &mapped_qs[0]; @@ -1125,8 +1127,8 @@ fn dump_standard_gate( return Ok(()); } StandardGate::RXY => { - let theta = resolve_param(&op.params[0], circuit, param_map); - let phi = resolve_param(&op.params[1], circuit, param_map); + let theta = resolve_param(op, 0, circuit, param_map)?; + let phi = resolve_param(op, 1, circuit, param_map)?; let phase: Parameter = phi.clone() - Parameter::pi() / 2.0; let lambda: Parameter = Parameter::pi() / 2.0 - phi.clone(); let q_str = &mapped_qs[0]; @@ -1149,7 +1151,7 @@ fn dump_standard_gate( return Ok(()); } StandardGate::GPhase => { - let param = resolve_param(&op.params[0], circuit, param_map); + let param = resolve_param(op, 0, circuit, param_map)?; writeln!(output, "// gphase({})", param)?; return Ok(()); } @@ -1162,7 +1164,7 @@ fn dump_standard_gate( )) })?; - let params_str = format_params(op, circuit, param_map); + let params_str = format_params(op, circuit, param_map)?; writeln!(output, "{}{} {};", name, params_str, mapped_qs.join(","))?; Ok(()) } @@ -1224,7 +1226,7 @@ fn dump_mc_gate( } }; - let params_str = format_params(op, circuit, param_map); + let params_str = format_params(op, circuit, param_map)?; let mapped_qs = map_qubits(op, qubit_map); writeln!(output, "{}{} {};", name, params_str, mapped_qs.join(","))?; @@ -1468,7 +1470,7 @@ fn operation_to_qasm( let mapped_qs = map_qubits(op, qubit_map); match gate { StandardGate::XY => { - let theta = resolve_param(&op.params[0], circuit, param_map); + let theta = resolve_param(op, 0, circuit, param_map)?; let phase: Parameter = theta.clone() - Parameter::pi() / 2.0; let lambda: Parameter = Parameter::pi() / 2.0 - theta; return Ok(format!( @@ -1480,8 +1482,8 @@ fn operation_to_qasm( )); } StandardGate::RXY => { - let theta = resolve_param(&op.params[0], circuit, param_map); - let phi = resolve_param(&op.params[1], circuit, param_map); + let theta = resolve_param(op, 0, circuit, param_map)?; + let phi = resolve_param(op, 1, circuit, param_map)?; let phase: Parameter = phi.clone() - Parameter::pi() / 2.0; let lambda: Parameter = Parameter::pi() / 2.0 - phi; return Ok(format!( @@ -1499,7 +1501,7 @@ fn operation_to_qasm( "gate {gate:?} cannot be used in an OpenQASM 2.0 conditional body" )) })?; - let params_str = format_params(op, circuit, param_map); + let params_str = format_params(op, circuit, param_map)?; Ok(format!("{}{} {}", name, params_str, mapped_qs.join(","))) } Instruction::McGate(mc_gate) => { @@ -1524,7 +1526,7 @@ fn operation_to_qasm( ))); } }; - let params_str = format_params(op, circuit, param_map); + let params_str = format_params(op, circuit, param_map)?; let mapped_qs = map_qubits(op, qubit_map); Ok(format!("{}{} {}", name, params_str, mapped_qs.join(","))) } @@ -1539,12 +1541,12 @@ fn operation_to_qasm( Directive::Measure => Err(QasmDumpError::MeasureInGateNotAllowed), }, Instruction::CircuitGate(cg) => { - let params_str = format_params(op, circuit, param_map); + let params_str = format_params(op, circuit, param_map)?; let mapped_qs = map_qubits(op, qubit_map); Ok(format!("{}{} {}", cg.name, params_str, mapped_qs.join(","))) } Instruction::UnitaryGate(unitary_gate) => { - let params_str = format_params(op, circuit, param_map); + let params_str = format_params(op, circuit, param_map)?; let mapped_qs = map_qubits(op, qubit_map); Ok(format!( "{}{} {}", diff --git a/crates/cqlib-core/src/ir/qasm2/dump_test.rs b/crates/cqlib-core/src/ir/qasm2/dump_test.rs index 304aa2a5..b2958e3e 100644 --- a/crates/cqlib-core/src/ir/qasm2/dump_test.rs +++ b/crates/cqlib-core/src/ir/qasm2/dump_test.rs @@ -1686,3 +1686,79 @@ fn test_dump_qasm_ordering() { assert!(h_pos < measure_pos, "H before measure"); assert!(measure_pos < if_pos, "Measure before if"); } + +#[test] +fn parameter_errors_propagate_from_gate_export_with_context() { + use crate::circuit::{CircuitParam, ParameterValue}; + use std::collections::HashMap; + + let mut circuit = Circuit::new(1); + let qubit = Qubit::new(0); + let qubit_map = HashMap::from([(qubit, "q[0]".to_string())]); + // RX uses general formatting; RXY uses the QASM2 special expansion path. + for (gate, params) in [ + (StandardGate::RX, vec![ParameterValue::Fixed(0.5)]), + ( + StandardGate::RXY, + vec![ParameterValue::Fixed(0.5), ParameterValue::Fixed(0.25)], + ), + ] { + circuit + .append(Instruction::Standard(gate), [qubit], params, None) + .unwrap(); + let valid = circuit.operations().last().unwrap(); + let param_index = valid.params.len() - 1; + for param in [ + CircuitParam::Index(99), + CircuitParam::Fixed(f64::NAN), + CircuitParam::Fixed(f64::INFINITY), + CircuitParam::Fixed(f64::NEG_INFINITY), + ] { + let mut operation = valid.clone(); + operation.params[param_index] = param; + let mut output = String::new(); + let error = super::dump_standard_gate( + &gate, + &operation, + &circuit, + &mut output, + &qubit_map, + &HashMap::new(), + ) + .unwrap_err(); + assert!( + matches!(error, QasmDumpError::FormatError(ref message) + if message.contains(&format!("parameter {param_index}")) + && message.contains(&format!("{gate:?}"))), + "{error}" + ); + let error = super::operation_to_qasm(&operation, &circuit, &qubit_map, &HashMap::new()) + .unwrap_err(); + assert!(matches!(error, QasmDumpError::FormatError(_))); + } + } +} + +#[test] +fn export_preserves_indexed_constants_and_symbol_substitution() { + use crate::circuit::ParameterValue; + use std::collections::HashMap; + + let mut circuit = Circuit::new(1); + circuit + .rx(Qubit::new(0), ParameterValue::Param(Parameter::pi())) + .unwrap(); + circuit + .rx( + Qubit::new(0), + ParameterValue::Param(Parameter::symbol("theta")), + ) + .unwrap(); + let qasm = dumps(&circuit).unwrap(); + assert!(qasm.contains("rx(pi)"), "{qasm}"); + let replacements = HashMap::from([("theta".to_string(), Parameter::symbol("angle0"))]); + assert_eq!( + super::format_params(&circuit.operations()[1], &circuit, &replacements).unwrap(), + "(angle0)" + ); +} diff --git a/crates/cqlib-core/src/ir/qasm3/dump.rs b/crates/cqlib-core/src/ir/qasm3/dump.rs index 63f02d22..914669cf 100644 --- a/crates/cqlib-core/src/ir/qasm3/dump.rs +++ b/crates/cqlib-core/src/ir/qasm3/dump.rs @@ -12,7 +12,6 @@ //! OpenQASM 3 serializer. -use crate::circuit::circuit_param::CircuitParam; use crate::circuit::gate::{ CircuitGate, ClassicalDataOp, Directive, FrozenCircuit, Instruction, StandardGate, }; @@ -324,10 +323,7 @@ fn dump_global_phase( if phase.is_zero() { return Ok(()); } - let mut phase = phase.clone(); - for (symbol, replacement) in param_map { - phase = phase.replace(symbol, replacement.clone()); - } + let phase = phase.substitute_many_simultaneous(param_map); writeln!(output, "gphase({});", phase.to_string().replace("π", "pi"))?; Ok(()) } @@ -606,7 +602,7 @@ fn dump_operations( } Instruction::Directive(directive) => dump_directive(directive, op, output, qubit_map)?, Instruction::CircuitGate(gate) => { - let params = format_params(op, circuit, param_map); + let params = format_params(op, circuit, param_map)?; let qubits = map_qubits(op, qubit_map); writeln!(output, "{}{} {};", gate.name, params, qubits.join(","))?; } @@ -1061,7 +1057,7 @@ fn dump_standard_gate( param_map: &HashMap, ) -> Result<(), Qasm3DumpError> { let name = standard_gate_name(*gate)?; - let params = format_params(op, circuit, param_map); + let params = format_params(op, circuit, param_map)?; let qubits = map_qubits(op, qubit_map); if *gate == StandardGate::GPhase { writeln!(output, "gphase{};", params)?; @@ -1142,7 +1138,7 @@ fn dump_mc_gate( ))); } }; - let params = format_params(op, circuit, param_map); + let params = format_params(op, circuit, param_map)?; let qubits = map_qubits(op, qubit_map); writeln!(output, "{}{} {};", name, params, qubits.join(","))?; Ok(()) @@ -1330,39 +1326,43 @@ fn classical_expr_to_qasm( } fn resolve_param( - circuit_param: &CircuitParam, + op: &Operation, + param_index: usize, circuit: &Circuit, param_map: &HashMap, -) -> Parameter { - let mut param = match circuit_param { - CircuitParam::Fixed(value) => Parameter::from(*value), - CircuitParam::Index(index) => circuit.parameters()[*index as usize].clone(), - }; - - for (symbol, replacement) in param_map { - param = param.replace(symbol, replacement.clone()); - } - param +) -> Result { + let param = circuit + .resolve_parameter(&op.params[param_index]) + .map_err(|error| { + let error = match error { + crate::circuit::CircuitError::InvalidParameterValue(_, value) => { + crate::circuit::CircuitError::InvalidParameterValue(param_index, value) + } + error => error, + }; + Qasm3DumpError::FormatError(format!( + "invalid parameter {param_index} for {:?}: {error}", + op.instruction + )) + })?; + Ok(param.substitute_many_simultaneous(param_map)) } fn format_params( op: &Operation, circuit: &Circuit, param_map: &HashMap, -) -> String { +) -> Result { if op.params.is_empty() { - return String::new(); + return Ok(String::new()); } - let params = op - .params - .iter() - .map(|param| { - resolve_param(param, circuit, param_map) - .to_string() - .replace("π", "pi") + let params = (0..op.params.len()) + .map(|index| { + resolve_param(op, index, circuit, param_map) + .map(|param| param.to_string().replace("π", "pi")) }) - .collect::>(); - format!("({})", params.join(",")) + .collect::, _>>()?; + Ok(format!("({})", params.join(","))) } fn map_qubits(op: &Operation, qubit_map: &HashMap) -> Vec { diff --git a/crates/cqlib-core/src/ir/qasm3/dump_test.rs b/crates/cqlib-core/src/ir/qasm3/dump_test.rs index 10263fec..9b93c967 100644 --- a/crates/cqlib-core/src/ir/qasm3/dump_test.rs +++ b/crates/cqlib-core/src/ir/qasm3/dump_test.rs @@ -608,6 +608,99 @@ measure q; assert!(qasm3_loads(&qasm).is_ok(), "got:\n{qasm}"); } +#[test] +fn parameter_renaming_preserves_overlapping_names_through_round_trip() { + let q0 = Qubit::new(0); + let mut gate = Circuit::new(1); + gate.x(q0).unwrap(); + let mut circuit = Circuit::new(1); + circuit + .append(gate.to_gate("a").unwrap(), [q0], [], None) + .unwrap(); + // Register the symbols separately so the allocator must produce + // a -> a0 followed by a0 -> a00. + circuit.rx(q0, Parameter::symbol("a")).unwrap(); + circuit.rz(q0, Parameter::symbol("a0")).unwrap(); + circuit.set_global_phase(Parameter::symbol("a") + Parameter::symbol("a0") * 2.0); + + let qasm = dumps(&circuit).unwrap(); + assert!(qasm.contains("input angle[64] a0;"), "got:\n{qasm}"); + assert!(qasm.contains("input angle[64] a00;"), "got:\n{qasm}"); + assert!(qasm.contains("rx(a0) q[0];"), "got:\n{qasm}"); + assert!(qasm.contains("rz(a00) q[0];"), "got:\n{qasm}"); + + let loaded = qasm3_loads(&qasm).unwrap(); + let values = Some(HashMap::from([("a0", 0.125), ("a00", 0.5)])); + assert_eq!(loaded.global_phase().evaluate(&values).unwrap(), 1.125); + let angles: Vec<_> = loaded + .operations() + .iter() + .filter(|op| { + matches!( + op.instruction, + Instruction::Standard(StandardGate::RX | StandardGate::RZ) + ) + }) + .map(|op| { + loaded + .resolve_parameter(&op.params[0]) + .unwrap() + .evaluate(&values) + .unwrap() + }) + .collect(); + assert_eq!(angles, [0.125, 0.5]); +} + +#[test] +fn parameter_renaming_resolves_swapped_symbols_simultaneously() { + let mut circuit = Circuit::new(1); + circuit + .rx( + Qubit::new(0), + Parameter::symbol("a") + Parameter::symbol("b") * 2.0, + ) + .unwrap(); + // A swap fails under sequential replacement in either HashMap order. + let param_map = HashMap::from([ + ("a".to_string(), Parameter::symbol("b")), + ("b".to_string(), Parameter::symbol("a")), + ]); + let renamed = resolve_param(&circuit.operations()[0], 0, &circuit, ¶m_map).unwrap(); + assert_eq!( + renamed + .evaluate(&Some(HashMap::from([("a", 2.0), ("b", 3.0)]))) + .unwrap(), + 7.0 + ); +} + +#[test] +fn parameter_renaming_swaps_global_phase_symbols_simultaneously() { + let mut circuit = Circuit::new(1); + circuit.set_global_phase(Parameter::symbol("a") + Parameter::symbol("b") * 2.0); + let param_map = HashMap::from([ + ("a".to_string(), Parameter::symbol("b")), + ("b".to_string(), Parameter::symbol("a")), + ]); + let mut output = String::new(); + dump_global_phase(&circuit, &mut output, ¶m_map).unwrap(); + let phase = Parameter::try_from( + output + .strip_prefix("gphase(") + .unwrap() + .strip_suffix(");\n") + .unwrap(), + ) + .unwrap(); + assert_eq!( + phase + .evaluate(&Some(HashMap::from([("a", 2.0), ("b", 3.0)]))) + .unwrap(), + 7.0 + ); +} + #[test] fn avoids_auto_measurement_name_collision_with_gate() { let source = r#"OPENQASM 3.0; @@ -692,3 +785,80 @@ fn dump_preserves_io_error_source() { Qasm3DumpError::IoError(error) if error.kind() == std::io::ErrorKind::NotFound )); } + +#[test] +fn parameter_errors_propagate_from_gate_export_with_context() { + use crate::circuit::{CircuitParam, ParameterValue}; + use std::collections::HashMap; + + let mut circuit = Circuit::new(1); + let qubit = Qubit::new(0); + let qubit_map = HashMap::from([(qubit, "q[0]".to_string())]); + // Exercise both single-parameter and multi-parameter formatting. + for (gate, params) in [ + (StandardGate::RX, vec![ParameterValue::Fixed(0.5)]), + ( + StandardGate::U, + vec![ + ParameterValue::Fixed(0.5), + ParameterValue::Fixed(0.25), + ParameterValue::Fixed(0.75), + ], + ), + ] { + circuit + .append(Instruction::Standard(gate), [qubit], params, None) + .unwrap(); + let valid = circuit.operations().last().unwrap(); + let param_index = valid.params.len() - 1; + for param in [ + CircuitParam::Index(99), + CircuitParam::Fixed(f64::NAN), + CircuitParam::Fixed(f64::INFINITY), + CircuitParam::Fixed(f64::NEG_INFINITY), + ] { + let mut operation = valid.clone(); + operation.params[param_index] = param; + let mut output = String::new(); + let error = super::dump_standard_gate( + &gate, + &operation, + &circuit, + &mut output, + &qubit_map, + &HashMap::new(), + ) + .unwrap_err(); + assert!( + matches!(error, Qasm3DumpError::FormatError(ref message) + if message.contains(&format!("parameter {param_index}")) + && message.contains(&format!("{gate:?}"))), + "{error}" + ); + } + } +} + +#[test] +fn export_preserves_indexed_constants_and_symbol_substitution() { + use crate::circuit::ParameterValue; + use std::collections::HashMap; + + let mut circuit = Circuit::new(1); + circuit + .rx(Qubit::new(0), ParameterValue::Param(Parameter::pi())) + .unwrap(); + circuit + .rx( + Qubit::new(0), + ParameterValue::Param(Parameter::symbol("theta")), + ) + .unwrap(); + let qasm = dumps(&circuit).unwrap(); + assert!(qasm.contains("rx(pi)"), "{qasm}"); + let replacements = HashMap::from([("theta".to_string(), Parameter::symbol("angle0"))]); + assert_eq!( + super::format_params(&circuit.operations()[1], &circuit, &replacements).unwrap(), + "(angle0)" + ); +} diff --git a/crates/cqlib-core/src/qis/evolution.rs b/crates/cqlib-core/src/qis/evolution.rs index 0e651a4e..2079e981 100644 --- a/crates/cqlib-core/src/qis/evolution.rs +++ b/crates/cqlib-core/src/qis/evolution.rs @@ -206,7 +206,7 @@ impl PauliEvolution for Circuit { // phase_complex.re is either 1.0 or -1.0 let angle_param: ParameterValue = angle.into(); let adjusted_angle = if phase_complex.re < 0.0 { - multiply_angle_by_factor(angle_param, -1.0) + angle_param.scaled(-1.0) } else { angle_param }; @@ -233,7 +233,7 @@ impl PauliEvolution for Circuit { if non_identity_positions.is_empty() { // The operation is e^(-iθ/2 * I) = e^(-iθ/2) * I // This is just a global phase - let global_phase_param = multiply_angle_by_factor(adjusted_angle, -0.5); + let global_phase_param = adjusted_angle.scaled(-0.5); // Add to circuit's global phase let current_phase = self.global_phase(); @@ -320,14 +320,6 @@ impl PauliEvolution for Circuit { } } -/// Helper function to multiply an angle parameter by a factor -pub(crate) fn multiply_angle_by_factor(angle: ParameterValue, factor: f64) -> ParameterValue { - match angle { - ParameterValue::Fixed(val) => ParameterValue::Fixed(val * factor), - ParameterValue::Param(param) => ParameterValue::Param(Parameter::from(factor) * param), - } -} - // These `pub(crate)` functions implement the product-formula math for both the // numeric API (`to_trotter_circuit` / `to_evolution_circuit`) and the parametric // ansatz (`PauliEvolutionAnsatz` in `circuit/ansatz/hamiltonian_evolution.rs`). @@ -336,7 +328,7 @@ pub(crate) fn multiply_angle_by_factor(angle: ParameterValue, factor: f64) -> Pa // e^{-i c t P} we pass θ = 2 c t. // // Both `ParameterValue::Fixed(f64)` and `ParameterValue::Param(Parameter)` are -// handled uniformly via `multiply_angle_by_factor`. +// handled uniformly via `ParameterValue::scaled`. /// Applies first-order Lie-Trotter (or randomized product-formula) decomposition. /// @@ -367,8 +359,17 @@ pub(crate) fn trotter_first_order_core( // Pre-compute one ParameterValue per term: θ_k = 2 c_k t / n. let angles: Vec = terms .iter() - .map(|(_, coeff)| multiply_angle_by_factor(t.clone(), 2.0 * coeff.re / steps as f64)) - .collect(); + .map(|(_, coeff)| { + // Even finite coefficients can overflow when computing the factor. + let factor = 2.0 * coeff.re / steps as f64; + if !factor.is_finite() { + return Err(CircuitError::InvalidOperation(format!( + "Hamiltonian evolution scale factor must be finite, got {factor}" + ))); + } + Ok(t.scaled(factor)) + }) + .collect::>()?; let mut indices: Vec = (0..terms.len()).collect(); @@ -416,8 +417,16 @@ pub(crate) fn trotter_second_order_core( // Forward and backward passes share the same value by symmetry. let angles: Vec = terms .iter() - .map(|(_, coeff)| multiply_angle_by_factor(t.clone(), coeff.re / steps as f64)) - .collect(); + .map(|(_, coeff)| { + let factor = coeff.re / steps as f64; + if !factor.is_finite() { + return Err(CircuitError::InvalidOperation(format!( + "Hamiltonian evolution scale factor must be finite, got {factor}" + ))); + } + Ok(t.scaled(factor)) + }) + .collect::>()?; for _ in 0..steps { // Forward half-step diff --git a/crates/cqlib-core/src/qis/evolution_test.rs b/crates/cqlib-core/src/qis/evolution_test.rs index 3c52e744..31312e9b 100644 --- a/crates/cqlib-core/src/qis/evolution_test.rs +++ b/crates/cqlib-core/src/qis/evolution_test.rs @@ -387,6 +387,36 @@ fn test_trotter_zero_steps_error() { assert!(result.is_err(), "Should error on zero steps"); } +#[test] +fn test_trotter_scale_factor_overflow_returns_error() { + let mut h = Hamiltonian::new(1); + h.add_term("Z".parse().unwrap(), f64::MAX.into()).unwrap(); + + for mode in [TrotterMode::FirstOrder, TrotterMode::Randomized(42)] { + // All inputs are finite, but doubling the coefficient overflows. + let result = h.to_trotter_circuit(0.25, 2, mode); + assert!(matches!(result, Err(QisError::UnsupportedOperation(_)))); + } +} + +#[test] +fn test_trotter_merged_coefficient_overflow_returns_error() { + let mut h = Hamiltonian::new(1); + for _ in 0..2 { + h.add_term("Z".parse().unwrap(), f64::MAX.into()).unwrap(); + } + + for mode in [ + TrotterMode::FirstOrder, + TrotterMode::SecondOrder, + TrotterMode::Randomized(42), + ] { + // Simplification can overflow even before the scale factor is computed. + let result = h.to_trotter_circuit(0.25, 2, mode); + assert!(matches!(result, Err(QisError::UnsupportedOperation(_)))); + } +} + #[test] fn test_trotter_randomized() { // H = 0.5 * ZZ + 0.3 * XX diff --git a/crates/cqlib-core/src/qis/mod.rs b/crates/cqlib-core/src/qis/mod.rs index 21a00bf0..cf196fb3 100644 --- a/crates/cqlib-core/src/qis/mod.rs +++ b/crates/cqlib-core/src/qis/mod.rs @@ -83,6 +83,7 @@ pub mod hamiltonian; pub mod metrics; pub mod observable; pub mod pauli; +pub mod sampling; pub mod state; pub use error::{PauliStringParseError, QisError}; @@ -90,6 +91,7 @@ pub use evolution::{PauliEvolution, TrotterMode}; pub use hamiltonian::Hamiltonian; pub use observable::Observable; pub use pauli::{Pauli, PauliIter, PauliString, Phase}; +pub use sampling::{Simulator, sample}; pub use state::density_matrix::DensityMatrix; pub use state::density_matrix_noise::DensityMatrixNoise; pub use state::stabilizer::StabilizerState; diff --git a/crates/cqlib-core/src/qis/observable.rs b/crates/cqlib-core/src/qis/observable.rs index 84c7edbd..26b8712d 100644 --- a/crates/cqlib-core/src/qis/observable.rs +++ b/crates/cqlib-core/src/qis/observable.rs @@ -159,7 +159,8 @@ fn apply_pauli_string_to_statevector( .into_par_iter() .map(|j| { let source_j = j ^ x_mask; - let z_parity = (j & z_mask).count_ones(); + // P = phase * i^num_y * X^x * Z^z: Z acts on the source. + let z_parity = (source_j & z_mask).count_ones(); let sign = if z_parity % 2 == 1 { -1.0 } else { 1.0 }; sv_data[source_j] * base_factor * sign }) @@ -209,18 +210,18 @@ impl Observable for Hamiltonian { .par_iter() .enumerate() .map(|(j, amp)| { - // X operator: flip bits where x_mask is 1 - let target_j = j ^ x_mask; - let target_amp = sv_data[target_j]; + // Output j receives the amplitude from source_j. + let source_j = j ^ x_mask; + let source_amp = sv_data[source_j]; - // Z operator: add phase (-1)^(number of overlapping Z bits) - let z_parity = (j & z_mask).count_ones(); + // Z acts before X, so its phase depends on the source. + let z_parity = (source_j & z_mask).count_ones(); let sign = if z_parity % 2 == 1 { -1.0 } else { 1.0 }; let phase_factor = base_factor * sign; // Contribution: conj(amp_j) * (P * psi)_j - amp.conj() * target_amp * phase_factor + amp.conj() * source_amp * phase_factor }) .sum(); @@ -411,12 +412,13 @@ impl Observable for PauliString { .par_iter() .enumerate() .map(|(j, amp)| { - let target_j = j ^ x_mask; - let target_amp = sv_data[target_j]; - let z_parity = (j & z_mask).count_ones(); + let source_j = j ^ x_mask; + let source_amp = sv_data[source_j]; + // Z acts on the source before X moves it to output j. + let z_parity = (source_j & z_mask).count_ones(); let sign = if z_parity % 2 == 1 { -1.0 } else { 1.0 }; let phase_factor = base_factor * sign; - amp.conj() * target_amp * phase_factor + amp.conj() * source_amp * phase_factor }) .sum(); diff --git a/crates/cqlib-core/src/qis/observable_test.rs b/crates/cqlib-core/src/qis/observable_test.rs index 08b71c93..1fc05c34 100644 --- a/crates/cqlib-core/src/qis/observable_test.rs +++ b/crates/cqlib-core/src/qis/observable_test.rs @@ -160,6 +160,94 @@ fn single_pauli(pauli: Pauli) -> PauliString { ps } +#[test] +fn test_statevector_pauli_phases_match_dense_matrices() { + use ndarray::Array1; + + for n in 1..=3 { + let mut amplitudes = Array1::from_iter( + (0..1usize << n) + .map(|j| Complex64::new((j + 1) as f64, ((7 * j + 3) % 11) as f64 - 5.0)), + ); + let norm = amplitudes.iter().map(|a| a.norm_sqr()).sum::().sqrt(); + amplitudes.mapv_inplace(|a| a / norm); + let sv = Statevector::from_state(n, amplitudes.to_vec()).unwrap(); + + for encoded in 0..4usize.pow(n as u32) { + let mut pauli = PauliString::new(n); + for qubit in 0..n { + pauli.set_pauli( + qubit, + [Pauli::I, Pauli::X, Pauli::Y, Pauli::Z][(encoded >> (2 * qubit)) & 3], + ); + } + for phase in [Phase::Plus, Phase::Minus, Phase::I, Phase::MinusI] { + pauli.phase = phase; + // The dense tensor-product implementation is independent of the + // source-index bit-mask kernels under test. + let applied = pauli.to_matrix().dot(&litudes); + let expected: Complex64 = amplitudes + .iter() + .zip(&applied) + .map(|(a, b)| a.conj() * b) + .sum(); + + let coefficient = Complex64::new(0.3, -0.7); + let actual = apply_pauli_string_to_statevector(&pauli, &sv, coefficient); + for (actual, expected) in actual.iter().zip(&applied) { + assert!( + (*actual - coefficient * expected).norm() < 1e-10, + "incorrect action for {pauli}" + ); + } + + if matches!(phase, Phase::Plus | Phase::Minus) { + assert_close(expected.im, 0.0); + assert_close(pauli.expectation_statevector(&sv).unwrap(), expected.re); + assert_close(sv.expectation(&pauli).unwrap(), expected.re); + } + + // Imaginary Pauli phases are valid inside a Hermitian Hamiltonian + // when its coefficient absorbs that phase. + let coefficient = -0.37 * phase.to_complex().conj(); + let h = Hamiltonian::from_list(vec![(pauli.clone(), coefficient)]).unwrap(); + assert_close((coefficient * expected).im, 0.0); + assert_close( + h.expectation_statevector(&sv).unwrap(), + (coefficient * expected).re, + ); + assert_close(sv.expectation(&h).unwrap(), (coefficient * expected).re); + } + } + } +} + +#[test] +fn test_hamiltonian_y_eigenstate_variance() { + // X + Y on its positive eigenstate exposes the mean's sign error. + // I + Y also exposes an incorrect H|psi> after fixing only the mean. + for (other, angle, expected) in [ + (Pauli::X, std::f64::consts::FRAC_PI_4, 2.0_f64.sqrt()), + (Pauli::I, std::f64::consts::FRAC_PI_2, 2.0), + ] { + let scale = std::f64::consts::FRAC_1_SQRT_2; + let sv = Statevector::from_state( + 1, + vec![ + Complex64::new(scale, 0.0), + Complex64::from_polar(scale, angle), + ], + ) + .unwrap(); + let mut h = Hamiltonian::from_pauli(single_pauli(other)); + let mut y = single_pauli(Pauli::Y); + y.phase = Phase::MinusI; + h.add_term(y, Complex64::new(0.0, 1.0)).unwrap(); + assert_close(h.expectation_statevector(&sv).unwrap(), expected); + assert_close(h.variance_statevector(&sv).unwrap(), 0.0); + } +} + #[test] fn test_pauli_variance_z_on_zero_is_zero() { let sv = Statevector::new(1); diff --git a/crates/cqlib-core/src/qis/pauli.rs b/crates/cqlib-core/src/qis/pauli.rs index 605f37fa..543efb97 100644 --- a/crates/cqlib-core/src/qis/pauli.rs +++ b/crates/cqlib-core/src/qis/pauli.rs @@ -43,7 +43,7 @@ use crate::qis::error::{PauliStringParseError, QisError}; use bitvec::prelude::*; -use ndarray::{Array2, arr2}; +use ndarray::{Array2, arr2, linalg::kron}; use num_complex::Complex64; use std::fmt; use std::ops::{Add, AddAssign, Mul, MulAssign}; @@ -617,7 +617,7 @@ impl PauliString { pub fn to_matrix(&self) -> Array2 { let mut matrix = Array2::from_elem((1, 1), Complex64::new(1.0, 0.0)); for qubit in (0..self.num_qubits).rev() { - matrix = kronecker_product(&matrix, &self.get_pauli(qubit).to_matrix()); + matrix = kron(&matrix, &self.get_pauli(qubit).to_matrix()); } let phase = self.phase.to_complex(); @@ -956,30 +956,6 @@ impl PauliString { } } -fn kronecker_product(left: &Array2, right: &Array2) -> Array2 { - let (left_rows, left_cols) = left.dim(); - let (right_rows, right_cols) = right.dim(); - let mut result = Array2::from_elem( - (left_rows * right_rows, left_cols * right_cols), - Complex64::new(0.0, 0.0), - ); - - for left_row in 0..left_rows { - for left_col in 0..left_cols { - for right_row in 0..right_rows { - for right_col in 0..right_cols { - result[[ - left_row * right_rows + right_row, - left_col * right_cols + right_col, - ]] = left[[left_row, left_col]] * right[[right_row, right_col]]; - } - } - } - } - - result -} - /// Multiplies two Pauli strings, returning a new instance. /// /// # Examples diff --git a/crates/cqlib-core/src/qis/pauli_test.rs b/crates/cqlib-core/src/qis/pauli_test.rs index ab42a09e..42478767 100644 --- a/crates/cqlib-core/src/qis/pauli_test.rs +++ b/crates/cqlib-core/src/qis/pauli_test.rs @@ -822,7 +822,9 @@ fn pauli_string_to_matrix_uses_little_endian_tensor_order() { ], ]); - assert_eq!(pauli.to_matrix(), expected); + let matrix = pauli.to_matrix(); + assert_eq!(matrix, expected); + assert!(matrix.is_standard_layout()); } #[test] diff --git a/crates/cqlib-core/src/qis/sampling.rs b/crates/cqlib-core/src/qis/sampling.rs new file mode 100644 index 00000000..b38c654f --- /dev/null +++ b/crates/cqlib-core/src/qis/sampling.rs @@ -0,0 +1,208 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +//! Direct circuit sampling with the existing ideal simulators. + +use std::collections::{HashMap, HashSet}; + +use rand::SeedableRng; +use rand::distr::{Distribution, weighted::WeightedIndex}; +use rand::rngs::SmallRng; + +use crate::circuit::gate::ClassicalDataOp; +use crate::circuit::{Circuit, Directive, Instruction, Qubit}; +use crate::device::{ExecutionResult, Outcome}; +use crate::qis::{DensityMatrix, QisError, StabilizerState, Statevector}; + +/// Ideal simulator used by [`sample`]. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum Simulator { + /// General pure-state simulation; the default. + #[default] + Statevector, + /// Density-matrix simulation, including deterministic reset channels. + DensityMatrix, + /// Simulation of the Clifford gates supported by [`StabilizerState`]. + Stabilizer, +} + +/// Samples a circuit from the all-zero initial state without changing it. +/// +/// An explicit `qubits` list selects the final Z-basis outputs in bit order, +/// with the first qubit displayed at the right of each result string. Otherwise, +/// terminal measurement declarations select the outputs in first-occurrence +/// order, with duplicates removed. Without declarations, all circuit qubits +/// are sampled in circuit order, including idle qubits. +/// +/// The circuit must have bound parameters and only terminal measurements. +/// Classical control flow is unsupported. Barrier and Delay are no-ops; Store +/// does not affect the returned quantum measurement counts. Reset requires +/// [`Simulator::DensityMatrix`]. A seed makes counts reproducible for the same +/// simulator and library version. `shots` and the selected output width must +/// both be positive. +/// +/// ``` +/// use cqlib_core::circuit::{Circuit, Qubit}; +/// use cqlib_core::qis::{sample, Simulator}; +/// +/// let mut circuit = Circuit::new(2); +/// circuit.h(Qubit::new(0)).unwrap(); +/// circuit.cx(Qubit::new(0), Qubit::new(1)).unwrap(); +/// let result = sample(&circuit, 1000, Some(42), Simulator::default(), None).unwrap(); +/// assert_eq!(result.counts().values().sum::(), 1000); +/// ``` +pub fn sample( + circuit: &Circuit, + shots: usize, + seed: Option, + simulator: Simulator, + qubits: Option<&[Qubit]>, +) -> Result { + if shots == 0 { + return Err(QisError::InvalidParameterValue( + "shots must be a positive integer".into(), + )); + } + + if circuit + .operations() + .iter() + .any(|op| matches!(op.instruction, Instruction::ClassicalControl(_))) + { + return Err(QisError::UnsupportedOperation( + "sample does not support classical control flow".into(), + )); + } + let circuit = circuit.decompose()?; + if simulator != Simulator::DensityMatrix + && circuit + .operations() + .iter() + .any(|op| matches!(op.instruction, Instruction::Directive(Directive::Reset))) + { + return Err(QisError::UnsupportedOperation( + "sample with reset requires the density_matrix simulator".into(), + )); + } + super::state::validate_terminal_measurements(&circuit)?; + + let circuit_qubits = circuit.qubits(); + let mut selected = qubits.map(<[Qubit]>::to_vec).unwrap_or_else(|| { + let mut seen = HashSet::new(); + circuit + .operations() + .iter() + .filter(|op| { + matches!( + op.instruction, + Instruction::ClassicalData( + ClassicalDataOp::MeasureBit { .. } | ClassicalDataOp::MeasureBits { .. } + ) + ) + }) + .flat_map(|op| op.qubits.iter().copied()) + .filter(|qubit| seen.insert(*qubit)) + .collect() + }); + if qubits.is_none() && selected.is_empty() { + selected = circuit_qubits.clone(); + } + if selected.is_empty() { + return Err(QisError::InvalidParameterValue( + "sample requires at least one output qubit".into(), + )); + } + let qubit_positions: HashMap<_, _> = circuit_qubits + .iter() + .enumerate() + .map(|(position, &qubit)| (qubit, position)) + .collect(); + let mut seen = HashSet::new(); + let positions = selected + .iter() + .map(|qubit| { + if !seen.insert(*qubit) { + return Err(QisError::InvalidParameterValue( + "qubits must not contain duplicates".into(), + )); + } + qubit_positions.get(qubit).copied().ok_or_else(|| { + QisError::InvalidParameterValue(format!( + "output qubit {} is not in the circuit", + qubit.id() + )) + }) + }) + .collect::, _>>()?; + + let mut counts = HashMap::new(); + let backend = match simulator { + Simulator::Statevector | Simulator::DensityMatrix => { + let (mut probabilities, backend) = match simulator { + Simulator::Statevector => ( + Statevector::from_circuit(&circuit)?.probabilities(), + "statevector", + ), + _ => ( + DensityMatrix::from_circuit(&circuit)?.probabilities(), + "density_matrix", + ), + }; + // Density-matrix evolution can leave tiny negative diagonal entries + // through roundoff, even for a gate followed by its inverse. + for probability in &mut probabilities { + if (-1e-12..0.0).contains(probability) { + *probability = 0.0; + } + } + let distribution = WeightedIndex::new(probabilities) + .map_err(|error| QisError::InvalidParameterValue(error.to_string()))?; + let mut rng = seed.map_or_else(SmallRng::from_os_rng, SmallRng::seed_from_u64); + for _ in 0..shots { + let basis = distribution.sample(&mut rng); + let outcome = project(&positions, |position| basis & (1usize << position) != 0); + *counts.entry(outcome).or_insert(0) += 1; + } + backend + } + Simulator::Stabilizer => { + let mut state = StabilizerState::from_circuit(&circuit)?; + if let Some(seed) = seed { + state.seed_rng(seed); + } + for full in state.sample_shots(shots) { + let outcome = project(&positions, |position| full.is_one(position)); + *counts.entry(outcome).or_insert(0) += 1; + } + "stabilizer" + } + }; + + Ok(ExecutionResult::from_counts( + format!("{backend}-sample"), + selected, + shots, + positions.len(), + Some(backend.into()), + counts, + )) +} + +fn project(positions: &[usize], is_one: impl Fn(usize) -> bool) -> Outcome { + Outcome::from_indices( + positions.len(), + positions + .iter() + .enumerate() + .filter_map(|(bit, &position)| is_one(position).then_some(bit)), + ) +} diff --git a/crates/cqlib-core/src/qis/state/circuit_validation.rs b/crates/cqlib-core/src/qis/state/circuit_validation.rs new file mode 100644 index 00000000..2f9c101a --- /dev/null +++ b/crates/cqlib-core/src/qis/state/circuit_validation.rs @@ -0,0 +1,54 @@ +// This code is part of Cqlib. +// +// (C) Copyright China Telecom Quantum Group 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +use crate::circuit::gate::{ClassicalDataOp, Directive}; +use crate::circuit::{Circuit, Instruction}; +use crate::qis::QisError; +use std::collections::HashMap; + +// Validate a decomposed circuit before state evolution starts. State-level APIs +// treat measurements as output declarations, so a measured qubit cannot be used +// by a later quantum operation. Independent qubits may still evolve. +pub(crate) fn validate_terminal_measurements(circuit: &Circuit) -> Result<(), QisError> { + let mut measured = HashMap::new(); + for (index, op) in circuit.operations().iter().enumerate() { + match &op.instruction { + Instruction::ClassicalData( + ClassicalDataOp::MeasureBit { .. } | ClassicalDataOp::MeasureBits { .. }, + ) => { + for qubit in &op.qubits { + measured.entry(*qubit).or_insert(index); + } + } + Instruction::ClassicalData(ClassicalDataOp::Store { .. }) + | Instruction::Directive(Directive::Barrier) + | Instruction::Delay => {} + _ => { + for qubit in &op.qubits { + if let Some(measurement_index) = measured.get(qubit) { + return Err(QisError::UnsupportedOperation(format!( + "mid-circuit measurement on qubit {} at operation {}: later '{}' at \ + operation {} uses the measured qubit (indices refer to the decomposed \ + circuit, starting at 0); from_circuit/apply_circuit only support \ + terminal measurement declarations", + qubit.id(), + measurement_index, + op.instruction.name(), + index, + ))); + } + } + } + } + } + Ok(()) +} diff --git a/crates/cqlib-core/src/qis/state/density_matrix.rs b/crates/cqlib-core/src/qis/state/density_matrix.rs index 62ed0583..b3560659 100644 --- a/crates/cqlib-core/src/qis/state/density_matrix.rs +++ b/crates/cqlib-core/src/qis/state/density_matrix.rs @@ -1,4 +1,5 @@ // This code is part of Cqlib. +// Modified to support reproducible state sampling. // // (C) Copyright China Telecom Quantum Group 2026 // @@ -36,6 +37,7 @@ use crate::device::{ExecutionResult, Outcome}; use crate::qis::Observable; use crate::qis::error::QisError; use num_complex::Complex64; +use rand::{Rng, SeedableRng, rngs::SmallRng}; use rayon::prelude::*; use smallvec::{SmallVec, smallvec}; use std::collections::{HashMap, HashSet}; @@ -103,6 +105,8 @@ fn interleave_bits(keep: &[usize], k_val: usize, trace: &[usize], t_val: usize) pub struct DensityMatrix { /// Flattened matrix elements. Length is $4^N$. data: Vec, + /// Circuit qubit identifiers mapped to storage positions. + pub(super) qubit_map: super::QubitMap, /// Number of qubits in the system ($N$). pub num_qubits: usize, } @@ -199,7 +203,11 @@ impl DensityMatrix { if size > 0 { data[0] = Complex64::new(1.0, 0.0); } - Self { data, num_qubits } + Self { + data, + num_qubits, + qubit_map: None, + } } /// Creates the maximally mixed state $I / 2^N$. @@ -217,7 +225,11 @@ impl DensityMatrix { for index in 0..dim { data[index * dim + index] = probability; } - Self { data, num_qubits } + Self { + data, + num_qubits, + qubit_map: None, + } } /// Creates a density matrix from an initial statevector (pure state). @@ -257,7 +269,11 @@ impl DensityMatrix { data.par_chunks_exact_mut(dim).enumerate().for_each(kernel); } - Ok(Self { data, num_qubits }) + Ok(Self { + data, + num_qubits, + qubit_map: None, + }) } /// Creates a density matrix directly from a flattened $2^N \times 2^N$ matrix. @@ -285,6 +301,7 @@ impl DensityMatrix { let dm = Self { data: dm_state, num_qubits, + qubit_map: None, }; dm.validate_physical(1e-10)?; Ok(dm) @@ -327,6 +344,14 @@ impl DensityMatrix { /// Applies a quantum circuit to this density matrix in-place. /// + /// Once bound to circuit qubit IDs, subsequent circuits must have the same + /// IDs in the same order. The mapping is committed only on success; gates + /// completed before an error are not rolled back. + /// + /// As in `from_circuit`, terminal measurements are ignored. A measured qubit + /// used by a later gate or Reset is rejected before the state is changed. + /// Independent gates, repeated measurements, Barrier, Delay, and Store are allowed. + /// /// The circuit is first decomposed into basic gates via [`Circuit::decompose`], /// then each operation is applied sequentially to the current density matrix. /// @@ -366,14 +391,10 @@ impl DensityMatrix { )); } let circuit = circuit.decompose()?; + super::validate_terminal_measurements(&circuit)?; let dm = self; - let qubits = circuit.qubits(); - let qubit_map: std::collections::HashMap<_, _> = qubits - .iter() - .enumerate() - .map(|(idx, q)| (*q, idx)) - .collect(); + let qubit_map = super::prepare_qubit_map(&circuit.qubits(), &dm.qubit_map)?; let parameter_values: Vec> = circuit .parameters() @@ -474,6 +495,7 @@ impl DensityMatrix { Instruction::ClassicalData(_) => {} } } + dm.qubit_map = Some(qubit_map); Ok(()) } @@ -556,6 +578,7 @@ impl DensityMatrix { Self { data: vec![Complex64::new(0.0, 0.0); size], num_qubits, + qubit_map: None, } } @@ -1366,6 +1389,19 @@ impl DensityMatrix { let (n_k, n_t) = (s_keep.len(), trace.len()); let (dim, r_dim) = (1 << self.num_qubits, 1 << n_k); let mut res = Self::zeros(n_k); + res.qubit_map = self.qubit_map.as_ref().map(|qubit_map| { + std::sync::Arc::new( + qubit_map + .iter() + .filter_map(|(&qubit, position)| { + s_keep + .binary_search(position) + .ok() + .map(|index| (qubit, index)) + }) + .collect(), + ) + }); res.data.par_iter_mut().enumerate().for_each(|(idx, val)| { let (i, j) = (idx / r_dim, idx % r_dim); let mut sum = Complex64::default(); @@ -1390,6 +1426,7 @@ impl DensityMatrix { } let mut res = Self::zeros(n); + res.qubit_map.clone_from(&self.qubit_map); res.data.par_iter_mut().enumerate().for_each(|(idx, val)| { let lower_swap_bits = idx & swap_mask; let upper_swap_bits = (idx >> n) & swap_mask; @@ -1429,6 +1466,7 @@ impl DensityMatrix { /// [`sample_shots`](Self::sample_shots) to reuse each thread's working copy. pub(crate) fn reset_from(&mut self, source: &DensityMatrix) { debug_assert_eq!(self.num_qubits, source.num_qubits); + self.qubit_map.clone_from(&source.qubit_map); self.data.copy_from_slice(&source.data); } @@ -1438,6 +1476,11 @@ impl DensityMatrix { /// The density matrix is projected into the subspace consistent with the /// outcome and renormalized: `ρ' = Π_b ρ Π_b / Tr(Π_b ρ)`. pub fn measure(&mut self, qubit: usize) -> Result { + self.measure_with_rng(qubit, &mut rand::rng()) + } + + /// Measures one qubit using the supplied random stream. + pub fn measure_with_rng(&mut self, qubit: usize, rng: &mut impl Rng) -> Result { self.validate_qubit(qubit)?; let dim = 1 << self.num_qubits; let mask = 1usize << qubit; @@ -1448,11 +1491,7 @@ impl DensityMatrix { .map(|i| self.data[i * dim + i].re) .sum(); - let outcome = { - use rand::Rng; - let mut rng = rand::rng(); - rng.random::() < p1 - }; + let outcome = rng.random::() < p1; // Project and renormalise: zero rows/cols inconsistent with outcome. let norm = if outcome { p1 } else { 1.0 - p1 }; @@ -1481,12 +1520,25 @@ impl DensityMatrix { /// Resets `qubit` to the |0⟩ state. /// - /// Reset is modeled as a Z-basis measurement followed by an X correction - /// when the measured outcome is |1⟩. This is destructive, matching circuit - /// reset semantics. + /// Applies the deterministic reset channel with Kraus operators |0⟩⟨0| + /// and |0⟩⟨1|. The outcome is discarded, preserving the reduced density + /// matrix of the other qubits, which may be mixed for an entangled input. pub fn reset(&mut self, qubit: usize) -> Result<(), QisError> { - if self.measure(qubit)? { - self.apply_x(qubit)?; + self.validate_qubit(qubit)?; + let dim = 1usize << self.num_qubits; + let mask = 1usize << qubit; + for row in (0..dim).filter(|row| row & mask == 0) { + for col in (0..dim).filter(|col| col & mask == 0) { + let i00 = row * dim + col; + let i01 = row * dim + (col | mask); + let i10 = (row | mask) * dim + col; + let i11 = (row | mask) * dim + (col | mask); + let sum = self.data[i00] + self.data[i11]; + self.data[i00] = sum; + self.data[i01] = Complex64::new(0.0, 0.0); + self.data[i10] = Complex64::new(0.0, 0.0); + self.data[i11] = Complex64::new(0.0, 0.0); + } } Ok(()) } @@ -1497,10 +1549,15 @@ impl DensityMatrix { /// The density matrix is fully collapsed after this call. /// Use [`Outcome::is_one(q)`](crate::device::Outcome::is_one) to read qubit `q`'s result. pub fn measure_all(&mut self) -> Outcome { + self.measure_all_with_rng(&mut rand::rng()) + } + + /// Measures all qubits in storage order using the supplied random stream. + pub fn measure_all_with_rng(&mut self, rng: &mut impl Rng) -> Outcome { let num_chunks = self.num_qubits.div_ceil(64); let mut chunks = SmallVec::from_elem(0u64, num_chunks); for q in 0..self.num_qubits { - if self.measure(q).unwrap() { + if self.measure_with_rng(q, rng).unwrap() { chunks[q / 64] |= 1u64 << (q % 64); } } @@ -1526,6 +1583,23 @@ impl DensityMatrix { /// assert!(shots.iter().all(|v| v.is_one(0) == v.is_one(1))); /// ``` pub fn sample_shots(&self, shots: usize) -> Vec { + self.sample_shots_with_seed(shots, None) + } + + /// Samples without changing this state. An explicit seed assigns a random + /// stream to each shot before parallel execution, independent of scheduling. + pub fn sample_shots_with_seed(&self, shots: usize, seed: Option) -> Vec { + if let Some(seed) = seed { + let mut rng = SmallRng::seed_from_u64(seed); + let seeds: Vec = (0..shots).map(|_| rng.random()).collect(); + return seeds + .into_par_iter() + .map_with(self.clone(), |work, seed| { + work.reset_from(self); + work.measure_all_with_rng(&mut SmallRng::seed_from_u64(seed)) + }) + .collect(); + } (0..shots) .into_par_iter() .map_with(self.clone(), |work, _| { @@ -1545,11 +1619,21 @@ impl DensityMatrix { measurement: &Measurement, shots: usize, ) -> Result { - measurement.check_qubits(self.num_qubits)?; + self.sample_with_seed(measurement, shots, None) + } + + /// Projects seeded full-state samples onto the requested measurement. + pub fn sample_with_seed( + &self, + measurement: &Measurement, + shots: usize, + seed: Option, + ) -> Result { + let projection = super::resolve_measurement(measurement, &self.qubit_map, self.num_qubits)?; let mut counts = HashMap::new(); - for full in self.sample_shots(shots) { - *counts.entry(measurement.project(&full)).or_insert(0usize) += 1; + for full in self.sample_shots_with_seed(shots, seed) { + *counts.entry(projection.project(&full)).or_insert(0usize) += 1; } Ok(ExecutionResult::from_counts( @@ -1568,7 +1652,7 @@ impl DensityMatrix { /// a self-contained output contract and aggregates computational-basis /// probabilities over unmeasured qubits. pub fn probs(&self, measurement: &Measurement) -> Result, QisError> { - measurement.check_qubits(self.num_qubits)?; + let projection = super::resolve_measurement(measurement, &self.qubit_map, self.num_qubits)?; let mut marginal = HashMap::new(); for (basis, prob) in self.probabilities().into_iter().enumerate() { @@ -1576,7 +1660,7 @@ impl DensityMatrix { continue; } *marginal - .entry(measurement.project_basis(basis)) + .entry(projection.project_basis(basis)) .or_insert(0.0) += prob; } Ok(marginal) diff --git a/crates/cqlib-core/src/qis/state/density_matrix_noise.rs b/crates/cqlib-core/src/qis/state/density_matrix_noise.rs index cc962e3b..7e0d41b1 100644 --- a/crates/cqlib-core/src/qis/state/density_matrix_noise.rs +++ b/crates/cqlib-core/src/qis/state/density_matrix_noise.rs @@ -1,4 +1,5 @@ // This code is part of Cqlib. +// Modified to support reproducible state sampling. // // (C) Copyright China Telecom Quantum Group 2026 // @@ -18,7 +19,6 @@ use crate::qis::error::QisError; use crate::qis::state::density_matrix::DensityMatrix; use ndarray::Array2; use num_complex::Complex64; -use rayon::prelude::*; use std::collections::HashMap; /// A density matrix quantum simulator with noise modeling capabilities. @@ -148,6 +148,14 @@ impl DensityMatrixNoise { /// Applies a quantum circuit to this noisy density matrix in-place. /// + /// Once bound to circuit qubit IDs, subsequent circuits must have the same + /// IDs in the same order. The mapping is committed only on success; gates + /// completed before an error are not rolled back. + /// + /// As in `from_circuit`, terminal measurements are ignored. A measured qubit + /// used by a later gate or Reset is rejected before the state is changed. + /// Independent gates, repeated measurements, Barrier, Delay, and Store are allowed. + /// /// The circuit is first decomposed into basis gates via [`Circuit::decompose`], /// then each operation is applied sequentially. Noise is applied according to /// the noise model immediately following each gate. @@ -180,14 +188,10 @@ impl DensityMatrixNoise { )); } let circuit = circuit.decompose()?; + super::validate_terminal_measurements(&circuit)?; let sim = self; - let qubits = circuit.qubits(); - let qubit_map: std::collections::HashMap<_, _> = qubits - .iter() - .enumerate() - .map(|(idx, q)| (*q, idx)) - .collect(); + let qubit_map = super::prepare_qubit_map(&circuit.qubits(), &sim.state.qubit_map)?; let parameter_values: Vec> = circuit .parameters() @@ -301,6 +305,7 @@ impl DensityMatrixNoise { Instruction::ClassicalData(_) => {} } } + sim.state.qubit_map = Some(qubit_map); Ok(()) } @@ -937,22 +942,25 @@ impl DensityMatrixNoise { /// # Arguments /// /// * `probs` - Mutable reference to the probability vector to modify. - /// * `qubits` - Indices of qubits to apply readout noise for. - fn apply_readout_noise(&self, probs: &mut Vec, qubits: &[usize]) -> Result<(), QisError> { + /// * `qubits` - Noise-model qubit IDs paired with probability-array bit positions. + fn apply_readout_noise( + &self, + probs: &mut Vec, + qubits: impl IntoIterator, + ) -> Result<(), QisError> { let Some(noise_model) = &self.noise_model else { return Ok(()); }; let mut next_probs = vec![0.0; probs.len()]; - for &q in qubits { - if q >= self.state.num_qubits { + for (qubit, position) in qubits { + if position >= self.state.num_qubits { return Err(QisError::IndexOutOfBounds { - index: q, + index: position, max: self.state.num_qubits.saturating_sub(1), }); } - let q_obj = Qubit::new(q as u32); - let Some(err) = noise_model.get_readout_error(&q_obj) else { + let Some(err) = noise_model.get_readout_error(&qubit) else { continue; }; @@ -963,13 +971,13 @@ impl DensityMatrixNoise { next_probs.fill(0.0); for (state, &p) in probs.iter().enumerate() { - let bit = (state >> q) & 1; + let bit = (state >> position) & 1; if bit == 0 { next_probs[state] += p * p_0_given_0; - next_probs[state | (1 << q)] += p * p_1_given_0; + next_probs[state | (1 << position)] += p * p_1_given_0; } else { next_probs[state] += p * p_1_given_1; - next_probs[state & !(1 << q)] += p * p_0_given_1; + next_probs[state & !(1 << position)] += p * p_0_given_1; } } std::mem::swap(probs, &mut next_probs); @@ -1007,7 +1015,10 @@ impl DensityMatrixNoise { /// ``` pub fn probabilities_with_readout(&self, qubits: &[usize]) -> Result, QisError> { let mut probs = self.probabilities(); - self.apply_readout_noise(&mut probs, qubits)?; + self.apply_readout_noise( + &mut probs, + qubits.iter().map(|&q| (Qubit::new(q as u32), q)), + )?; Ok(probs) } @@ -1025,25 +1036,31 @@ impl DensityMatrixNoise { /// This preserves the existing simulator convention: [`probabilities`](Self::probabilities) /// and [`probs`](Self::probs) describe the noisy quantum state before readout /// error, while this method additionally applies readout errors configured - /// for `measurement.qubits()`. + /// for `measurement.qubits()`. These IDs select the noise entries, while + /// the state's qubit mapping determines the affected probability-array bits. pub fn probs_with_readout( &self, measurement: &Measurement, ) -> Result, QisError> { - measurement.check_qubits(self.state.num_qubits)?; + let projection = + super::resolve_measurement(measurement, &self.state.qubit_map, self.state.num_qubits)?; - let qubits: Vec = measurement.qubits().iter().map(Qubit::index).collect(); + let mut probs = self.probabilities(); + self.apply_readout_noise( + &mut probs, + measurement + .qubits() + .iter() + .copied() + .zip(projection.qubits().iter().map(Qubit::index)), + )?; let mut marginal = HashMap::new(); - for (basis, prob) in self - .probabilities_with_readout(&qubits)? - .into_iter() - .enumerate() - { + for (basis, prob) in probs.into_iter().enumerate() { if prob == 0.0 { continue; } *marginal - .entry(measurement.project_basis(basis)) + .entry(projection.project_basis(basis)) .or_insert(0.0) += prob; } Ok(marginal) @@ -1131,13 +1148,13 @@ impl DensityMatrixNoise { /// assert!(shots.iter().all(|v| v.is_one(0) == v.is_one(1))); /// ``` pub fn sample_shots(&self, shots: usize) -> Vec { - (0..shots) - .into_par_iter() - .map_with(self.clone(), |work, _| { - work.state.reset_from(&self.state); - work.state.measure_all() - }) - .collect() + self.sample_shots_with_seed(shots, None) + } + + /// Samples the prepared density matrix with an optional seed. Like + /// `sample_shots`, this does not apply readout noise. + pub fn sample_shots_with_seed(&self, shots: usize, seed: Option) -> Vec { + self.state.sample_shots_with_seed(shots, seed) } /// Samples the noisy quantum state using a circuit [`Measurement`] as the output contract. @@ -1149,11 +1166,22 @@ impl DensityMatrixNoise { measurement: &Measurement, shots: usize, ) -> Result { - measurement.check_qubits(self.state.num_qubits)?; + self.sample_with_seed(measurement, shots, None) + } + + /// Projects seeded full-state samples onto the requested measurement. + pub fn sample_with_seed( + &self, + measurement: &Measurement, + shots: usize, + seed: Option, + ) -> Result { + let projection = + super::resolve_measurement(measurement, &self.state.qubit_map, self.state.num_qubits)?; let mut counts = HashMap::new(); - for full in self.sample_shots(shots) { - *counts.entry(measurement.project(&full)).or_insert(0usize) += 1; + for full in self.sample_shots_with_seed(shots, seed) { + *counts.entry(projection.project(&full)).or_insert(0usize) += 1; } Ok(ExecutionResult::from_counts( diff --git a/crates/cqlib-core/src/qis/state/density_matrix_test.rs b/crates/cqlib-core/src/qis/state/density_matrix_test.rs index a00107f1..34bc2572 100644 --- a/crates/cqlib-core/src/qis/state/density_matrix_test.rs +++ b/crates/cqlib-core/src/qis/state/density_matrix_test.rs @@ -175,6 +175,63 @@ fn test_apply_circuit_reset_directive() { assert_relative_eq!(probs[1], 0.0); } +#[test] +fn reset_bell_state_preserves_the_other_qubits_mixed_state() { + for (qubit, expected_probs) in [(0, [0.5, 0.0, 0.5, 0.0]), (1, [0.5, 0.5, 0.0, 0.0])] { + let mut dm = DensityMatrix::new(2); + dm.apply_h(0).unwrap(); + dm.apply_cx(0, 1).unwrap(); + dm.reset(qubit).unwrap(); + for (row, &probability) in expected_probs.iter().enumerate() { + for col in 0..4 { + let expected = if row == col { probability } else { 0.0 }; + assert_relative_eq!(dm.data()[row * 4 + col].re, expected, epsilon = 1e-12); + assert_relative_eq!(dm.data()[row * 4 + col].im, 0.0, epsilon = 1e-12); + } + } + assert_relative_eq!(purity_mixed(&dm).unwrap(), 0.5, epsilon = 1e-12); + } +} + +#[test] +fn reset_matches_kraus_channel_for_complex_mixed_states_on_every_qubit() { + let mut pure = DensityMatrix::new(3); + pure.apply_h(0).unwrap(); + pure.apply_ry(1, 0.7).unwrap(); + pure.apply_rx(2, 0.4).unwrap(); + pure.apply_cx(0, 1).unwrap(); + pure.apply_cx(1, 2).unwrap(); + pure.apply_s(1).unwrap(); + let mixed = DensityMatrix::maximally_mixed(3); + let data = pure + .data() + .iter() + .zip(mixed.data()) + .map(|(a, b)| 0.7 * a + 0.3 * b) + .collect(); + let initial = DensityMatrix::from_density_matrix_state(3, data).unwrap(); + let zero = Complex64::new(0.0, 0.0); + let one = Complex64::new(1.0, 0.0); + let kraus = [vec![one, zero, zero, zero], vec![zero, one, zero, zero]]; + + for qubit in 0..3 { + let mut actual = initial.clone(); + let mut expected = initial.clone(); + actual.reset(qubit).unwrap(); + expected.apply_kraus(&kraus, &[qubit]).unwrap(); + for (a, b) in actual.data().iter().zip(expected.data()) { + assert_relative_eq!(a.re, b.re, epsilon = 1e-12); + assert_relative_eq!(a.im, b.im, epsilon = 1e-12); + } + assert_relative_eq!(actual.trace().re, 1.0, epsilon = 1e-12); + let before = actual.data().to_vec(); + actual.reset(qubit).unwrap(); + assert_eq!(actual.data(), before); + assert!(actual.reset(3).is_err()); + assert_eq!(actual.data(), before); + } +} + #[test] fn test_apply_circuit_classical_control_flow_error() { use crate::circuit::ClassicalExpr; @@ -542,6 +599,7 @@ fn test_is_hermitian_invalid() { state[3] = Complex64::new(1.0, 0.0); // |1><1| to make trace 2 let dm = DensityMatrix { + qubit_map: None, data: state, num_qubits: 1, }; @@ -559,6 +617,7 @@ fn test_is_hermitian_complex_off_diagonal() { state[3] = Complex64::new(0.5, 0.0); let dm = DensityMatrix { + qubit_map: None, data: state, num_qubits: 1, }; @@ -574,6 +633,7 @@ fn test_is_hermitian_requires_real_diagonal() { state[3] = Complex64::new(0.5, -0.1); // Conjugate won't help let dm = DensityMatrix { + qubit_map: None, data: state, num_qubits: 1, }; @@ -589,6 +649,7 @@ fn test_is_positive_semidefinite_valid() { state[3] = Complex64::new(0.5, 0.0); let dm = DensityMatrix { + qubit_map: None, data: state, num_qubits: 1, }; @@ -604,6 +665,7 @@ fn test_is_positive_semidefinite_negative_diagonal() { state[3] = Complex64::new(1.5, 0.0); let dm = DensityMatrix { + qubit_map: None, data: state, num_qubits: 1, }; @@ -619,6 +681,7 @@ fn test_validate_physical_valid_mixed_state() { state[3] = Complex64::new(0.5, 0.0); let dm = DensityMatrix { + qubit_map: None, data: state, num_qubits: 1, }; @@ -635,6 +698,7 @@ fn test_validate_physical_non_hermitian() { state[3] = Complex64::new(0.5, 0.0); let dm = DensityMatrix { + qubit_map: None, data: state, num_qubits: 1, }; @@ -656,6 +720,7 @@ fn test_validate_physical_not_normalized() { state[3] = Complex64::new(1.0, 0.0); // Trace = 2 let dm = DensityMatrix { + qubit_map: None, data: state, num_qubits: 1, }; @@ -729,6 +794,7 @@ fn test_tolerance_handling() { state[3] = Complex64::new(0.5, 0.0); let dm = DensityMatrix { + qubit_map: None, data: state, num_qubits: 1, }; @@ -942,6 +1008,7 @@ fn test_psd_gershgorin_false_negative_equal_superposition() { *item = val; } let dm = DensityMatrix { + qubit_map: None, data, num_qubits: n, }; @@ -975,6 +1042,7 @@ fn test_psd_valid_pure_non_diagonally_dominant() { data[3 * dim] = Complex64::new(sqrt_06 * sqrt_04, 0.0); data[3 * dim + 3] = Complex64::new(0.4, 0.0); let dm = DensityMatrix { + qubit_map: None, data, num_qubits: n, }; @@ -1004,6 +1072,7 @@ fn test_psd_valid_mixed_state_non_diagonally_dominant() { } } let dm = DensityMatrix { + qubit_map: None, data, num_qubits: n, }; @@ -1025,6 +1094,7 @@ fn test_psd_zero_eigenvalue_boundary() { let mut data = vec![Complex64::new(0.0, 0.0); size]; data[0] = Complex64::new(1.0, 0.0); let dm = DensityMatrix { + qubit_map: None, data, num_qubits: n, }; @@ -1044,6 +1114,7 @@ fn test_psd_negative_eigenvalue_rejected() { data[0] = Complex64::new(-0.1, 0.0); data[dim + 1] = Complex64::new(1.1, 0.0); let dm = DensityMatrix { + qubit_map: None, data, num_qubits: n, }; @@ -1067,6 +1138,7 @@ fn test_psd_tiny_negative_eigenvalue_accepted_with_tolerance() { data[0] = Complex64::new(1.0 + eps, 0.0); data[dim + 1] = Complex64::new(-eps, 0.0); let dm = DensityMatrix { + qubit_map: None, data, num_qubits: n, }; @@ -1093,6 +1165,7 @@ fn test_psd_non_hermitian_rejected() { data[3] = Complex64::new(0.5, 0.0); data[1] = Complex64::new(0.0, 0.2); // non-Hermitian off-diagonal let dm = DensityMatrix { + qubit_map: None, data, num_qubits: n, }; @@ -1111,6 +1184,7 @@ fn test_psd_trace_not_one() { data[0] = Complex64::new(1.0, 0.0); data[3] = Complex64::new(1.0, 0.0); let dm = DensityMatrix { + qubit_map: None, data, num_qubits: n, }; @@ -1129,6 +1203,7 @@ fn test_psd_nan_rejected() { data[0] = Complex64::new(f64::NAN, 0.0); data[3] = Complex64::new(f64::NAN, 0.0); let dm = DensityMatrix { + qubit_map: None, data, num_qubits: n, }; @@ -1145,6 +1220,7 @@ fn test_psd_inf_rejected() { data[0] = Complex64::new(f64::INFINITY, 0.0); data[3] = Complex64::new(0.0, 0.0); let dm = DensityMatrix { + qubit_map: None, data, num_qubits: n, }; diff --git a/crates/cqlib-core/src/qis/state/mod.rs b/crates/cqlib-core/src/qis/state/mod.rs index 73eebbd3..f5216562 100644 --- a/crates/cqlib-core/src/qis/state/mod.rs +++ b/crates/cqlib-core/src/qis/state/mod.rs @@ -28,6 +28,20 @@ //! state-vector methods using the Aaronson-Gottesman symplectic tableau algorithm. //! Supports thousands of qubits; rejects non-Clifford gates with a clear error. //! +//! Circuit simulation assigns storage position `i` to `circuit.qubits()[i]`. +//! States retain this mapping for `sample` and `probs`; states constructed from +//! dimensions or arrays use IDs `0..num_qubits` until the first successful +//! `apply_circuit`. Once bound, a state rejects circuits with a different qubit +//! ID-to-position mapping before executing gates. Failed applications preserve +//! the existing mapping; gates completed before an error are not rolled back. +//! Low-level gate indices and full probability arrays refer to storage positions. +//! +//! Use the compilation result's `final_layout` to translate logical output IDs +//! to final physical IDs, then select those IDs in a measurement of the physical +//! circuit. The state maps each physical ID to its position in that circuit's +//! `qubits()` order. Measurement input order defines result bits from least to +//! most significant, with bit 0 displayed at the right of a bitstring. +//! //! # Choosing a Simulator //! //! | Simulator | Use Case | Max Qubits | Noise Support | @@ -77,12 +91,78 @@ //! ``` mod aligned_buffer; +mod circuit_validation; pub mod classical; pub mod density_matrix; pub mod density_matrix_noise; pub mod stabilizer; pub mod statevector; +use crate::circuit::{Measurement, Qubit}; +use crate::qis::QisError; +pub(crate) use circuit_validation::validate_terminal_measurements; +use std::collections::HashMap; +use std::sync::Arc; + +// None denotes the default identity order. Sharing the map keeps state copies +// used for sampling from duplicating circuit metadata. +type QubitMap = Option>>; + +/// Prepares a circuit mapping without changing an already bound state's labels. +fn prepare_qubit_map( + qubits: &[Qubit], + existing: &QubitMap, +) -> Result>, QisError> { + if let Some(existing) = existing { + if existing.len() != qubits.len() + || qubits + .iter() + .enumerate() + .any(|(position, qubit)| existing.get(qubit) != Some(&position)) + { + return Err(QisError::InvalidParameterValue( + "circuit qubit IDs and order must match the state's existing mapping".to_string(), + )); + } + return Ok(Arc::clone(existing)); + } + Ok(Arc::new( + qubits + .iter() + .enumerate() + .map(|(position, &qubit)| (qubit, position)) + .collect(), + )) +} + +/// Resolves a measurement's IDs to storage positions for the existing projection helpers. +fn resolve_measurement( + measurement: &Measurement, + qubit_map: &QubitMap, + num_qubits: usize, +) -> Result { + let resolved = if let Some(qubit_map) = qubit_map { + let qubits = measurement + .qubits() + .iter() + .map(|qubit| { + qubit_map + .get(qubit) + .map(|&position| Qubit::new(position as u32)) + .ok_or(QisError::IndexOutOfBounds { + index: qubit.index(), + max: num_qubits.saturating_sub(1), + }) + }) + .collect::>()?; + Measurement::new(measurement.value(), qubits) + } else { + measurement.clone() + }; + resolved.check_qubits(num_qubits)?; + Ok(resolved) +} + pub use classical::{ClassicalState, RuntimeValue}; pub use density_matrix::DensityMatrix; pub use density_matrix_noise::DensityMatrixNoise; diff --git a/crates/cqlib-core/src/qis/state/stabilizer.rs b/crates/cqlib-core/src/qis/state/stabilizer.rs index 562116d9..c62ec501 100644 --- a/crates/cqlib-core/src/qis/state/stabilizer.rs +++ b/crates/cqlib-core/src/qis/state/stabilizer.rs @@ -1,4 +1,5 @@ // This code is part of Cqlib. +// Modified to support reproducible state sampling. // // (C) Copyright China Telecom Quantum Group 2026 // @@ -9,6 +10,8 @@ // Any modifications or derivative works of this code must retain this // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. +// +// Modified to validate terminal measurements and correct density matrix reset. //! Stabilizer state simulator using the Aaronson-Gottesman tableau algorithm. //! @@ -205,6 +208,8 @@ enum CircuitClassicalMode { /// of an X-block and Z-block of packed `u64` words, plus a phase per row. #[derive(Debug)] pub struct StabilizerState { + /// Circuit qubit identifiers mapped to storage positions. + qubit_map: super::QubitMap, /// Number of qubits. pub num_qubits: usize, /// Number of `u64` words per block (X-block or Z-block) per row. @@ -375,6 +380,7 @@ impl StabilizerState { StabilizerState { num_qubits: n, + qubit_map: None, row_len, tableau, phases, @@ -392,6 +398,7 @@ impl StabilizerState { /// pre-allocated working buffer per Rayon worker thread. fn reset_from(&mut self, source: &StabilizerState) { debug_assert_eq!(self.num_qubits, source.num_qubits); + self.qubit_map.clone_from(&source.qubit_map); debug_assert_eq!(self.row_len, source.row_len); self.tableau .as_mut_slice() @@ -1319,10 +1326,18 @@ impl StabilizerState { /// for shot in &results { assert_eq!(shot.is_one(0), shot.is_one(1)); } /// ``` pub fn sample_shots(&self, shots: usize) -> Vec { + self.sample_shots_with_seed(shots, None) + } + + /// Samples using per-shot random streams independent of Rayon scheduling. + /// With no seed, preserves the state's existing random stream. + pub fn sample_shots_with_seed(&self, shots: usize, seed: Option) -> Vec { // Derive independent seeds sequentially so the overall sequence is // deterministic and reproducible from the same initial RNG state. let seeds: Vec = { - let mut rng = self.rng.clone(); + let mut rng = seed + .map(SmallRng::seed_from_u64) + .unwrap_or_else(|| self.rng.clone()); (0..shots).map(|_| rng.random()).collect() }; seeds @@ -1335,6 +1350,11 @@ impl StabilizerState { .collect() } + /// Seeds the internal RNG for direct circuit sampling. + pub(crate) fn seed_rng(&mut self, seed: u64) { + self.rng = SmallRng::seed_from_u64(seed); + } + /// Samples the state using a circuit [`Measurement`] as the output contract. /// /// Unlike [`apply_circuit`](Self::apply_circuit), this method does not read @@ -1370,11 +1390,21 @@ impl StabilizerState { measurement: &Measurement, shots: usize, ) -> Result { - measurement.check_qubits(self.num_qubits)?; + self.sample_with_seed(measurement, shots, None) + } + + /// Projects seeded full-state samples onto the requested measurement. + pub fn sample_with_seed( + &self, + measurement: &Measurement, + shots: usize, + seed: Option, + ) -> Result { + let projection = super::resolve_measurement(measurement, &self.qubit_map, self.num_qubits)?; let mut counts = HashMap::new(); - for full in self.sample_shots(shots) { - *counts.entry(measurement.project(&full)).or_insert(0usize) += 1; + for full in self.sample_shots_with_seed(shots, seed) { + *counts.entry(projection.project(&full)).or_insert(0usize) += 1; } Ok(ExecutionResult::from_counts( @@ -1396,7 +1426,7 @@ impl StabilizerState { /// v1 intentionally reuses [`probabilities`](Self::probabilities), so it has /// the same `n <= 20` limit. Use [`sample`](Self::sample) for larger states. pub fn probs(&self, measurement: &Measurement) -> Result, QisError> { - measurement.check_qubits(self.num_qubits)?; + let projection = super::resolve_measurement(measurement, &self.qubit_map, self.num_qubits)?; let mut marginal = HashMap::new(); for (basis, prob) in self.probabilities()?.into_iter().enumerate() { @@ -1404,7 +1434,7 @@ impl StabilizerState { continue; } *marginal - .entry(measurement.project_basis(basis)) + .entry(projection.project_basis(basis)) .or_insert(0.0) += prob; } Ok(marginal) @@ -1413,13 +1443,13 @@ impl StabilizerState { /// Constructs a `StabilizerState` by simulating a Clifford circuit. /// /// This state-level entry point only performs quantum state evolution. - /// Measurement and store operations produced by `Circuit::measure*` are + /// Terminal measurement and store operations produced by `Circuit::measure*` are /// treated as output declarations and ignored here: they do not collapse /// the state and do not populate runtime classical data. /// /// Use [`sample`](Self::sample) or [`probs`](Self::probs) with the returned /// [`Measurement`] to query measurement distributions from the final state. - /// Use [`apply_circuit`](Self::apply_circuit) when you need execution + /// Use [`run_circuit`](Self::run_circuit) when you need execution /// semantics where measurements collapse the state and write classical data. /// /// # Supported instructions @@ -1427,7 +1457,7 @@ impl StabilizerState { /// - Classical data: `MeasureBit`, `MeasureBits`, `Store` are ignored /// - Directives: `Reset` (returns qubit to |0⟩), `Barrier`/`Delay` (no-op) /// - /// [`apply_circuit`]: StabilizerState::apply_circuit + /// [`run_circuit`]: StabilizerState::run_circuit /// /// # Example /// ```rust @@ -1455,6 +1485,14 @@ impl StabilizerState { /// Applies a Clifford circuit to this stabilizer state in-place. /// + /// Once bound to circuit qubit IDs, subsequent circuits must have the same + /// IDs in the same order. The mapping is committed only on success; gates + /// completed before an error are not rolled back. + /// + /// As in `from_circuit`, terminal measurements are ignored. A measured qubit + /// used by a later gate or Reset is rejected before the state is changed. + /// Independent gates, repeated measurements, Barrier, Delay, and Store are allowed. + /// /// This state-level entry point ignores terminal measurement declarations, /// matching [`from_circuit`](Self::from_circuit). Use /// [`run_circuit`](Self::run_circuit) when runtime classical measurement @@ -1474,12 +1512,8 @@ impl StabilizerState { } let circuit = input_circuit.decompose()?; - let qubits = circuit.qubits(); - let qubit_map: std::collections::HashMap<_, _> = qubits - .iter() - .enumerate() - .map(|(idx, q)| (*q, idx)) - .collect(); + super::validate_terminal_measurements(&circuit)?; + let qubit_map = super::prepare_qubit_map(&circuit.qubits(), &self.qubit_map)?; let parameter_values: Vec> = circuit .parameters() @@ -1495,7 +1529,9 @@ impl StabilizerState { ¶meter_values, &mut classical, CircuitClassicalMode::Ignore, - ) + )?; + self.qubit_map = Some(qubit_map); + Ok(()) } /// Executes a Clifford circuit and returns both the final state and runtime @@ -1560,6 +1596,8 @@ impl StabilizerState { .enumerate() .map(|(idx, q)| (*q, idx)) .collect(); + let qubit_map = std::sync::Arc::new(qubit_map); + state.qubit_map = Some(qubit_map.clone()); let parameter_values: Vec> = circuit .parameters() @@ -1978,6 +2016,7 @@ impl Clone for StabilizerState { fn clone(&self) -> Self { StabilizerState { num_qubits: self.num_qubits, + qubit_map: self.qubit_map.clone(), row_len: self.row_len, tableau: self.tableau.clone(), phases: self.phases.clone(), diff --git a/crates/cqlib-core/src/qis/state/state_test.rs b/crates/cqlib-core/src/qis/state/state_test.rs index 08b6a350..86e40296 100644 --- a/crates/cqlib-core/src/qis/state/state_test.rs +++ b/crates/cqlib-core/src/qis/state/state_test.rs @@ -1,4 +1,5 @@ // This code is part of Cqlib. +// Modified to support reproducible state sampling. // // (C) Copyright China Telecom Quantum Group 2026 // @@ -9,15 +10,130 @@ // Any modifications or derivative works of this code must retain this // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. +// +// Modified to validate terminal measurements and clarify or correct reset behavior. //! Simulation consistency tests between Statevector and DensityMatrix use crate::circuit::{Circuit, Qubit}; use crate::qis::pauli::{Pauli, PauliString}; -use crate::qis::{DensityMatrix, StabilizerState, Statevector}; +use crate::qis::{DensityMatrix, DensityMatrixNoise, QisError, StabilizerState, Statevector}; use std::f64::consts::PI; const EPSILON: f64 = 1e-10; +#[test] +fn circuit_qubit_positions_are_used_for_measurement_queries() { + use crate::device::Outcome; + use std::collections::HashMap; + + for ids in [[0, 1], [1, 0], [5, 9], [9, 5]] { + let qubits = ids.map(Qubit::new); + let mut circuit = Circuit::from_qubits(qubits.to_vec()).unwrap(); + circuit.x(qubits[0]).unwrap(); + let measurements = [ + (circuit.measure_bits(qubits).unwrap(), "01"), + (circuit.measure_bits([qubits[1], qubits[0]]).unwrap(), "10"), + (circuit.measure(qubits[0]).unwrap(), "1"), + (circuit.measure(qubits[1]).unwrap(), "0"), + ]; + let sv = Statevector::from_circuit(&circuit).unwrap(); + let dm = DensityMatrix::from_circuit(&circuit).unwrap(); + let stab = StabilizerState::from_circuit(&circuit).unwrap(); + let noisy = DensityMatrixNoise::from_circuit(&circuit, None).unwrap(); + + for (measurement, bits) in measurements { + let outcome = Outcome::from_bitstring(bits).unwrap(); + for (sample, probs) in [ + (sv.sample(&measurement, 4), sv.probs(&measurement)), + (dm.sample(&measurement, 4), dm.probs(&measurement)), + (stab.sample(&measurement, 4), stab.probs(&measurement)), + (noisy.sample(&measurement, 4), noisy.probs(&measurement)), + ] { + let result = sample.unwrap(); + assert_eq!(result.qubits(), measurement.qubits()); + assert_eq!(result.counts(), &HashMap::from([(outcome.clone(), 4)])); + assert_eq!(probs.unwrap(), HashMap::from([(outcome.clone(), 1.0)])); + } + } + } +} + +#[test] +fn mid_circuit_measurement_is_rejected_before_mutating_any_state() { + let mut circuit = Circuit::new(1); + circuit.h(Qubit::new(0)).unwrap(); + circuit.measure(Qubit::new(0)).unwrap(); + circuit.h(Qubit::new(0)).unwrap(); + + let constructors = [ + Statevector::from_circuit(&circuit).map(|_| ()), + DensityMatrix::from_circuit(&circuit).map(|_| ()), + DensityMatrixNoise::from_circuit(&circuit, None).map(|_| ()), + StabilizerState::from_circuit(&circuit).map(|_| ()), + ]; + for result in constructors { + let Err(QisError::UnsupportedOperation(message)) = result else { + panic!("expected unsupported mid-circuit measurement"); + }; + assert!(message.contains("mid-circuit measurement on qubit 0 at operation 1")); + assert!(message.contains("later 'H' at operation 2")); + } + + let mut sv = Statevector::new(1); + let mut dm = DensityMatrix::new(1); + let mut noisy = DensityMatrixNoise::new(1, None); + let mut stab = StabilizerState::new(1); + let sv_before = sv.data().to_vec(); + let dm_before = dm.data().to_vec(); + let noisy_before = noisy.state.data().to_vec(); + let stab_before = stab.to_stim_format(); + for result in [ + sv.apply_circuit(&circuit), + dm.apply_circuit(&circuit), + noisy.apply_circuit(&circuit), + stab.apply_circuit(&circuit), + ] { + assert!(matches!(result, Err(QisError::UnsupportedOperation(_)))); + } + assert_eq!(sv.data(), sv_before); + assert_eq!(dm.data(), dm_before); + assert_eq!(noisy.state.data(), noisy_before); + assert_eq!(stab.to_stim_format(), stab_before); +} + +#[test] +fn terminal_measurements_allow_independent_gates_and_ignored_operations() { + use crate::circuit::ClassicalType; + + let mut circuit = Circuit::new(2); + circuit.h(Qubit::new(0)).unwrap(); + let bit = circuit.var(ClassicalType::Bit); + circuit.measure_into(Qubit::new(0), bit).unwrap(); + circuit.barrier(vec![Qubit::new(0), Qubit::new(1)]).unwrap(); + circuit.delay(Qubit::new(0), 1.0.into()).unwrap(); + circuit.h(Qubit::new(1)).unwrap(); + circuit + .measure_bits([Qubit::new(1), Qubit::new(0)]) + .unwrap(); + + let probabilities = [ + Statevector::from_circuit(&circuit).unwrap().probabilities(), + DensityMatrix::from_circuit(&circuit) + .unwrap() + .probabilities(), + DensityMatrixNoise::from_circuit(&circuit, None) + .unwrap() + .probabilities(), + StabilizerState::from_circuit(&circuit) + .unwrap() + .probabilities() + .unwrap(), + ]; + for probs in probabilities { + compare_probs(&probs, &[0.25; 4], "terminal measurements"); + } +} + /// Compare probabilities from statevector and density matrix fn compare_probs(sv_probs: &[f64], dm_probs: &[f64], desc: &str) { assert_eq!( @@ -725,3 +841,37 @@ fn test_clifford_circuits_match_stabilizer_state() { } } } + +#[test] +fn full_measurement_uses_the_supplied_random_stream() { + // Extreme deterministic draws force opposite outcomes of an unbiased qubit. + // This detects accidentally falling back to thread-local randomness. + struct ConstantRng(u64); + impl rand::RngCore for ConstantRng { + fn next_u32(&mut self) -> u32 { + self.0 as u32 + } + fn next_u64(&mut self) -> u64 { + self.0 + } + fn fill_bytes(&mut self, dest: &mut [u8]) { + for chunk in dest.chunks_mut(8) { + chunk.copy_from_slice(&self.0.to_le_bytes()[..chunk.len()]); + } + } + } + for (draw, expected) in [(0, true), (u64::MAX, false)] { + let mut sv = Statevector::new(1); + sv.apply_h(0).unwrap(); + assert_eq!( + sv.measure_all_with_rng(&mut ConstantRng(draw)).is_one(0), + expected + ); + let mut dm = DensityMatrix::new(1); + dm.apply_h(0).unwrap(); + assert_eq!( + dm.measure_all_with_rng(&mut ConstantRng(draw)).is_one(0), + expected + ); + } +} diff --git a/crates/cqlib-core/src/qis/state/statevector.rs b/crates/cqlib-core/src/qis/state/statevector.rs index bdf319a5..307972d7 100644 --- a/crates/cqlib-core/src/qis/state/statevector.rs +++ b/crates/cqlib-core/src/qis/state/statevector.rs @@ -1,4 +1,5 @@ // This code is part of Cqlib. +// Modified to support reproducible state sampling. // // (C) Copyright China Telecom Quantum Group 2026 // @@ -9,6 +10,8 @@ // Any modifications or derivative works of this code must retain this // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. +// +// Modified to validate terminal measurements and correct density matrix reset. //! Statevector quantum simulation. //! @@ -53,7 +56,7 @@ use crate::qis::error::QisError; use crate::qis::observable::Observable; use crate::qis::pauli::{PauliString, Phase}; use num_complex::Complex64; -use rand::Rng; +use rand::{Rng, SeedableRng, rngs::SmallRng}; use rayon::prelude::*; use smallvec::SmallVec; use std::collections::HashMap; @@ -296,6 +299,8 @@ pub struct Statevector { /// Complex amplitudes for each basis state. Length is 2^N where N = num_qubits. /// 64-byte aligned for SIMD compatibility (see [`AlignedBuffer`]). data: AlignedBuffer, + /// Circuit qubit identifiers mapped to storage positions. + qubit_map: super::QubitMap, /// Number of qubits in the system. pub num_qubits: usize, } @@ -351,7 +356,11 @@ impl Statevector { // 64-byte aligned allocation — zero-initialised (all amplitudes = 0+0i). let mut data = AlignedBuffer::::new_zeroed(size); data[0] = Complex64::new(1.0, 0.0); // |0...0⟩ - Statevector { data, num_qubits } + Statevector { + data, + num_qubits, + qubit_map: None, + } } /// Creates a statevector from initial amplitudes with normalization check. @@ -395,7 +404,11 @@ impl Statevector { // Copy caller-provided Vec into 64-byte aligned buffer. let mut data = AlignedBuffer::::new_zeroed(size); data.as_mut_slice().copy_from_slice(&initial_state); - Ok(Statevector { data, num_qubits }) + Ok(Statevector { + data, + num_qubits, + qubit_map: None, + }) } /// Constructs a statevector by simulating a quantum circuit. @@ -438,6 +451,14 @@ impl Statevector { /// Applies a quantum circuit to this statevector in-place. /// + /// Once bound to circuit qubit IDs, subsequent circuits must have the same + /// IDs in the same order. The mapping is committed only on success; gates + /// completed before an error are not rolled back. + /// + /// As in `from_circuit`, terminal measurements are ignored. A measured qubit + /// used by a later gate or Reset is rejected before the state is changed. + /// Independent gates, repeated measurements, Barrier, Delay, and Store are allowed. + /// /// The circuit is first decomposed into basic gates via [`Circuit::decompose`], /// then each operation is applied sequentially to the current state. /// @@ -478,15 +499,10 @@ impl Statevector { } // Decompose circuit to basic gates let circuit = circuit.decompose()?; + super::validate_terminal_measurements(&circuit)?; let sv = self; - // Build qubit index mapping: Qubit -> physical index - let qubits = circuit.qubits(); - let qubit_map: std::collections::HashMap<_, _> = qubits - .iter() - .enumerate() - .map(|(idx, q)| (*q, idx)) - .collect(); + let qubit_map = super::prepare_qubit_map(&circuit.qubits(), &sv.qubit_map)?; // Precompute all parameter values let parameter_values: Vec> = circuit @@ -510,7 +526,7 @@ impl Statevector { }) .collect::, QisError>>()?; - // Get physical qubit indices + // Get storage positions let qubit_indices: Result, QisError> = op.qubits .iter() @@ -608,6 +624,7 @@ impl Statevector { } } + sv.qubit_map = Some(qubit_map); Ok(()) } @@ -2769,6 +2786,7 @@ impl Statevector { /// [`sample_shots`](Self::sample_shots) to reuse each thread's working copy. fn reset_from(&mut self, source: &Statevector) { debug_assert_eq!(self.num_qubits, source.num_qubits); + self.qubit_map.clone_from(&source.qubit_map); self.data .as_mut_slice() .copy_from_slice(source.data.as_slice()); @@ -2780,10 +2798,15 @@ impl Statevector { /// The statevector is fully collapsed after this call. /// Use [`Outcome::is_one(q)`](crate::device::Outcome::is_one) to read qubit `q`'s result. pub fn measure_all(&mut self) -> Outcome { + self.measure_all_with_rng(&mut rand::rng()) + } + + /// Measures all qubits in storage order using the supplied random stream. + pub fn measure_all_with_rng(&mut self, rng: &mut impl Rng) -> Outcome { let num_chunks = self.num_qubits.div_ceil(64); let mut chunks = SmallVec::from_elem(0u64, num_chunks); for q in 0..self.num_qubits { - if self.measure(q).unwrap() { + if self.measure_with_rng(q, rng).unwrap() { chunks[q / 64] |= 1u64 << (q % 64); } } @@ -2809,6 +2832,23 @@ impl Statevector { /// assert!(shots.iter().all(|v| v.is_one(0) == v.is_one(1))); /// ``` pub fn sample_shots(&self, shots: usize) -> Vec { + self.sample_shots_with_seed(shots, None) + } + + /// Samples without changing this state. An explicit seed assigns a random + /// stream to each shot before parallel execution, independent of scheduling. + pub fn sample_shots_with_seed(&self, shots: usize, seed: Option) -> Vec { + if let Some(seed) = seed { + let mut rng = SmallRng::seed_from_u64(seed); + let seeds: Vec = (0..shots).map(|_| rng.random()).collect(); + return seeds + .into_par_iter() + .map_with(self.clone(), |work, seed| { + work.reset_from(self); + work.measure_all_with_rng(&mut SmallRng::seed_from_u64(seed)) + }) + .collect(); + } (0..shots) .into_par_iter() .map_with(self.clone(), |work, _| { @@ -2828,11 +2868,21 @@ impl Statevector { measurement: &Measurement, shots: usize, ) -> Result { - measurement.check_qubits(self.num_qubits)?; + self.sample_with_seed(measurement, shots, None) + } + + /// Projects seeded full-state samples onto the requested measurement. + pub fn sample_with_seed( + &self, + measurement: &Measurement, + shots: usize, + seed: Option, + ) -> Result { + let projection = super::resolve_measurement(measurement, &self.qubit_map, self.num_qubits)?; let mut counts = HashMap::new(); - for full in self.sample_shots(shots) { - *counts.entry(measurement.project(&full)).or_insert(0usize) += 1; + for full in self.sample_shots_with_seed(shots, seed) { + *counts.entry(projection.project(&full)).or_insert(0usize) += 1; } Ok(ExecutionResult::from_counts( @@ -2851,7 +2901,7 @@ impl Statevector { /// a self-contained output contract and aggregates the full statevector /// computational-basis probabilities over unmeasured qubits. pub fn probs(&self, measurement: &Measurement) -> Result, QisError> { - measurement.check_qubits(self.num_qubits)?; + let projection = super::resolve_measurement(measurement, &self.qubit_map, self.num_qubits)?; let mut marginal = HashMap::new(); for (basis, prob) in self.probabilities().into_iter().enumerate() { @@ -2859,7 +2909,7 @@ impl Statevector { continue; } *marginal - .entry(measurement.project_basis(basis)) + .entry(projection.project_basis(basis)) .or_insert(0.0) += prob; } Ok(marginal) diff --git a/crates/cqlib-core/src/visualization/test_utils.rs b/crates/cqlib-core/src/visualization/test_utils.rs index 20bbf9be..af76b013 100644 --- a/crates/cqlib-core/src/visualization/test_utils.rs +++ b/crates/cqlib-core/src/visualization/test_utils.rs @@ -11,9 +11,79 @@ // that they have been altered from the originals. use super::VisualizationError; +use sha2::{Digest, Sha256}; use std::env; use std::fs; use std::path::{Path, PathBuf}; +use std::process::Command; +use std::sync::OnceLock; + +const FONT_SHA256: &str = "f8ace1f892b2bd9dc1792ba7f097fa7588f84fed48321480e04de5390828221f"; +const FONT_URL: &str = + "https://cdn.jsdelivr.net/npm/pdfjs-dist@5.4.624/standard_fonts/LiberationSans-Regular.ttf"; + +fn visual_font() -> &'static [u8] { + // Share successful downloads and failures across parallel tests alike. + static FONT: OnceLock, String>> = OnceLock::new(); + FONT.get_or_init(prepare_visual_font) + .as_ref() + .unwrap_or_else(|error| panic!("visual-test font: {error}")) +} + +fn prepare_visual_font() -> Result, String> { + let target = env::var_os("CARGO_TARGET_DIR").map_or_else( + || PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target"), + PathBuf::from, + ); + let override_path = env::var_os("CQLIB_VISUAL_FONT"); + let path = override_path.as_ref().map_or_else( + || target.join("visual-fonts/LiberationSans-Regular.ttf"), + PathBuf::from, + ); + let valid = |data: &[u8]| format!("{:x}", Sha256::digest(data)) == FONT_SHA256; + if let Ok(data) = fs::read(&path) + && valid(&data) + { + return Ok(data); + } + if override_path.is_some() { + return Err(format!( + "{} is missing or has the wrong SHA-256", + path.display() + )); + } + let output = Command::new("curl") + .args([ + "--fail", + "--location", + "--silent", + "--show-error", + "--proto", + "=https", + "--proto-redir", + "=https", + "--max-time", + "45", + "--retry", + "1", + FONT_URL, + ]) + .output() + .map_err(|error| { + format!("cannot run curl: {error}; set CQLIB_VISUAL_FONT for offline tests") + })?; + if !output.status.success() || !valid(&output.stdout) { + return Err(format!( + "download failed or SHA-256 mismatch ({FONT_URL}): {}; \ + set CQLIB_VISUAL_FONT to a verified font for offline tests", + String::from_utf8_lossy(&output.stderr) + )); + } + fs::create_dir_all(path.parent().expect("font cache directory")) + .map_err(|error| error.to_string())?; + fs::write(&path, &output.stdout).map_err(|error| error.to_string())?; + Ok(output.stdout) +} #[derive(Debug, Clone)] struct RgbImage { @@ -26,6 +96,7 @@ struct RgbImage { struct VisualCasePaths { actual_svg: PathBuf, actual_png: PathBuf, + exported_png: PathBuf, reference_png: PathBuf, diff_png: PathBuf, } @@ -58,6 +129,7 @@ fn visual_case_paths(output_dir: &[&str], filename: &str) -> VisualCasePaths { VisualCasePaths { actual_svg: visual_root.join(filename.replace(".png", ".svg")), actual_png: visual_root.join(filename), + exported_png: visual_root.join(filename.replace(".png", "_export.png")), reference_png: references_dir.join(filename), diff_png: diffs_dir.join(format!("diff_{filename}")), } @@ -96,6 +168,34 @@ fn load_png_rgb(path: &Path) -> RgbImage { } } +/// Render the production SVG at its intrinsic size with only our pinned font. +/// This module is test-only; production PNG export still uses system fonts. +fn rasterize_visual_reference(svg_path: &Path, png_path: &Path) { + let mut options = resvg::usvg::Options { + font_family: "Liberation Sans".to_string(), + ..Default::default() + }; + let fonts = options.fontdb_mut(); + fonts.load_font_data(visual_font().to_vec()); + fonts.set_sans_serif_family("Liberation Sans"); + fonts.set_serif_family("Liberation Sans"); + fonts.set_monospace_family("Liberation Sans"); + let svg = fs::read(svg_path).expect("failed to read generated SVG"); + let tree = resvg::usvg::Tree::from_data(&svg, &options) + .expect("failed to parse generated SVG with pinned font"); + let size = tree.size().to_int_size(); + let mut pixmap = resvg::tiny_skia::Pixmap::new(size.width(), size.height()) + .expect("failed to allocate visual-test pixmap"); + resvg::render( + &tree, + resvg::tiny_skia::Transform::identity(), + &mut pixmap.as_mut(), + ); + pixmap + .save_png(png_path) + .expect("failed to save visual-test PNG"); +} + fn pad_rgb_to_canvas(img: &RgbImage, width: u32, height: u32) -> Vec { let mut out = vec![255u8; (width as usize) * (height as usize) * 3]; for y in 0..img.height { @@ -158,17 +258,6 @@ fn save_diff_png( fn save_diff_and_similarity(actual_png: &Path, reference_png: &Path, diff_png: &Path) -> f64 { let actual = load_png_rgb(actual_png); - if !reference_png.exists() { - fs::copy(actual_png, reference_png).unwrap_or_else(|e| { - panic!( - "failed to bootstrap reference `{}` from `{}`: {e}", - reference_png.display(), - actual_png.display() - ) - }); - return 1.0; - } - let reference = load_png_rgb(reference_png); let width = actual.width.max(reference.width); let height = actual.height.max(reference.height); @@ -177,6 +266,14 @@ fn save_diff_and_similarity(actual_png: &Path, reference_png: &Path, diff_png: & let ratio = similarity_ratio(&actual_padded, &reference_padded); save_diff_png(&actual_padded, &reference_padded, width, height, diff_png) .expect("failed to write diff png"); + assert_eq!( + (actual.width, actual.height), + (reference.width, reference.height), + "image dimensions changed: actual `{}`, reference `{}`, diff `{}`", + actual_png.display(), + reference_png.display(), + diff_png.display() + ); ratio } @@ -187,12 +284,38 @@ where let paths = visual_case_paths(output_dir, filename); render(&paths.actual_svg).expect("failed to render svg"); - render(&paths.actual_png).expect("failed to render png"); + // Exercise the real PNG exporter as a smoke check, but compare the SVG + // through a hermetic rasterizer so OS font discovery cannot change goldens. + render(&paths.exported_png).expect("failed to render png"); + let exported = load_png_rgb(&paths.exported_png); + assert!(exported.width > 0 && exported.height > 0); + rasterize_visual_reference(&paths.actual_svg, &paths.actual_png); + if env::var("CQLIB_UPDATE_VISUAL_REFERENCES").as_deref() == Ok("1") { + assert!( + env::var_os("CI").is_none(), + "visual references must be reviewed and updated locally, not in CI" + ); + fs::copy(&paths.actual_png, &paths.reference_png) + .expect("failed to update visual reference"); + return; + } + assert!( + paths.reference_png.is_file(), + "missing visual reference `{}`; actual image: `{}`. To explicitly regenerate \ + references locally, run CQLIB_UPDATE_VISUAL_REFERENCES=1 cargo test -p cqlib-core visualization::", + paths.reference_png.display(), + paths.actual_png.display() + ); let ratio = save_diff_and_similarity(&paths.actual_png, &paths.reference_png, &paths.diff_png); let threshold = visual_threshold(); assert!( ratio >= threshold, - "Similarity ratio {ratio:.4} < {threshold:.4} for {filename}" + "Similarity ratio {ratio:.6} < {threshold:.6} for {filename}; \ + actual: {}; reference: {}; diff: {}; SVG: {}; font: prepared Liberation Sans", + paths.actual_png.display(), + paths.reference_png.display(), + paths.diff_png.display(), + paths.actual_svg.display() ); } diff --git a/docs/api/c/0_overview.md b/docs/api/c/0_overview.md deleted file mode 100644 index 0976645a..00000000 --- a/docs/api/c/0_overview.md +++ /dev/null @@ -1,7 +0,0 @@ -# C API 概览 - -## 欢迎查阅 Cqlib C API 参考手册! - ---- - -## 文档导航 diff --git a/docs/api/cn/c/0_circuit/0_overview.md b/docs/api/cn/c/0_circuit/0_overview.md new file mode 100644 index 00000000..cef149e2 --- /dev/null +++ b/docs/api/cn/c/0_circuit/0_overview.md @@ -0,0 +1,71 @@ +# 量子电路(circuit) + +C API 的量子电路模块以不透明句柄 `CCircuit*` 为中心,提供线路构造与查询、符号参数、门操作、经典数据与控制流、符号矩阵、参数化模板(Ansatz)、控制流图与线路 DAG 等能力。 + +错误码、内存所有权与数组两步式输出等全局约定见 [Overview](../0_overview.md)。 + +--- + +## 模块导航 + +| 分类 | 页面 | 主要对象 | 说明 | +| --- | --- | --- | --- | +| 线路容器 | [Circuit](1_circuit.md) | `CCircuit*` | 线路构造、属性查询、组合、反演、分解、全局相位与参数绑定。 | +| 量子比特 | [量子比特](2_qubit.md) | `CQubit` | 逻辑比特标识:创建、编号访问、受检转换、比较与字符串表示。 | +| 符号参数 | [符号参数](3_parameter.md) | `CParameter*`、`CCircuitParam`、`CParameterValue` | 表达式解析、求值与线路参数表驻留。 | +| 操作与指令 | [操作与指令](4_operation_instruction.md) | `COperation*`、`CValueOperation*` | 存储层操作、线路操作快照与延迟指令。 | +| 标准门 | [标准门](5_gate_standard.md) | `circuit_h` 等便捷函数 | 内置标准门,数值角与符号角(`*_param`)两种变体。 | +| 自定义酉门 | [自定义酉门](6_gate_unitary.md) | `circuit_unitary` 等 | 数值矩阵门与符号矩阵门。 | +| 多控制门 | [多控制门](7_gate_mc_gate.md) | `circuit_multi_control` | 在标准门前追加控制位。 | +| 复合门 | [复合门](8_gate_circuit_gate.md) | `CCircuitGate*`、`circuit_to_gate` | 将线路冻结为可复用复合门。 | +| 经典数据与控制流 | [经典数据与控制流](9_classical_control_flow.md) | `CClassicalVar*`、`CClassicalExpr*`、`circuit_if` 等 | 测量、经典表达式与结构化控制流。 | +| 符号矩阵 | [符号矩阵](10_symbolic_matrix.md) | `CSymbolicMatrix*` | 保留参数的密集符号矩阵与等价检查。 | +| Ansatz | [Ansatz](11_ansatz.md) | `CTwoLocal`、`CQAOAAnsatz` 等 | 参数化线路模板。 | +| 控制流图 | [CFG](12_cfg.md) | `CCircuitCFG*` | 线路控制流图视图、查询与重建。 | +| 矩阵转换 | [线路转矩阵](13_circuit_to_matrix.md) | `circuit_to_matrix` 等 | 纯酉线路的数值矩阵与全局相位。 | +| 线路 DAG | [线路 DAG](14_circuit_dag.md) | `CCircuitDag*` | 依赖有向无环图视图与线路重建。 | + +--- + +## 核心概念 + +| 概念 | 说明 | +| --- | --- | +| **不透明句柄** | `struct CCircuit` 等类型在头文件中仅前向声明,不导出字段;全部访问都通过函数完成,句柄用对应的 `*_free` 释放。 | +| **量子比特编号** | 逻辑比特用 `uint32_t` id 表示。`circuit_new(n)` 创建连续 id `0..n-1`;`circuit_from_qubits` 允许显式给出(含稀疏的)id 列表。 | +| **门函数前缀** | 门操作统一以 `circuit_` 开头:固定门如 `circuit_h`、`circuit_cx`;带角度的门提供数值版(`circuit_rx`)与符号版(`circuit_rx_param`,接受 `CParameter*`)。 | +| **符号参数** | `param_parse` 从字符串解析表达式;参数驻留进线路参数表后以 `CCircuitParam`(固定值或表下标)表示。 | +| **测量与控制流** | 测量产生线路所有的不可变经典值,经典表达式与 `circuit_if`/`circuit_while`/`circuit_for_uint`/`circuit_switch` 描述运行时控制流。 | + +受控门遵循“控制位在前、目标位在后”的顺序;例如 `circuit_cx(qc, control, target)` 中 `control` 为控制位。 + +--- + +## 最小示例 + +```c +#include +#include +#include + +int main(void) { + CCircuit *qc = circuit_new(2); + circuit_h(qc, 0); /* 0 成功,负值为错误码 */ + circuit_cx(qc, 0, 1); + + printf("qubits = %zu\n", circuit_num_qubits(qc)); + printf("depth = %zd\n", circuit_depth(qc, true)); + printf("operations= %zu\n", circuit_num_operations(qc)); + + /* 两步式读取比特列表 */ + uintptr_t n = circuit_qubits_len(qc); + uint32_t *ids = malloc(n * sizeof(uint32_t)); + circuit_qubits(qc, ids, n); /* ids = {0, 1} */ + + free(ids); + circuit_free(qc); + return 0; +} +``` + +线路构造完成后,通常交由 [态矢量](../3_qis/1_statevector.md) 或 [密度矩阵](../3_qis/2_density_matrix.md) 模拟执行;含测量或控制流的线路不再具有单一固定的酉矩阵表示,矩阵转换接口只适用于纯量子子线路(见 [线路转矩阵](13_circuit_to_matrix.md))。 diff --git a/docs/api/cn/c/0_circuit/10_symbolic_matrix.md b/docs/api/cn/c/0_circuit/10_symbolic_matrix.md new file mode 100644 index 00000000..31cc579b --- /dev/null +++ b/docs/api/cn/c/0_circuit/10_symbolic_matrix.md @@ -0,0 +1,527 @@ +# 符号矩阵 + +本页介绍 C API 中的符号矩阵函数:符号复数、符号矩阵的构造、形状与元素查询、化简、符号替换、参数绑定求值、就地门应用(`apply_*` 家族),以及线路级和矩阵级的全局相位等价性检查。错误码与内存管理约定见 [Overview](../0_overview.md)。 + +--- + +## 核心概念 + +符号矩阵(`CSymbolicMatrix*` 不透明句柄)的每个元素都是符号复数:实部和虚部为参数表达式,可包含未绑定的符号参数。与数值矩阵转换不同,符号矩阵转换保留线路中的参数表达式,后续可通过绑定字符串代入具体数值。 + +数值求值结果以 `Complex64` 结构输出: + +```c +typedef struct Complex64 { + double re; /* 实部 */ + double im; /* 虚部 */ +} Complex64; +``` + +所有返回 `CSymbolicMatrix*` 的函数都分配新句柄,用 `symbolic_matrix_free` 释放;返回 `CSymbolicComplex*` 的函数分配新的符号复数句柄,用 `symbolic_complex_free` 释放。 + +--- + +## 符号复数 + +单个矩阵元素是符号复数(`CSymbolicComplex*` 不透明句柄):实部和虚部各为一个参数表达式。本节函数是元素级 API,也用于给 `symbolic_matrix_new` 和各 `apply_*` 置换/对角门提供元素。 + +### symbolic_complex_new(re, im) + +由实部和虚部参数表达式构造符号复数。 + +- `re` (`const CParameter*`):实部表达式句柄。 +- `im` (`const CParameter*`):虚部表达式句柄。 + +返回新分配的 `CSymbolicComplex*`;任一输入为 NULL 时返回 NULL。 + +### symbolic_complex_zero() / symbolic_complex_one() / symbolic_complex_i() + +返回加法单位元 `0 + 0i`、乘法单位元 `1 + 0i` 和虚数单位 `0 + 1i`。 + +无参数,均返回新分配的 `CSymbolicComplex*`。 + +### symbolic_complex_from_real(re) + +由实部表达式构造虚部为零的符号复数。 + +- `re` (`const CParameter*`):实部表达式句柄。 + +返回新分配的 `CSymbolicComplex*`;输入为 NULL 时返回 NULL。 + +### symbolic_complex_from_complex(re, im) + +由一对浮点常量构造符号复数常量。 + +- `re` (`double`):实部。 +- `im` (`double`):虚部。 + +返回新分配的 `CSymbolicComplex*`;任一分量非有限(NaN/Inf)时返回 NULL。 + +### symbolic_complex_exp_i(theta) + +构造 `exp(i*theta)`,即 `cos(theta) + i*sin(theta)`。 + +- `theta` (`const CParameter*`):相位表达式句柄。 + +返回新分配的 `CSymbolicComplex*`;输入为 NULL 时返回 NULL。 + +### symbolic_complex_re(ptr) / symbolic_complex_im(ptr) + +返回实部/虚部表达式的克隆句柄(用 `param_free` 释放)。 + +- `ptr` (`const CSymbolicComplex*`):符号复数句柄。 + +返回新分配的 `CParameter*`;句柄为 NULL 时返回 NULL。 + +### symbolic_complex_evaluate(ptr, bindings, out) + +在参数绑定下把符号复数求值为数值。 + +- `ptr` (`const CSymbolicComplex*`):符号复数句柄。 +- `bindings` (`const char*`):绑定字符串,格式为 `"name:value,..."`(传 NULL 表示无绑定)。 +- `out` (`Complex64*`):接收结果。 + +返回 0 表示成功;-1 表示输入为 NULL;-3 表示存在无法求值的符号;-4 表示绑定字符串格式非法。 + +### symbolic_complex_simplify(ptr) + +对实部和虚部分别执行符号化简,返回化简后的新符号复数。 + +- `ptr` (`const CSymbolicComplex*`):原句柄。 + +返回新分配的 `CSymbolicComplex*`;输入为 NULL 或化简失败时返回 NULL。 + +### symbolic_complex_replace(ptr, symbol, replacement) + +把实部和虚部中出现的符号 `symbol` 全部替换为 `replacement` 表达式,返回替换后的新符号复数。 + +- `ptr` (`const CSymbolicComplex*`):原句柄。 +- `symbol` (`const char*`):符号名。 +- `replacement` (`const CParameter*`):替换表达式句柄。 + +返回新分配的 `CSymbolicComplex*`;任一输入为 NULL 或符号名 UTF-8 非法时返回 NULL。 + +### symbolic_complex_is_zero_exact(ptr) / symbolic_complex_is_one_exact(ptr) + +判断实部和虚部是否**恰好**为零/一(不做化简,仅检查常数结构)。 + +- `ptr` (`const CSymbolicComplex*`):符号复数句柄。 + +返回 1/0;句柄为 NULL 时返回 -1。 + +### symbolic_complex_simplifies_to_zero(ptr) + +判断符号复数化简后是否恰好为零。 + +- `ptr` (`const CSymbolicComplex*`):符号复数句柄。 + +返回 1 表示化简后为零,0 表示不为零;-1 表示句柄为 NULL;-3 表示化简失败。 + +### symbolic_complex_free(ptr) + +释放符号复数句柄。 + +- `ptr` (`CSymbolicComplex*`):待释放句柄,允许传 NULL。 + +无返回值。 + +--- + +## 矩阵构造与释放 + +### symbolic_eye(dim) + +创建 `dim x dim` 的符号单位矩阵。 + +- `dim` (`uintptr_t`):矩阵维度。 + +返回新分配的 `CSymbolicMatrix*`;仅在内存分配失败时返回 NULL。 + +### symbolic_matrix_new(rows, cols, elements, len) + +由元素句柄数组构造 `rows x cols` 符号矩阵,元素按行主序排列。 + +- `rows` (`uintptr_t`):行数,必须大于 0。 +- `cols` (`uintptr_t`):列数,必须大于 0。 +- `elements` (`const CSymbolicComplex* const*`):`rows * cols` 个符号复数句柄。 +- `len` (`uintptr_t`):数组长度,必须等于 `rows * cols`。 + +返回新分配的 `CSymbolicMatrix*`;`elements` 为 NULL、长度不匹配、维度为零或任一元素句柄为 NULL 时返回 NULL。 + +### standard_gate_symbolic_matrix(gate_name, params, len) + +按名称构造标准门的符号酉矩阵(例如 `"RX"`、`"RZZ"`;名称集合与 `circuit_multi_control` 相同)。 + +- `gate_name` (`const char*`):标准门名称。 +- `params` (`const CParameter* const*`):门参数句柄数组。 +- `len` (`uintptr_t`):参数数量。 + +返回新分配的 `CSymbolicMatrix*`;输入为 NULL、UTF-8 非法、门名未知或参数数量不匹配时返回 NULL。 + +### circuit_to_symbolic_matrix(ptr, order, order_len) + +计算线路的符号酉矩阵。 + +- `ptr` (`const CCircuit*`):目标线路句柄。 +- `order` (`const uint32_t*`):量子比特 id 数组,从最低有效位到最高有效位排列;传 NULL 时按量子比特编号排序。 +- `order_len` (`uintptr_t`):数组长度,0 表示使用默认排序。 + +返回新分配的 `CSymbolicMatrix*`;输入为 NULL、顺序与量子比特集合不匹配或线路含非酉操作时返回 NULL。 + +### symbolic_matrix_controlled(base, num_ctrls) + +把 `base` 嵌入到更大单位矩阵的右下角分块,为其添加 `num_ctrls` 个控制量子比特。 + +- `base` (`const CSymbolicMatrix*`):基础矩阵。 +- `num_ctrls` (`uintptr_t`):控制量子比特数量。 + +返回新分配的 `CSymbolicMatrix*`;`base` 为 NULL 时返回 NULL。 + +### symbolic_matrix_free(ptr) + +释放符号矩阵句柄。 + +- `ptr` (`CSymbolicMatrix*`):待释放句柄,允许传 NULL。 + +无返回值。 + +--- + +## 形状与元素查询 + +### symbolic_matrix_rows(ptr) + +返回矩阵行数。 + +- `ptr` (`const CSymbolicMatrix*`):符号矩阵句柄。 + +返回行数;句柄为 NULL 时返回 0。 + +### symbolic_matrix_cols(ptr) + +返回矩阵列数。 + +- `ptr` (`const CSymbolicMatrix*`):符号矩阵句柄。 + +返回列数;句柄为 NULL 时返回 0。 + +### symbolic_matrix_element_str(ptr, row, col) + +把单个矩阵元素格式化为可读的参数表达式字符串(例如 `"cos(theta*0.5)"`)。 + +- `ptr` (`const CSymbolicMatrix*`):符号矩阵句柄。 +- `row` (`uintptr_t`):行下标。 +- `col` (`uintptr_t`):列下标。 + +返回新分配的 C 字符串(用 `cqlib_string_free` 释放);输入为 NULL 或下标越界时返回 NULL。 + +### symbolic_matrix_element(ptr, row, col) + +返回 `[row, col]` 元素的克隆句柄(用 `symbolic_complex_free` 释放),可用于逐元素读取符号矩阵。 + +- `ptr` (`const CSymbolicMatrix*`):符号矩阵句柄。 +- `row` (`uintptr_t`):行下标。 +- `col` (`uintptr_t`):列下标。 + +返回新分配的 `CSymbolicComplex*`;句柄为 NULL 或下标越界时返回 NULL。 + +### symbolic_matrix_evaluate_len(ptr) + +返回展平后的元素数量(`rows * cols`),即数值求值所需的缓冲区长度。 + +- `ptr` (`const CSymbolicMatrix*`):符号矩阵句柄。 + +返回元素数量;句柄为 NULL 时返回 0。 + +--- + +## 化简与替换 + +### symbolic_matrix_simplify(ptr) + +对矩阵的每个元素执行符号化简,返回化简后的新矩阵。 + +- `ptr` (`const CSymbolicMatrix*`):原矩阵句柄。 + +返回新分配的 `CSymbolicMatrix*`;输入为 NULL 或化简失败时返回 NULL。 + +### symbolic_matrix_substitute(ptr, names, params, len) + +对矩阵中的符号执行同时替换:`names[i]` 是符号名,`params[i]` 是替换表达式。 + +- `ptr` (`const CSymbolicMatrix*`):原矩阵句柄。 +- `names` (`const char* const*`):符号名数组。 +- `params` (`const CParameter* const*`):替换表达式句柄数组。 +- `len` (`uintptr_t`):替换数量。 + +返回新分配的 `CSymbolicMatrix*`;输入为 NULL、UTF-8 非法或替换出错(例如保留前缀)时返回 NULL。 + +--- + +## 数值求值 + +### symbolic_matrix_evaluate(ptr, bindings, buffer, len) + +在参数绑定下把矩阵的每个元素求值为数值复数,按行主序写入 `buffer`。 + +- `ptr` (`const CSymbolicMatrix*`):符号矩阵句柄。 +- `bindings` (`const char*`):绑定字符串,格式为 `"name:value,name:value"`(传 NULL 表示无绑定)。 +- `buffer` (`Complex64*`):接收结果的数组。 +- `len` (`uintptr_t`):数组长度,必须不小于 `symbolic_matrix_evaluate_len`。 + +返回 0 表示成功;-1 表示输入为 NULL;-3 表示存在无法求值的符号;-4 表示绑定字符串格式非法;-8 表示 `len` 过小。 + +--- + +## 就地门应用(apply_* 家族) + +`apply_*` 系列把量子门**原地**作用到符号矩阵上:等价于左乘门算子在目标比特张成的子空间上的表示,形状保持不变。目标比特约束(各函数通用): + +- 矩阵行数必须是 2 的幂,`bits` 中每个比特都必须小于 `log2(rows)`; +- `bits` 数组不能为空、不能有重复;多比特门的门矩阵维度必须等于 `1 << bits_len`。 + +### 目标比特顺序 + +`apply_*` 的 `bits` 参数使用系统的 **Little-Endian(最低有效位在前)** 约定:`bits[k]` 对应门矩阵局部行下标的第 `k` 位。对照线路 API:受控门的量子比特按 `[control, target]` 顺序传入(最高有效位在前),`circuit_to_symbolic_matrix` 内部会先反转该序列再应用。因此用 `apply_standard_gate` 复现线路效果时需要**自行传入反转后的顺序**:`circuit_cx(c, 0, 1)` 等价于以 `bits = [1, 0]` 应用 `"CX"`。 + +### symbolic_matrix_apply_standard_gate(ptr, gate_name, bits, bits_len, params, params_len) + +按名称应用标准门(名称集合与 `circuit_multi_control` 相同,如 `"H"`、`"CX"`、`"RX"`),门参数为符号表达式。 + +- `ptr` (`CSymbolicMatrix*`):目标矩阵(就地修改)。 +- `gate_name` (`const char*`):标准门名称。 +- `bits` (`const uintptr_t*`):目标比特数组。 +- `bits_len` (`uintptr_t`):目标比特数量。 +- `params` (`const CParameter* const*`):门参数句柄数组,无参门可传 NULL。 +- `params_len` (`uintptr_t`):门参数数量。 + +返回 0 表示成功;-1 表示矩阵、门名或参数句柄为 NULL;-3 表示门的比特数/参数数量与矩阵不匹配;-4 表示门名 UTF-8 非法;-8 表示门名未知或 `bits` 非法。 + +### symbolic_matrix_apply_gate(ptr, gate, bits, bits_len) + +把符号门矩阵(维度须为 `1 << bits_len`)作用到目标比特。 + +- `ptr` (`CSymbolicMatrix*`):目标矩阵(就地修改)。 +- `gate` (`const CSymbolicMatrix*`):门矩阵句柄。 +- `bits` (`const uintptr_t*`):目标比特数组。 +- `bits_len` (`uintptr_t`):目标比特数量。 + +返回 0 表示成功;-1 表示输入为 NULL;-3 表示门矩阵维度与目标比特不匹配;-8 表示 `bits` 非法。 + +### symbolic_matrix_apply_gate_num(ptr, gate, gate_dim, bits, bits_len) + +`symbolic_matrix_apply_gate` 的数值变体:`gate` 指向 `gate_dim * gate_dim` 个行主序 `Complex64`,`gate_dim` 必须等于 `1 << bits_len`。 + +- `ptr` (`CSymbolicMatrix*`):目标矩阵(就地修改)。 +- `gate` (`const Complex64*`):门矩阵数值数组。 +- `gate_dim` (`uintptr_t`):门矩阵维度。 +- `bits` (`const uintptr_t*`):目标比特数组。 +- `bits_len` (`uintptr_t`):目标比特数量。 + +返回 0 表示成功;-1 表示矩阵或 `gate` 为 NULL;-3 表示门矩阵与目标比特不匹配;-8 表示 `gate_dim` 为 0、不等于 `1 << bits_len`,或 `bits` 非法。 + +### symbolic_matrix_apply_single_qubit_gate(ptr, gate, bit) + +把 `2x2` 符号门作用到单个目标比特。 + +- `ptr` (`CSymbolicMatrix*`):目标矩阵(就地修改)。 +- `gate` (`const CSymbolicMatrix*`):`2x2` 门矩阵句柄。 +- `bit` (`uintptr_t`):目标比特。 + +返回 0 表示成功;-1 表示输入为 NULL;-8 表示门矩阵不是 `2x2`、矩阵行数非 2 的幂或 `bit` 越界。 + +### symbolic_matrix_apply_single_qubit_gate_num(ptr, gate, bit) + +`symbolic_matrix_apply_single_qubit_gate` 的数值变体:`gate` 指向 4 个行主序 `Complex64`。 + +- `ptr` (`CSymbolicMatrix*`):目标矩阵(就地修改)。 +- `gate` (`const Complex64*`):4 个门矩阵元素。 +- `bit` (`uintptr_t`):目标比特。 + +返回 0 表示成功;-1 表示输入为 NULL;-8 表示矩阵行数非 2 的幂或 `bit` 越界。 + +### symbolic_matrix_apply_two_qubit_gate(ptr, gate, b0, b1) + +把 `4x4` 符号门作用到两个目标比特。 + +- `ptr` (`CSymbolicMatrix*`):目标矩阵(就地修改)。 +- `gate` (`const CSymbolicMatrix*`):`4x4` 门矩阵句柄。 +- `b0`, `b1` (`uintptr_t`):两个目标比特。 + +返回 0 表示成功;-1 表示输入为 NULL;-8 表示门矩阵不是 `4x4`、矩阵行数非 2 的幂、比特重复或越界。 + +### symbolic_matrix_apply_two_qubit_gate_num(ptr, gate, b0, b1) + +`symbolic_matrix_apply_two_qubit_gate` 的数值变体:`gate` 指向 16 个行主序 `Complex64`。错误码同上。 + +### symbolic_matrix_apply_general_gate(ptr, gate, bits, bits_len) + +把任意元数的符号门(维度 `1 << bits_len` 的方阵)作用到目标比特。 + +- `ptr` (`CSymbolicMatrix*`):目标矩阵(就地修改)。 +- `gate` (`const CSymbolicMatrix*`):门矩阵句柄(维度为 2 的幂的方阵)。 +- `bits` (`const uintptr_t*`):目标比特数组。 +- `bits_len` (`uintptr_t`):目标比特数量。 + +返回 0 表示成功;-1 表示输入为 NULL;-8 表示门矩阵形状与 `1 << bits_len` 不符或 `bits` 非法。 + +### symbolic_matrix_apply_general_gate_num(ptr, gate, bits, bits_len) + +`symbolic_matrix_apply_general_gate` 的数值变体:`gate` 指向 `(1 << bits_len)` 平方个行主序 `Complex64`。 + +- `ptr` (`CSymbolicMatrix*`):目标矩阵(就地修改)。 +- `gate` (`const Complex64*`):门矩阵数值数组。 +- `bits` (`const uintptr_t*`):目标比特数组。 +- `bits_len` (`uintptr_t`):目标比特数量。 + +返回 0 表示成功;-1 表示矩阵或 `gate` 为 NULL;-8 表示 `bits_len` 溢出或 `bits` 非法。 + +### symbolic_matrix_apply_permutation_gate(ptr, indices, values, len, bits, bits_len) + +就地应用符号置换门:`permutation[i] = (indices[i], values[i])` 表示输出的第 `i` 个局部行等于 `values[i]` 乘以输入的第 `indices[i]` 个局部行。 + +- `ptr` (`CSymbolicMatrix*`):目标矩阵(就地修改)。 +- `indices` (`const uintptr_t*`):源局部行下标数组。 +- `values` (`const CSymbolicComplex* const*`):与 `indices` 配对的符号乘子句柄数组。 +- `len` (`uintptr_t`):置换长度,必须等于 `1 << bits_len`。 +- `bits` (`const uintptr_t*`):目标比特数组。 +- `bits_len` (`uintptr_t`):目标比特数量。 + +返回 0 表示成功;-1 表示矩阵为 NULL、`indices[i]` 越界或 `values[i]` 为 NULL;-8 表示 `len` 不等于 `1 << bits_len`、`indices`/`values` 为 NULL 或 `bits` 非法。 + +### symbolic_matrix_apply_permutation_gate_num(ptr, indices, values, len, bits, bits_len) + +`symbolic_matrix_apply_permutation_gate` 的数值变体:`values` 指向 `len` 个 `Complex64` 乘子。 + +- `ptr` (`CSymbolicMatrix*`):目标矩阵(就地修改)。 +- `indices` (`const uintptr_t*`):源局部行下标数组。 +- `values` (`const Complex64*`):数值乘子数组。 +- `len` (`uintptr_t`):置换长度,必须等于 `1 << bits_len`。 +- `bits` (`const uintptr_t*`):目标比特数组。 +- `bits_len` (`uintptr_t`):目标比特数量。 + +返回 0 表示成功;-1 表示矩阵或 `values` 为 NULL;-8 表示 `indices[i]` 越界、`len` 不等于 `1 << bits_len`、`indices` 为 NULL 或 `bits` 非法。 + +### symbolic_matrix_apply_diagonal_gate(ptr, diagonal, len, bits, bits_len) + +就地应用符号对角门:`diagonal[i]` 缩放第 `i` 个局部行。 + +- `ptr` (`CSymbolicMatrix*`):目标矩阵(就地修改)。 +- `diagonal` (`const CSymbolicComplex* const*`):对角元素句柄数组。 +- `len` (`uintptr_t`):对角长度,必须等于 `1 << bits_len`。 +- `bits` (`const uintptr_t*`):目标比特数组。 +- `bits_len` (`uintptr_t`):目标比特数量。 + +返回 0 表示成功;-1 表示矩阵为 NULL 或任一对角元素句柄为 NULL;-8 表示 `len` 不等于 `1 << bits_len`、`diagonal` 为 NULL 或 `bits` 非法。 + +### symbolic_matrix_apply_diagonal_gate_num(ptr, diagonal, len, bits, bits_len) + +`symbolic_matrix_apply_diagonal_gate` 的数值变体:`diagonal` 指向 `len` 个 `Complex64`。 + +- `ptr` (`CSymbolicMatrix*`):目标矩阵(就地修改)。 +- `diagonal` (`const Complex64*`):对角数值数组。 +- `len` (`uintptr_t`):对角长度,必须等于 `1 << bits_len`。 +- `bits` (`const uintptr_t*`):目标比特数组。 +- `bits_len` (`uintptr_t`):目标比特数量。 + +返回 0 表示成功;-1 表示矩阵或 `diagonal` 为 NULL;-8 表示 `len` 不等于 `1 << bits_len` 或 `bits` 非法。 + +### 示例:就地复现线路矩阵 + +```c +/* 复现 circuit_h(c, 0) + circuit_cx(c, 0, 1) 的符号矩阵。 + * CX 的 [control, target] 对在 apply_* 的 Little-Endian + * 约定下需反转为 [target, control]。 */ +struct CSymbolicMatrix *matrix = symbolic_eye(4); +const uintptr_t h_bits[1] = {0}; +const uintptr_t cx_bits[2] = {1, 0}; +symbolic_matrix_apply_standard_gate(matrix, "H", h_bits, 1, NULL, 0); +symbolic_matrix_apply_standard_gate(matrix, "CX", cx_bits, 2, NULL, 0); +/* matrix 现在等价于 circuit_to_symbolic_matrix(c, NULL, 0) */ +``` + +--- + +## 等价性检查 + +### symbolic_matrices_equivalent(lhs, rhs) + +判断两个符号矩阵是否可证明仅相差全局相位。 + +- `lhs` (`const CSymbolicMatrix*`):左侧矩阵。 +- `rhs` (`const CSymbolicMatrix*`):右侧矩阵。 + +返回 1 表示等价,0 表示不等价;-1 表示输入为 NULL;-3 表示化简失败。 + +### circuits_equivalent(lhs, rhs, order, order_len) + +在指定量子比特顺序下比较两条线路的矩阵是否仅相差全局相位。 + +- `lhs` (`const CCircuit*`):左侧线路句柄。 +- `rhs` (`const CCircuit*`):右侧线路句柄。 +- `order` (`const uint32_t*`):量子比特 id 数组,从最低有效位到最高有效位排列;传 NULL(且 `order_len` 为 0)时按量子比特编号排序。 +- `order_len` (`uintptr_t`):数组长度。 + +返回 1 表示等价,0 表示不等价;-1 表示输入为 NULL;-3 表示矩阵构造失败(含顺序与量子比特集合不匹配);-8 表示 `order` 为 NULL 而 `order_len` 大于 0。 + +--- + +## 量子比特顺序约定 + +`circuit_to_symbolic_matrix` 和 `circuits_equivalent` 的 `order` 参数确定矩阵基态下标与线路量子比特的对应关系: + +- `order` 为 NULL(`order_len` 为 0):默认按量子比特编号排序; +- `order` 显式给定:数组中第 `i` 个量子比特对应基态下标的第 `i` 位(最低有效位在前); +- 显式顺序必须与线路中的量子比特集合完全一致,不能遗漏、重复或包含未知量子比特。 + +例如线路包含量子比特 0 和 2 时,`[0, 2]` 与 `[2, 0]` 都是合法顺序,但会得到不同轴序下的矩阵表示。 + +--- + +## 示例:含参线路 → 符号矩阵 → 代入求值 + +```c +#include +#include +#include "cqlib_c.h" + +int main(void) { + /* 1. 构造含参线路:RX(theta) 作用于 qubit 0 */ + struct CCircuit *circuit = circuit_new(1); + struct CParameter *theta = param_parse("theta"); + if (circuit_rx_param(circuit, 0, theta) != 0) { + return 1; + } + + /* 2. 转换为符号矩阵(默认按量子比特编号排序) */ + struct CSymbolicMatrix *matrix = circuit_to_symbolic_matrix(circuit, NULL, 0); + if (matrix == NULL) { + return 1; + } + + /* 查看元素的符号表达形式,例如 "cos(theta*0.5)" */ + char *element = symbolic_matrix_element_str(matrix, 0, 0); + if (element != NULL) { + printf("matrix[0][0] = %s\n", element); + cqlib_string_free(element); + } + + /* 3. 绑定 theta = pi/2,代入求值为数值矩阵 */ + uintptr_t len = symbolic_matrix_evaluate_len(matrix); + Complex64 *values = malloc(len * sizeof(Complex64)); + int32_t status = symbolic_matrix_evaluate(matrix, "theta:1.5707963267948966", + values, len); + if (status == 0) { + /* values[0] 即 cos(pi/4),values 按行主序排列 */ + printf("matrix[0][0] = %f + %fi\n", values[0].re, values[0].im); + } + + free(values); + symbolic_matrix_free(matrix); + param_free(theta); + circuit_free(circuit); + return status; +} +``` + +参数表达式解析(`param_parse` / `param_free`)见 [参数](3_parameter.md),参数化旋转门(`circuit_rx_param`)见 [标准门](5_gate_standard.md),线路构造见 [Circuit](1_circuit.md)。 diff --git a/docs/api/cn/c/0_circuit/11_ansatz.md b/docs/api/cn/c/0_circuit/11_ansatz.md new file mode 100644 index 00000000..6bfa4704 --- /dev/null +++ b/docs/api/cn/c/0_circuit/11_ansatz.md @@ -0,0 +1,1129 @@ +# Ansatz + +本页介绍 C API 中的变分线路模板:TwoLocal、BasicEntanglerLayers、QAOAAnsatz、feature map 家族(ZFeatureMap、ZZFeatureMap、IQPFeatureMap、PauliFeatureMap)、StronglyEntanglingLayers 和 Pauli 演化 ansatz,以及 facade 构造函数和纠缠拓扑辅助函数。每个模板都按"创建句柄 → 配置 → 校验 → 构建线路"的流程使用,生成的线路以 `{prefix}` 前缀命名符号参数。错误码与内存管理约定见 [Overview](../0_overview.md)。 + +--- + +## 标签常量 + +纠缠拓扑标签(用于 `two_local_entanglement`、feature map、facade 构造函数和拓扑辅助函数): + +| 拓扑标签 | 值 | 含义 | +| --- | --- | --- | +| `ENTANGLEMENT_LINEAR` | 0 | 线性最近邻链。 | +| `ENTANGLEMENT_CIRCULAR` | 1 | 线性链加首尾环绕边。 | +| `ENTANGLEMENT_FULL` | 2 | 全连接(任意量子比特对)。 | +| `ENTANGLEMENT_CUSTOM` | 3 | 自定义显式连接对。 | + +演化策略标签(用于 `qaoa_ansatz_evolution_strategy`): + +| 策略标签 | 值 | 含义 | +| --- | --- | --- | +| `EVOLUTION_STRATEGY_EXACT` | 0 | 逐项精确 Pauli 演化。 | +| `EVOLUTION_STRATEGY_AUTO` | 1 | 自动选择精确或 Trotter 分解(使用 `steps`)。 | +| `EVOLUTION_STRATEGY_TROTTER` | 2 | 显式 Trotter 乘积公式(使用 `mode` / `steps` / `seed`)。 | + +Trotter 模式标签(`EVOLUTION_STRATEGY_TROTTER` 接受的 `mode` 取值,以及 `pauli_evolution_ansatz_evolution_info` 报告的 `trotter_mode` 取值): + +| 模式标签 | 值 | 含义 | +| --- | --- | --- | +| `TROTTER_FIRST_ORDER` | 0 | 一阶乘积公式。 | +| `TROTTER_SECOND_ORDER` | 1 | 二阶乘积公式。 | +| `TROTTER_RANDOMIZED` | 2 | 随机化一阶公式(`seed` 为随机种子)。 | +| `TROTTER_MODE_NONE` | -1 | 未生成 Trotter 分解(精确单次路径);只出现在 `CPauliEvolutionInfo.trotter_mode` 中,不能作为 `mode` 输入。 | + +--- + +## TwoLocal + +TwoLocal 由交替出现的单量子比特旋转层和多量子比特纠缠层组成,适用于 VQE、量子机器学习和通用变分线路构造。`two_local_new` 的默认配置为:`reps = 3`、旋转门 `[RY]`、纠缠门 `CX`、线性拓扑、保留最终旋转层。 + +### two_local_new(num_qubits) + +创建一个 TwoLocal 模板句柄。 + +- `num_qubits` (`uintptr_t`):量子比特数量。 + +返回新分配的 `CTwoLocal*`。 + +### two_local_free(ptr) + +释放 TwoLocal 句柄。 + +- `ptr` (`CTwoLocal*`):待释放句柄,允许传 NULL。 + +无返回值。 + +### two_local_reps(ptr, reps) + +设置重复层数。 + +- `ptr` (`CTwoLocal*`):模板句柄。 +- `reps` (`uintptr_t`):重复层数。 + +返回 0 表示成功;-1 表示输入为 NULL。 + +### two_local_rotation_gates(ptr, names, len) + +按名称设置每层的单量子比特参数旋转门(必须是 RX、RY 或 RZ,构建时校验)。 + +- `ptr` (`CTwoLocal*`):模板句柄。 +- `names` (`const char* const*`):门名称数组。 +- `len` (`uintptr_t`):数组长度。 + +返回 0 表示成功;-1 表示输入为 NULL;-4 表示 UTF-8 非法;-8 表示门名未知。 + +### two_local_entanglement_gate(ptr, name) + +按名称设置纠缠层使用的双量子比特门(CX、CY 或 CZ,构建时校验)。 + +- `ptr` (`CTwoLocal*`):模板句柄。 +- `name` (`const char*`):门名称。 + +返回 0 表示成功;-1 表示输入为 NULL;-4 表示 UTF-8 非法;-8 表示门名未知。 + +### two_local_entanglement(ptr, topology, pairs, pairs_len) + +设置纠缠拓扑。拓扑为 `ENTANGLEMENT_CUSTOM` 时,`pairs` 是长度为 `pairs_len / 2` 的控制/目标量子比特 id 对的扁平数组。 + +- `ptr` (`CTwoLocal*`):模板句柄。 +- `topology` (`int32_t`):`ENTANGLEMENT_*` 拓扑标签。 +- `pairs` (`const uint32_t*`):自定义连接对的扁平数组(其他拓扑传 NULL)。 +- `pairs_len` (`uintptr_t`):扁平数组长度。 + +返回 0 表示成功;-1 表示输入为 NULL(含 `pairs` 为 NULL 而 `pairs_len > 0`);-8 表示拓扑未知或 `pairs_len` 为奇数。 + +### two_local_skip_final_rotation_layer(ptr, skip) + +设置是否跳过最终旋转层。 + +- `ptr` (`CTwoLocal*`):模板句柄。 +- `skip` (`bool`):true 表示跳过。 + +返回 0 表示成功;-1 表示输入为 NULL。 + +### two_local_validate(ptr) + +校验模板配置。 + +- `ptr` (`const CTwoLocal*`):模板句柄。 + +返回 0 表示配置合法;-1 表示输入为 NULL;-3 表示配置非法。 + +### two_local_num_parameters(ptr) + +返回模板生成线路所需的独立参数数量。 + +- `ptr` (`const CTwoLocal*`):模板句柄。 + +返回参数数量;句柄为 NULL 时返回 0。 + +### two_local_num_qubits(ptr) + +返回模板作用的量子比特数量。 + +- `ptr` (`const CTwoLocal*`):模板句柄。 + +返回量子比特数量;句柄为 NULL 时返回 0。 + +### two_local_build_circuit(ptr, prefix) + +根据当前配置构建参数化线路,参数命名为 `"{prefix}_{index}"`。 + +- `ptr` (`const CTwoLocal*`):模板句柄。 +- `prefix` (`const char*`):参数名前缀。 + +返回新分配的 `CCircuit*`(用 `circuit_free` 释放);输入为 NULL、UTF-8 非法或构建失败时返回 NULL。 + +### TwoLocal 示例 + +```c +#include +#include "cqlib_c.h" + +int main(void) { + struct CTwoLocal *ansatz = two_local_new(3); + + two_local_reps(ansatz, 2); + const char *rotations[] = {"RY", "RZ"}; + two_local_rotation_gates(ansatz, rotations, 2); + two_local_entanglement_gate(ansatz, "CX"); + two_local_entanglement(ansatz, ENTANGLEMENT_LINEAR, NULL, 0); + + if (two_local_validate(ansatz) != 0) { + two_local_free(ansatz); + return 1; + } + + /* 参数名形如 "theta_0", "theta_1", ... */ + struct CCircuit *circuit = two_local_build_circuit(ansatz, "theta"); + if (circuit == NULL) { + two_local_free(ansatz); + return 1; + } + printf("parameters: %zu, qubits: %zu\n", + two_local_num_parameters(ansatz), + two_local_num_qubits(ansatz)); + + circuit_free(circuit); + two_local_free(ansatz); + return 0; +} +``` + +--- + +## BasicEntanglerLayers + +BasicEntanglerLayers 由单量子比特旋转层和固定模式的纠缠层组成,适合构造结构简单、参数数量可控的训练线路。`basic_entangler_layers_new` 的默认配置为:`reps = 1`、旋转门 `RX`、纠缠门 `CX`。 + +### basic_entangler_layers_new(num_qubits) + +创建一个 BasicEntanglerLayers 模板句柄。 + +- `num_qubits` (`uintptr_t`):量子比特数量。 + +返回新分配的 `CBasicEntanglerLayers*`。 + +### basic_entangler_layers_free(ptr) + +释放 BasicEntanglerLayers 句柄。 + +- `ptr` (`CBasicEntanglerLayers*`):待释放句柄,允许传 NULL。 + +无返回值。 + +### basic_entangler_layers_reps(ptr, reps) + +设置层数。 + +- `ptr` (`CBasicEntanglerLayers*`):模板句柄。 +- `reps` (`uintptr_t`):层数。 + +返回 0 表示成功;-1 表示输入为 NULL。 + +### basic_entangler_layers_rotation_gate(ptr, name) + +按名称设置单量子比特旋转门(RX、RY、RZ 或 Phase)。 + +- `ptr` (`CBasicEntanglerLayers*`):模板句柄。 +- `name` (`const char*`):门名称。 + +返回 0 表示成功;-1 表示输入为 NULL;-4 表示 UTF-8 非法;-8 表示门名未知。 + +### basic_entangler_layers_entanglement_gate(ptr, name) + +按名称设置纠缠门(CX、CY 或 CZ)。 + +- `ptr` (`CBasicEntanglerLayers*`):模板句柄。 +- `name` (`const char*`):门名称。 + +返回 0 表示成功;-1 表示输入为 NULL;-4 表示 UTF-8 非法;-8 表示门名未知。 + +### basic_entangler_layers_num_parameters(ptr) + +返回模板生成线路所需的独立参数数量。 + +- `ptr` (`const CBasicEntanglerLayers*`):模板句柄。 + +返回参数数量;句柄为 NULL 时返回 0。 + +### basic_entangler_layers_num_qubits(ptr) + +返回模板作用的量子比特数量。 + +- `ptr` (`const CBasicEntanglerLayers*`):模板句柄。 + +返回量子比特数量;句柄为 NULL 时返回 0。 + +### basic_entangler_layers_build_circuit(ptr, prefix) + +根据当前配置构建参数化线路,参数命名为 `"{prefix}_{index}"`。 + +- `ptr` (`const CBasicEntanglerLayers*`):模板句柄。 +- `prefix` (`const char*`):参数名前缀。 + +返回新分配的 `CCircuit*`(用 `circuit_free` 释放);输入为 NULL、UTF-8 非法或构建失败时返回 NULL。 + +--- + +## QAOAAnsatz + +QAOAAnsatz 用于构造量子近似优化算法(QAOA)的参数化线路,交替应用 cost Hamiltonian 与 mixer Hamiltonian 的时间演化: + +```text +U(β, γ) = ∏_l exp(-i β_l H_M) exp(-i γ_l H_C) +``` + +`qaoa_ansatz_new` 从 cost Hamiltonian 推断量子比特数量,mixer 默认为标准 X mixer,`reps = 1`。每层包含一个 `γ_l` 和一个 `β_l` 参数,因此参数总数为 `2 * reps`,参数命名为 `"{prefix}_gamma_{layer}"` / `"{prefix}_beta_{layer}"`。 + +### qaoa_ansatz_new(cost) + +从 cost Hamiltonian 创建 QAOA 模板(Hamiltonian 会被克隆)。 + +- `cost` (`const CHamiltonian*`):cost Hamiltonian 句柄。 + +返回新分配的 `CQAOAAnsatz*`;输入为 NULL 或 cost 算子非法时返回 NULL。 + +### qaoa_ansatz_free(ptr) + +释放 QAOA 句柄。 + +- `ptr` (`CQAOAAnsatz*`):待释放句柄,允许传 NULL。 + +无返回值。 + +### qaoa_ansatz_reps(ptr, reps) + +设置交替层数(QAOA 深度 p)。 + +- `ptr` (`CQAOAAnsatz*`):模板句柄。 +- `reps` (`uintptr_t`):层数。 + +返回 0 表示成功;-1 表示输入为 NULL。 + +### qaoa_ansatz_mixer(ptr, mixer) + +覆盖 mixer Hamiltonian(会被克隆;量子比特数量必须与 cost 算子一致)。 + +- `ptr` (`CQAOAAnsatz*`):模板句柄。 +- `mixer` (`const CHamiltonian*`):mixer Hamiltonian 句柄。 + +返回 0 表示成功;-1 表示输入为 NULL;-3 表示量子比特数量不匹配或 mixer 非法。 + +### qaoa_ansatz_initial_state(ptr, circuit) + +设置前置初态线路(会被克隆;量子比特数量必须一致)。 + +- `ptr` (`CQAOAAnsatz*`):模板句柄。 +- `circuit` (`const CCircuit*`):初态线路句柄。 + +返回 0 表示成功;-1 表示输入为 NULL;-3 表示量子比特数量不匹配。 + +### qaoa_ansatz_evolution_strategy(ptr, strategy, mode, steps, seed) + +设置 Hamiltonian 演化策略。`EVOLUTION_STRATEGY_EXACT` 忽略其余参数;`EVOLUTION_STRATEGY_AUTO` 使用 `steps`;`EVOLUTION_STRATEGY_TROTTER` 使用 `mode`(`TROTTER_FIRST_ORDER`、`TROTTER_SECOND_ORDER` 或 `TROTTER_RANDOMIZED`)、`steps` 和 `seed`。 + +- `ptr` (`CQAOAAnsatz*`):模板句柄。 +- `strategy` (`int32_t`):`EVOLUTION_STRATEGY_*` 策略标签。 +- `mode` (`int32_t`):Trotter 模式标签,仅 TROTTER 策略使用。 +- `steps` (`uintptr_t`):Trotter 步数,EXACT 策略忽略。 +- `seed` (`uint64_t`):随机种子,仅 `TROTTER_RANDOMIZED` 使用。 + +返回 0 表示成功;-1 表示输入为 NULL;-8 表示标签未知。 + +### qaoa_ansatz_validate(ptr) + +校验模板配置。 + +- `ptr` (`const CQAOAAnsatz*`):模板句柄。 + +返回 0 表示配置合法;-1 表示输入为 NULL;-3 表示配置非法。 + +### qaoa_ansatz_num_parameters(ptr) + +返回参数数量(每层 2 个)。 + +- `ptr` (`const CQAOAAnsatz*`):模板句柄。 + +返回参数数量;句柄为 NULL 时返回 0。 + +### qaoa_ansatz_num_qubits(ptr) + +返回模板作用的量子比特数量。 + +- `ptr` (`const CQAOAAnsatz*`):模板句柄。 + +返回量子比特数量;句柄为 NULL 时返回 0。 + +### qaoa_ansatz_build_circuit(ptr, prefix) + +根据当前配置构建参数化线路,参数命名为 `"{prefix}_gamma_{layer}"` / `"{prefix}_beta_{layer}"`。 + +- `ptr` (`const CQAOAAnsatz*`):模板句柄。 +- `prefix` (`const char*`):参数名前缀。 + +返回新分配的 `CCircuit*`(用 `circuit_free` 释放);输入为 NULL、UTF-8 非法或构建失败时返回 NULL。 + +### QAOA 示例 + +```c +#include +#include "cqlib_c.h" + +int main(void) { + /* cost Hamiltonian:H_C = Z0 * Z1 */ + struct CPauliString *zz = pauli_string_parse("ZZ"); + struct CHamiltonian *cost = hamiltonian_from_pauli(zz); /* 接管 zz 句柄 */ + + struct CQAOAAnsatz *ansatz = qaoa_ansatz_new(cost); + qaoa_ansatz_reps(ansatz, 2); + qaoa_ansatz_evolution_strategy(ansatz, EVOLUTION_STRATEGY_AUTO, + TROTTER_FIRST_ORDER, 1, 0); + + if (qaoa_ansatz_validate(ansatz) != 0) { + qaoa_ansatz_free(ansatz); + hamiltonian_free(cost); + return 1; + } + + /* 参数名形如 "q_gamma_0", "q_beta_0", "q_gamma_1", "q_beta_1" */ + struct CCircuit *circuit = qaoa_ansatz_build_circuit(ansatz, "q"); + if (circuit == NULL) { + qaoa_ansatz_free(ansatz); + hamiltonian_free(cost); + return 1; + } + printf("parameters: %zu, qubits: %zu\n", + qaoa_ansatz_num_parameters(ansatz), + qaoa_ansatz_num_qubits(ansatz)); + + circuit_free(circuit); + qaoa_ansatz_free(ansatz); + hamiltonian_free(cost); + return 0; +} +``` + +--- + +## Feature maps + +Feature map(特征映射)将经典特征向量 `x_0, x_1, ...` 编码到量子态中,是量子机器学习和量子核方法的标准输入层。四个模板(ZFeatureMap、ZZFeatureMap、IQPFeatureMap、PauliFeatureMap)遵循相同的约定:每个重复层以 Hadamard 层开始,符号参数命名为 `"{prefix}_{index}"`,每个输入特征对应一个参数,且相同参数对象在各重复层之间复用,因此 `*_num_parameters` 返回 `num_qubits`(`reps = 0` 时返回 0,构建出空线路)。注意 `circuit_num_parameters` 统计的是不同的角度表达式对象:内嵌在角度表达式中的常量(如 `pi`)不算独立参数(见下方 ZZFeatureMap 示例)。 + +### ZFeatureMap + +ZFeatureMap 是不含纠缠的一阶映射:每个重复层先对所有量子比特应用 Hadamard 层,再对每个量子比特应用独立的 `RZ(2 * x_i)` 旋转。`z_feature_map_new` 默认 `reps = 2`。 + +### z_feature_map_new(num_qubits) + +创建一个 ZFeatureMap 模板句柄。 + +- `num_qubits` (`uintptr_t`):量子比特数量(即输入特征数量)。 + +返回新分配的 `CZFeatureMap*`。 + +### z_feature_map_free(ptr) + +释放 ZFeatureMap 句柄。 + +- `ptr` (`CZFeatureMap*`):待释放句柄,允许传 NULL。 + +无返回值。 + +### z_feature_map_reps(ptr, reps) + +设置重复层数。 + +- `ptr` (`CZFeatureMap*`):模板句柄。 +- `reps` (`uintptr_t`):重复层数。 + +返回 0 表示成功;-1 表示输入为 NULL。 + +### z_feature_map_validate(ptr) + +校验模板配置(至少需要 1 个量子比特)。 + +- `ptr` (`const CZFeatureMap*`):模板句柄。 + +返回 0 表示配置合法;-1 表示输入为 NULL;-3 表示配置非法。 + +### z_feature_map_num_parameters(ptr) + +返回输入特征数量(每个量子比特一个参数;`reps = 0` 时为 0)。 + +- `ptr` (`const CZFeatureMap*`):模板句柄。 + +返回参数数量;句柄为 NULL 时返回 0。 + +### z_feature_map_num_qubits(ptr) + +返回模板作用的量子比特数量。 + +- `ptr` (`const CZFeatureMap*`):模板句柄。 + +返回量子比特数量;句柄为 NULL 时返回 0。 + +### z_feature_map_build_circuit(ptr, prefix) + +根据当前配置构建参数化线路,参数命名为 `"{prefix}_{index}"`。 + +- `ptr` (`const CZFeatureMap*`):模板句柄。 +- `prefix` (`const char*`):参数名前缀。 + +返回新分配的 `CCircuit*`(用 `circuit_free` 释放);输入为 NULL、UTF-8 非法或构建失败时返回 NULL。 + +### ZZFeatureMap + +ZZFeatureMap 在此基础上引入二阶 ZZ 相互作用:每个重复层先应用 Hadamard 层和每个量子比特上的 `RZ(2 * x_i)`,然后对纠缠拓扑中的每一对 `(i, j)` 应用 Pauli 演化 `e^{-i * 2 * (pi - x_i) * (pi - x_j) * Z_i Z_j}`。默认配置:`reps = 2`、全连接拓扑。自定义拓扑由 setter 保存、由校验检查:越界下标、自环和重复(无向)边都会被拒绝。 + +### zz_feature_map_new(num_qubits) + +创建一个 ZZFeatureMap 模板句柄。 + +- `num_qubits` (`uintptr_t`):量子比特数量(即输入特征数量)。 + +返回新分配的 `CZZFeatureMap*`。 + +### zz_feature_map_free(ptr) + +释放 ZZFeatureMap 句柄。 + +- `ptr` (`CZZFeatureMap*`):待释放句柄,允许传 NULL。 + +无返回值。 + +### zz_feature_map_reps(ptr, reps) + +设置重复层数。 + +- `ptr` (`CZZFeatureMap*`):模板句柄。 +- `reps` (`uintptr_t`):重复层数。 + +返回 0 表示成功;-1 表示输入为 NULL。 + +### zz_feature_map_entanglement(ptr, topology, pairs, pairs_len) + +设置 ZZ 相互作用的纠缠拓扑。拓扑为 `ENTANGLEMENT_CUSTOM` 时,`pairs` 是长度为 `pairs_len / 2` 的控制/目标量子比特 id 对的扁平数组。 + +- `ptr` (`CZZFeatureMap*`):模板句柄。 +- `topology` (`int32_t`):`ENTANGLEMENT_*` 拓扑标签。 +- `pairs` (`const uint32_t*`):自定义连接对的扁平数组(其他拓扑传 NULL)。 +- `pairs_len` (`uintptr_t`):扁平数组长度。 + +返回 0 表示成功;-1 表示输入为 NULL(含 `pairs` 为 NULL 而 `pairs_len > 0`);-8 表示拓扑未知或 `pairs_len` 为奇数。 + +### zz_feature_map_validate(ptr) + +校验模板配置(至少 1 个量子比特;自定义拓扑要求下标不越界、无自环、无重复边)。 + +- `ptr` (`const CZZFeatureMap*`):模板句柄。 + +返回 0 表示配置合法;-1 表示输入为 NULL;-3 表示配置非法。 + +### zz_feature_map_num_parameters(ptr) + +返回输入特征数量(每个量子比特一个参数;`reps = 0` 时为 0)。 + +- `ptr` (`const CZZFeatureMap*`):模板句柄。 + +返回参数数量;句柄为 NULL 时返回 0。 + +### zz_feature_map_num_qubits(ptr) + +返回模板作用的量子比特数量。 + +- `ptr` (`const CZZFeatureMap*`):模板句柄。 + +返回量子比特数量;句柄为 NULL 时返回 0。 + +### zz_feature_map_build_circuit(ptr, prefix) + +根据当前配置构建参数化线路,参数命名为 `"{prefix}_{index}"`。 + +- `ptr` (`const CZZFeatureMap*`):模板句柄。 +- `prefix` (`const char*`):参数名前缀。 + +返回新分配的 `CCircuit*`(用 `circuit_free` 释放);输入为 NULL、UTF-8 非法或构建失败时返回 NULL。 + +### ZZFeatureMap 示例 + +```c +#include +#include "cqlib_c.h" + +int main(void) { + struct CZZFeatureMap *map = zz_feature_map_new(2); + + /* 返回 2 个输入特征 x_0、x_1。 */ + printf("features: %zu\n", zz_feature_map_num_parameters(map)); + + /* circuit_num_parameters 统计不同的角度表达式:2 个 RZ 角度 + 加上共享的 ZZ 表达式 4 * (pi - x_i) * (pi - x_j);表达式内的 + pi 常量不算独立参数。 */ + struct CCircuit *circuit = zz_feature_map_build_circuit(map, "x"); + if (circuit == NULL) { + zz_feature_map_free(map); + return 1; + } + printf("circuit parameters: %zu\n", circuit_num_parameters(circuit)); + + circuit_free(circuit); + zz_feature_map_free(map); + return 0; +} +``` + +### IQPFeatureMap + +IQPFeatureMap 是 IQP 风格的对角编码,与 ZZFeatureMap 使用相同的线路结构——Hadamard 层、一阶 Z 旋转和按纠缠拓扑展开的二阶 ZZ 相互作用——但提供独立句柄。默认配置:`reps = 2`、全连接拓扑。 + +### iqp_feature_map_new(num_qubits) + +创建一个 IQPFeatureMap 模板句柄。 + +- `num_qubits` (`uintptr_t`):量子比特数量(即输入特征数量)。 + +返回新分配的 `CIQPFeatureMap*`。 + +### iqp_feature_map_free(ptr) + +释放 IQPFeatureMap 句柄。 + +- `ptr` (`CIQPFeatureMap*`):待释放句柄,允许传 NULL。 + +无返回值。 + +### iqp_feature_map_reps(ptr, reps) + +设置重复层数。 + +- `ptr` (`CIQPFeatureMap*`):模板句柄。 +- `reps` (`uintptr_t`):重复层数。 + +返回 0 表示成功;-1 表示输入为 NULL。 + +### iqp_feature_map_entanglement(ptr, topology, pairs, pairs_len) + +设置双量子比特对角相互作用的纠缠拓扑。拓扑为 `ENTANGLEMENT_CUSTOM` 时,`pairs` 是长度为 `pairs_len / 2` 的控制/目标量子比特 id 对的扁平数组。 + +- `ptr` (`CIQPFeatureMap*`):模板句柄。 +- `topology` (`int32_t`):`ENTANGLEMENT_*` 拓扑标签。 +- `pairs` (`const uint32_t*`):自定义连接对的扁平数组(其他拓扑传 NULL)。 +- `pairs_len` (`uintptr_t`):扁平数组长度。 + +返回 0 表示成功;-1 表示输入为 NULL(含 `pairs` 为 NULL 而 `pairs_len > 0`);-8 表示拓扑未知或 `pairs_len` 为奇数。 + +### iqp_feature_map_validate(ptr) + +校验模板配置(至少 1 个量子比特;自定义拓扑要求下标不越界、无自环、无重复边)。 + +- `ptr` (`const CIQPFeatureMap*`):模板句柄。 + +返回 0 表示配置合法;-1 表示输入为 NULL;-3 表示配置非法。 + +### iqp_feature_map_num_parameters(ptr) + +返回输入特征数量(每个量子比特一个参数;`reps = 0` 时为 0)。 + +- `ptr` (`const CIQPFeatureMap*`):模板句柄。 + +返回参数数量;句柄为 NULL 时返回 0。 + +### iqp_feature_map_num_qubits(ptr) + +返回模板作用的量子比特数量。 + +- `ptr` (`const CIQPFeatureMap*`):模板句柄。 + +返回量子比特数量;句柄为 NULL 时返回 0。 + +### iqp_feature_map_build_circuit(ptr, prefix) + +根据当前配置构建参数化线路,参数命名为 `"{prefix}_{index}"`。 + +- `ptr` (`const CIQPFeatureMap*`):模板句柄。 +- `prefix` (`const char*`):参数名前缀。 + +返回新分配的 `CCircuit*`(用 `circuit_free` 释放);输入为 NULL、UTF-8 非法或构建失败时返回 NULL。 + +### PauliFeatureMap + +PauliFeatureMap 把 Z 和 ZZ 映射推广到任意 Pauli 模板。每个重复层先应用 Hadamard 层,然后对每个配置的 Pauli 模板(支持集大小为 `k`)以及从纠缠拓扑中取出的每个 `k` 元组应用 Pauli 演化: + +- `k = 1`:角度为 `2 * x_i`,实现 `e^{-i * x_i * P}`; +- `k >= 2`:角度为 `4 * prod_j (pi - x_j)`,实现 `e^{-i * 2 * prod_j (pi - x_j) * P}`。 + +支持集为空的模板只贡献全局相位,会被跳过。默认配置:`reps = 2`、Pauli 模板 `["Z", "ZZ"]`、全连接拓扑、参数前缀 `"x"`。`pauli_feature_map_build_circuit` 收到空前缀时回退到已配置的参数前缀。 + +### pauli_feature_map_new(num_qubits) + +创建一个 PauliFeatureMap 模板句柄。 + +- `num_qubits` (`uintptr_t`):量子比特数量(即输入特征数量)。 + +返回新分配的 `CPauliFeatureMap*`。 + +### pauli_feature_map_free(ptr) + +释放 PauliFeatureMap 句柄。 + +- `ptr` (`CPauliFeatureMap*`):待释放句柄,允许传 NULL。 + +无返回值。 + +### pauli_feature_map_reps(ptr, reps) + +设置重复层数。 + +- `ptr` (`CPauliFeatureMap*`):模板句柄。 +- `reps` (`uintptr_t`):重复层数。 + +返回 0 表示成功;-1 表示输入为 NULL。 + +### pauli_feature_map_paulis(ptr, texts, labels, len) + +设置 Pauli 演化模板。`texts` 和 `labels` 是长度均为 `len` 的平行 NUL 结尾 C 字符串数组;每个 text 按 PauliString 解析(例如 `"Z"`、`"ZZ"`、`"XY"`),每个 label 为模板命名。长度超过量子比特数量的模板会被保存,但校验会失败。调用失败时保留原有配置。 + +- `ptr` (`CPauliFeatureMap*`):模板句柄。 +- `texts` (`const char* const*`):Pauli 字符串文本数组。 +- `labels` (`const char* const*`):模板标签数组。 +- `len` (`uintptr_t`):数组长度。 + +返回 0 表示成功;-1 表示输入为 NULL(句柄、`len > 0` 时的数组、或单个元素为 NULL);-4 表示 UTF-8 非法;-8 表示 Pauli 字符串解析失败。 + +### pauli_feature_map_entanglement(ptr, topology, pairs, pairs_len) + +设置多量子比特相互作用的纠缠拓扑。拓扑为 `ENTANGLEMENT_CUSTOM` 时,`pairs` 是长度为 `pairs_len / 2` 的控制/目标量子比特 id 对的扁平数组。 + +- `ptr` (`CPauliFeatureMap*`):模板句柄。 +- `topology` (`int32_t`):`ENTANGLEMENT_*` 拓扑标签。 +- `pairs` (`const uint32_t*`):自定义连接对的扁平数组(其他拓扑传 NULL)。 +- `pairs_len` (`uintptr_t`):扁平数组长度。 + +返回 0 表示成功;-1 表示输入为 NULL(含 `pairs` 为 NULL 而 `pairs_len > 0`);-8 表示拓扑未知或 `pairs_len` 为奇数。 + +### pauli_feature_map_parameter_prefix(ptr, prefix) + +设置回退参数前缀,`pauli_feature_map_build_circuit` 收到空前缀时使用。 + +- `ptr` (`CPauliFeatureMap*`):模板句柄。 +- `prefix` (`const char*`):参数名前缀。 + +返回 0 表示成功;-1 表示输入为 NULL;-4 表示 UTF-8 非法。 + +### pauli_feature_map_validate(ptr) + +校验模板配置(至少 1 个量子比特;Pauli 模板长度不超过量子比特数量;自定义拓扑要求下标不越界、无自环、无重复边)。 + +- `ptr` (`const CPauliFeatureMap*`):模板句柄。 + +返回 0 表示配置合法;-1 表示输入为 NULL;-3 表示配置非法。 + +### pauli_feature_map_num_parameters(ptr) + +返回输入特征数量(每个量子比特一个参数;`reps = 0` 时为 0)。 + +- `ptr` (`const CPauliFeatureMap*`):模板句柄。 + +返回参数数量;句柄为 NULL 时返回 0。 + +### pauli_feature_map_num_qubits(ptr) + +返回模板作用的量子比特数量。 + +- `ptr` (`const CPauliFeatureMap*`):模板句柄。 + +返回量子比特数量;句柄为 NULL 时返回 0。 + +### pauli_feature_map_build_circuit(ptr, prefix) + +根据当前配置构建参数化线路,参数命名为 `"{prefix}_{index}"`。空前缀回退到已配置的参数前缀。 + +- `ptr` (`const CPauliFeatureMap*`):模板句柄。 +- `prefix` (`const char*`):参数名前缀(空字符串使用已配置的回退前缀)。 + +返回新分配的 `CCircuit*`(用 `circuit_free` 释放);输入为 NULL、UTF-8 非法或构建失败时返回 NULL。 + +### PauliFeatureMap 示例 + +```c +#include +#include "cqlib_c.h" + +int main(void) { + struct CPauliFeatureMap *map = pauli_feature_map_new(2); + + const char *texts[] = {"Z", "ZZ"}; + const char *labels[] = {"Z", "ZZ"}; + pauli_feature_map_paulis(map, texts, labels, 2); + pauli_feature_map_parameter_prefix(map, "p"); + + if (pauli_feature_map_validate(map) != 0) { + pauli_feature_map_free(map); + return 1; + } + + /* 空前缀回退到 "p":参数名形如 "p_0", "p_1"。 */ + struct CCircuit *circuit = pauli_feature_map_build_circuit(map, ""); + if (circuit == NULL) { + pauli_feature_map_free(map); + return 1; + } + printf("features: %zu\n", pauli_feature_map_num_parameters(map)); + + circuit_free(circuit); + pauli_feature_map_free(map); + return 0; +} +``` + +--- + +## Facade 构造函数 + +facade 构造函数一次调用即可创建常用模板的预配置句柄。它们返回与对应 `*_new` 构造函数相同的句柄类型,因此结果对象可以配合所有 `two_local_*`、`zz_feature_map_*` 和 `pauli_feature_map_*` 函数使用。拓扑参数非法(标签未知、自定义连接对数量为奇数、或 `pairs` 为 NULL 而 `pairs_len > 0`)时四个函数都返回 NULL;`pauli_feature_map_build` 在 Pauli 字符串解析失败时同样返回 NULL。 + +### efficient_su2(num_qubits, reps, topology, pairs, pairs_len) + +创建 EfficientSU2 ansatz:`[RY, RZ]` 旋转层配合 `CX` 纠缠,保留最终旋转层,参数总数为 `(reps + 1) * num_qubits * 2`。 + +- `num_qubits` (`uintptr_t`):量子比特数量。 +- `reps` (`uintptr_t`):重复层数。 +- `topology` (`int32_t`):`ENTANGLEMENT_*` 拓扑标签。 +- `pairs` (`const uint32_t*`):自定义连接对的扁平数组(其他拓扑传 NULL)。 +- `pairs_len` (`uintptr_t`):扁平数组长度。 + +返回新分配的 `CTwoLocal*`;拓扑参数非法时返回 NULL。 + +### real_amplitudes(num_qubits, reps, topology, pairs, pairs_len) + +创建 RealAmplitudes ansatz:`[RY]` 旋转层配合 `CX` 纠缠,保留最终旋转层,参数总数为 `(reps + 1) * num_qubits`。 + +- `num_qubits` (`uintptr_t`):量子比特数量。 +- `reps` (`uintptr_t`):重复层数。 +- `topology` (`int32_t`):`ENTANGLEMENT_*` 拓扑标签。 +- `pairs` (`const uint32_t*`):自定义连接对的扁平数组(其他拓扑传 NULL)。 +- `pairs_len` (`uintptr_t`):扁平数组长度。 + +返回新分配的 `CTwoLocal*`;拓扑参数非法时返回 NULL。 + +### zz_feature_map_build(num_qubits, reps, topology, pairs, pairs_len) + +创建指定重复层数和拓扑的 ZZFeatureMap。 + +- `num_qubits` (`uintptr_t`):量子比特数量(即输入特征数量)。 +- `reps` (`uintptr_t`):重复层数。 +- `topology` (`int32_t`):`ENTANGLEMENT_*` 拓扑标签。 +- `pairs` (`const uint32_t*`):自定义连接对的扁平数组(其他拓扑传 NULL)。 +- `pairs_len` (`uintptr_t`):扁平数组长度。 + +返回新分配的 `CZZFeatureMap*`;拓扑参数非法时返回 NULL。 + +### pauli_feature_map_build(num_qubits, reps, texts, labels, paulis_len, topology, pairs, pairs_len) + +创建带显式 Pauli 模板的 PauliFeatureMap。`texts` 和 `labels` 是长度均为 `paulis_len` 的平行 NUL 结尾 C 字符串数组;每个 text 按 PauliString 解析。 + +- `num_qubits` (`uintptr_t`):量子比特数量(即输入特征数量)。 +- `reps` (`uintptr_t`):重复层数。 +- `texts` (`const char* const*`):Pauli 字符串文本数组。 +- `labels` (`const char* const*`):模板标签数组。 +- `paulis_len` (`uintptr_t`):两个数组的长度。 +- `topology` (`int32_t`):`ENTANGLEMENT_*` 拓扑标签。 +- `pairs` (`const uint32_t*`):自定义连接对的扁平数组(其他拓扑传 NULL)。 +- `pairs_len` (`uintptr_t`):扁平数组长度。 + +返回新分配的 `CPauliFeatureMap*`;拓扑参数非法或 Pauli 字符串解析失败时返回 NULL。 + +### Facade 示例 + +```c +#include +#include "cqlib_c.h" + +int main(void) { + /* EfficientSU2:[RY, RZ] + CX -> (1 + 1) * 2 * 2 = 8 个参数 */ + struct CTwoLocal *su2 = efficient_su2(2, 1, ENTANGLEMENT_FULL, NULL, 0); + printf("efficient_su2 parameters: %zu\n", two_local_num_parameters(su2)); + two_local_free(su2); + + /* RealAmplitudes:[RY] + CX -> (2 + 1) * 3 = 9 个参数 */ + struct CTwoLocal *ra = real_amplitudes(3, 2, ENTANGLEMENT_LINEAR, NULL, 0); + printf("real_amplitudes parameters: %zu\n", two_local_num_parameters(ra)); + two_local_free(ra); + + /* 环形拓扑的 ZZ feature map */ + struct CZZFeatureMap *zz = zz_feature_map_build(3, 2, ENTANGLEMENT_CIRCULAR, NULL, 0); + printf("zz_feature_map features: %zu\n", zz_feature_map_num_parameters(zz)); + zz_feature_map_free(zz); + return 0; +} +``` + +--- + +## 纠缠拓扑辅助函数 + +这些辅助函数把 `ENTANGLEMENT_*` 拓扑标签展开为显式量子比特分组,语义与各模板一致。输出采用两步模式:先调用 `*_len` 函数获取所需的扁平长度,分配缓冲区后再调用拷贝函数。 + +### entanglement_generate_pairs_len(topology, pairs, pairs_len, num_qubits) + +返回 `entanglement_generate_pairs` 生成的扁平长度(每对 2 个元素);参数非法(拓扑标签未知、自定义连接对数量为奇数、或 `pairs` 为 NULL 而 `pairs_len > 0`)时返回 0。拓扑为 `ENTANGLEMENT_CUSTOM` 时原样返回自定义连接对,忽略 `num_qubits`。 + +- `topology` (`int32_t`):`ENTANGLEMENT_*` 拓扑标签。 +- `pairs` (`const uint32_t*`):自定义连接对的扁平数组(其他拓扑传 NULL)。 +- `pairs_len` (`uintptr_t`):扁平数组长度。 +- `num_qubits` (`uintptr_t`):预定义拓扑使用的量子比特数量。 + +返回 `entanglement_generate_pairs` 所需的 `out` 缓冲区长度;参数非法时返回 0。 + +### entanglement_generate_pairs(topology, pairs, pairs_len, num_qubits, out, len) + +把扁平的连接对 `[c0, t0, c1, t1, ...]` 拷贝到 `out`。预定义拓扑生成:linear 为 `(0,1), (1,2), ...`;circular 额外增加环绕边 `(n-1, 0)`;full 为按字典序排列的全连接对。 + +- `topology` (`int32_t`):`ENTANGLEMENT_*` 拓扑标签。 +- `pairs` (`const uint32_t*`):自定义连接对的扁平数组(其他拓扑传 NULL)。 +- `pairs_len` (`uintptr_t`):扁平数组长度。 +- `num_qubits` (`uintptr_t`):预定义拓扑使用的量子比特数量。 +- `out` (`uint32_t*`):输出缓冲区。 +- `len` (`uintptr_t`):缓冲区长度,即 `entanglement_generate_pairs_len` 的返回值。 + +返回 0 表示成功;-1 表示输入为 NULL(`len > 0` 时 `out` 为 NULL);-8 表示 `len` 与所需长度不符或参数非法。 + +### entanglement_generate_k_tuples_len(topology, pairs, pairs_len, k, num_qubits) + +返回 `entanglement_generate_k_tuples` 生成的扁平长度(每个元组 `k` 个元素);参数组合非法(拓扑未知或自定义连接对非法、`k = 0`、`k > num_qubits`、或 `num_qubits = 0`)时返回 0。 + +- `topology` (`int32_t`):`ENTANGLEMENT_*` 拓扑标签。 +- `pairs` (`const uint32_t*`):自定义连接对的扁平数组(其他拓扑传 NULL)。 +- `pairs_len` (`uintptr_t`):扁平数组长度。 +- `k` (`uintptr_t`):元组大小。 +- `num_qubits` (`uintptr_t`):量子比特数量。 + +返回 `entanglement_generate_k_tuples` 所需的 `out` 缓冲区长度;参数非法时返回 0。 + +### entanglement_generate_k_tuples(topology, pairs, pairs_len, k, num_qubits, out, len) + +把扁平的 k 元组 `[t0_0, t0_1, ..., t1_0, ...]` 拷贝到 `out`。`k = 1` 时每个量子比特单独成组;`k = 2` 时返回拓扑的连接对(含自定义连接对);`k >= 3` 时 linear 生成连续窗口 `(i, ..., i+k-1)`,circular 生成模 n 的旋转窗口,full 生成按字典序排列的 k 组合——自定义拓扑只定义连接对,此时回退到 full。 + +- `topology` (`int32_t`):`ENTANGLEMENT_*` 拓扑标签。 +- `pairs` (`const uint32_t*`):自定义连接对的扁平数组(其他拓扑传 NULL)。 +- `pairs_len` (`uintptr_t`):扁平数组长度。 +- `k` (`uintptr_t`):元组大小。 +- `num_qubits` (`uintptr_t`):量子比特数量。 +- `out` (`uint32_t*`):输出缓冲区。 +- `len` (`uintptr_t`):缓冲区长度,即 `entanglement_generate_k_tuples_len` 的返回值。 + +返回 0 表示成功;-1 表示输入为 NULL(`len > 0` 时 `out` 为 NULL);-8 表示 `len` 与所需长度不符或参数非法。 + +### 纠缠拓扑辅助函数示例 + +```c +#include +#include +#include "cqlib_c.h" + +int main(void) { + /* 两步模式:先查长度,再拷贝。 */ + uintptr_t len = entanglement_generate_pairs_len(ENTANGLEMENT_CIRCULAR, NULL, 0, 4); + uint32_t *pairs = malloc(len * sizeof(uint32_t)); + + if (entanglement_generate_pairs(ENTANGLEMENT_CIRCULAR, NULL, 0, 4, pairs, len) == 0) { + /* 输出:0 1 1 2 2 3 3 0 */ + for (uintptr_t i = 0; i < len; i++) { + printf("%u ", pairs[i]); + } + printf("\n"); + } + free(pairs); + return 0; +} +``` + +--- + +## StronglyEntanglingLayers + +StronglyEntanglingLayers 的每一层先对每个量子比特应用一个 U 门(3 个参数),再用纠缠门把量子比特 `i` 连接到 `(i + r) mod num_qubits`,其中 `r` 是该层的 range。默认配置:`reps = 1`、纠缠门 `CX`、range 按 `1..num_qubits` 循环取值(第 `l` 层使用 `(l mod (num_qubits - 1)) + 1`)。参数命名为 `"{prefix}_{index}"` 并连续编号,总数为 `reps * num_qubits * 3`。 + +### strongly_entangling_layers_new(num_qubits) + +创建一个 StronglyEntanglingLayers 模板句柄。 + +- `num_qubits` (`uintptr_t`):量子比特数量。 + +返回新分配的 `CStronglyEntanglingLayers*`。 + +### strongly_entangling_layers_free(ptr) + +释放 StronglyEntanglingLayers 句柄。 + +- `ptr` (`CStronglyEntanglingLayers*`):待释放句柄,允许传 NULL。 + +无返回值。 + +### strongly_entangling_layers_reps(ptr, reps) + +设置层数。 + +- `ptr` (`CStronglyEntanglingLayers*`):模板句柄。 +- `reps` (`uintptr_t`):层数。 + +返回 0 表示成功;-1 表示输入为 NULL。 + +### strongly_entangling_layers_entanglement_gate(ptr, name) + +按名称设置纠缠层使用的双量子比特门(CX、CY 或 CZ)。 + +- `ptr` (`CStronglyEntanglingLayers*`):模板句柄。 +- `name` (`const char*`):门名称。 + +返回 0 表示成功;-1 表示输入为 NULL;-4 表示 UTF-8 非法;-8 表示门名未知。 + +### strongly_entangling_layers_ranges(ptr, ranges, len) + +设置显式 range 列表,在各层之间循环复用(第 `l` 层使用 `ranges[l mod len]`)。校验时每个 range 必须落在 `1..num_qubits` 内。空列表(`len == 0`)会保存空列表,在被替换之前校验会失败。 + +- `ptr` (`CStronglyEntanglingLayers*`):模板句柄。 +- `ranges` (`const uintptr_t*`):range 数组。 +- `len` (`uintptr_t`):数组长度。 + +返回 0 表示成功;-1 表示输入为 NULL(句柄为 NULL,或 `len > 0` 时 `ranges` 为 NULL)。 + +### strongly_entangling_layers_validate(ptr) + +校验模板配置(至少 1 个量子比特、纠缠门合法,以及——若已设置——range 列表非空且每个 range 都在 `1..num_qubits` 内)。 + +- `ptr` (`const CStronglyEntanglingLayers*`):模板句柄。 + +返回 0 表示配置合法;-1 表示输入为 NULL;-3 表示配置非法。 + +### strongly_entangling_layers_num_parameters(ptr) + +返回独立参数数量(`reps * num_qubits * 3`)。 + +- `ptr` (`const CStronglyEntanglingLayers*`):模板句柄。 + +返回参数数量;句柄为 NULL 时返回 0。 + +### strongly_entangling_layers_num_qubits(ptr) + +返回模板作用的量子比特数量。 + +- `ptr` (`const CStronglyEntanglingLayers*`):模板句柄。 + +返回量子比特数量;句柄为 NULL 时返回 0。 + +### strongly_entangling_layers_build_circuit(ptr, prefix) + +根据当前配置构建参数化线路,参数命名为 `"{prefix}_{index}"`。 + +- `ptr` (`const CStronglyEntanglingLayers*`):模板句柄。 +- `prefix` (`const char*`):参数名前缀。 + +返回新分配的 `CCircuit*`(用 `circuit_free` 释放);输入为 NULL、UTF-8 非法或构建失败时返回 NULL。 + +### StronglyEntanglingLayers 示例 + +```c +#include +#include "cqlib_c.h" + +int main(void) { + struct CStronglyEntanglingLayers *layers = strongly_entangling_layers_new(3); + + strongly_entangling_layers_reps(layers, 2); + uintptr_t ranges[] = {1, 2}; /* 循环复用:第 0 层 -> 1,第 1 层 -> 2 */ + strongly_entangling_layers_ranges(layers, ranges, 2); + strongly_entangling_layers_entanglement_gate(layers, "CZ"); + + if (strongly_entangling_layers_validate(layers) != 0) { + strongly_entangling_layers_free(layers); + return 1; + } + + /* 2 层 * 3 量子比特 * 3 个 U 门角度 = 18 个参数,名为 "t_0", "t_1", ... */ + struct CCircuit *circuit = strongly_entangling_layers_build_circuit(layers, "t"); + if (circuit == NULL) { + strongly_entangling_layers_free(layers); + return 1; + } + printf("parameters: %zu\n", strongly_entangling_layers_num_parameters(layers)); + + circuit_free(circuit); + strongly_entangling_layers_free(layers); + return 0; +} +``` + +--- + +## PauliEvolutionAnsatz + +PauliEvolutionAnsatz 表示 Hamiltonian 下的时间演化。`pauli_evolution_ansatz_new` 会克隆输入 Hamiltonian 并做简化(合并重复项、把相位吸收进系数、移除近似为零的项);默认演化策略为自动精确/Trotter 选择、步数为 1。 + +### pauli_evolution_ansatz_new(hamiltonian) + +从 Hamiltonian 创建 Pauli 演化 ansatz(过程中会被克隆并简化)。 + +- `hamiltonian` (`const CHamiltonian*`):Hamiltonian 句柄。 + +返回新分配的 `CPauliEvolutionAnsatz*`;输入为 NULL 或算子为空时返回 NULL。 + +### pauli_evolution_ansatz_free(ptr) + +释放 Pauli 演化 ansatz 句柄。 + +- `ptr` (`CPauliEvolutionAnsatz*`):待释放句柄,允许传 NULL。 + +无返回值。 + +### pauli_evolution_ansatz_with_time_param_name(ptr, name) + +覆盖演化 lowering 使用的时间参数符号。默认按 `"{prefix}_t"` 派生;设置名称后使用该名称。空名称重置为默认值。 + +- `ptr` (`CPauliEvolutionAnsatz*`):模板句柄。 +- `name` (`const char*`):新的时间参数符号(空字符串重置为默认值)。 + +返回 0 表示成功;-1 表示输入为 NULL;-4 表示 UTF-8 非法。 + +### pauli_evolution_ansatz_evolution_info(ptr, out) + +读取演化策略信息(核心层 `PauliEvolutionAnsatz::evolution_info` 的对应接口)写入 `out`。`out` 描述实际使用的策略:当前配置生成的分解与简化后 Hamiltonian 的结构信息,也可据此得知 `EVOLUTION_STRATEGY_AUTO` 的选择结果。 + +- `ptr` (`const CPauliEvolutionAnsatz*`):模板句柄。 +- `out` (`CPauliEvolutionInfo*`):接收快照的输出结构体。 + +返回 0 表示成功;-1 表示输入为 NULL(`ptr` 或 `out`)。 + +`CPauliEvolutionInfo` 是布局稳定的透明结构体: + +| 字段 | 类型 | 含义 | +| --- | --- | --- | +| `is_exact` | `uint8_t` | 分解在数学上精确时为 1,否则为 0。 | +| `all_terms_commute` | `uint8_t` | 所有 Hamiltonian 项相互对易时为 1,否则为 0。 | +| `_pad` | `uint8_t[6]` | 保留填充字段,用于保持结构体布局稳定。 | +| `steps` | `uintptr_t` | 写入线路的分解重复次数。 | +| `num_terms` | `uintptr_t` | (简化后)Hamiltonian 的 Pauli 项数。 | +| `trotter_mode` | `int32_t` | `TROTTER_FIRST_ORDER`、`TROTTER_SECOND_ORDER` 或 `TROTTER_RANDOMIZED`;走精确单次路径时为 `TROTTER_MODE_NONE`。 | +| `trotter_seed` | `uint64_t` | `TROTTER_RANDOMIZED` 的随机种子;其他情况为 0。 | + +### PauliEvolutionAnsatz 示例 + +```c +#include +#include "cqlib_c.h" + +int main(void) { + /* 单量子比特 H = X 下的演化。 */ + struct CPauliString *px = pauli_string_parse("X"); + struct CHamiltonian *ham = hamiltonian_from_pauli(px); /* 接管 px 句柄 */ + + struct CPauliEvolutionAnsatz *ansatz = pauli_evolution_ansatz_new(ham); + if (ansatz == NULL) { + hamiltonian_free(ham); + return 1; + } + + /* 重命名时间参数,再重置为默认值。 */ + pauli_evolution_ansatz_with_time_param_name(ansatz, "tau"); + pauli_evolution_ansatz_with_time_param_name(ansatz, ""); + + /* 读取实际使用的策略:唯一一项且对易,走精确单次路径 + (trotter_mode == TROTTER_MODE_NONE)。 */ + struct CPauliEvolutionInfo info; + if (pauli_evolution_ansatz_evolution_info(ansatz, &info) == 0) { + printf("exact=%d commute=%d steps=%zu terms=%zu mode=%d\n", + info.is_exact, info.all_terms_commute, + info.steps, info.num_terms, info.trotter_mode); + } + + pauli_evolution_ansatz_free(ansatz); + hamiltonian_free(ham); + return 0; +} +``` + +--- + +Hamiltonian 构造函数(`hamiltonian_from_pauli` / `hamiltonian_free`)见 [Hamiltonian](../3_qis/7_hamiltonian.md),Pauli 字符串解析(`pauli_string_parse`)见 [Pauli](../3_qis/6_pauli.md),线路句柄管理(`circuit_free`、`circuit_num_parameters`)见 [Circuit](1_circuit.md)。 diff --git a/docs/api/cn/c/0_circuit/12_cfg.md b/docs/api/cn/c/0_circuit/12_cfg.md new file mode 100644 index 00000000..bff39547 --- /dev/null +++ b/docs/api/cn/c/0_circuit/12_cfg.md @@ -0,0 +1,614 @@ +# 线路控制流图(CCircuitCFG) + +`CCircuitCFG*` 是量子线路的控制流图(Control Flow Graph, CFG)分析视图:基本块保存一段顺序执行的操作,块尾终结符描述控制转移,控制流区域记录结构化 `if` / `while` / `for` / `switch` 的边界与入口。块以稳定节点索引(`uint32_t`)寻址,`UINT32_MAX` 为"缺失块 / 未设置入口"哨兵值;本页接口除通用错误码外,另以 `-2` 表示块不存在或索引为哨兵值。典型工作流:`circuit_cfg_from_circuit` → 检查块 / 终结符 / 边 / 区域 → `circuit_cfg_validate` → `circuit_cfg_to_circuit`。错误码、句柄释放与两步式数组输出等通用约定见 [Overview](../0_overview.md)。 + +--- + +## 构造与转换 + +### circuit_cfg_new(num_qubits) + +创建一个空 CFG,包含密集编号的量子比特 `0..num_qubits`。新图没有任何块,也未设置入口块,编辑之后才能通过 `circuit_cfg_validate`。 + +- `num_qubits` (`uintptr_t`):逻辑量子比特数量。 + +返回:新分配的 `CCircuitCFG*`。 + +### circuit_cfg_from_qubits(qubits, qubits_len) + +按给定逻辑量子比特编号集合创建空 CFG:编号按插入顺序保留,重复编号会被去除,适合稀疏逻辑编号。 + +- `qubits` (`const uint32_t*`):逻辑量子比特编号数组。 +- `qubits_len` (`uintptr_t`):数组长度。 + +返回:新分配的 `CCircuitCFG*`(用 `circuit_cfg_free` 释放);`qubits_len > 0` 而 `qubits` 为 NULL 时返回 NULL。 + +### circuit_cfg_from_circuit(ptr) + +把结构化线路展开为通过校验的 CFG。每个条件、循环与 `switch` 头块带有一个控制流区域,记录体入口、汇合块与外层操作元数据;线性线路展开为单个入口块,标签为 `entry`,终结符为 `CFG_TERMINATOR_RETURN`。 + +- `ptr` (`const struct CCircuit*`):源线路句柄。 + +返回:新分配的 `CCircuitCFG*`(用 `circuit_cfg_free` 释放);`ptr` 为 NULL 或展开失败(存在无法结构化的控制流)时返回 NULL。 + +```c +struct CCircuit* circuit = circuit_new(2); +circuit_h(circuit, 0); +circuit_cx(circuit, 0, 1); + +struct CCircuitCFG* cfg = circuit_cfg_from_circuit(circuit); +/* circuit_cfg_num_blocks(cfg) == 1:线性线路只有一个入口块 */ +``` + +### circuit_cfg_free(ptr) + +释放 CFG 句柄。 + +- `ptr` (`struct CCircuitCFG*`):待释放句柄,允许传 NULL。 + +### circuit_cfg_to_circuit(ptr) + +把 CFG 还原为结构化线路。符号表、参数表与全局相位随还原一并写回;内部先执行一次与 `circuit_cfg_validate` 相同的结构检查。 + +- `ptr` (`const struct CCircuitCFG*`):CFG 句柄。 + +返回:新分配的 `CCircuit*`(用 `circuit_free` 释放);`ptr` 为 NULL 或还原失败(图结构被破坏或控制流区域无法映射回结构化形式)时返回 NULL。 + +### circuit_cfg_validate(ptr) + +检查图、操作、参数、边、终结符与区域元数据的不变量:入口块是否有效、每个块是否恰有一个终结符且出边数量与类型一致、`Return` 是否无出边、全部块是否从入口可达、区域元数据是否与图结构一致、`Branch` 条件是否为 `Bool` 表达式、`Switch` 目标是否为 `UInt` 表达式且出边数等于 case 数加一等。 + +- `ptr` (`const struct CCircuitCFG*`):CFG 句柄。 + +返回:`0` 成功;`-1` `ptr` 为 NULL;`-3` 校验失败。 + +--- + +## 全局信息 + +### circuit_cfg_num_qubits(ptr) + +返回逻辑量子比特数量。 + +- `ptr` (`const struct CCircuitCFG*`):CFG 句柄。 + +返回:量子比特数量;`ptr` 为 NULL 时返回 0。 + +### circuit_cfg_num_blocks(ptr) + +返回图中块的数量。 + +- `ptr` (`const struct CCircuitCFG*`):CFG 句柄。 + +返回:块数量;`ptr` 为 NULL 时返回 0。 + +### circuit_cfg_qubits(ptr, buffer, len) + +按插入顺序把逻辑量子比特编号拷贝进 `buffer`(两步式:先 `circuit_cfg_num_qubits` 取长度)。 + +- `ptr` (`const struct CCircuitCFG*`):CFG 句柄。 +- `buffer` (`uint32_t*`):输出缓冲区。 +- `len` (`uintptr_t`):缓冲区容量。 + +返回:`0` 成功;`-1` `ptr` 为 NULL,或编号集合非空时 `buffer` 为 NULL;`-8` `len` 小于量子比特数量。 + +### circuit_cfg_classical_vars_len(ptr) + +返回可变经典变量静态类型表的长度。 + +- `ptr` (`const struct CCircuitCFG*`):CFG 句柄。 + +返回:变量数量;`ptr` 为 NULL 时返回 0。 + +### circuit_cfg_classical_vars(ptr, buffer, len) + +把可变经典变量类型按 `(tag, width)` 快照拷贝进 `buffer`(两步式:先 `circuit_cfg_classical_vars_len`)。`tag` 取 `CQLIB_CLASSICAL_TYPE_BIT` / `CQLIB_CLASSICAL_TYPE_BOOL` / `CQLIB_CLASSICAL_TYPE_UINT` / `CQLIB_CLASSICAL_TYPE_BIT_VEC` 之一;手工创建的 CFG 该表为空,`circuit_cfg_from_circuit` 创建的 CFG 保存源线路的静态快照。 + +- `ptr` (`const struct CCircuitCFG*`):CFG 句柄。 +- `buffer` (`struct CClassicalType*`):输出数组,元素为 `{ uint32_t tag; uint32_t width; }`。 +- `len` (`uintptr_t`):缓冲区容量。 + +返回:`0` 成功;`-1` `ptr` 为 NULL,或表非空时 `buffer` 为 NULL;`-8` `len` 不足。 + +### circuit_cfg_classical_values_len(ptr) + +返回不可变经典值静态类型表的长度。 + +- `ptr` (`const struct CCircuitCFG*`):CFG 句柄。 + +返回:经典值数量;`ptr` 为 NULL 时返回 0。 + +### circuit_cfg_classical_values(ptr, buffer, len) + +把不可变经典值类型(如测量产生的 `BitVec`)按 `(tag, width)` 快照拷贝进 `buffer`,约定与 `circuit_cfg_classical_vars` 相同。 + +- `ptr` (`const struct CCircuitCFG*`):CFG 句柄。 +- `buffer` (`struct CClassicalType*`):输出数组。 +- `len` (`uintptr_t`):缓冲区容量。 + +返回:`0` 成功;`-1` `ptr` 为 NULL,或表非空时 `buffer` 为 NULL;`-8` `len` 不足。 + +### circuit_cfg_entry_block(ptr) + +返回当前指定的入口块索引。 + +- `ptr` (`const struct CCircuitCFG*`):CFG 句柄。 + +返回:入口块索引;尚未设置入口块或 `ptr` 为 NULL 时返回 `UINT32_MAX`。 + +--- + +## 块与操作 + +### circuit_cfg_add_block(ptr) + +向图末尾添加一个空块。 + +- `ptr` (`struct CCircuitCFG*`):CFG 句柄。 + +返回:新块的稳定节点索引;`ptr` 为 NULL 时返回 `UINT32_MAX`。 + +### circuit_cfg_push_operation(ptr, node, op) + +向块末尾追加一条普通(非控制流)操作;操作被克隆,源句柄仍归调用者所有。 + +- `ptr` (`struct CCircuitCFG*`):CFG 句柄。 +- `node` (`uint32_t`):块索引。 +- `op` (`const struct COperation*`):操作句柄(用 `operation_new` 创建,见 [操作与指令](4_operation_instruction.md))。 + +返回:`0` 成功;`-1` `ptr` 或 `op` 为 NULL;`-2` 块不存在。 + +### circuit_cfg_extend_operations(ptr, node, ops, len) + +向块 `node` 末尾批量追加克隆的操作。`ops` 中的每个句柄都会在修改前先完成校验与克隆,因此只要任一元素为 NULL,块就保持原样。 + +- `ptr` (`struct CCircuitCFG*`):CFG 句柄。 +- `node` (`uint32_t`):块索引。 +- `ops` (`const struct COperation* const*`):操作句柄数组。 +- `len` (`uintptr_t`):操作数量(可为 0)。 + +返回:`0` 成功;`-1` NULL 输入;`-2` `node` 不指向任何块。 + +### circuit_cfg_blocks(ptr, buffer, len) + +按图顺序把全部块的稳定节点索引拷贝进 `buffer`(两步式:先 `circuit_cfg_num_blocks`)。 + +- `ptr` (`const struct CCircuitCFG*`):CFG 句柄。 +- `buffer` (`uint32_t*`):输出缓冲区。 +- `len` (`uintptr_t`):缓冲区容量。 + +返回:`0` 成功;`-1` `ptr` 为 NULL,或图非空时 `buffer` 为 NULL;`-8` `len` 小于块数量。 + +### circuit_cfg_block_label(ptr, node) + +读取块的诊断标签。`circuit_cfg_from_circuit` 展开的块带有结构化标签(如 `entry`、`while_body_…`)。 + +- `ptr` (`const struct CCircuitCFG*`):CFG 句柄。 +- `node` (`uint32_t`):块索引。 + +返回:新分配的 C 字符串(用 `cqlib_string_free` 释放);块无标签、不存在或 `ptr` 为 NULL 时返回 NULL。 + +### circuit_cfg_with_label(ptr, node, label) + +设置或替换块 `node` 的标签。 + +- `ptr` (`struct CCircuitCFG*`):CFG 句柄。 +- `node` (`uint32_t`):块索引。 +- `label` (`const char*`):新标签字符串。 + +返回:`0` 成功;`-1` NULL 输入;`-2` `node` 不指向任何块;`-4` `label` 不是合法 UTF-8。 + +### circuit_cfg_block_is_empty(ptr, node) + +判断块是否既无操作也无终结符。 + +- `ptr` (`const struct CCircuitCFG*`):CFG 句柄。 +- `node` (`uint32_t`):块索引。 + +返回:`1` 为空;`0` 非空;`-1` `ptr` 为 NULL;`-2` 块不存在。 + +### circuit_cfg_block_operations_len(ptr, node) + +返回块内普通操作数量(终结符不计入)。 + +- `ptr` (`const struct CCircuitCFG*`):CFG 句柄。 +- `node` (`uint32_t`):块索引。 + +返回:操作数量;`ptr` 为 NULL 或块不存在时返回 0。 + +### circuit_cfg_block_operations(ptr, node, out, len) + +把块内普通操作克隆进 `out`(两步式:先 `circuit_cfg_block_operations_len`)。每个写出元素是新分配的 `COperation*`,需分别用 `operation_free` 释放。 + +- `ptr` (`const struct CCircuitCFG*`):CFG 句柄。 +- `node` (`uint32_t`):块索引。 +- `out` (`struct COperation**`):输出数组。 +- `len` (`uintptr_t`):数组容量。 + +返回:`0` 成功;`-1` `ptr` 为 NULL,或操作非空时 `out` 为 NULL;`-2` 块不存在;`-8` `len` 不足。 + +--- + +## 终结符 + +合法 CFG 中,每个基本块必须恰有一个终结符,且其出边数量与类型和终结符一致;`Return` 不允许出边。`Branch` / `ForLoop` / `Switch` 终结符由 `circuit_cfg_from_circuit` 展开 `circuit_if_else` / `circuit_for_uint` / `circuit_switch` 等结构化线路生成,见 [经典数据与控制流](9_classical_control_flow.md);`Jump` / `Return` / `Break` / `Continue` 可用下列四个设置接口替换。 + +### circuit_cfg_block_has_terminator(ptr, node) + +判断块是否已设置终结符。 + +- `ptr` (`const struct CCircuitCFG*`):CFG 句柄。 +- `node` (`uint32_t`):块索引。 + +返回:`1` 已设置;`0` 未设置;`-1` `ptr` 为 NULL;`-2` 块不存在。 + +### circuit_cfg_block_terminator_tag(ptr, node) + +返回块终结符标签。 + +- `ptr` (`const struct CCircuitCFG*`):CFG 句柄。 +- `node` (`uint32_t`):块索引。 + +返回:`CFG_TERMINATOR_*` 常量之一;未设置终结符时返回 `0`;`-1` `ptr` 为 NULL;`-2` 块不存在。 + +### circuit_cfg_terminator_target(ptr, node) + +返回 `Jump` / `Break` / `Continue` 终结符的跳转目标块。 + +- `ptr` (`const struct CCircuitCFG*`):CFG 句柄。 +- `node` (`uint32_t`):块索引。 + +返回:目标块索引;其他终结符、未设置终结符、块不存在或 `ptr` 为 NULL 时返回 `UINT32_MAX`。 + +### circuit_cfg_terminator_condition(ptr, node) + +克隆 `Branch` / `Switch` 终结符的条件表达式。 + +- `ptr` (`const struct CCircuitCFG*`):CFG 句柄。 +- `node` (`uint32_t`):块索引。 + +返回:新分配的 `CClassicalExpr*`(用 `classical_expr_free` 释放);`ptr` 为 NULL、块不存在、未设置终结符或终结符不带条件时返回 NULL。 + +### circuit_cfg_terminator_for_info(ptr, node, var, start, stop, step) + +写出 `ForLoop` 终结符的循环变量与 start / stop / step 三个表达式(半开区间 `[start, stop)`)。四个句柄均为克隆:`var` 用 `classical_var_free` 释放,三个表达式用 `classical_expr_free` 释放。 + +- `ptr` (`const struct CCircuitCFG*`):CFG 句柄。 +- `node` (`uint32_t`):块索引。 +- `var` (`struct CClassicalVar**`):循环变量输出。 +- `start` (`struct CClassicalExpr**`):起始表达式输出。 +- `stop` (`struct CClassicalExpr**`):结束表达式输出。 +- `step` (`struct CClassicalExpr**`):步长表达式输出。 + +返回:`0` 成功;`-1` `ptr` 为 NULL 或任一输出指针为 NULL;`-2` 块不存在;`-8` 终结符不是 `ForLoop`。 + +### circuit_cfg_set_terminator_jump(ptr, node, target) + +把块终结符替换为指向 `target` 的无条件跳转。 + +- `ptr` (`struct CCircuitCFG*`):CFG 句柄。 +- `node` (`uint32_t`):块索引。 +- `target` (`uint32_t`):目标块索引。 + +返回:`0` 成功;`-1` `ptr` 为 NULL;`-2` 块不存在。 + +### circuit_cfg_set_terminator_return(ptr, node) + +把块终结符替换为 `Return`(执行结束)。 + +- `ptr` (`struct CCircuitCFG*`):CFG 句柄。 +- `node` (`uint32_t`):块索引。 + +返回:`0` 成功;`-1` `ptr` 为 NULL;`-2` 块不存在。 + +### circuit_cfg_set_terminator_break(ptr, node, target) + +把块终结符替换为结构化 `Break`,指向所跳出循环或 `switch` 结构的头块。 + +- `ptr` (`struct CCircuitCFG*`):CFG 句柄。 +- `node` (`uint32_t`):块索引。 +- `target` (`uint32_t`):头块索引。 + +返回:`0` 成功;`-1` `ptr` 为 NULL;`-2` 块不存在。 + +### circuit_cfg_set_terminator_continue(ptr, node, target) + +把块终结符替换为结构化 `Continue`,指向循环头块。 + +- `ptr` (`struct CCircuitCFG*`):CFG 句柄。 +- `node` (`uint32_t`):块索引。 +- `target` (`uint32_t`):循环头块索引。 + +返回:`0` 成功;`-1` `ptr` 为 NULL;`-2` 块不存在。 + +--- + +## 边与遍历 + +### circuit_cfg_add_edge(ptr, source, target, flow, case_lo, case_hi) + +在两个已存在的块之间添加一条有向语义边。 + +- `ptr` (`struct CCircuitCFG*`):CFG 句柄。 +- `source` (`uint32_t`):起点块索引。 +- `target` (`uint32_t`):终点块索引。 +- `flow` (`uint32_t`):`CFG_FLOW_*` 标签之一。 +- `case_lo` (`uint64_t`):`CFG_FLOW_CASE` 的 128 位 case 值低 64 位(最低有效),其余标签传 0。 +- `case_hi` (`uint64_t`):case 值高 64 位。 + +返回:`0` 成功;`-1` `ptr` 为 NULL;`-2` 任一端点不存在(含哨兵值);`-8` 未知 `flow` 标签。 + +### circuit_cfg_set_entry_block(ptr, node) + +设置图入口块;索引是否指向真实块由 `circuit_cfg_validate` 检查。 + +- `ptr` (`struct CCircuitCFG*`):CFG 句柄。 +- `node` (`uint32_t`):入口块索引。 + +返回:`0` 成功;`-1` `ptr` 为 NULL;`-2` `node` 为哨兵值 `UINT32_MAX`。 + +### circuit_cfg_outgoing_edges_len(ptr, node) + +返回块的出边数量。 + +- `ptr` (`const struct CCircuitCFG*`):CFG 句柄。 +- `node` (`uint32_t`):块索引。 + +返回:出边数量;`ptr` 为 NULL 或块不存在时返回 0。 + +### circuit_cfg_outgoing_edges(ptr, node, out_targets, flow_tags, case_lo, case_hi, len) + +把块出边拷贝进四个并行数组(两步式:先 `circuit_cfg_outgoing_edges_len`)。边按插入顺序写出,条件头块的真分支在前。 + +- `ptr` (`const struct CCircuitCFG*`):CFG 句柄。 +- `node` (`uint32_t`):块索引。 +- `out_targets` (`uint32_t*`):目标块索引数组。 +- `flow_tags` (`uint32_t*`):`CFG_FLOW_*` 标签数组。 +- `case_lo` (`uint64_t*`):`CFG_FLOW_CASE` 边的 128 位 case 值低 64 位,其余边写 0。 +- `case_hi` (`uint64_t*`):case 值高 64 位数组。 +- `len` (`uintptr_t`):数组容量。 + +返回:`0` 成功;`-1` `ptr` 为 NULL,或出边非空时任一数组为 NULL;`-2` 块不存在;`-8` `len` 不足。 + +--- + +## 区域与循环 + +控制流区域记录结构化控制流的边界与语义:仅靠图边无法完整恢复 `if` / `while` / `for` / `switch` 的结构化语义,区域元数据描述每个结构的分支入口、退出块与汇合块。修改图结构时应同步维护区域元数据——保持区域与图边一致是 `circuit_cfg_to_circuit` 成功还原结构化线路的关键。 + +### circuit_cfg_region_tag(ptr, node) + +返回块拥有的控制流区域标签。 + +- `ptr` (`const struct CCircuitCFG*`):CFG 句柄。 +- `node` (`uint32_t`):块索引。 + +返回:`CFG_REGION_*` 常量之一;块无区域时返回 `0`;`-1` `ptr` 为 NULL;`-2` 块不存在。 + +### circuit_cfg_is_loop_header(ptr, node) + +判断块是否为循环头:拥有 `While` 或 `For` 区域返回 `1`;`Switch` 区域入口不算循环头。 + +- `ptr` (`const struct CCircuitCFG*`):CFG 句柄。 +- `node` (`uint32_t`):块索引。 + +返回:`1` 是;`0` 否;`-1` `ptr` 为 NULL;`-2` 块不存在。 + +### circuit_cfg_region_if(ptr, node, then_entry, else_entry, merge_block, has_else) + +写出 `If` 区域布局。 + +- `ptr` (`const struct CCircuitCFG*`):CFG 句柄。 +- `node` (`uint32_t`):头块索引。 +- `then_entry` (`uint32_t*`):then 分支入口块。 +- `else_entry` (`uint32_t*`):else 分支入口块;无 else 体时为汇合块。 +- `merge_block` (`uint32_t*`):汇合块。 +- `has_else` (`int32_t*`):源线路是否存在 else 分支体(1/0)。 + +返回:`0` 成功;`-1` `ptr` 为 NULL 或任一输出指针为 NULL;`-2` 块不存在;`-8` 块没有 `If` 区域。 + +### circuit_cfg_region_loop(ptr, node, body_entry, exit_block) + +写出 `While` / `For` 区域布局。 + +- `ptr` (`const struct CCircuitCFG*`):CFG 句柄。 +- `node` (`uint32_t`):头块索引。 +- `body_entry` (`uint32_t*`):循环体入口块。 +- `exit_block` (`uint32_t*`):退出块。 + +返回:`0` 成功;`-1` `ptr` 为 NULL 或输出指针为 NULL;`-2` 块不存在;`-8` 块没有循环区域。 + +### circuit_cfg_region_switch_cases_len(ptr, node) + +返回 `Switch` 区域显式 case 的数量。 + +- `ptr` (`const struct CCircuitCFG*`):CFG 句柄。 +- `node` (`uint32_t`):头块索引。 + +返回:case 数量;`ptr` 为 NULL、块不存在或区域不是 `Switch` 时返回 0。 + +### circuit_cfg_region_switch(ptr, node, case_lo, case_hi, case_entries, len, default_entry, merge_block, has_default) + +写出 `Switch` 区域布局(两步式:先 `circuit_cfg_region_switch_cases_len`)。 + +- `ptr` (`const struct CCircuitCFG*`):CFG 句柄。 +- `node` (`uint32_t`):头块索引。 +- `case_lo` (`uint64_t*`):case `i` 的 128 位匹配值低 64 位。 +- `case_hi` (`uint64_t*`):case `i` 的 128 位匹配值高 64 位。 +- `case_entries` (`uint32_t*`):case `i` 的分支入口块。 +- `len` (`uintptr_t`):case 数组容量。 +- `default_entry` (`uint32_t*`):默认分支入口块;无默认体时为汇合块。 +- `merge_block` (`uint32_t*`):汇合块。 +- `has_default` (`int32_t*`):源线路是否存在默认分支体(1/0)。 + +返回:`0` 成功;`-1` `ptr` 为 NULL,或必填输出指针为 NULL;`-2` 块不存在;`-8` 块没有 `Switch` 区域,或 `len` 小于 case 数量。 + +### circuit_cfg_set_if_region(ptr, node, then_entry, else_entry, merge_block, has_else, outer_op) + +向头块挂接 `If` 区域。入口块索引是否指向真实块由 `circuit_cfg_validate` 检查。 + +- `ptr` (`struct CCircuitCFG*`):CFG 句柄。 +- `node` (`uint32_t`):头块索引。 +- `then_entry` (`uint32_t`):then 分支入口块。 +- `else_entry` (`uint32_t`):else 分支入口块。 +- `merge_block` (`uint32_t`):汇合块。 +- `has_else` (`int32_t`):是否存在 else 分支体(1/0)。 +- `outer_op` (`const struct COperation*`):外层操作元数据(作用的量子比特、参数、标签),传 NULL 表示空元数据。 + +返回:`0` 成功;`-1` `ptr` 为 NULL;`-2` 头块不存在。 + +### circuit_cfg_set_while_region(ptr, node, body_entry, exit_block, outer_op) + +向头块挂接 `While` 区域。 + +- `ptr` (`struct CCircuitCFG*`):CFG 句柄。 +- `node` (`uint32_t`):头块索引。 +- `body_entry` (`uint32_t`):循环体入口块。 +- `exit_block` (`uint32_t`):退出块。 +- `outer_op` (`const struct COperation*`):外层操作元数据,可为 NULL。 + +返回:`0` 成功;`-1` `ptr` 为 NULL;`-2` 头块不存在。 + +### circuit_cfg_set_for_region(ptr, node, body_entry, exit_block, outer_op) + +向头块挂接 `For` 区域,参数含义与 `circuit_cfg_set_while_region` 相同。 + +- `ptr` (`struct CCircuitCFG*`):CFG 句柄。 +- `node` (`uint32_t`):头块索引。 +- `body_entry` (`uint32_t`):循环体入口块。 +- `exit_block` (`uint32_t`):退出块。 +- `outer_op` (`const struct COperation*`):外层操作元数据,可为 NULL。 + +返回:`0` 成功;`-1` `ptr` 为 NULL;`-2` 头块不存在。 + +### circuit_cfg_set_switch_region(ptr, node, case_lo, case_hi, case_entries, len, default_entry, merge_block, has_default, outer_op) + +向头块挂接 `Switch` 区域:case `i` 的 128 位匹配值由 `case_lo[i]`(低 64 位)与 `case_hi[i]`(高 64 位)组成,`case_entries[i]` 为其分支入口块。 + +- `ptr` (`struct CCircuitCFG*`):CFG 句柄。 +- `node` (`uint32_t`):头块索引。 +- `case_lo` (`const uint64_t*`):各 case 匹配值低 64 位。 +- `case_hi` (`const uint64_t*`):各 case 匹配值高 64 位。 +- `case_entries` (`const uint32_t*`):各 case 分支入口块。 +- `len` (`uintptr_t`):case 数量。 +- `default_entry` (`uint32_t`):默认分支入口块。 +- `merge_block` (`uint32_t`):汇合块。 +- `has_default` (`int32_t`):是否存在默认分支体(1/0)。 +- `outer_op` (`const struct COperation*`):外层操作元数据,可为 NULL。 + +返回:`0` 成功;`-1` `ptr` 为 NULL,或 `len > 0` 时任一 case 数组为 NULL;`-2` 头块不存在。 + +--- + +## 标签常量 + +出边标签(`circuit_cfg_add_edge` 的 `flow`、`circuit_cfg_outgoing_edges` 的 `flow_tags`): + +| 常量 | 值 | 含义 | +| --- | --- | --- | +| `CFG_FLOW_TRUE_BRANCH` | 1 | `if` / `while` / `for` 头块的真分支出边 | +| `CFG_FLOW_FALSE_BRANCH` | 2 | 假分支出边 | +| `CFG_FLOW_UNCONDITIONAL` | 3 | 顺序跳转 / 结构化汇合 | +| `CFG_FLOW_CASE` | 4 | `switch` 精确值匹配边(128 位值在 case 半字中) | +| `CFG_FLOW_DEFAULT_CASE` | 5 | `switch` 默认分支边 | +| `CFG_FLOW_BREAK` | 6 | 结构化 `break` 边 | +| `CFG_FLOW_CONTINUE` | 7 | 结构化 `continue` 边 | + +终结符标签(`circuit_cfg_block_terminator_tag`): + +| 常量 | 值 | 含义 | +| --- | --- | --- | +| `CFG_TERMINATOR_BRANCH` | 1 | 条件分支头(`Branch`) | +| `CFG_TERMINATOR_FOR_LOOP` | 2 | 无符号区间循环头(`ForLoop`) | +| `CFG_TERMINATOR_SWITCH` | 3 | 精确值多路分支头(`Switch`) | +| `CFG_TERMINATOR_JUMP` | 4 | 无条件跳转 | +| `CFG_TERMINATOR_BREAK` | 5 | 结构化 `break` | +| `CFG_TERMINATOR_CONTINUE` | 6 | 结构化 `continue` | +| `CFG_TERMINATOR_RETURN` | 7 | 执行结束(`Return`) | + +区域标签(`circuit_cfg_region_tag`): + +| 常量 | 值 | 含义 | +| --- | --- | --- | +| `CFG_REGION_IF` | 1 | 结构化条件区域 | +| `CFG_REGION_WHILE` | 2 | 结构化 `while` 循环区域 | +| `CFG_REGION_FOR` | 3 | 结构化区间循环区域 | +| `CFG_REGION_SWITCH` | 4 | 结构化精确值 `switch` 区域 | + +--- + +## 示例:构建 if-else CFG 并转回线路 + +先在线路层构造 `if (flag) { x(1) } else { z(1) }`,展开为 CFG 后读取 `If` 区域布局与头块出边,最后校验并还原为结构化线路: + +```c +#include +#include +#include "cqlib_c.h" + +/* then 分支体:X(1) */ +static int32_t append_x(struct CCircuit* circuit, void* user_data) { + (void)user_data; + return circuit_x(circuit, 1); +} + +/* else 分支体:Z(1) */ +static int32_t append_z(struct CCircuit* circuit, void* user_data) { + (void)user_data; + return circuit_z(circuit, 1); +} + +int main(void) { + struct CCircuit* circuit = circuit_new(2); + struct CClassicalVar* flag = circuit_var(circuit, CQLIB_CLASSICAL_TYPE_BOOL, 0); + struct CClassicalExpr* condition = classical_expr_var(flag); + if (circuit_if_else(circuit, condition, append_x, append_z, NULL) != 0) { + return 1; + } + + /* 1. 展开为 CFG */ + struct CCircuitCFG* cfg = circuit_cfg_from_circuit(circuit); + if (cfg == NULL) { + return 1; + } + + /* 2. 定位 if 头块,读取区域布局与出边 */ + uint32_t then_entry = UINT32_MAX; + uint32_t else_entry = UINT32_MAX; + uint32_t merge_block = UINT32_MAX; + int32_t has_else = 0; + uint32_t block_count = (uint32_t)circuit_cfg_num_blocks(cfg); + + for (uint32_t node = 0; node < block_count; node++) { + if (circuit_cfg_region_tag(cfg, node) == CFG_REGION_IF) { + circuit_cfg_region_if(cfg, node, &then_entry, &else_entry, + &merge_block, &has_else); + + /* 头块终结符为 Branch,两条出边按插入顺序:真分支在前 */ + uint32_t targets[2] = {0, 0}; + uint32_t tags[2] = {0, 0}; + uint64_t lo[2] = {0, 0}; + uint64_t hi[2] = {0, 0}; + circuit_cfg_outgoing_edges(cfg, node, targets, tags, lo, hi, 2); + /* targets[0] == then_entry 且 tags[0] == CFG_FLOW_TRUE_BRANCH, + targets[1] == else_entry 且 tags[1] == CFG_FLOW_FALSE_BRANCH */ + break; + } + } + + /* 3. 校验并转回结构化线路 */ + struct CCircuit* lowered = NULL; + if (circuit_cfg_validate(cfg) == 0) { + lowered = circuit_cfg_to_circuit(cfg); + } + + circuit_free(lowered); + circuit_cfg_free(cfg); + classical_expr_free(condition); + classical_var_free(flag); + circuit_free(circuit); + return 0; +} +``` + +线路构造接口(`circuit_new`、`circuit_if_else` 等)见 [线路](1_circuit.md),经典表达式构造(`classical_expr_var` 等)见 [经典数据与控制流](9_classical_control_flow.md)。 diff --git a/docs/api/cn/c/0_circuit/13_circuit_to_matrix.md b/docs/api/cn/c/0_circuit/13_circuit_to_matrix.md new file mode 100644 index 00000000..f5bd33dd --- /dev/null +++ b/docs/api/cn/c/0_circuit/13_circuit_to_matrix.md @@ -0,0 +1,116 @@ +# 线路转矩阵 + +`circuit_to_matrix_len` 与 `circuit_to_matrix` 把数值线路转换为其酉矩阵:`n` 个量子比特对应 `2^n × 2^n` 维矩阵,行主序、交错 `(re, im)` 双精度输出,并包含线路的全局相位。矩阵规模随量子比特数按 `4^n` 增长,适合小规模线路。保留符号表达式的转换见 [符号矩阵](10_symbolic_matrix.md);错误码、句柄释放与两步式数组输出等通用约定见 [Overview](../0_overview.md)。 + +--- + +## 两步式输出 + +### circuit_to_matrix_len(ptr, qubits_order, order_len) + +返回酉矩阵的复数元素个数(行数 × 列数),用于分配输出缓冲区。传 `qubits_order` = NULL 使用默认排序。 + +- `ptr` (`const struct CCircuit*`):线路句柄。 +- `qubits_order` (`const uintptr_t*`):量子比特顺序数组,可为 NULL。 +- `order_len` (`uintptr_t`):顺序数组长度。 + +返回:复数元素个数;`ptr` 为 NULL 或转换失败(含未解析符号参数、非酉操作、量子比特顺序非法等)时返回 0。 + +### circuit_to_matrix(ptr, qubits_order, order_len, out, buffer_len) + +把酉矩阵按交错 `(实部, 虚部)` 双精度拷贝进 `out`,返回写入的复数元素个数。 + +- `ptr` (`const struct CCircuit*`):线路句柄。 +- `qubits_order` (`const uintptr_t*`):量子比特顺序数组,可为 NULL。 +- `order_len` (`uintptr_t`):顺序数组长度。 +- `out` (`double*`):输出缓冲区,须容纳至少 `2 × circuit_to_matrix_len(...)` 个 double。 +- `buffer_len` (`uintptr_t`):缓冲区容量(double 数量)。 + +返回:写入的复数元素个数;`ptr` 或 `out` 为 NULL、`buffer_len` 不足(写入前拒绝)或转换失败时返回 0。 + +第 `i` 个复数元素为 `{ out[2*i], out[2*i+1] }`,位于矩阵第 `i / 2^n` 行、第 `i % 2^n` 列(行主序)。 + +--- + +## 量子比特顺序 + +`qubits_order` 指定矩阵基态索引中量子比特的排列,必须与线路的量子比特集合完全一致——不能遗漏、重复或包含未知量子比特,否则视为转换失败(两个接口均返回 0)。 + +- `NULL` / `order_len == 0`:默认按量子比特编号升序排列; +- 顺序中第一个量子比特对应基态索引的最低有效位(little-endian),默认排序下即编号最小的量子比特(如 qubit 0)为最低位。 + +```c +/* 显式指定:qubit 1 为最低位 */ +uintptr_t order[2] = {1, 0}; +uintptr_t len = circuit_to_matrix_len(circuit, order, 2); +``` + +--- + +## 参数化线路 + +数值矩阵要求线路中的符号参数均已解析为具体数值。线路含未解析符号参数时,两个接口均返回 0;先用 `circuit_assign_params` 绑定参数(见 [线路](1_circuit.md))再转换: + +```c +struct CParameter* theta = param_parse("theta"); + +struct CCircuit* circuit = circuit_new(1); +circuit_rx_param(circuit, 0, theta); + +/* theta 未绑定:返回 0 */ +uintptr_t unbound = circuit_to_matrix_len(circuit, NULL, 0); + +/* 绑定 theta = 0.5 后转换:1 个量子比特 → 2 × 2 = 4 */ +struct CCircuit* bound = circuit_assign_params(circuit, "theta:0.5"); +uintptr_t len = circuit_to_matrix_len(bound, NULL, 0); +``` + +--- + +## 全局相位 + +矩阵包含线路的全局相位 `theta`:设不含全局相位时线路矩阵为 `U`,则转换结果为 `exp(i * theta) * U`。 + +--- + +## 示例 + +两步式取回 2 量子比特线路(H + CX)的 4 × 4 酉矩阵: + +```c +#include +#include +#include +#include "cqlib_c.h" + +int main(void) { + struct CCircuit* circuit = circuit_new(2); + circuit_h(circuit, 0); + circuit_cx(circuit, 0, 1); + + /* 第一步:查询复数元素个数(2^2 × 2^2 = 16) */ + uintptr_t len = circuit_to_matrix_len(circuit, NULL, 0); + if (len == 0) { + circuit_free(circuit); + return 1; + } + + /* 第二步:分配至少 2 * len 个 double 并拷贝 */ + double* buffer = (double*)malloc(len * 2 * sizeof(double)); + uintptr_t written = circuit_to_matrix(circuit, NULL, 0, buffer, len * 2); + if (written != len) { + free(buffer); + circuit_free(circuit); + return 1; + } + + /* 例如读取第 0 行第 3 列元素:位于 buffer[6](实部)与 buffer[7](虚部) */ + printf("U[0][3] = %g + %gi\n", buffer[6], buffer[7]); + + free(buffer); + circuit_free(circuit); + return 0; +} +``` + +线路构造与参数绑定接口(`circuit_new`、`circuit_assign_params` 等)见 [线路](1_circuit.md)。 diff --git a/docs/api/cn/c/0_circuit/14_circuit_dag.md b/docs/api/cn/c/0_circuit/14_circuit_dag.md new file mode 100644 index 00000000..70d76927 --- /dev/null +++ b/docs/api/cn/c/0_circuit/14_circuit_dag.md @@ -0,0 +1,943 @@ +# 线路 DAG + +`CCircuitDag*` 是线路在操作依赖层面的视图:节点表示一条操作,边表示两条操作因共享同一份资源而产生的先后顺序。可共享的资源包括量子比特、可变经典变量、不可变经典值,以及承载无具体数据资源之操作的全局序;每条线在图中对应一对输入 / 输出哨兵节点。典型流程:`circuit_dag_from_circuit` → 检查与校验 → `circuit_dag_to_circuit`;DAG 还支持深入查询(节点识别、遍历、分层、线时间线、门连续段、控制流),并在还原回线路之前就地修改(追加、删除、替换)。错误码、句柄释放与两步式数组输出等通用约定见 [Overview](../0_overview.md)。 + +--- + +## 构造与释放 + +### circuit_dag_from_circuit(ptr) + +从已有线路构造依赖 DAG,同时复制线路的量子比特、参数表、符号名、经典变量与经典值类型表以及全局相位;构造结束时 DAG 已通过内部校验。 + +- `ptr` (`const struct CCircuit*`):源线路句柄。 + +返回:新分配的 `CCircuitDag*`(用 `circuit_dag_free` 释放);`ptr` 为 NULL 或构造失败时返回 NULL。 + +### circuit_dag_from_operations(qubits, qubits_len, ops, ops_len) + +从显式给定的量子比特与操作切片构造依赖 DAG,面向局部分析的窄入口:不接收参数表、经典声明与全局相位;`qubits` 的每个元素直接作为量子比特编号,每条操作必须作用于已注册编号,重复编号、越界引用或非自包含操作(含参数表索引、经典变量 / 值引用或控制流体)都会使构造失败。 + +- `qubits` (`const uint32_t*`):量子比特编号数组。 +- `qubits_len` (`uintptr_t`):量子比特数量。 +- `ops` (`const struct COperation* const*`):操作句柄数组(用 `operation_new` 创建,见 [操作与指令](4_operation_instruction.md))。 +- `ops_len` (`uintptr_t`):操作数量。 + +返回:新分配的 `CCircuitDag*`(用 `circuit_dag_free` 释放);输入数组为 NULL 而其长度为正(`qubits_len > 0` 时 `qubits` 为 NULL、`ops_len > 0` 时 `ops` 为 NULL、数组含 NULL 元素)或构造失败时返回 NULL。 + +### circuit_dag_free(ptr) + +释放 DAG 句柄。 + +- `ptr` (`struct CCircuitDag*`):待释放句柄,允许传 NULL。 + +--- + +## 转换与校验 + +### circuit_dag_to_circuit(ptr) + +按源顺序从 DAG 重建线路,并恢复量子比特集合、经典类型表与全局相位;源句柄保持存活且不变。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 + +返回:新分配的 `CCircuit*`(用 `circuit_free` 释放);`ptr` 为 NULL 或还原失败(图含环、非法参数索引等)时返回 NULL。 + +### circuit_dag_validate(ptr) + +校验 DAG 的一致性:图是否含环、每条线的输入输出哨兵是否合法、操作引用的量子比特与参数索引是否有效、边承载的资源是否合法等。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 + +返回:`0` 成功;`-1` `ptr` 为 NULL;`-3` 图含环或非法。 + +--- + +## 元数据与统计 + +### circuit_dag_num_qubits(ptr) + +返回量子比特数量。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 + +返回:量子比特数量;`ptr` 为 NULL 时返回 0。 + +### circuit_dag_num_ops(ptr) + +返回操作节点数量。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 + +返回:操作节点数量;`ptr` 为 NULL 时返回 0。 + +### circuit_dag_is_empty(ptr) + +判断 DAG 是否没有操作节点。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 + +返回:`1` 为空;`0` 非空;`-1` `ptr` 为 NULL。 + +### circuit_dag_qubits_len(ptr) + +返回 DAG 中量子比特编号的数量,用于分配输出缓冲区。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 + +返回:编号数量;`ptr` 为 NULL 时返回 0。 + +### circuit_dag_qubits(ptr, buffer, len) + +按注册顺序把量子比特编号拷贝进 `buffer`(两步式:先 `circuit_dag_qubits_len`)。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 +- `buffer` (`uint32_t*`):输出缓冲区。 +- `len` (`uintptr_t`):缓冲区容量。 + +返回:`0` 成功;`-1` `ptr` 为 NULL,或编号非空时 `buffer` 为 NULL;`-8` `len` 不足。 + +### circuit_dag_parameters_len(ptr) + +返回已注册参数的数量。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 + +返回:参数数量;`ptr` 为 NULL 时返回 0。 + +### circuit_dag_parameters(ptr, out, len) + +按注册顺序把参数名拷贝进 `out`(两步式:先 `circuit_dag_parameters_len`)。每个写出元素是新分配的 C 字符串,需分别用 `cqlib_string_free` 释放。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 +- `out` (`char**`):输出数组。 +- `len` (`uintptr_t`):数组容量。 + +返回:`0` 成功;`-1` `ptr` 为 NULL,或参数非空时 `out` 为 NULL;`-8` `len` 不足。 + +### circuit_dag_add_parameter(ptr, param, out_index, out_inserted) + +把 `param` 的拷贝驻留到 DAG 的参数表中:参数不存在时插入,并注册其全部符号名。 + +- `ptr` (`struct CCircuitDag*`):DAG 句柄。 +- `param` (`const struct CParameter*`):参数句柄,用 `param_parse` 创建(见 [符号参数](3_parameter.md))。 +- `out_index` (`uintptr_t*`):参数在表中的索引的可选输出;允许传 NULL。 +- `out_inserted` (`int32_t*`):可选输出标志;`1` 表示本次新插入,`0` 表示已存在;允许传 NULL。 + +返回:`0` 成功;`-1` `ptr` 或 `param` 为 NULL。 + +### circuit_dag_symbols_len(ptr) + +返回 DAG 引用的自由符号名数量。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 + +返回:符号数量;`ptr` 为 NULL 时返回 0。 + +### circuit_dag_symbols(ptr, out, len) + +把符号名拷贝进 `out`(两步式:先 `circuit_dag_symbols_len`)。每个写出元素是新分配的 C 字符串,需分别用 `cqlib_string_free` 释放。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 +- `out` (`char**`):输出数组。 +- `len` (`uintptr_t`):数组容量。 + +返回:`0` 成功;`-1` `ptr` 为 NULL,或符号非空时 `out` 为 NULL;`-8` `len` 不足。 + +--- + +## 线 + +边承载的资源称为线(wire),同时也是线的身份。量子比特线在构造时全部物化;经典变量线与经典值线只在被操作引用时物化;全局序线始终存在。线由 `(tag, id)` 标识: + +| 常量 | 值 | 含义 | +| --- | --- | --- | +| `DAG_WIRE_QUBIT` | 0 | 量子比特线,`id` 为量子比特编号 | +| `DAG_WIRE_CLASSICAL_VAR` | 1 | 可变经典变量线,`id` 为变量编号 | +| `DAG_WIRE_CLASSICAL_VALUE` | 2 | 不可变经典值线,`id` 为值索引 | +| `DAG_WIRE_GLOBAL_ORDER` | 3 | 全局序线,无载荷(`id` 为 0) | + +### circuit_dag_wires_len(ptr) + +返回 DAG 中已物化线的数量。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 + +返回:线数量;`ptr` 为 NULL 时返回 0。 + +### circuit_dag_wires(ptr, tags, ids, len) + +把已物化线的标签与载荷编号拷贝进 `tags` 与 `ids` 两个并行数组(两步式:先 `circuit_dag_wires_len`)。全局序线在前,其后依次为各条量子比特线;`DAG_WIRE_GLOBAL_ORDER` 线的 `id` 为 0。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 +- `tags` (`uint32_t*`):`DAG_WIRE_*` 标签数组。 +- `ids` (`uint32_t*`):载荷编号数组。 +- `len` (`uintptr_t`):数组容量。 + +返回:`0` 成功;`-1` `ptr` 为 NULL,或线非空时任一数组为 NULL;`-8` `len` 不足。 + +### circuit_dag_has_wire(ptr, tag, id) + +判断 DAG 是否已物化给定线。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 +- `tag` (`uint32_t`):`DAG_WIRE_QUBIT`(`id` 为量子比特编号)或 `DAG_WIRE_GLOBAL_ORDER`(`id` 被忽略)。 +- `id` (`uint32_t`):线的载荷编号。 + +返回:`1` 已物化;`0` 未物化;`-1` `ptr` 为 NULL;`-8` 其他 `tag` 取值。 + +--- + +## 节点识别 + +图中每个节点——操作节点或线哨兵——都由一个 `uint32_t` 节点编号标识。以下接口用于区分节点种类并取回存储内容。 + +### circuit_dag_is_operation(ptr, node) + +判断 `node` 是否为操作节点。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 +- `node` (`uint32_t`):节点编号。 + +返回:`1` 为操作节点;`0` 为线哨兵;`-1` `ptr` 为 NULL;`-2` 未知节点编号。 + +### circuit_dag_node_kind(ptr, node) + +以新分配的 C 字符串返回 `node` 的种类:`"wire_in"`、`"wire_out"` 或 `"operation"`。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 +- `node` (`uint32_t`):节点编号。 + +返回:种类字符串(用 `cqlib_string_free` 释放);`ptr` 为 NULL 或节点编号未知时返回 NULL。 + +### circuit_dag_operation(ptr, node) + +返回操作节点所存操作的一份拷贝。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 +- `node` (`uint32_t`):节点编号。 + +返回:新分配的 `COperation*`(用 `operation_free` 释放,见 [操作与指令](4_operation_instruction.md));`ptr` 为 NULL 或该节点不是操作节点时返回 NULL。 + +--- + +## 图遍历 + +所有遍历接口都采用两步式数组输出:先用 `_len` 变体查询数量,再调用拷贝变体。各查询列举的内容有所不同: + +- `circuit_dag_op_nodes` / `circuit_dag_topological_op_nodes` 分别按源顺序和确定性拓扑顺序列举操作节点(并列时按字典序打破,因此顺序在多次运行之间稳定)。 +- `circuit_dag_predecessors` / `circuit_dag_successors` 返回节点的原始图邻居:包含线哨兵,每条连接边计一次(两个节点共享多份资源时,每条线各连一条边——同一条线上绝不产生平行边),且顺序不指定。用 `_len` 变体确定缓冲区大小总是足够的。 +- 按线过滤的查询(`*_on_wire`、`quantum_*`、`classical_*`)只返回操作邻居,已去重并按确定性拓扑键排序。 + +### circuit_dag_op_nodes_len(ptr) + +返回按源顺序计的操作节点数量。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 + +返回:操作节点数量;`ptr` 为 NULL 时返回 0。 + +### circuit_dag_op_nodes(ptr, buffer, len) + +按源顺序把操作节点编号拷贝进 `buffer`(两步式:先 `circuit_dag_op_nodes_len`)。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 +- `buffer` (`uint32_t*`):输出缓冲区。 +- `len` (`uintptr_t`):缓冲区容量。 + +返回:`0` 成功;`-1` `ptr` 为 NULL,或节点非空时 `buffer` 为 NULL;`-8` `len` 不足。 + +### circuit_dag_topological_op_nodes_len(ptr) + +返回按确定性拓扑顺序计的操作节点数量。图含环时同样返回 0,因此在图健康状况未知时应与 `circuit_dag_validate` 配合使用。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 + +返回:操作节点数量;`ptr` 为 NULL 或图含环时返回 0。 + +### circuit_dag_topological_op_nodes(ptr, buffer, len) + +按确定性拓扑顺序把操作节点编号拷贝进 `buffer`(两步式:先 `circuit_dag_topological_op_nodes_len`)。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 +- `buffer` (`uint32_t*`):输出缓冲区。 +- `len` (`uintptr_t`):缓冲区容量。 + +返回:`0` 成功;`-1` `ptr` 为 NULL,或节点非空时 `buffer` 为 NULL;`-3` 图含环;`-8` `len` 不足。 + +### circuit_dag_predecessors_len(ptr, node) + +返回 `node` 的原始直接前驱条目数——包含哨兵,每条连接边计一条(见本节开头的说明)。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 +- `node` (`uint32_t`):节点编号。 + +返回:条目数量;`ptr` 为 NULL 或 `node` 未知时返回 0。 + +### circuit_dag_predecessors(ptr, node, buffer, len) + +把 `node` 的原始直接前驱拷贝进 `buffer`(两步式:先 `circuit_dag_predecessors_len`)。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 +- `node` (`uint32_t`):节点编号。 +- `buffer` (`uint32_t*`):输出缓冲区。 +- `len` (`uintptr_t`):缓冲区容量。 + +返回:`0` 成功;`-1` `ptr` 为 NULL,或条目非空时 `buffer` 为 NULL;`-2` 未知节点编号;`-8` `len` 不足。 + +### circuit_dag_successors_len(ptr, node) + +返回 `node` 的原始直接后继条目数——包含哨兵,每条连接边计一条。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 +- `node` (`uint32_t`):节点编号。 + +返回:条目数量;`ptr` 为 NULL 或 `node` 未知时返回 0。 + +### circuit_dag_successors(ptr, node, buffer, len) + +把 `node` 的原始直接后继拷贝进 `buffer`(两步式:先 `circuit_dag_successors_len`)。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 +- `node` (`uint32_t`):节点编号。 +- `buffer` (`uint32_t*`):输出缓冲区。 +- `len` (`uintptr_t`):缓冲区容量。 + +返回:`0` 成功;`-1` `ptr` 为 NULL,或条目非空时 `buffer` 为 NULL;`-2` 未知节点编号;`-8` `len` 不足。 + +### circuit_dag_predecessors_on_wire_len(ptr, node, tag, id) + +返回 `node` 通过线 `(tag, id)` 相连的操作前驱数量;结果已去重并按确定性拓扑键排序。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 +- `node` (`uint32_t`):节点编号。 +- `tag` (`uint32_t`):`DAG_WIRE_QUBIT`(`id` 为量子比特编号)或 `DAG_WIRE_GLOBAL_ORDER`(`id` 被忽略);经典标签携带线路作用域的句柄,C 侧无法重建,将以 `-8` 拒绝。下文所有 `(tag, id)` 线输入同样如此。 +- `id` (`uint32_t`):线的载荷编号。 + +返回:邻居数量;`ptr` 为 NULL、`node` 未知、标签未知或线 / 节点组合非法时返回 0。 + +### circuit_dag_predecessors_on_wire(ptr, node, tag, id, buffer, len) + +把 `node` 通过线 `(tag, id)` 相连的操作前驱拷贝进 `buffer`(两步式:先 `circuit_dag_predecessors_on_wire_len`)。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 +- `node` (`uint32_t`):节点编号。 +- `tag` (`uint32_t`):`DAG_WIRE_QUBIT` 或 `DAG_WIRE_GLOBAL_ORDER`。 +- `id` (`uint32_t`):线的载荷编号。 +- `buffer` (`uint32_t*`):输出缓冲区。 +- `len` (`uintptr_t`):缓冲区容量。 + +返回:`0` 成功;`-1` `ptr` 为 NULL,或邻居非空时 `buffer` 为 NULL;`-2` 未知节点编号;`-3` 线 / 节点组合非法(例如该线不属于本 DAG);`-8` 未知线标签或缓冲区不足。 + +### circuit_dag_successors_on_wire_len(ptr, node, tag, id) + +返回 `node` 通过线 `(tag, id)` 相连的操作后继数量;结果已去重并按确定性拓扑键排序。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 +- `node` (`uint32_t`):节点编号。 +- `tag` (`uint32_t`):`DAG_WIRE_QUBIT` 或 `DAG_WIRE_GLOBAL_ORDER`。 +- `id` (`uint32_t`):线的载荷编号。 + +返回:邻居数量;`ptr` 为 NULL、`node` 未知、标签未知或线 / 节点组合非法时返回 0。 + +### circuit_dag_successors_on_wire(ptr, node, tag, id, buffer, len) + +把 `node` 通过线 `(tag, id)` 相连的操作后继拷贝进 `buffer`(两步式:先 `circuit_dag_successors_on_wire_len`)。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 +- `node` (`uint32_t`):节点编号。 +- `tag` (`uint32_t`):`DAG_WIRE_QUBIT` 或 `DAG_WIRE_GLOBAL_ORDER`。 +- `id` (`uint32_t`):线的载荷编号。 +- `buffer` (`uint32_t*`):输出缓冲区。 +- `len` (`uintptr_t`):缓冲区容量。 + +返回:`0` 成功;`-1` `ptr` 为 NULL,或邻居非空时 `buffer` 为 NULL;`-2` 未知节点编号;`-3` 线 / 节点组合非法;`-8` 未知线标签或缓冲区不足。 + +### circuit_dag_quantum_predecessors_len(ptr, node) + +返回 `node` 通过任意量子比特线相连的操作前驱数量;结果已去重并按确定性拓扑键排序。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 +- `node` (`uint32_t`):节点编号。 + +返回:邻居数量;`ptr` 为 NULL、`node` 未知或查询失败时返回 0。 + +### circuit_dag_quantum_predecessors(ptr, node, buffer, len) + +把 `node` 通过量子比特线相连的操作前驱拷贝进 `buffer`(两步式:先 `circuit_dag_quantum_predecessors_len`)。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 +- `node` (`uint32_t`):节点编号。 +- `buffer` (`uint32_t*`):输出缓冲区。 +- `len` (`uintptr_t`):缓冲区容量。 + +返回:`0` 成功;`-1` `ptr` 为 NULL;`-2` 未知节点编号;`-3` 查询失败;`-8` `len` 不足。 + +### circuit_dag_quantum_successors_len(ptr, node) + +返回 `node` 通过任意量子比特线相连的操作后继数量;结果已去重并按确定性拓扑键排序。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 +- `node` (`uint32_t`):节点编号。 + +返回:邻居数量;`ptr` 为 NULL、`node` 未知或查询失败时返回 0。 + +### circuit_dag_quantum_successors(ptr, node, buffer, len) + +把 `node` 通过量子比特线相连的操作后继拷贝进 `buffer`(两步式:先 `circuit_dag_quantum_successors_len`)。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 +- `node` (`uint32_t`):节点编号。 +- `buffer` (`uint32_t*`):输出缓冲区。 +- `len` (`uintptr_t`):缓冲区容量。 + +返回:`0` 成功;`-1` `ptr` 为 NULL;`-2` 未知节点编号;`-3` 查询失败;`-8` `len` 不足。 + +### circuit_dag_classical_predecessors_len(ptr, node) + +返回 `node` 通过任意经典线(经典变量线与经典值线)相连的操作前驱数量;结果已去重并按确定性拓扑键排序。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 +- `node` (`uint32_t`):节点编号。 + +返回:邻居数量;`ptr` 为 NULL、`node` 未知或查询失败时返回 0。 + +### circuit_dag_classical_predecessors(ptr, node, buffer, len) + +把 `node` 通过经典线相连的操作前驱拷贝进 `buffer`(两步式:先 `circuit_dag_classical_predecessors_len`)。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 +- `node` (`uint32_t`):节点编号。 +- `buffer` (`uint32_t*`):输出缓冲区。 +- `len` (`uintptr_t`):缓冲区容量。 + +返回:`0` 成功;`-1` `ptr` 为 NULL;`-2` 未知节点编号;`-3` 查询失败;`-8` `len` 不足。 + +### circuit_dag_classical_successors_len(ptr, node) + +返回 `node` 通过任意经典线相连的操作后继数量;结果已去重并按确定性拓扑键排序。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 +- `node` (`uint32_t`):节点编号。 + +返回:邻居数量;`ptr` 为 NULL、`node` 未知或查询失败时返回 0。 + +### circuit_dag_classical_successors(ptr, node, buffer, len) + +把 `node` 通过经典线相连的操作后继拷贝进 `buffer`(两步式:先 `circuit_dag_classical_successors_len`)。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 +- `node` (`uint32_t`):节点编号。 +- `buffer` (`uint32_t*`):输出缓冲区。 +- `len` (`uintptr_t`):缓冲区容量。 + +返回:`0` 成功;`-1` `ptr` 为 NULL;`-2` 未知节点编号;`-3` 查询失败;`-8` `len` 不足。 + +--- + +## 分层与深度 + +分层遵循 ASAP(as soon as possible,尽早调度)语义:每条操作落在其操作前驱所在最晚层的下一层,因此第 0 层即前层(front layer),且每条操作恰好出现在一个层中。层内节点保持确定性拓扑顺序。 + +`circuit_dag_layers` 与下文的连续段收集接口采用 CSR 风格的分组输出:`nodes` 接收所有分组的节点编号首尾相接的平铺结果,`offsets` 接收 `groups_len + 1` 个边界值且 `offsets[0] == 0`;第 `k` 组占据 `nodes[offsets[k]..offsets[k+1]]`。`nodes_cap` 应足以容纳所有节点,`offsets_cap` 应足以容纳分组数加一。 + +### circuit_dag_front_layer_len(ptr) + +返回没有操作前驱的操作节点数量——即立即可执行的集合,按确定性拓扑顺序排列。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 + +返回:前层大小;`ptr` 为 NULL 或图含环时返回 0。 + +### circuit_dag_front_layer(ptr, buffer, len) + +把前层操作节点编号拷贝进 `buffer`(两步式:先 `circuit_dag_front_layer_len`)。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 +- `buffer` (`uint32_t*`):输出缓冲区。 +- `len` (`uintptr_t`):缓冲区容量。 + +返回:`0` 成功;`-1` `ptr` 为 NULL,或节点非空时 `buffer` 为 NULL;`-3` 图含环;`-8` `len` 不足。 + +### circuit_dag_layers_len(ptr) + +返回 ASAP 层数,即依赖深度。空 DAG 返回 0。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 + +返回:层数量;`ptr` 为 NULL 或图含环时返回 0。 + +### circuit_dag_layers(ptr, nodes, nodes_cap, offsets, offsets_cap) + +以 CSR 风格拷贝 ASAP 分层:各层节点按层序平铺进 `nodes`,边界写入 `offsets`(见本节开头的说明)。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 +- `nodes` (`uint32_t*`):平铺的层节点;每层内按确定性拓扑顺序排列。 +- `nodes_cap` (`uintptr_t`):`nodes` 容量;必须足以容纳所有操作节点。 +- `offsets` (`uintptr_t*`):层边界,`offsets[0] == 0`。 +- `offsets_cap` (`uintptr_t`):`offsets` 容量;必须足以容纳层数加一。 + +返回:`0` 成功;`-1` `ptr` 为 NULL,或数据非空时任一缓冲区为 NULL;`-3` 图含环;`-8` 任一容量不足。 + +### circuit_dag_node_layers_len(ptr) + +返回按节点计的层条目数量,即操作节点数量。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 + +返回:条目数量;`ptr` 为 NULL 或图含环时返回 0。 + +### circuit_dag_node_layers(ptr, nodes, layers, len) + +把每条操作节点的 ASAP 层号以 `(node, layer)` 对的形式拷贝进并行数组 `nodes` 与 `layers`,按确定性拓扑顺序排列(两步式:先 `circuit_dag_node_layers_len`)。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 +- `nodes` (`uint32_t*`):节点编号。 +- `layers` (`uintptr_t*`):同一位置节点的层号。 +- `len` (`uintptr_t`):两个数组的容量。 + +返回:`0` 成功;`-1` `ptr` 为 NULL,或条目非空时任一数组为 NULL;`-3` 图含环;`-8` `len` 不足。 + +### circuit_dag_depth(ptr) + +返回 ASAP 依赖深度,即层数。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 + +返回:以有符号值表示的深度(空 DAG 为 `0`);`-1` `ptr` 为 NULL;`-3` 图含环。 + +--- + +## 线时间线 + +每条线都是一条时间线:`circuit_dag_wire_in` / `circuit_dag_wire_out` 报告其端点,`circuit_dag_nodes_on_wire` 按线穿引操作的顺序遍历。本节所有 `(tag, id)` 输入均接受 `DAG_WIRE_QUBIT`(`id` 为量子比特编号)与 `DAG_WIRE_GLOBAL_ORDER`(`id` 被忽略);经典标签将以 `-8` 拒绝,因为经典线携带线路作用域的句柄,C 侧无法重建——需要基于节点的经典连通性时,请使用 [图遍历](#图遍历)中的 `circuit_dag_classical_predecessors` / `circuit_dag_classical_successors`。 + +### circuit_dag_is_wire_idle(ptr, tag, id) + +判断该线上是否没有操作节点。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 +- `tag` (`uint32_t`):`DAG_WIRE_QUBIT` 或 `DAG_WIRE_GLOBAL_ORDER`。 +- `id` (`uint32_t`):线的载荷编号。 + +返回:`1` 空闲;`0` 在用;`-1` `ptr` 为 NULL;`-3` 该线不属于本 DAG;`-8` 其他 `tag` 取值。 + +### circuit_dag_nodes_on_wire_len(ptr, tag, id) + +按线上顺序返回线上操作节点的数量;`ptr` 为 NULL、标签未知或该线不属于本 DAG 时返回 0。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 +- `tag` (`uint32_t`):`DAG_WIRE_QUBIT` 或 `DAG_WIRE_GLOBAL_ORDER`。 +- `id` (`uint32_t`):线的载荷编号。 + +返回:节点数量;上述非法状态返回 0。 + +### circuit_dag_nodes_on_wire(ptr, tag, id, buffer, len) + +按线上顺序把线上的操作节点拷贝进 `buffer`(两步式:先 `circuit_dag_nodes_on_wire_len`)。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 +- `tag` (`uint32_t`):`DAG_WIRE_QUBIT` 或 `DAG_WIRE_GLOBAL_ORDER`。 +- `id` (`uint32_t`):线的载荷编号。 +- `buffer` (`uint32_t*`):输出缓冲区。 +- `len` (`uintptr_t`):缓冲区容量。 + +返回:`0` 成功;`-1` `ptr` 为 NULL,或节点非空时 `buffer` 为 NULL;`-3` 该线不属于本 DAG;`-8` 未知线标签或缓冲区不足。 + +### circuit_dag_wire_in(ptr, tag, id) + +返回该线的输入哨兵节点编号。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 +- `tag` (`uint32_t`):`DAG_WIRE_QUBIT` 或 `DAG_WIRE_GLOBAL_ORDER`。 +- `id` (`uint32_t`):线的载荷编号。 + +返回:哨兵节点编号;`-1` `ptr` 为 NULL;`-3` 该线不属于本 DAG;`-8` 其他 `tag` 取值。 + +### circuit_dag_wire_out(ptr, tag, id) + +返回该线的输出哨兵节点编号。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 +- `tag` (`uint32_t`):`DAG_WIRE_QUBIT` 或 `DAG_WIRE_GLOBAL_ORDER`。 +- `id` (`uint32_t`):线的载荷编号。 + +返回:哨兵节点编号;`-1` `ptr` 为 NULL;`-3` 该线不属于本 DAG;`-8` 其他 `tag` 取值。 + +--- + +## 控制流与测量标志 + +### circuit_dag_has_control_flow(ptr) + +判断顶层操作中是否存在结构化控制流(`if` / `while` / `for` / `switch` / `break` / `continue`)。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 + +返回:`1` 为真;`0` 否;`-1` `ptr` 为 NULL。 + +### circuit_dag_has_nested_control_flow(ptr) + +判断是否有控制流体内部还包含结构化控制流。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 + +返回:`1` 为真;`0` 否;`-1` `ptr` 为 NULL。 + +### circuit_dag_has_measurement(ptr) + +判断是否有顶层操作测量量子比特,包括直接测量和控制流体内的递归测量。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 + +返回:`1` 为真;`0` 否;`-1` `ptr` 为 NULL。 + +--- + +## 操作计数 + +### circuit_dag_operation_count_by_name_len(ptr) + +返回顶层操作中不同指令名的数量。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 + +返回:不同名称数量;`ptr` 为 NULL 时返回 0。 + +### circuit_dag_operation_count_by_name(ptr, names, counts, len) + +把顶层操作按名称的计数拷贝进并行数组 `names` 与 `counts`,按首次出现(插入)顺序排列(两步式:先 `circuit_dag_operation_count_by_name_len`)。每个写出的名称是新分配的 C 字符串,需分别用 `cqlib_string_free` 释放。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 +- `names` (`char**`):指令名输出数组。 +- `counts` (`uintptr_t*`):各名称计数的输出数组。 +- `len` (`uintptr_t`):两个数组的容量。 + +返回:`0` 成功;`-1` `ptr` 为 NULL,或条目非空时任一输出数组为 NULL;`-8` `len` 不足。 + +### circuit_dag_operation_count_by_name_recursive_len(ptr) + +返回顶层操作以及(递归地)控制流体内部操作中不同指令名的数量。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 + +返回:不同名称数量;`ptr` 为 NULL 时返回 0。 + +### circuit_dag_operation_count_by_name_recursive(ptr, names, counts, len) + +把顶层与嵌套操作按名称的计数拷贝进并行数组 `names` 与 `counts`,按首次出现(插入)顺序排列(两步式:先 `circuit_dag_operation_count_by_name_recursive_len`)。每个写出的名称是新分配的 C 字符串,需分别用 `cqlib_string_free` 释放。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 +- `names` (`char**`):指令名输出数组。 +- `counts` (`uintptr_t*`):各名称计数的输出数组。 +- `len` (`uintptr_t`):两个数组的容量。 + +返回:`0` 成功;`-1` `ptr` 为 NULL,或条目非空时任一输出数组为 NULL;`-8` `len` 不足。 + +--- + +## 连续段收集 + +一个连续段(run)是沿一条线的时间线上连续的操作节点极大区段,其中每个节点都满足判据;连续段沿该线按时间线顺序报告。判据如下: + +| 常量 | 值 | 含义 | +| --- | --- | --- | +| `DAG_RUN_CRITERION_1Q_GATE` | 0 | 单量子比特门:操作恰好作用于 1 个量子比特,且其指令为 `Standard`、`McGate`、`UnitaryGate` 或 `CircuitGate` | +| `DAG_RUN_CRITERION_2Q_GATE` | 1 | 双量子比特门:在相同指令限制下恰好作用于 2 个量子比特 | + +拷贝变体采用 [分层与深度](#分层与深度)所述的 CSR 风格分组输出:连续段节点首尾相接平铺进 `nodes`,边界写入 `offsets` 且 `offsets[0] == 0`。 + +### circuit_dag_collect_1q_runs_len(ptr) + +返回所有量子比特线上连续单量子比特门段的数量;`ptr` 为 NULL 或收集失败时返回 0。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 + +返回:连续段数量;上述非法状态返回 0。 + +### circuit_dag_collect_1q_runs(ptr, nodes, nodes_cap, offsets, offsets_cap) + +以 CSR 风格拷贝单量子比特门连续段(见本节开头的说明)。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 +- `nodes` (`uint32_t*`):平铺的连续段节点。 +- `nodes_cap` (`uintptr_t`):`nodes` 容量;必须足以容纳所有连续段节点。 +- `offsets` (`uintptr_t*`):连续段边界,`offsets[0] == 0`。 +- `offsets_cap` (`uintptr_t`):`offsets` 容量;必须足以容纳连续段数量加一。 + +返回:`0` 成功;`-1` `ptr` 为 NULL,或数据非空时任一缓冲区为 NULL;`-3` 时间线断裂;`-8` 任一容量不足。 + +### circuit_dag_collect_2q_runs_len(ptr) + +返回所有量子比特线上连续双量子比特门段的数量;`ptr` 为 NULL 或收集失败时返回 0。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 + +返回:连续段数量;上述非法状态返回 0。 + +### circuit_dag_collect_2q_runs(ptr, nodes, nodes_cap, offsets, offsets_cap) + +以 CSR 风格拷贝双量子比特门连续段(见本节开头的说明)。双量子比特门触及两条量子比特线,因此同一节点可能同时出现在其两条线各自的连续段中。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 +- `nodes` (`uint32_t*`):平铺的连续段节点。 +- `nodes_cap` (`uintptr_t`):`nodes` 容量;必须足以容纳所有连续段节点。 +- `offsets` (`uintptr_t*`):连续段边界,`offsets[0] == 0`。 +- `offsets_cap` (`uintptr_t`):`offsets` 容量;必须足以容纳连续段数量加一。 + +返回:`0` 成功;`-1` `ptr` 为 NULL,或数据非空时任一缓冲区为 NULL;`-3` 时间线断裂;`-8` 任一容量不足。 + +### circuit_dag_collect_runs_on_wire_len(ptr, tag, id, criterion) + +返回一条线上匹配 `criterion` 的连续门段数量;`ptr` 为 NULL、标签或判据未知或收集失败时返回 0。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 +- `tag` (`uint32_t`):`DAG_WIRE_QUBIT` 或 `DAG_WIRE_GLOBAL_ORDER`。 +- `id` (`uint32_t`):线的载荷编号。 +- `criterion` (`uint32_t`):`DAG_RUN_CRITERION_1Q_GATE` 或 `DAG_RUN_CRITERION_2Q_GATE`。 + +返回:连续段数量;上述非法状态返回 0。 + +### circuit_dag_collect_runs_on_wire(ptr, tag, id, criterion, nodes, nodes_cap, offsets, offsets_cap) + +以 CSR 风格拷贝一条线上匹配 `criterion` 的连续门段(见本节开头的说明)。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 +- `tag` (`uint32_t`):`DAG_WIRE_QUBIT` 或 `DAG_WIRE_GLOBAL_ORDER`。 +- `id` (`uint32_t`):线的载荷编号。 +- `criterion` (`uint32_t`):`DAG_RUN_CRITERION_1Q_GATE` 或 `DAG_RUN_CRITERION_2Q_GATE`。 +- `nodes` (`uint32_t*`):平铺的连续段节点。 +- `nodes_cap` (`uintptr_t`):`nodes` 容量;必须足以容纳所有连续段节点。 +- `offsets` (`uintptr_t*`):连续段边界,`offsets[0] == 0`。 +- `offsets_cap` (`uintptr_t`):`offsets` 容量;必须足以容纳连续段数量加一。 + +返回:`0` 成功;`-1` `ptr` 为 NULL,或数据非空时任一缓冲区为 NULL;`-3` 线非法或时间线断裂;`-8` 未知线标签或判据,或容量不足。 + +--- + +## 修改 DAG + +以下函数就地编辑 DAG。每次变更都会重建底层图,因此变更前捕获的节点编号可能不再指向原来的节点——每次成功变更后应重新查询(例如用 `circuit_dag_op_nodes`)。传入的操作会被拷贝(构造层操作先降级),因此调用者保留其句柄的所有权。 + +### circuit_dag_apply_operation_back(ptr, operation, out_node) + +把 `operation` 的拷贝追加到 DAG 末端。 + +- `ptr` (`struct CCircuitDag*`):DAG 句柄。 +- `operation` (`const struct COperation*`):待追加的操作。 +- `out_node` (`uint32_t*`):新节点编号的可选输出;允许传 NULL。 + +返回:`0` 成功;`-1` `ptr` 或 `operation` 为 NULL;`-3` 失败(例如操作引用了 DAG 不认识的资源)。 + +### circuit_dag_apply_value_operation_back(ptr, operation, out_node) + +把自包含的构造层操作 `operation` 降级(见 [操作与指令](4_operation_instruction.md))并追加到 DAG 末端。 + +- `ptr` (`struct CCircuitDag*`):DAG 句柄。 +- `operation` (`const struct CValueOperation*`):待追加的构造层操作。 +- `out_node` (`uint32_t*`):新节点编号的可选输出;允许传 NULL。 + +返回:`0` 成功;`-1` `ptr` 或 `operation` 为 NULL;`-3` 降级失败或 DAG 失败。 + +### circuit_dag_apply_operation_front(ptr, operation, out_node) + +把 `operation` 的拷贝前置到 DAG 首端。 + +- `ptr` (`struct CCircuitDag*`):DAG 句柄。 +- `operation` (`const struct COperation*`):待前置的操作。 +- `out_node` (`uint32_t*`):新节点编号的可选输出;允许传 NULL。 + +返回:`0` 成功;`-1` `ptr` 或 `operation` 为 NULL;`-3` 失败。 + +### circuit_dag_apply_value_operation_front(ptr, operation, out_node) + +把自包含的构造层操作 `operation` 降级并前置到 DAG 首端。 + +- `ptr` (`struct CCircuitDag*`):DAG 句柄。 +- `operation` (`const struct CValueOperation*`):待前置的构造层操作。 +- `out_node` (`uint32_t*`):新节点编号的可选输出;允许传 NULL。 + +返回:`0` 成功;`-1` `ptr` 或 `operation` 为 NULL;`-3` 降级失败或 DAG 失败。 + +### circuit_dag_remove_op_node(ptr, node) + +删除操作节点 `node`;各线的时间线将重新穿引剩余操作。 + +- `ptr` (`struct CCircuitDag*`):DAG 句柄。 +- `node` (`uint32_t`):待删除的节点编号。 + +返回:被删除的操作,为新分配的 `COperation*`(用 `operation_free` 释放);`ptr` 为 NULL、节点不存在或不是操作节点时返回 NULL。 + +### circuit_dag_substitute_node(ptr, node, operation) + +用 `operation` 的拷贝替换 `node` 处存储的操作。 + +- `ptr` (`struct CCircuitDag*`):DAG 句柄。 +- `node` (`uint32_t`):待替换的节点编号。 +- `operation` (`const struct COperation*`):替换用操作。 + +返回:`0` 成功;`-1` `ptr` 或 `operation` 为 NULL;`-2` `node` 不是操作节点;`-3` 新操作不适配本 DAG(未知量子比特、未注册参数、非法经典引用等)。 + +### circuit_dag_substitute_value_node(ptr, node, operation) + +把自包含的构造层操作 `operation` 降级,并用它替换 `node` 处存储的操作。 + +- `ptr` (`struct CCircuitDag*`):DAG 句柄。 +- `node` (`uint32_t`):待替换的节点编号。 +- `operation` (`const struct CValueOperation*`):替换用构造层操作。 + +返回:`0` 成功;`-1` `ptr` 或 `operation` 为 NULL;`-2` `node` 不是操作节点;`-3` 降级失败或 DAG 失败。 + +### circuit_dag_substitute_node_with_dag(ptr, node, replacement) + +用 `replacement` DAG 的拷贝替换操作节点 `node`。以下两条规则保证调度合法,违反时以 `-3` 失败: + +- 资源足迹:`replacement` 中的每条操作只能读取被替换节点读取或写入的线,且只能写入被替换节点写入的线;全局序资源不受此限。 +- 控制流形态:控制流操作只能被恰好包含一条同类型控制流操作的替换 DAG 替换;普通操作可在资源足迹规则内被含多条操作的 DAG 替换。 + +替换 DAG 自身也必须通过 DAG 校验(含环或非法的替换以 `-3` 失败)。 + +- `ptr` (`struct CCircuitDag*`):DAG 句柄。 +- `node` (`uint32_t`):待替换的节点编号。 +- `replacement` (`const struct CCircuitDag*`):替换用 DAG。 + +返回:`0` 成功;`-1` `ptr` 或 `replacement` 为 NULL;`-2` `node` 不是操作节点;`-3` 校验失败。 + +--- + +## 控制流检查 + +`CDagControlFlow*` 是操作节点所附(递归)控制流结构的只读快照,由 `circuit_dag_control_flow` 产生,用 `dag_control_flow_free` 释放。结构的种类: + +| 常量 | 值 | 含义 | +| --- | --- | --- | +| `DAG_CONTROL_FLOW_IF` | 0 | `if` / `else`:体依次为 `then` 体、(存在时的)`else` 体 | +| `DAG_CONTROL_FLOW_WHILE` | 1 | `while`:单个体 | +| `DAG_CONTROL_FLOW_FOR` | 2 | `for`:单个体 | +| `DAG_CONTROL_FLOW_SWITCH` | 3 | `switch`:每个 case 一个体,按 case 顺序;可选的 default 体单独报告 | +| `DAG_CONTROL_FLOW_BREAK` | 4 | `break`:无体 | +| `DAG_CONTROL_FLOW_CONTINUE` | 5 | `continue`:无体 | + +此处返回的体 DAG 是独立的深拷贝(`CCircuitDag*`,用 `circuit_dag_free` 释放);对拷贝的修改不会传播回父 DAG。 + +### circuit_dag_control_flow(ptr, node, out) + +把 `node` 所附的控制流结构快照到 `out`。 + +- `ptr` (`const struct CCircuitDag*`):DAG 句柄。 +- `node` (`uint32_t`):节点编号。 +- `out` (`struct CDagControlFlow**`):快照句柄的输出。 + +返回:`1` 成功(`*out` 持有新分配的 `CDagControlFlow*`);`0` 该节点是普通操作(`*out` 被置为 NULL);`-1` `ptr` 或 `out` 为 NULL;`-2` 未知节点编号或非操作节点。 + +### dag_control_flow_kind(ptr) + +返回快照的种类标签。 + +- `ptr` (`const struct CDagControlFlow*`):快照句柄。 + +返回:`DAG_CONTROL_FLOW_*` 标签;`-1` `ptr` 为 NULL。 + +### dag_control_flow_len(ptr) + +返回体 DAG 的数量:`if` 携带 `then` 体加可选的 `else` 体,`while` 与 `for` 各一个体,`switch` 每个 case 一个体,`break` 与 `continue` 无体。 + +- `ptr` (`const struct CDagControlFlow*`):快照句柄。 + +返回:体数量;`ptr` 为 NULL 时返回 0。 + +### dag_control_flow_body(ptr, index) + +按上表所列顺序返回第 `index` 个体的一份拷贝。 + +- `ptr` (`const struct CDagControlFlow*`):快照句柄。 +- `index` (`uintptr_t`):体索引。 + +返回:新分配的 `CCircuitDag*`(用 `circuit_dag_free` 释放);`ptr` 为 NULL 或 `index` 越界时返回 NULL。 + +### dag_control_flow_has_default(ptr) + +判断 `switch` 快照是否携带 default 体。 + +- `ptr` (`const struct CDagControlFlow*`):快照句柄。 + +返回:`1` 存在;`0` 不存在或结构不是 `switch`;`-1` `ptr` 为 NULL。 + +### dag_control_flow_default_body(ptr) + +返回 `switch` default 体的一份拷贝。 + +- `ptr` (`const struct CDagControlFlow*`):快照句柄。 + +返回:新分配的 `CCircuitDag*`(用 `circuit_dag_free` 释放);`ptr` 为 NULL、结构不是 `switch` 或没有 default 体时返回 NULL。 + +### dag_control_flow_body_value(ptr, index, lo, hi) + +把附着在 `switch` 第 `index` 个体上的 128 位 case 值写入 `lo` 与 `hi`。 + +- `ptr` (`const struct CDagControlFlow*`):快照句柄。 +- `index` (`uintptr_t`):case 索引,与 `dag_control_flow_body` 对应。 +- `lo` (`uint64_t*`):case 值低 64 位的输出。 +- `hi` (`uint64_t*`):case 值高 64 位的输出。 + +返回:`0` 成功;`-1` `ptr`、`lo` 或 `hi` 为 NULL;`-8` 结构不是 `switch` 或 `index` 越界。 + +### dag_control_flow_free(ptr) + +释放控制流快照句柄。 + +- `ptr` (`struct CDagControlFlow*`):快照句柄,允许传 NULL。 + +--- + +## 示例:线路 → DAG → 统计 → 回线路 + +```c +#include +#include +#include "cqlib_c.h" + +int main(void) { + struct CCircuit* circuit = circuit_new(2); + circuit_h(circuit, 0); + circuit_cx(circuit, 0, 1); + + /* 线路 → DAG */ + struct CCircuitDag* dag = circuit_dag_from_circuit(circuit); + + /* 校验与统计 */ + int32_t ok = circuit_dag_validate(dag); /* 0 */ + uintptr_t num_ops = circuit_dag_num_ops(dag); /* 2 */ + int32_t empty = circuit_dag_is_empty(dag); /* 0 */ + + uint32_t ids[2] = {0, 0}; + circuit_dag_qubits(dag, ids, 2); /* ids = {0, 1} */ + + /* 线清单:全局序线在前,随后两条量子比特线 */ + uintptr_t wires = circuit_dag_wires_len(dag); /* 3 */ + uint32_t tags[3] = {0, 0, 0}; + uint32_t wire_ids[3] = {0, 0, 0}; + circuit_dag_wires(dag, tags, wire_ids, 3); + /* tags = {DAG_WIRE_GLOBAL_ORDER, DAG_WIRE_QUBIT, DAG_WIRE_QUBIT} + wire_ids = {0, 0, 1} */ + + int32_t has_q0 = circuit_dag_has_wire(dag, DAG_WIRE_QUBIT, 0); /* 1 */ + int32_t has_q5 = circuit_dag_has_wire(dag, DAG_WIRE_QUBIT, 5); /* 0 */ + + /* DAG → 线路:重建结果与源线路形状一致 + * (2 量子比特、2 条操作) */ + struct CCircuit* lowered = circuit_dag_to_circuit(dag); + + printf("validate=%d ops=%llu empty=%d wires=%llu lowered_ops=%llu\n", + (int)ok, (unsigned long long)num_ops, (int)empty, + (unsigned long long)wires, + (unsigned long long)circuit_num_operations(lowered)); + + circuit_free(lowered); + circuit_dag_free(dag); + circuit_free(circuit); + return 0; +} +``` + +线路构造接口见 [线路](1_circuit.md),操作句柄构造见 [操作与指令](4_operation_instruction.md)。 diff --git a/docs/api/cn/c/0_circuit/1_circuit.md b/docs/api/cn/c/0_circuit/1_circuit.md new file mode 100644 index 00000000..a2f0dd3d --- /dev/null +++ b/docs/api/cn/c/0_circuit/1_circuit.md @@ -0,0 +1,429 @@ +# Circuit(线路) + +`CCircuit*` 是量子线路的主容器句柄,保存逻辑量子比特集合、操作序列、参数表、经典变量与经典值、控制流作用域以及全局相位。本页覆盖线路的构造释放、属性查询、结构变换、检查点事务、全局相位与参数绑定;门函数见 [标准门](5_gate_standard.md),符号参数细节见 [符号参数](3_parameter.md)。错误码与内存约定见 [Overview](../0_overview.md)。 + +--- + +## 构造与释放 + +### circuit_new(num_qubits) + +创建一条包含连续逻辑量子比特的空线路,比特 id 从 `0` 到 `num_qubits - 1`。 + +- `num_qubits` (`uintptr_t`):量子比特数量。 + +返回:新分配的 `CCircuit*`,用 `circuit_free` 释放。 + +### circuit_from_qubits(qubits, len) + +用显式给出的比特 id 列表创建线路,允许稀疏编号(例如 `{2, 5}`)。`len` 为 0 时创建空线路,此时 `qubits` 可以为 NULL。 + +- `qubits` (`const uint32_t*`):比特 id 数组。 +- `len` (`uintptr_t`):数组长度。 + +返回:新分配的 `CCircuit*`;`len > 0` 时 `qubits` 为 NULL 或 id 重复返回 `NULL`。 + +### circuit_from_operations(qubits, qubits_len, operations, operations_len) + +用一组比特加上一批已解析的值级操作(由 `circuit_index` 产生,见 [操作与指令](4_operation_instruction.md))创建线路。操作按顺序经值级降序逐个追加,因此会校验比特归属,符号参数也会驻留进新线路的参数表。 + +- `qubits` (`const uint32_t*`):比特 id 数组。 +- `qubits_len` (`uintptr_t`):数组长度;为 0 时创建空比特集合,`qubits` 可为 NULL。 +- `operations` (`const CValueOperation* const*`):已解析操作快照数组。 +- `operations_len` (`uintptr_t`):操作数量。 + +返回:新分配的 `CCircuit*`,用 `circuit_free` 释放;输入为 NULL(含 `operations` 中出现 NULL 元素)、比特重复或任一操作追加失败时返回 `NULL`。 + +### circuit_append_value_operation(ptr, op) + +向线路追加一条已解析的值级操作(由 `circuit_index` 产生)。 + +- `ptr` (`CCircuit*`):线路句柄。 +- `op` (`const CValueOperation*`):已解析操作快照。 + +返回:`0` 成功;`-1` NULL;`-3` 应用失败(例如操作引用了线路之外的比特)。 + +### circuit_free(ptr) + +释放线路句柄。允许传 `NULL`。 + +- `ptr` (`CCircuit*`):线路句柄。 + +```c +/* 稀疏编号线路:比特 id 为 2 和 5 */ +uint32_t ids[2] = {2, 5}; +CCircuit *qc = circuit_from_qubits(ids, 2); +if (qc != NULL) { + /* circuit_width(qc) == 2 */ + circuit_free(qc); +} +``` + +--- + +## 属性查询 + +### circuit_num_qubits(ptr) + +返回量子比特数量。 + +- `ptr` (`const CCircuit*`):线路句柄。 + +返回:比特数量;`NULL` 时返回 0。 + +### circuit_width(ptr) + +返回线路宽度,即量子比特数量,语义与 `circuit_num_qubits` 一致。 + +- `ptr` (`const CCircuit*`):线路句柄。 + +返回:比特数量;`NULL` 时返回 0。 + +### circuit_num_operations(ptr) + +返回顶层操作数量。 + +- `ptr` (`const CCircuit*`):线路句柄。 + +返回:操作数量;`NULL` 时返回 0。 + +### circuit_num_parameters(ptr) + +返回线路参数表中驻留的符号参数数量,与 `circuit_parameters_len` 相同。 + +- `ptr` (`const CCircuit*`):线路句柄。 + +返回:参数数量;`NULL` 时返回 0。 + +### circuit_qubits_len(ptr) + +返回比特数量,用于为 `circuit_qubits` 分配缓冲。 + +- `ptr` (`const CCircuit*`):线路句柄。 + +返回:比特数量;`NULL` 时返回 0。 + +### circuit_qubits(ptr, out, len) + +按插入顺序把比特 id 拷贝进 `out`。`out` 为 NULL 或 `len` 不足时只返回数量、不拷贝。 + +- `ptr` (`const CCircuit*`):线路句柄。 +- `out` (`uint32_t*`):输出缓冲,可为 NULL(仅查询数量)。 +- `len` (`uintptr_t`):缓冲长度。 + +返回:比特总数。 + +### circuit_depth(ptr, recurse) + +计算线路深度。`recurse = true` 时递归展开控制流体中的操作。 + +- `ptr` (`const CCircuit*`):线路句柄。 +- `recurse` (`bool`):是否递归统计控制流内部操作。 + +返回:线路深度;`-1` 表示 NULL 句柄,`-3` 表示失败(例如线路含控制流而未递归)。 + +### circuit_validate(ptr) + +验证线路一致性:经典句柄归属、控制流作用域、数据依赖与结构不变量。外部导入或自动生成的线路建议先验证再进入后续流程。 + +- `ptr` (`const CCircuit*`):线路句柄。 + +返回:`0` 成功;`-1` NULL;`-3` 验证失败。 + +```c +CCircuit *qc = circuit_new(2); +circuit_h(qc, 0); +circuit_cx(qc, 0, 1); + +if (circuit_validate(qc) == 0) { + /* num_qubits=2, num_operations=2, depth=2 */ + uintptr_t n = circuit_qubits_len(qc); + uint32_t *ids = malloc(n * sizeof(uint32_t)); + circuit_qubits(qc, ids, n); /* ids == {0, 1} */ + free(ids); +} +circuit_free(qc); +``` + +### circuit_contains_qubit(ptr, qubit) + +判断 `qubit` 是否属于该线路。 + +- `ptr` (`const CCircuit*`):线路句柄。 +- `qubit` (`uint32_t`):比特 id。 + +返回:`1` 属于;`0` 不属于;`-1` NULL。 + +### circuit_has_same_qubits(a, b) + +判断两条线路是否拥有相同的有序比特域。 + +- `a` (`const CCircuit*`):第一条线路句柄。 +- `b` (`const CCircuit*`):第二条线路句柄。 + +返回:`1` 相同;`0` 不同;`-1` NULL。 + +### circuit_operations_structurally_equal(a, b) + +判断两条线路是否结构相等:有序比特、参数表与操作序列全部一致(参数下标跨两张参数表重映射后比较,线路内的经典句柄按结构比较)。 + +- `a` (`const CCircuit*`):第一条线路句柄。 +- `b` (`const CCircuit*`):第二条线路句柄。 + +返回:`1` 相等;`0` 不相等;`-1` NULL。 + +--- + +## 量子比特管理 + +### circuit_add_qubits(ptr, qubits, count) + +向线路追加新的逻辑量子比特。 + +- `ptr` (`CCircuit*`):线路句柄。 +- `qubits` (`const uint32_t*`):新比特 id 数组。 +- `count` (`uintptr_t`):追加数量。 + +返回:`0` 成功;`-1` NULL;`-3` 失败(例如与已有比特重复)。 + +--- + +## 结构变换 + +### circuit_compose(ptr, other, qubits_map, map_len) + +把 `other` 追加到当前线路末尾。`qubits_map` 按 `other` 的比特顺序将其比特映射到当前线路的比特;传 NULL 映射(`map_len` 为 0)时按 id 恒等合并,`other` 中不存在于当前线路的比特会被并入。 + +- `ptr` (`CCircuit*`):目标线路(被修改)。 +- `other` (`const CCircuit*`):被组合线路(不修改)。 +- `qubits_map` (`const uint32_t*`):比特映射数组,可为 NULL。 +- `map_len` (`uintptr_t`):映射长度。 + +返回:`0` 成功;`-1` NULL;`-3` 失败(映射不合法或结构校验失败)。 + +```c +CCircuit *lhs = circuit_new(3); +CCircuit *rhs = circuit_new(2); +circuit_cx(rhs, 0, 1); + +/* rhs 的比特 0、1 依次映射到 lhs 的比特 1、2 */ +uint32_t map[2] = {1, 2}; +int32_t rc = circuit_compose(lhs, rhs, map, 2); /* rc == 0 */ + +circuit_free(rhs); +circuit_free(lhs); +``` + +### circuit_inverse(ptr) + +返回当前线路的反线路:操作逆序并逐个取逆,全局相位取反。原线路不变。 + +- `ptr` (`const CCircuit*`):线路句柄。 + +返回:新分配的 `CCircuit*`,用 `circuit_free` 释放;包含不可逆操作(测量、reset 等)或失败时返回 `NULL`。 + +### circuit_decompose(ptr) + +返回展开复合门后的线路副本:由线路定义的门(`circuit_circuit_gate` 追加的复合门)被展开为基础操作。原线路不变。 + +- `ptr` (`const CCircuit*`):线路句柄。 + +返回:新分配的 `CCircuit*`;失败返回 `NULL`。 + +### circuit_remove_operation(ptr, index) + +删除下标为 `index` 的顶层操作。 + +- `ptr` (`CCircuit*`):线路句柄。 +- `index` (`uintptr_t`):操作下标。 + +返回:`0` 成功;`-1` NULL;`-3` 下标越界或删除后结构校验失败。 + +### circuit_remove_operations(ptr, indices, len) + +按下标批量删除顶层操作。`indices` 按删除前的操作列表解释;重复下标被忽略;任一失败时整体回滚(原子操作)。 + +- `ptr` (`CCircuit*`):线路句柄。 +- `indices` (`const uintptr_t*`):操作下标数组。 +- `len` (`uintptr_t`):下标数量(可为 0)。 + +返回:`0` 成功(含空下标列表);`-1` NULL;`-3` 任一下标越界或有经典值仍被引用。 + +```c +CCircuit *qc = circuit_new(1); +circuit_rx(qc, 0, 0.7); + +CCircuit *inv = circuit_inverse(qc); /* 反线路:RX(-0.7) */ +if (inv != NULL && circuit_validate(inv) == 0) { + /* 使用 inv ... */ +} +circuit_free(inv); +circuit_free(qc); +``` + +--- + +## 检查点事务 + +检查点函数族通过不透明的 `CCheckpoint*` 令牌捕获与恢复线路状态(操作列表、参数与符号表、经典表、控制流作用域)。令牌由 `circuit_checkpoint` / `circuit_begin` 产生,且只能被 `circuit_commit`、`circuit_rollback_to` 或 `circuit_rollback_control_body_transaction` 消费一次;未消费的令牌用 `checkpoint_free` 释放。 + +```c +CCircuit *qc = circuit_new(1); +CCheckpoint *cp = circuit_checkpoint(qc); +circuit_h(qc, 0); /* 试验性操作 */ + +if (circuit_validate(qc) != 0) { + circuit_rollback_to(qc, cp); /* 撤销检查点之后的全部变更 */ +} else { + circuit_commit(qc, cp); /* 保留这些操作 */ +} + +circuit_free(qc); +``` + +### circuit_checkpoint(ptr) + +捕获线路状态检查点。返回的令牌只能被 `circuit_commit`、`circuit_rollback_to` 或 `circuit_rollback_control_body_transaction` 消费一次。 + +- `ptr` (`const CCircuit*`):线路句柄。 + +返回:新分配的 `CCheckpoint*`;NULL 输入时返回 `NULL`。 + +### circuit_begin(ptr) + +开启一个外部驱动的构造事务;等价于 `circuit_checkpoint`。 + +- `ptr` (`const CCircuit*`):线路句柄。 + +返回:新分配的 `CCheckpoint*`;NULL 输入时返回 `NULL`。 + +### circuit_commit(ptr, checkpoint) + +提交自令牌捕获以来新增的状态,并消费该令牌。 + +- `ptr` (`CCircuit*`):线路句柄。 +- `checkpoint` (`CCheckpoint*`):`circuit_checkpoint` / `circuit_begin` 产生的令牌。 + +返回:`0` 成功;`-1` NULL。 + +### circuit_rollback_to(ptr, checkpoint) + +回滚自令牌捕获以来新增的全部状态,并消费该令牌。 + +- `ptr` (`CCircuit*`):线路句柄。 +- `checkpoint` (`CCheckpoint*`):`circuit_checkpoint` / `circuit_begin` 产生的令牌。 + +返回:`0` 成功;`-1` NULL。 + +### circuit_rollback_control_body_transaction(ptr, checkpoint) + +回滚自令牌捕获以来新增的全部状态,并消费该令牌;行为与 `circuit_rollback_to` 相同,按核心事务名保留。 + +- `ptr` (`CCircuit*`):线路句柄。 +- `checkpoint` (`CCheckpoint*`):`circuit_checkpoint` / `circuit_begin` 产生的令牌。 + +返回:`0` 成功;`-1` NULL。 + +### checkpoint_free(ptr) + +释放未消费的检查点令牌。允许传 `NULL`。 + +- `ptr` (`CCheckpoint*`):待释放令牌。 + +--- + +## 全局相位 + +### circuit_global_phase(ptr, out) + +读取线路全局相位。结果写入 `CParameterValue`:固定数值,或符号情形下新分配的 `CParameter*`(用 `param_free` 释放,见 [符号参数](3_parameter.md))。 + +- `ptr` (`const CCircuit*`):线路句柄。 +- `out` (`CParameterValue*`):写出结构。 + +返回:`0` 成功;`-1` NULL。 + +### circuit_set_global_phase(ptr, phase) + +把全局相位设置为数值。 + +- `ptr` (`CCircuit*`):线路句柄。 +- `phase` (`double`):全局相位(弧度)。 + +返回:`0` 成功;`-1` NULL;`-3` 相位为非有限值。 + +### circuit_set_global_phase_param(ptr, param) + +把全局相位设置为符号参数(参数被克隆)。 + +- `ptr` (`CCircuit*`):线路句柄。 +- `param` (`const CParameter*`):符号参数句柄。 + +返回:`0` 成功;`-1` NULL。 + +--- + +## 参数绑定 + +### circuit_assign_params(circuit, bindings) + +返回一条新线路:能由绑定串求值的驻留参数替换为固定数值,其余保持符号形式;原线路不变,可作为模板复用。 + +- `circuit` (`const CCircuit*`):线路句柄。 +- `bindings` (`const char*`):绑定串,格式为 `"name:value,name2:value2"`(与 `param_evaluate` 相同);传 NULL 或格式非法时按不提供绑定处理。 + +返回:新分配的 `CCircuit*`,用 `circuit_free` 释放;线路含经典控制流或替换求值失败时返回 `NULL`。 + +```c +CCircuit *tpl = circuit_new(1); +CParameter *theta = param_parse("theta"); +circuit_rx_param(tpl, 0, theta); +param_free(theta); + +CCircuit *bound = circuit_assign_params(tpl, "theta:0.5"); +/* bound 中不再引用符号 theta */ + +circuit_free(bound); +circuit_free(tpl); +``` + +--- + +## 非酉指令与恒等门 + +### circuit_measure(ptr, qubit) + +在 `qubit` 上追加计算基测量指令,产生线路所有的不可变经典 `Bit` 值。读取测量结果、写入已有经典变量等变体见 [经典数据与控制流](9_classical_control_flow.md)。 + +- `ptr` (`CCircuit*`):线路句柄。 +- `qubit` (`uint32_t`):被测比特 id。 + +返回:`0` 成功;`-1` NULL;`-2` 比特越界;`-3` 失败。 + +### circuit_reset(ptr, qubit) + +在 `qubit` 上追加复位指令,将比特复位到 `|0>`。 + +- `ptr` (`CCircuit*`):线路句柄。 +- `qubit` (`uint32_t`):比特 id。 + +返回:`0` 成功;`-1` NULL;`-2` 比特越界;`-3` 失败。 + +### circuit_barrier(ptr, qubits, count) + +在给定比特上插入 barrier,阻止相关操作跨越该边界重排。`qubits` 传 NULL 或 `count` 为 0 时对所有线路比特插入全局 barrier。 + +- `ptr` (`CCircuit*`):线路句柄。 +- `qubits` (`const uint32_t*`):比特 id 数组,可为 NULL。 +- `count` (`uintptr_t`):比特数量。 + +返回:`0` 成功;`-1` NULL;`-3` 比特不存在或失败。 + +### circuit_id(ptr, qubit) + +在 `qubit` 上追加恒等门,是 `circuit_i` 的别名,为前端命名兼容保留。 + +- `ptr` (`CCircuit*`):线路句柄。 +- `qubit` (`uint32_t`):比特 id。 + +返回:`0` 成功;`-1` NULL;`-2` 比特越界;`-3` 失败。 diff --git a/docs/api/cn/c/0_circuit/2_qubit.md b/docs/api/cn/c/0_circuit/2_qubit.md new file mode 100644 index 00000000..4138be89 --- /dev/null +++ b/docs/api/cn/c/0_circuit/2_qubit.md @@ -0,0 +1,183 @@ +# 量子比特(C) + +本页覆盖逻辑量子比特标识 `CQubit` 的 C ABI。`CQubit` 是按值传递的轻量句柄,内部保存一个 `uint32_t` 编号,用于在量子线路、编译 IR、映射表和集合结构中稳定标识某个逻辑量子比特。错误码与字符串释放约定遵循 [Overview](../0_overview.md)。 + +--- + +## 结构体 + +`CQubit` 按值传递,无需释放: + +```c +typedef struct CQubit { + uint32_t id; +} CQubit; +``` + +| 字段 | 类型 | 含义 | +| --- | --- | --- | +| `id` | `uint32_t` | 逻辑量子比特的数值编号。 | + +编号是逻辑标识:不携带线路身份,也不对应态矢量或矩阵中的存储位置;实际顺序由 `Circuit` 保存的量子比特列表与各转换接口的顺序约定决定。编号允许稀疏(如 10、20),调用方不应假设它是密集存储的有效下标。 + +--- + +## 创建与访问 + +### qubit_new(id) + +```c +struct CQubit qubit_new(uint32_t id); +``` + +根据数值编号创建逻辑量子比特,按值返回。 + +- `id` (`uint32_t`):逻辑量子比特编号。 + +### qubit_id(qubit) + +```c +uint32_t qubit_id(struct CQubit qubit); +``` + +返回原始内部编号(紧凑 4 字节表示)。 + +- `qubit` (`struct CQubit`):逻辑量子比特。 + +### qubit_index(qubit) + +```c +uintptr_t qubit_index(struct CQubit qubit); +``` + +将编号转换为 `uintptr_t`,便于需要下标类型的场景。该值不映射到任何线路、矩阵或模拟器中的位置。 + +--- + +## 受检整数转换 + +### qubit_try_from_i64(value, out) + +```c +int32_t qubit_try_from_i64(int64_t value, struct CQubit *out); +``` + +从有符号整数受检转换:负数或超出 `uint32_t` 范围的值不会静默截断,而是返回错误。 + +- `value` (`int64_t`):输入整数。 +- `out` (`struct CQubit*`):输出量子比特。 + +返回:`0` 成功;`-1` `out` 为 NULL;`-8` `value` 为负或超出 `uint32_t` 范围。 + +### qubit_try_from_u64(value, out) + +```c +int32_t qubit_try_from_u64(uint64_t value, struct CQubit *out); +``` + +从无符号整数受检转换:超出 `uint32_t` 范围的值返回错误。 + +- `value` (`uint64_t`):输入整数。 +- `out` (`struct CQubit*`):输出量子比特。 + +返回:`0` 成功;`-1` `out` 为 NULL;`-8` `value` 超出 `uint32_t` 范围。 + +--- + +## 比较与排序 + +比较、排序与哈希均基于内部编号:两个 `CQubit` 编号相同即视为同一逻辑比特。 + +### qubit_equal(a, b) + +```c +int32_t qubit_equal(struct CQubit a, struct CQubit b); +``` + +返回 `1` 表示两者编号相同,`0` 表示不同。 + +- `a`、`b` (`struct CQubit`):待比较的量子比特。 + +### qubit_compare(a, b) + +```c +int32_t qubit_compare(struct CQubit a, struct CQubit b); +``` + +按编号比较:`-1` 表示 `a < b`,`0` 表示相等,`1` 表示 `a > b`。 + +- `a`、`b` (`struct CQubit`):待比较的量子比特。 + +--- + +## 字符串表示 + +### qubit_to_string(qubit) + +```c +char *qubit_to_string(struct CQubit qubit); +``` + +格式化为 `Q`(如 `Q12`)。返回的字符串用 [cqlib_string_free](../0_overview.md) 释放;分配失败时返回 NULL。 + +- `qubit` (`struct CQubit`):逻辑量子比特。 + +--- + +## 错误码 + +| 函数 | `0` | `-1` | `-8` | +| --- | --- | --- | --- | +| `qubit_try_from_i64` | 转换成功 | `out` 为 NULL | 值为负或超出 `uint32_t` 范围 | +| `qubit_try_from_u64` | 转换成功 | `out` 为 NULL | 值超出 `uint32_t` 范围 | + +`qubit_new`、`qubit_id`、`qubit_index`、`qubit_equal`、`qubit_compare` 不失败;`qubit_to_string` 仅在分配失败时返回 NULL。 + +--- + +## 示例 + +```c +#include +#include +#include +#include + +int main(void) { + /* 创建与访问 */ + CQubit q = qubit_new(12); + assert(q.id == 12); + assert(qubit_id(q) == 12u); + assert(qubit_index(q) == 12u); + + /* 受检转换:负数与超范围都报错,不截断 */ + CQubit out = {0}; + assert(qubit_try_from_i64(-1, &out) == -8); + assert(qubit_try_from_i64(3, &out) == 0); + assert(qubit_id(out) == 3u); + + /* 比较 */ + assert(qubit_equal(q, qubit_new(12)) == 1); + assert(qubit_compare(qubit_new(0), qubit_new(1)) == -1); + + /* 字符串表示 */ + char *text = qubit_to_string(q); + assert(strcmp(text, "Q12") == 0); + cqlib_string_free(text); + + /* 与线路交互:稀疏逻辑编号 10、20 */ + const uint32_t ids[2] = {qubit_id(qubit_new(10)), qubit_id(qubit_new(20))}; + CCircuit *qc = circuit_from_qubits(ids, 2); + assert(circuit_num_qubits(qc) == 2); + circuit_free(qc); + return 0; +} +``` + +--- + +## 相关页面 + +- [Circuit](1_circuit.md):`circuit_from_qubits` 接受显式编号列表(含稀疏编号)。 +- [量子比特标识](../2_device/6_qubits.md):设备侧 `CLogicalQubit` / `CPhysicalQubit`。 +- [Overview](../0_overview.md):错误码、内存所有权与全局约定。 diff --git a/docs/api/cn/c/0_circuit/3_parameter.md b/docs/api/cn/c/0_circuit/3_parameter.md new file mode 100644 index 00000000..b368f381 --- /dev/null +++ b/docs/api/cn/c/0_circuit/3_parameter.md @@ -0,0 +1,420 @@ +# Parameter(符号参数) + +`CParameter*` 是符号参数表达式的不透明句柄:既可以是自由符号(如 `theta`)或数值常量,也可以是算术组合表达式(如 `2 * theta + pi / 2`)。本页覆盖表达式的解析、构造、算术与数学运算、符号与状态查询、化简替换与求导,以及线路侧的参数驻留、存储形式(`CCircuitParam`)、解析值(`CParameterValue`)与符号表查询;按绑定生成新线路的 `circuit_assign_params` 见 [Circuit](1_circuit.md)。错误码与内存约定见 [Overview](../0_overview.md)。 + +--- + +## 表达式解析与求值 + +### param_parse(expr) + +解析符号参数表达式并返回新分配的句柄。表达式支持自由符号名(包括 `θ` 等 Unicode 符号)、数值常量、算术运算(`+`、`-`、`*`、`/`、一元负号);常量 `pi`/`π` 与 `e` 在求值时自动可用,显式绑定可覆盖。 + +- `expr` (`const char*`):NUL 结尾的表达式字符串。 + +返回:新分配的 `CParameter*`,用 `param_free` 释放;`expr` 为 NULL、非 UTF-8 或无法解析为合法表达式时返回 `NULL`。 + +### param_free(ptr) + +释放参数句柄。允许传 `NULL`。 + +- `ptr` (`CParameter*`):参数句柄。 + +### param_evaluate(ptr, bindings) + +在给定绑定下把表达式求值为数值。 + +- `ptr` (`const CParameter*`):参数句柄。 +- `bindings` (`const char*`):绑定串,格式为 `"name:value,name2:value2"`;传 NULL 或格式非法(值非有限浮点数)时按不提供绑定处理。 + +返回:求值结果;`ptr` 为 NULL、表达式仍含未绑定符号或求值失败(含结果为 NaN)时返回 `0.0`。 + +```c +CParameter *expr = param_parse("2 * theta + pi / 2"); +double v = param_evaluate(expr, "theta:0.5"); /* v == 1.0 + pi/2 */ +param_free(expr); +``` + +--- + +## 表达式构造与算术 + +### param_symbol(name) + +从符号名构造自由符号参数。 + +- `name` (`const char*`):NUL 结尾的符号名,支持 `θ` 等 Unicode 符号。 + +返回:新分配的 `CParameter*`,用 `param_free` 释放;`name` 为 NULL、非 UTF-8 或非法符号名时返回 `NULL`。 + +### param_pi() / param_e() + +构造内置常量 π 与 e。二者是常量表达式(`param_is_constant` 返回 1),无需绑定即可求值。 + +返回:新分配的 `CParameter*`。 + +### param_from_double(value) + +从数值构造常量参数。 + +- `value` (`double`):常量值。 + +返回:新分配的 `CParameter*`;`value` 非 finite 时返回 `NULL`。 + +### param_add(a, b) / param_sub(a, b) / param_mul(a, b) / param_div(a, b) + +两个表达式的加、减、乘、除,返回组合后的新表达式。除法构造时不检查除零,求值阶段产生非有限值。 + +- `a`、`b` (`const CParameter*`):操作数句柄。 + +返回:新分配的 `CParameter*`;任一输入为 NULL 时返回 `NULL`。 + +### param_neg(a) + +一元负号:返回 `-a`。 + +- `a` (`const CParameter*`):操作数句柄。 + +返回:新分配的 `CParameter*`;`a` 为 NULL 时返回 `NULL`。 + +### param_pow(a, exp) + +幂运算:返回 `a^exp`。 + +- `a`、`exp` (`const CParameter*`):底数与指数句柄。 + +返回:新分配的 `CParameter*`;任一输入为 NULL 时返回 `NULL`。 + +```c +CParameter *theta = param_symbol("theta"); +CParameter *amp = param_mul(param_from_double(0.5), param_sin(theta)); +CParameter *shifted = param_add(amp, param_pi()); /* 0.5*sin(theta) + pi */ +param_free(shifted); +param_free(amp); +param_free(theta); +``` + +--- + +## 数学函数 + +以下一元数学函数签名一致:形如 `param_xxx(const CParameter* a)`,返回新分配的 `CParameter*`(`a` 为 NULL 时返回 `NULL`),表示对该表达式施加相应数学函数后的组合表达式;构造阶段不做域检查,求值发生在 `param_evaluate` 或化简时。 + +### param_abs / param_sqrt / param_exp / param_ln + +绝对值、平方根、自然指数、自然对数。 + +### param_sin / param_cos / param_tan / param_asin / param_acos / param_atan + +三角函数与反三角函数。 + +### param_sinh / param_cosh / param_tanh + +双曲函数。 + +### param_floor / param_ceil / param_round + +向下取整、向上取整、四舍五入。 + +### param_log(a, base) + +对指定底取对数。 + +- `a` (`const CParameter*`):真数句柄。 +- `base` (`const CParameter*`):底数句柄。 + +返回:新分配的 `CParameter*`;任一输入为 NULL 时返回 `NULL`。 + +--- + +## 符号与状态查询 + +### param_symbols_len(a) + +返回表达式中自由符号的数量(不含 π、e 等内置常量)。 + +- `a` (`const CParameter*`):参数句柄。 + +返回:自由符号数量;`NULL` 时返回 0。 + +### param_symbols(a, out, len) + +两步式读取自由符号名:先调用 `param_symbols_len` 分配缓冲,再把按名称排序的符号名写入 `out`。 + +- `a` (`const CParameter*`):参数句柄。 +- `out` (`char**`):输出缓冲,可为 NULL(仅查询数量);每个写入元素是新分配的 C 字符串,用 `cqlib_string_free` 释放。 +- `len` (`uintptr_t`):缓冲长度。 + +返回:自由符号总数;`-1` NULL;`-8` 缓冲不足(不写入)。 + +### param_as_symbol(a) + +当表达式恰为单个自由符号本身时返回其名称。 + +返回:新分配的 C 字符串,用 `cqlib_string_free` 释放;表达式不是平凡符号或输入为 NULL 时返回 `NULL`。 + +### param_is_constant(a) + +判断表达式是否不含自由符号(π、e 等内置常量视为已知量)。 + +返回:`1` 常量;`0` 含自由符号;`-1` NULL。 + +### param_is_exact_zero(a) / param_is_zero(a) / param_is_one(a) + +- `param_is_exact_zero`:不含自由符号且求值恰为 0 时返回 1;含自由符号时返回 0。 +- `param_is_zero`/`param_is_one`:不带绑定直接求值判断(是否为 0 / 是否为 1,容差 `f64::EPSILON`);无法求值(含自由符号)时返回 0。 + +返回:`1`/`0`;`-1` NULL。 + +--- + +## 化简、替换与求导 + +### param_simplify(a) + +对表达式做域安全的符号化简。 + +返回:新分配的 `CParameter*`;NULL 输入或化简失败时返回 `NULL`。 + +### param_canonicalized(a) + +化简表达式;若结果不含自由符号,进一步折叠为数值节点(常量求值为非有限值时失败)。 + +返回:新分配的 `CParameter*`;NULL 输入或化简失败时返回 `NULL`。 + +### param_replace(a, symbol, replacement) + +把表达式中所有 `symbol` 替换为 `replacement`,返回新表达式。 + +- `a` (`const CParameter*`):参数句柄。 +- `symbol` (`const char*`):被替换的符号名。 +- `replacement` (`const CParameter*`):替换后的参数句柄。 + +返回:新分配的 `CParameter*`;NULL 输入或 `symbol` 非 UTF-8 时返回 `NULL`。 + +### param_substitute_many(a, names, values, len) + +一次替换多个符号并对结果化简。`names` 与 `values` 是长度为 `len` 的平行数组。 + +- `a` (`const CParameter*`):参数句柄。 +- `names` (`const char* const*`):符号名数组,元素为 NULL 视为错误。 +- `values` (`const CParameter* const*`):与 `names` 平行的新值句柄数组。 +- `len` (`uintptr_t`):数组长度;为 0 时返回 `a` 的克隆。 + +返回:新分配的 `CParameter*`;NULL 输入、数组元素为 NULL 或名字非 UTF-8 时返回 `NULL`。 + +### param_derivative(a, var) + +返回对 `var` 的符号偏导数。 + +- `a` (`const CParameter*`):参数句柄。 +- `var` (`const char*`):求导变量名。 + +返回:新分配的 `CParameter*`;NULL 输入、`var` 非 UTF-8 或求导失败时返回 `NULL`。 + +```c +CParameter *expr = param_parse("2 * theta"); +CParameter *deriv = param_derivative(expr, "theta"); /* deriv == 2 */ +double v = param_evaluate(deriv, NULL); /* v == 2.0 */ +param_free(deriv); +param_free(expr); +``` + +--- + +## 等价性判断 + +### param_provably_equal(a, b, tolerance) + +保守判断两个表达式在 `tolerance` 内相等(直接相等或均可无绑定求值且数值差不超过容差)。 + +- `a`、`b` (`const CParameter*`):待比较句柄。 +- `tolerance` (`double`):数值容差。 + +返回:`1` 相等;`0` 不相等或无法证明;`-1` NULL。 + +### param_provably_equal_modulo(a, b, modulus, tolerance) + +保守判断 `a - b` 是 `modulus` 的整数倍(差值直测或模数化简后数值差不超过 `tolerance`)。 + +- `a`、`b` (`const CParameter*`):待比较句柄。 +- `modulus` (`const CParameter*`):模数句柄。 +- `tolerance` (`double`):数值容差。 + +返回:`1` 等价;`0` 不等价或无法证明;`-1` NULL。 + +```c +CParameter *p = param_pi(); +CParameter *three_pi = param_mul(param_from_double(3.0), param_pi()); +CParameter *mod = param_mul(param_from_double(2.0), param_pi()); +/* p 与 three_pi 相差 2*pi 的整数倍 */ +int32_t eq = param_provably_equal_modulo(p, three_pi, mod, 1e-9); /* eq == 1 */ +param_free(mod); +param_free(three_pi); +param_free(p); +``` + +--- + +## 线路参数驻留 + +线路维护一张参数表:符号参数经规范化驻留后,以固定值或表下标两种存储形式出现在操作与全局相位中。驻留只注册表达式本身,不会把参数标记为被引用。 + +### circuit_add_parameter(ptr, param) + +把符号参数克隆并驻留进线路参数表。驻留仅注册表达式及其符号,不标记为被使用。 + +- `ptr` (`CCircuit*`):线路句柄。 +- `param` (`const CParameter*`):参数句柄。 + +返回:`0` 成功;`-1` NULL。 + +### circuit_parameters_len(ptr) + +返回参数表中驻留的参数数量。 + +- `ptr` (`const CCircuit*`):线路句柄。 + +返回:驻留参数数量;`NULL` 时返回 0。 + +### circuit_parameters(ptr, out, len) + +两步式读取参数表:先调用 `circuit_parameters_len` 分配缓冲,再把克隆的参数句柄写入 `out`。 + +- `ptr` (`const CCircuit*`):线路句柄。 +- `out` (`CParameter**`):输出缓冲,可为 NULL(仅查询数量);每个写入元素是新分配的 `CParameter*`,逐个用 `param_free` 释放。 +- `len` (`uintptr_t`):缓冲长度。 + +返回:驻留参数总数。 + +--- + +## 存储形式与解析值 + +操作与全局相位中的参数以存储形式 `CCircuitParam` 表示;读取时既可还原为完整符号表达式,也可解析为固定数值或符号形式。 + +### CCircuitParam + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| `tag` | `uint8_t` | `CIRCUIT_PARAM_TAG_FIXED`(0,固定数值)或 `CIRCUIT_PARAM_TAG_INDEX`(1,参数表下标)。 | +| `index` | `uint32_t` | `tag` 为 `INDEX` 时的参数表下标。 | +| `value` | `double` | `tag` 为 `FIXED` 时的固定数值。 | + +### CParameterValue + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| `tag` | `uint8_t` | `PARAMETER_VALUE_TAG_FIXED`(0,固定数值)或 `PARAMETER_VALUE_TAG_PARAM`(1,符号参数)。 | +| `value` | `double` | `tag` 为 `FIXED` 时的固定数值。 | +| `param` | `CParameter*` | `tag` 为 `PARAM` 时新分配的符号参数句柄,用 `param_free` 释放;固定情形为 NULL。 | + +### circuit_map_param(ptr, param, out) + +把符号参数规范化并驻留进线路参数表,写出其存储形式(固定数值或表下标)。 + +- `ptr` (`CCircuit*`):线路句柄。 +- `param` (`const CParameter*`):参数句柄。 +- `out` (`CCircuitParam*`):写出的存储形式。 + +返回:`0` 成功;`-1` NULL;`-3` 求值失败。 + +### circuit_resolve_parameter(ptr, param) + +把存储形式还原为完整的符号参数表达式。 + +- `ptr` (`const CCircuit*`):线路句柄。 +- `param` (`const CCircuitParam*`):存储形式。 + +返回:新分配的 `CParameter*`,用 `param_free` 释放;NULL 输入、非法 tag 或解析失败时返回 `NULL`。 + +### circuit_parameter_value(ptr, param, out) + +解析存储形式的当前取值,不引入新的驻留。 + +- `ptr` (`const CCircuit*`):线路句柄。 +- `param` (`const CCircuitParam*`):存储形式。 +- `out` (`CParameterValue*`):写出的解析值;符号情形的 `param` 字段用 `param_free` 释放。 + +返回:`0` 成功;`-1` NULL;`-3` 表下标无效;`-8` tag 无效。 + +```c +CCircuit *qc = circuit_new(1); +CParameter *theta = param_parse("theta"); +circuit_rx_param(qc, 0, theta); + +CCircuitParam storage; +CParameterValue value; +if (circuit_map_param(qc, theta, &storage) == 0 && + circuit_parameter_value(qc, &storage, &value) == 0) { + if (value.tag == PARAMETER_VALUE_TAG_PARAM) { + param_free(value.param); /* 符号情形:释放句柄 */ + } +} +param_free(theta); +circuit_free(qc); +``` + +--- + +## 符号表查询 + +线路区分两个符号集合:注册表(可能包含已驻留但不再被操作引用的符号)与可执行 IR 实际引用的符号(全局相位与操作参数,含嵌套控制流体)。 + +### circuit_symbols_len(ptr) + +返回注册表中的符号名数量;注册表可能包含已驻留但不再被操作引用的符号。 + +- `ptr` (`const CCircuit*`):线路句柄。 + +返回:符号数量;`NULL` 时返回 0。 + +### circuit_symbols(ptr, out, len) + +两步式读取注册表符号名:每个写入元素是新分配的 C 字符串,用 `cqlib_string_free` 释放。 + +- `ptr` (`const CCircuit*`):线路句柄。 +- `out` (`char**`):输出缓冲,可为 NULL(仅查询数量)。 +- `len` (`uintptr_t`):缓冲长度。 + +返回:注册表符号总数。 + +### circuit_used_symbols_len(ptr) + +返回可执行 IR 实际引用的符号数量。 + +- `ptr` (`const CCircuit*`):线路句柄。 + +返回:符号数量;`NULL` 时返回 0。 + +### circuit_used_symbols(ptr, out, len) + +两步式读取 IR 引用的符号名:每个写入元素是新分配的 C 字符串,用 `cqlib_string_free` 释放。 + +- `ptr` (`const CCircuit*`):线路句柄。 +- `out` (`char**`):输出缓冲,可为 NULL(仅查询数量)。 +- `len` (`uintptr_t`):缓冲长度。 + +返回:IR 引用的符号总数。 + +### circuit_uses_symbol(ptr, name) + +判断符号是否被可执行 IR 引用(全局相位与操作参数,含嵌套控制流体)。 + +- `ptr` (`const CCircuit*`):线路句柄。 +- `name` (`const char*`):符号名。 + +返回:`1` 被引用;`0` 未被引用;`-1` NULL;`-4` 符号名非 UTF-8。 + +```c +uintptr_t n = circuit_used_symbols_len(qc); +char **names = malloc(n * sizeof(char *)); +circuit_used_symbols(qc, names, n); +for (uintptr_t i = 0; i < n; ++i) { + if (circuit_uses_symbol(qc, names[i]) == 1) { + /* names[i] 被 IR 引用 */ + } + cqlib_string_free(names[i]); +} +free(names); +``` diff --git a/docs/api/cn/c/0_circuit/4_operation_instruction.md b/docs/api/cn/c/0_circuit/4_operation_instruction.md new file mode 100644 index 00000000..850ef5e1 --- /dev/null +++ b/docs/api/cn/c/0_circuit/4_operation_instruction.md @@ -0,0 +1,435 @@ +# Operation / Instruction(操作与指令) + +`COperation*` 与 `CValueOperation*` 是读取线路操作的两个只读视图。存储层操作 `COperation` 的参数可能以所属线路参数表下标的形式记录,需结合线路上下文解析;`circuit_index` 返回的 `CValueOperation` 是解析后的自包含快照,参数为 `CParameterValue`(固定数值或完整符号表达式),独立于线路后续变更。延迟指令见本页末尾。门函数与门名清单见 [标准门](5_gate_standard.md),`CParameterValue` 的内存规则见 [符号参数](3_parameter.md),错误码与内存约定见 [Overview](../0_overview.md)。 + +--- + +## 存储层操作 + +`COperation*` 表示"指令 + 作用比特 + 参数 + 可选标签"的一次具体应用。`operation_new` 按门名创建标准门操作,参数为固定 double 数值。 + +### operation_new(gate_name, qubits, qubits_len, params, params_len) + +按标准门名(如 `"H"`、`"CX"`、`"RZZ"`)创建操作,`params` 为固定 double 参数值。 + +- `gate_name` (`const char*`):标准门名。 +- `qubits` (`const uint32_t*`):作用比特 id 数组。 +- `qubits_len` (`uintptr_t`):比特数量。 +- `params` (`const double*`):固定参数值数组。 +- `params_len` (`uintptr_t`):参数数量。 + +返回:新分配的 `COperation*`,用 `operation_free` 释放;`gate_name` 为 NULL 或未知门名时返回 `NULL`;`qubits_len > 0` 时 `qubits` 为 NULL、或 `params_len > 0` 时 `params` 为 NULL 同样返回 `NULL`。 + +### operation_free(ptr) + +释放操作句柄。允许传 `NULL`。 + +- `ptr` (`COperation*`):操作句柄。 + +### operation_name(ptr) + +返回指令名(如 `"H"`、`"RZZ"`)。 + +- `ptr` (`const COperation*`):操作句柄。 + +返回:新分配的 C 字符串,用 `cqlib_string_free` 释放;`NULL` 时返回 `NULL`。 + +### operation_is_standard_gate(ptr) + +判断操作是否由标准门指令驱动。 + +- `ptr` (`const COperation*`):操作句柄。 + +返回:`1` 是;`0` 否;`-1` NULL。 + +### operation_num_qubits(ptr) + +返回操作作用的比特数量。 + +- `ptr` (`const COperation*`):操作句柄。 + +返回:比特数量;`NULL` 时返回 0。 + +### operation_qubits_len(ptr) + +返回操作作用的比特数量,用于为 `operation_qubits` 分配缓冲。 + +- `ptr` (`const COperation*`):操作句柄。 + +返回:比特数量;`NULL` 时返回 0。 + +### operation_qubits(ptr, buffer, len) + +按作用顺序把比特 id 拷贝进 `buffer`。 + +- `ptr` (`const COperation*`):操作句柄。 +- `buffer` (`uint32_t*`):输出缓冲。 +- `len` (`uintptr_t`):缓冲长度。 + +返回:`0` 成功;`-1` NULL;`-8` `len` 小于比特数量。 + +### operation_params_len(ptr) + +返回操作携带的参数数量。 + +- `ptr` (`const COperation*`):操作句柄。 + +返回:参数数量;`NULL` 时返回 0。 + +### operation_params(ptr, tags, values, len) + +把参数拷贝进 `tags` 与 `values` 两个平行数组。`tags[i]` 为 `OPERATION_PARAM_*` 常量;`values[i]` 为固定数值,或 `OPERATION_PARAM_INDEX` 标记下的参数表下标(以 double 表示)。 + +- `ptr` (`const COperation*`):操作句柄。 +- `tags` (`uint32_t*`):输出参数标签数组。 +- `values` (`double*`):输出参数值数组。 +- `len` (`uintptr_t`):缓冲长度。 + +返回:`0` 成功;`-1` NULL;`-8` `len` 小于参数数量。 + +参数标签: + +| 常量 | 值 | 含义 | +| --- | --- | --- | +| `OPERATION_PARAM_FIXED` | 0 | 固定数值,`values[i]` 为数值本身。 | +| `OPERATION_PARAM_INDEX` | 1 | 所属线路参数表下标,`values[i]` 为下标值,需结合线路参数表解析。 | + +### operation_label(ptr) + +返回操作的可选元数据标签。 + +- `ptr` (`const COperation*`):操作句柄。 + +返回:新分配的 C 字符串,用 `cqlib_string_free` 释放;操作无标签或句柄为 NULL 时返回 `NULL`。 + +```c +uint32_t qubits[2] = {0, 1}; +double params[1] = {0.7}; +COperation *op = operation_new("RZZ", qubits, 2, params, 1); +if (op != NULL) { + char *name = operation_name(op); /* "RZZ" */ + cqlib_string_free(name); + + uintptr_t n = operation_qubits_len(op); + uint32_t *ids = malloc(n * sizeof(uint32_t)); + operation_qubits(op, ids, n); /* ids == {0, 1} */ + free(ids); + + operation_free(op); +} +``` + +--- + +## 指令种类判别 + +每个判别函数检查操作携带的指令种类,返回值统一为:`1` 是、`0` 否、`-1` 句柄为 NULL。 + +### operation_is_unitary(ptr) + +判断操作是否使用用户自定义幺正指令。 + +- `ptr` (`const COperation*`):操作句柄。 + +返回:`1` 是;`0` 否;`-1` NULL。 + +### operation_is_mcgate(ptr) + +判断操作是否使用多受控门指令。 + +- `ptr` (`const COperation*`):操作句柄。 + +返回:`1` 是;`0` 否;`-1` NULL。 + +### operation_is_circuit_gate(ptr) + +判断操作是否使用线路定义的门指令。 + +- `ptr` (`const COperation*`):操作句柄。 + +返回:`1` 是;`0` 否;`-1` NULL。 + +### operation_is_directive(ptr) + +判断操作是否使用非酉 directive 指令(barrier、measure、reset)。 + +- `ptr` (`const COperation*`):操作句柄。 + +返回:`1` 是;`0` 否;`-1` NULL。 + +### operation_is_delay(ptr) + +判断操作是否使用延迟指令。 + +- `ptr` (`const COperation*`):操作句柄。 + +返回:`1` 是;`0` 否;`-1` NULL。 + +### operation_is_classical_control(ptr) + +判断操作是否使用结构化经典控制流指令(`if` / `while` / `for` / `switch` / `break` / `continue`)。 + +- `ptr` (`const COperation*`):操作句柄。 + +返回:`1` 是;`0` 否;`-1` NULL。 + +### operation_is_classical_data(ptr) + +判断操作是否使用经典数据指令(store、测量写入值)。 + +- `ptr` (`const COperation*`):操作句柄。 + +返回:`1` 是;`0` 否;`-1` NULL。 + +### operation_is_quantum_gate(ptr) + +判断操作是否为酉量子门(标准门、多受控门、用户自定义幺正门或线路定义的门)。 + +- `ptr` (`const COperation*`):操作句柄。 + +返回:`1` 是;`0` 否;`-1` NULL。 + +### operation_is_instruction(ptr) + +判断操作携带的是普通指令而非结构化经典控制流。 + +- `ptr` (`const COperation*`):操作句柄。 + +返回:`1` 是;`0` 为经典控制流操作;`-1` NULL。 + +--- + +## 指令内省 + +### operation_gate_arity(ptr, qubits, params) + +写出指令固有的 `(qubit_count, parameter_count)`。 + +- `ptr` (`const COperation*`):操作句柄。 +- `qubits` (`uintptr_t*`):写出的量子比特数量。 +- `params` (`uintptr_t*`):写出的参数数量。 + +返回:`0` 成功;`-1` NULL;`-3` 数量为可变(barrier、多比特测量、经典控制流)。 + +### operation_as_instruction(ptr) + +当操作携带普通(非控制流)指令时,返回其拥有所有权的克隆。 + +- `ptr` (`const COperation*`):操作句柄。 + +返回:新分配的 `COperation*`,用 `operation_free` 释放;输入为 NULL 或操作是经典控制流指令时返回 `NULL`。 + +### operation_directive(ptr) + +以 `OPERATION_DIRECTIVE_*` 标签形式返回操作携带的 directive。 + +- `ptr` (`const COperation*`):操作句柄。 + +返回:directive 标签;操作不是 directive 时返回 `OPERATION_DIRECTIVE_NONE`(0);`-1` NULL。 + +directive 标签: + +| 常量 | 值 | 含义 | +| --- | --- | --- | +| `OPERATION_DIRECTIVE_NONE` | 0 | 操作不携带 directive 指令。 | +| `OPERATION_DIRECTIVE_BARRIER` | 1 | Barrier directive。 | +| `OPERATION_DIRECTIVE_MEASURE` | 2 | Measure directive。 | +| `OPERATION_DIRECTIVE_RESET` | 3 | Reset directive。 | + +### operation_result(ptr, out) + +把测量操作产生的不可变经典值快照写入 `out`:线路内值表下标加上 (tag, width) 类型分量(`CClassicalValueInfo` 布局见 [经典数据与控制流](9_classical_control_flow.md))。 + +- `ptr` (`const COperation*`):操作句柄。 +- `out` (`CClassicalValueInfo*`):输出结构。 + +返回:`0` 成功;`-1` NULL;`-3` 操作不是测量(无经典结果)。 + +### operation_reads_value(ptr, expr) + +判断操作是否直接或递归读取 `expr` 引用的不可变经典值。 + +- `ptr` (`const COperation*`):操作句柄。 +- `expr` (`const CClassicalExpr*`):引用经典值的表达式句柄。 + +返回:`1` 是;`0` 否;`-1` NULL;`-8` `expr` 不是普通的值读取表达式。 + +--- + +## 操作矩阵 + +三个接口配合使用:先查长度,再取维度与数据。非酉指令或含未解析符号参数的操作没有数值矩阵。 + +### operation_matrix_len(ptr) + +返回幺正矩阵的复数元素总数(行数 × 列数)。 + +- `ptr` (`const COperation*`):操作句柄。 + +返回:复数元素数量;`NULL` 输入或操作无数值矩阵(非酉指令或未解析的符号参数)时返回 0。 + +### operation_matrix_dims(ptr, rows, cols) + +把矩阵形状写入 `rows` 与 `cols`。 + +- `ptr` (`const COperation*`):操作句柄。 +- `rows` (`uintptr_t*`):写出行数。 +- `cols` (`uintptr_t*`):写出列数。 + +返回:`0` 成功;`-1` NULL;`-3` 操作无数值矩阵。 + +### operation_matrix(ptr, out, buffer_len) + +把幺正矩阵按行主序、交错 `(re, im)` double 拷贝进 `out`,缓冲至少容纳 `2 × operation_matrix_len` 个 double。 + +- `ptr` (`const COperation*`):操作句柄。 +- `out` (`double*`):输出缓冲。 +- `buffer_len` (`uintptr_t`):缓冲长度(double 数量)。 + +返回:写出的复数元素数量;失败时返回 0。 + +```c +uint32_t q0 = 0; +COperation *h = operation_new("H", &q0, 1, NULL, 0); +if (h != NULL) { + uintptr_t n = operation_matrix_len(h); /* 4(2x2 复数元素) */ + double *buf = malloc(2 * n * sizeof(double)); + uintptr_t written = operation_matrix(h, buf, 2 * n); /* written == 4 */ + free(buf); + operation_free(h); +} +``` + +--- + +## 解析快照 + +`circuit_index` 返回顶层操作的解析快照 `CValueOperation*`:只读、自包含,参数为 `CParameterValue`(固定数值或完整符号表达式,符号情形的 `param` 字段用 `param_free` 释放,见 [符号参数](3_parameter.md)),不随线路后续变更而改变。 + +### circuit_index(ptr, index) + +返回下标 `index` 处顶层操作的解析快照。 + +- `ptr` (`const CCircuit*`):线路句柄。 +- `index` (`uintptr_t`):操作下标。 + +返回:新分配的 `CValueOperation*`,用 `value_operation_free` 释放;NULL 输入或下标越界时返回 `NULL`。 + +### value_operation_free(ptr) + +释放快照句柄。允许传 `NULL`。 + +- `ptr` (`CValueOperation*`):快照句柄。 + +### value_operation_name(ptr) + +返回操作的指令名(如 `"H"`、`"delay"`)。 + +- `ptr` (`const CValueOperation*`):快照句柄。 + +返回:新分配的 C 字符串,用 `cqlib_string_free` 释放;`NULL` 时返回 `NULL`。 + +### value_operation_instruction_type(ptr) + +返回操作的指令类别(如 `"standard"`、`"delay"`)。 + +- `ptr` (`const CValueOperation*`):快照句柄。 + +返回:新分配的 C 字符串,用 `cqlib_string_free` 释放;`NULL` 时返回 `NULL`。 + +### value_operation_label(ptr) + +返回操作的可选元数据标签。 + +- `ptr` (`const CValueOperation*`):快照句柄。 + +返回:新分配的 C 字符串,用 `cqlib_string_free` 释放;操作无标签或句柄为 NULL 时返回 `NULL`。 + +### value_operation_num_qubits(ptr) + +返回操作作用的比特数量,用于为 `value_operation_qubits` 分配缓冲。 + +- `ptr` (`const CValueOperation*`):快照句柄。 + +返回:比特数量;`NULL` 时返回 0。 + +### value_operation_qubits(ptr, out, len) + +两步式读取作用比特 id:先调用 `value_operation_num_qubits` 分配缓冲,再拷贝进 `out`。 + +- `ptr` (`const CValueOperation*`):快照句柄。 +- `out` (`uint32_t*`):输出缓冲,可为 NULL(仅查询数量)。 +- `len` (`uintptr_t`):缓冲长度。 + +返回:比特总数。 + +### value_operation_num_params(ptr) + +返回操作携带的参数数量。 + +- `ptr` (`const CValueOperation*`):快照句柄。 + +返回:参数数量;`NULL` 时返回 0。 + +### value_operation_param(ptr, index, out) + +读取下标 `index` 处的参数,写出 `CParameterValue`。 + +- `ptr` (`const CValueOperation*`):快照句柄。 +- `index` (`uintptr_t`):参数下标。 +- `out` (`CParameterValue*`):写出的解析值;符号情形的 `param` 字段用 `param_free` 释放。 + +返回:`0` 成功;`-1` NULL;`-3` 下标越界。 + +```c +CCircuit *qc = circuit_new(1); +circuit_rx(qc, 0, 0.7); + +CValueOperation *op = circuit_index(qc, 0); +if (op != NULL) { + char *type = value_operation_instruction_type(op); /* "standard" */ + cqlib_string_free(type); + + CParameterValue v; + if (value_operation_param(op, 0, &v) == 0) { + /* v.tag == PARAMETER_VALUE_TAG_FIXED, v.value == 0.7 */ + } + value_operation_free(op); +} +circuit_free(qc); +``` + +--- + +## 延迟指令 + +### circuit_delay(ptr, qubit, duration) + +在 `qubit` 上追加数值时长的延迟(空闲时段)指令。 + +- `ptr` (`CCircuit*`):线路句柄。 +- `qubit` (`uint32_t`):比特 id。 +- `duration` (`double`):时长。 + +返回:`0` 成功;`-1` NULL;`-2` 比特越界;`-3` 时长非有限值。 + +### circuit_delay_param(ptr, qubit, duration) + +在 `qubit` 上追加符号时长的延迟指令(时长参数被克隆)。 + +- `ptr` (`CCircuit*`):线路句柄。 +- `qubit` (`uint32_t`):比特 id。 +- `duration` (`const CParameter*`):符号时长句柄。 + +返回:`0` 成功;`-1` NULL;`-2` 比特越界;`-3` 失败。 + +```c +CCircuit *qc = circuit_new(2); +circuit_delay(qc, 0, 100.0); /* 数值时长 */ + +CParameter *tau = param_parse("tau"); +circuit_delay_param(qc, 1, tau); /* 符号时长 */ +param_free(tau); + +circuit_free(qc); +``` diff --git a/docs/api/cn/c/0_circuit/5_gate_standard.md b/docs/api/cn/c/0_circuit/5_gate_standard.md new file mode 100644 index 00000000..d242c368 --- /dev/null +++ b/docs/api/cn/c/0_circuit/5_gate_standard.md @@ -0,0 +1,169 @@ +# 标准门 + +本页汇总 C API 中向线路追加标准量子门的全部函数;错误码与内存管理的全局约定见 [Overview](../0_overview.md)。 + +--- + +## 约定 + +- 本页所有函数返回 `int32_t` 错误码:`0` 成功,`-1` 传入 NULL 指针,`-2` 比特索引越界,`-3` 线路错误(数值角度为 NaN 或无穷时同样返回 `-3`)。 +- 数值变体以 `double` 传入角度(弧度);对应的 `_param` 变体以 `const CParameter*` 符号表达式传入角度,参数句柄为 NULL 时返回 `-1`。`CParameter` 句柄由 `param_parse` 创建、`param_free` 释放,详见[参数](3_parameter.md)。 +- 每个门的作用比特数量固定:单比特门 1 个,双比特门 2 个,三比特门 3 个;`qubit`/`control`/`target`/`a`/`b` 均为线路内的比特索引。 + +--- + +## 无参数单比特门 + +以下 13 个函数签名均为 `(struct CCircuit *ptr, uint32_t qubit)`,直接追加固定矩阵的单比特门。 + +| 函数 | 门 | 说明 | +| --- | --- | --- | +| `circuit_h(ptr, qubit)` | H | Hadamard 门,在计算基与叠加基之间变换。 | +| `circuit_x(ptr, qubit)` | X | Pauli-X 门,比特翻转。 | +| `circuit_y(ptr, qubit)` | Y | Pauli-Y 门,比特翻转并附加相位。 | +| `circuit_z(ptr, qubit)` | Z | Pauli-Z 门,相位翻转。 | +| `circuit_i(ptr, qubit)` | I | 恒等门,不改变量子态。 | +| `circuit_s(ptr, qubit)` | S | Clifford 相位门,diag(1, i)。 | +| `circuit_sdg(ptr, qubit)` | SDG | S 的逆门,diag(1, −i)。 | +| `circuit_t(ptr, qubit)` | T | π/8 相位门,diag(1, e^(iπ/4))。 | +| `circuit_tdg(ptr, qubit)` | TDG | T 的逆门,diag(1, e^(−iπ/4))。 | +| `circuit_x2p(ptr, qubit)` | X2P | √X 门,绕 X 轴 +90° 旋转。 | +| `circuit_x2m(ptr, qubit)` | X2M | √X† 门,绕 X 轴 −90° 旋转。 | +| `circuit_y2p(ptr, qubit)` | Y2P | √Y 门,绕 Y 轴 +90° 旋转。 | +| `circuit_y2m(ptr, qubit)` | Y2M | √Y† 门,绕 Y 轴 −90° 旋转。 | + +--- + +## 参数化单比特门 + +数值变体以 `double` 传角;`_param` 变体以 `const CParameter*` 传角,二者追加同一标准门。 + +| 函数 | 门 | 说明 | +| --- | --- | --- | +| `circuit_rx(ptr, qubit, theta)` | RX | 绕 X 轴旋转 θ,exp(−iθX/2)。 | +| `circuit_rx_param(ptr, qubit, param_ptr)` | RX | 同上,角度为符号表达式。 | +| `circuit_ry(ptr, qubit, theta)` | RY | 绕 Y 轴旋转 θ,exp(−iθY/2)。 | +| `circuit_ry_param(ptr, qubit, param_ptr)` | RY | 同上,角度为符号表达式。 | +| `circuit_rz(ptr, qubit, theta)` | RZ | 绕 Z 轴旋转 θ,exp(−iθZ/2)。 | +| `circuit_rz_param(ptr, qubit, param_ptr)` | RZ | 同上,角度为符号表达式。 | +| `circuit_phase(ptr, qubit, lambda)` | Phase | 相位门 diag(1, e^(iλ)),对 `1` 态分量施加相位。 | +| `circuit_phase_param(ptr, qubit, param)` | Phase | 同上,相位为符号表达式。 | +| `circuit_u(ptr, qubit, theta, phi, lambda)` | U | 通用单比特门 U(θ, φ, λ),3 个角度参数。 | +| `circuit_u_param(ptr, qubit, theta, phi, lambda)` | U | 同上,三个角度均为符号表达式。 | +| `circuit_xy(ptr, qubit, theta)` | XY | XY 交互族门,非对角元为 −i·e^(∓iθ)。 | +| `circuit_xy_param(ptr, qubit, param)` | XY | 同上,相位为符号表达式。 | +| `circuit_xy2p(ptr, qubit, theta)` | XY2P | 绕 θ 选取的 XY 平面轴 +90° 旋转。 | +| `circuit_xy2p_param(ptr, qubit, param)` | XY2P | 同上,轴角为符号表达式。 | +| `circuit_xy2m(ptr, qubit, theta)` | XY2M | 绕 θ 选取的 XY 平面轴 −90° 旋转。 | +| `circuit_xy2m_param(ptr, qubit, param)` | XY2M | 同上,轴角为符号表达式。 | +| `circuit_rxy(ptr, qubit, theta, phi)` | RXY | 绕 XY 平面内由 φ 确定的轴旋转 θ。 | +| `circuit_rxy_param(ptr, qubit, theta, phi)` | RXY | 同上,两个角度均为符号表达式。 | + +--- + +## 双比特门 + +`cx`/`cy`/`cz`/`swap` 为固定矩阵门;其余以角度参数化,`_param` 变体以 `const CParameter*` 传角。 + +| 函数 | 门 | 说明 | +| --- | --- | --- | +| `circuit_cx(ptr, control, target)` | CX | 受控 X(CNOT),控制位为 `1` 时翻转目标位。 | +| `circuit_cy(ptr, control, target)` | CY | 受控 Y,控制位为 `1` 时对目标位作用 Y。 | +| `circuit_cz(ptr, control, target)` | CZ | 受控 Z,控制位为 `1` 时对目标位作用 Z。 | +| `circuit_swap(ptr, a, b)` | SWAP | 交换两个比特的状态。 | +| `circuit_rxx(ptr, a, b, theta)` | RXX | 二体 Pauli 旋转 exp(−iθ X⊗X/2)。 | +| `circuit_rxx_param(ptr, a, b, param)` | RXX | 同上,角度为符号表达式。 | +| `circuit_ryy(ptr, a, b, theta)` | RYY | 二体 Pauli 旋转 exp(−iθ Y⊗Y/2)。 | +| `circuit_ryy_param(ptr, a, b, param)` | RYY | 同上,角度为符号表达式。 | +| `circuit_rzz(ptr, a, b, theta)` | RZZ | 二体 Pauli 旋转 exp(−iθ Z⊗Z/2)。 | +| `circuit_rzz_param(ptr, a, b, param)` | RZZ | 同上,角度为符号表达式。 | +| `circuit_rzx(ptr, a, b, theta)` | RZX | 二体 Pauli 旋转 exp(−iθ Z⊗X/2),`a` 上为 Z、`b` 上为 X。 | +| `circuit_rzx_param(ptr, a, b, param)` | RZX | 同上,角度为符号表达式。 | +| `circuit_crx(ptr, control, target, theta)` | CRX | 受控 RX(θ),控制位为 `1` 时旋转目标位。 | +| `circuit_crx_param(ptr, control, target, param)` | CRX | 同上,角度为符号表达式。 | +| `circuit_cry(ptr, control, target, theta)` | CRY | 受控 RY(θ),控制位为 `1` 时旋转目标位。 | +| `circuit_cry_param(ptr, control, target, param)` | CRY | 同上,角度为符号表达式。 | +| `circuit_crz(ptr, control, target, theta)` | CRZ | 受控 RZ(θ),控制位为 `1` 时旋转目标位。 | +| `circuit_crz_param(ptr, control, target, param)` | CRZ | 同上,角度为符号表达式。 | +| `circuit_fsim(ptr, a, b, theta, phi)` | fSim | fSim 门:`01`/`10` 子块按 θ 旋转,`11` 分量附加 e^(−iφ) 相位。 | +| `circuit_fsim_param(ptr, a, b, theta, phi)` | fSim | 同上,两个角度均为符号表达式。 | + +--- + +## 三比特门 + +| 函数 | 门 | 说明 | +| --- | --- | --- | +| `circuit_ccx(ptr, control1, control2, target)` | CCX | Toffoli 门,两个控制位均为 `1` 时翻转目标位。 | + +--- + +## 数学约定 + +旋转门采用半角指数定义: + +```text +RX(θ) = exp(-i θ X / 2) +RY(θ) = exp(-i θ Y / 2) +RZ(θ) = exp(-i θ Z / 2) +RXY(θ, φ) = 绕 XY 平面内方位角为 φ 的轴旋转 θ(半角约定) +``` + +二体 Pauli 旋转门使用相同约定: + +```text +RXX(θ) = exp(-i θ X⊗X / 2) +RYY(θ) = exp(-i θ Y⊗Y / 2) +RZZ(θ) = exp(-i θ Z⊗Z / 2) +RZX(θ) = exp(-i θ Z⊗X / 2) +``` + +其他门的矩阵形式: + +```text +U(θ, φ, λ) = [[cos(θ/2), -e^(-iλ)·sin(θ/2)], + [e^(iφ)·sin(θ/2), e^(i(φ+λ))·cos(θ/2)]] + +fSim(θ, φ) = [[1, 0, 0, 0 ], + [0, cos(θ), -i·sin(θ), 0 ], + [0, -i·sin(θ), cos(θ), 0 ], + [0, 0, 0, e^(-iφ)]] +``` + +--- + +## 示例 + +```c +#include "cqlib_c.h" + +/* Bell 线路:H(0) + CX(0, 1) */ +CCircuit *c = circuit_new(2); + +if (circuit_h(c, 0) != 0) { + /* 处理错误 */ +} +if (circuit_cx(c, 0, 1) != 0) { + /* 处理错误 */ +} + +/* 符号参数:RZ(0, theta),theta 为符号表达式 */ +CParameter *theta = param_parse("theta"); + +if (circuit_rz_param(c, 0, theta) != 0) { + /* 处理错误 */ +} + +param_free(theta); +circuit_free(c); +``` + +--- + +## 相关页面 + +- [参数](3_parameter.md):`CParameter` 的创建、求值与释放。 +- [线路](1_circuit.md) 与 [操作指令](4_operation_instruction.md):`circuit_measure`、`circuit_reset`、`circuit_barrier`、`circuit_delay` 等非酉指令。 +- [酉门](6_gate_unitary.md):矩阵定义的自定义酉门。 +- [多控制门](7_gate_mc_gate.md):向任意标准门追加控制位。 +- [线路转矩阵](13_circuit_to_matrix.md):整线路酉矩阵的数值输出。 diff --git a/docs/api/cn/c/0_circuit/6_gate_unitary.md b/docs/api/cn/c/0_circuit/6_gate_unitary.md new file mode 100644 index 00000000..5a11e7fd --- /dev/null +++ b/docs/api/cn/c/0_circuit/6_gate_unitary.md @@ -0,0 +1,183 @@ +# 酉门 + +本页介绍 C API 中以矩阵定义的自定义酉门追加接口;错误码与内存管理的全局约定见 [Overview](../0_overview.md)。 + +--- + +## circuit_unitary(ptr, label, num_qubits, matrix, qubits, qubits_len) + +向线路追加一个由数值矩阵定义的自定义酉门。 + +参数: + +- `ptr` (`struct CCircuit *`):目标线路句柄。 +- `label` (`const char *`):门的可读名称,用于显示、可视化与 IR 输出。 +- `num_qubits` (`uintptr_t`):门作用的量子比特数量 n,最大为 20。 +- `matrix` (`const Complex64 *`):行主序展平的 2^n × 2^n 复数矩阵,共 2^n·2^n 个 `Complex64` 元素;每个元素为 `{double re; double im}` 结构,实虚成对交错,布局与 `circuit_to_matrix` 输出一致。 +- `qubits` (`const uint32_t *`):作用比特索引数组。 +- `qubits_len` (`uintptr_t`):`qubits` 的长度,须等于 `num_qubits`。 + +返回:`int32_t`,`0` 成功;`-1` NULL 输入(`ptr`、`label` 或 `matrix` 为 NULL);`-2` 比特越界;`-3` 矩阵非酉、形状不符或应用失败;`-4` `label` 含无效 UTF-8;`-8` `num_qubits` 超过 20。 + +矩阵在追加时做酉性校验:U†U 须在容差范围内为单位矩阵,否则返回 `-3`。数值矩阵定义的门不带应用参数,`qubits` 按数组顺序映射到门定义的比特。 + +```c +#include +#include "cqlib_c.h" + +/* 自定义 2×2 酉门:sqrt(X) */ +CCircuit *c = circuit_new(1); + +double s = 1.0 / sqrt(2.0); +Complex64 sqrt_x[4] = { + {s, 0.0}, {0.0, -s}, + {0.0, -s}, {s, 0.0}, +}; + +uint32_t qubits[1] = {0}; +if (circuit_unitary(c, "sqrt_x", 1, sqrt_x, qubits, 1) != 0) { + /* 处理错误 */ +} + +circuit_free(c); +``` + +--- + +## circuit_unitary_with_params(ptr, label, num_qubits, re_exprs, im_exprs, param_names, param_names_len, qubits, qubits_len, params, params_len) + +向线路追加一个由符号矩阵定义的参数化自定义酉门。 + +参数: + +- `ptr` (`struct CCircuit *`):目标线路句柄。 +- `label` (`const char *`):门的可读名称。 +- `num_qubits` (`uintptr_t`):门作用的量子比特数量 n,最大为 20。 +- `re_exprs` (`const char *const *`):行主序展平的 2^n·2^n 个实部表达式字符串,如 `"cos(theta)"`、`"1"`。 +- `im_exprs` (`const char *const *`):同布局的虚部表达式字符串数组;可为 NULL,此时所有虚部为 0。 +- `param_names` (`const char *const *`):位置参数名列表,声明应用参数与矩阵中符号名的绑定顺序。 +- `param_names_len` (`uintptr_t`):`param_names` 的长度。 +- `qubits` (`const uint32_t *`):作用比特索引数组。 +- `qubits_len` (`uintptr_t`):`qubits` 的长度,须等于 `num_qubits`。 +- `params` (`const struct CParameter *const *`):应用时的参数句柄数组,第 i 个参数绑定到 `param_names[i]`。 +- `params_len` (`uintptr_t`):`params` 的长度,须等于 `param_names_len`。 + +返回:`int32_t`,`0` 成功;`-1` NULL 输入;`-2` 比特越界;`-3` 形状、符号声明或应用失败;`-4` 无效 UTF-8 或表达式语法错误;`-8` 尺寸无效(`num_qubits` 超过 20 或 `param_names_len` 超过 65535)。 + +表达式使用参数表达式语法(与 `param_parse` 一致);矩阵中出现的自由符号必须已在 `param_names` 中声明。数值角度以表达式形式书写,例如 `"pi/2"`。 + +```c +#include "cqlib_c.h" + +/* 参数化对角相位门 diag(1, e^(i·theta)),应用时绑定 theta = pi/2 */ +CCircuit *c = circuit_new(1); + +const char *re[4] = {"1", "0", "0", "cos(theta)"}; +const char *im[4] = {"0", "0", "0", "sin(theta)"}; +const char *names[1] = {"theta"}; + +CParameter *angle = param_parse("pi/2"); +const struct CParameter *params[1] = {angle}; + +uint32_t qubits[1] = {0}; +if (circuit_unitary_with_params(c, "phase_e", 1, + re, im, names, 1, + qubits, 1, params, 1) != 0) { + /* 处理错误 */ +} + +param_free(angle); +circuit_free(c); +``` + +--- + +## 在参数取值处求值酉矩阵 + +C ABI 没有独立的酉门句柄,因此在线路之外求值一个门定义时复用 `circuit_unitary_with_params` 的输入方式:符号矩阵加上按位置绑定的形式参数名。 + +### unitary_gate_matrix_for_params_len(matrix) + +```c +uintptr_t unitary_gate_matrix_for_params_len(const struct CSymbolicMatrix *matrix); +``` + +返回 `unitary_gate_matrix_for_params` 为 `matrix` 写出的 `Complex64` 元素个数(`rows * cols`);NULL 或非方阵时返回 0。 + +### unitary_gate_matrix_for_params(matrix, param_names, param_names_len, params, params_len, buffer, buffer_len) + +```c +int32_t unitary_gate_matrix_for_params(const struct CSymbolicMatrix *matrix, + const char *const *param_names, + uintptr_t param_names_len, + const double *params, + uintptr_t params_len, + Complex64 *buffer, + uintptr_t buffer_len); +``` + +在给定的数值参数取值处求值自定义酉门的符号矩阵,并把得到的数值矩阵按行主序写入 `buffer`。门的构造与校验和 `circuit_unitary_with_params` 完全一致(形状、参数个数、数值有限性、酉性)。 + +参数: + +- `matrix` (`const struct CSymbolicMatrix *`):`2^n × 2^n` 符号矩阵(构造见[符号矩阵](10_symbolic_matrix.md))。 +- `param_names` (`const char *const *`):形式参数名列表,按顺序把应用参数绑定到矩阵符号;`param_names_len` 为 0 时可为 NULL。 +- `param_names_len` (`uintptr_t`):`param_names` 的长度,不超过 65535。 +- `params` (`const double *`):数值参数取值;`params_len` 为 0 时可为 NULL。 +- `params_len` (`uintptr_t`):`params` 的长度。 +- `buffer` (`Complex64 *`):接收求值结果的缓冲,按行主序。 +- `buffer_len` (`uintptr_t`):缓冲长度,至少为 `unitary_gate_matrix_for_params_len(matrix)`。 + +返回:`int32_t`,`0` 成功;`-1` NULL 输入;`-3` 形状、参数个数、非有限数值或酉性校验失败;`-4` 参数名不是合法 UTF-8;`-8` 尺寸非法(空矩阵或非 2 的幂、超过 20 比特、缓冲过小)。 + +```c +#include +#include "cqlib_c.h" + +/* 在 theta = pi/2 处求值 U(theta) = cos(theta)*I + i*sin(theta)*X(结果为 iX) */ +struct CParameter *zero = param_parse("0"); +struct CParameter *cos_t = param_parse("cos(theta)"); +struct CParameter *sin_t = param_parse("sin(theta)"); +struct CSymbolicComplex *e00 = symbolic_complex_new(cos_t, zero); +struct CSymbolicComplex *e01 = symbolic_complex_new(zero, sin_t); +struct CSymbolicComplex *e10 = symbolic_complex_new(zero, sin_t); +struct CSymbolicComplex *e11 = symbolic_complex_new(cos_t, zero); +const struct CSymbolicComplex *elements[4] = {e00, e01, e10, e11}; +struct CSymbolicMatrix *matrix = symbolic_matrix_new(2, 2, elements, 4); + +const char *names[1] = {"theta"}; +double params[1] = {3.141592653589793 / 2.0}; + +uintptr_t len = unitary_gate_matrix_for_params_len(matrix); /* 4 */ +Complex64 out[4]; +if (unitary_gate_matrix_for_params(matrix, names, 1, params, 1, out, len) == 0) { + /* out = {{0,0},{0,1},{0,1},{0,0}},即矩阵 iX */ +} + +symbolic_matrix_free(matrix); +symbolic_complex_free(e00); +symbolic_complex_free(e01); +symbolic_complex_free(e10); +symbolic_complex_free(e11); +param_free(zero); +param_free(cos_t); +param_free(sin_t); +``` + +--- + +## 两种定义方式对比 + +| 函数 | 矩阵定义 | 应用参数 | 典型用途 | +| --- | --- | --- | --- | +| `circuit_unitary` | 数值 `Complex64` 数组 | 无 | 固定黑盒矩阵、校准门、测试 oracle。 | +| `circuit_unitary_with_params` | 实虚部表达式字符串 | `CParameter` 数组按 `param_names` 位置绑定 | 参数化自定义矩阵门。 | + +--- + +## 相关页面 + +- [标准门](5_gate_standard.md):内置标准门集合。 +- [参数](3_parameter.md):`CParameter` 的创建与表达式语法。 +- [线路门](8_gate_circuit_gate.md):以子线路定义的复合门。 +- [线路转矩阵](13_circuit_to_matrix.md):与 `circuit_unitary` 输入一致的矩阵布局输出。 diff --git a/docs/api/cn/c/0_circuit/7_gate_mc_gate.md b/docs/api/cn/c/0_circuit/7_gate_mc_gate.md new file mode 100644 index 00000000..08db1d7b --- /dev/null +++ b/docs/api/cn/c/0_circuit/7_gate_mc_gate.md @@ -0,0 +1,101 @@ +# 多控制门 + +本页介绍 C API 中向任意标准门追加控制位的 `circuit_multi_control` 接口;错误码与内存管理的全局约定见 [Overview](../0_overview.md)。 + +--- + +## circuit_multi_control(ptr, gate_name, controls, controls_len, targets, targets_len, params, params_len) + +向线路追加一个多控制标准门:仅当所有控制位处于 `1` 态时,基础门才作用于其目标比特。 + +参数: + +- `ptr` (`struct CCircuit *`):目标线路句柄。 +- `gate_name` (`const char *`):基础标准门名,取值见下文门名表。 +- `controls` (`const uint32_t *`):控制比特索引数组。 +- `controls_len` (`uintptr_t`):控制位数量,可为 0。 +- `targets` (`const uint32_t *`):基础门作用比特数组,长度须等于基础门自身的比特数。 +- `targets_len` (`uintptr_t`):`targets` 的长度。 +- `params` (`const struct CParameter *const *`):基础门参数句柄数组,长度须等于基础门的参数数;无参数门传 `NULL, 0`。 +- `params_len` (`uintptr_t`):`params` 的长度。 + +操作的比特列表为 `[controls..., targets...]`:新增控制位排在最前,其后是基础门自身所需的比特;若基础门自带控制位(如 `"CX"`),其内部控制位仍按该门原有顺序保留。例如以 `"X"` 为基础门、两个控制位作用于 `[c0, c1, t]` 时,`c0`、`c1` 为控制位,`t` 为翻转目标;以 `"CX"` 为基础门、一个新增控制位时,三个比特依次为 `[新控制位, CX 控制位, CX 目标位]`。 + +返回:`int32_t`,`0` 成功;`-1` NULL 输入(`ptr` 或 `gate_name` 为 NULL,或长度非 0 时数组为 NULL);`-2` 比特越界;`-3` 基础门不可受控或比特、参数数量不匹配;`-4` `gate_name` 含无效 UTF-8;`-8` 未知门名。 + +参数以 `CParameter` 符号表达式传入(数值以表达式形式书写,如 `"pi/2"`),按位置对应基础门的参数(例如 `"U"` 需 3 个、`"RXY"` 与 `"fSim"` 各需 2 个、`"RX"` 需 1 个)。 + +### 支持的基础门名 + +`I`、`H`、`X`、`Y`、`Z`、`S`、`SDG`、`T`、`TDG`、`X2P`、`X2M`、`Y2P`、`Y2M`、`RX`、`RY`、`RZ`、`Phase`、`GPhase`、`U`、`XY`、`XY2P`、`XY2M`、`RXY`、`SWAP`、`RXX`、`RYY`、`RZZ`、`RZX`、`CX`、`CY`、`CZ`、`CCX`、`CRX`、`CRY`、`CRZ`、`fSim`(`FSIM` 与 `fSim` 均可)。 + +### MCX 示例 + +```c +#include "cqlib_c.h" + +CCircuit *c = circuit_new(4); + +/* 双控制 X(Toffoli 语义):controls = {0, 1}, target = 2 */ +uint32_t ccx_controls[2] = {0, 1}; +uint32_t ccx_target[1] = {2}; + +if (circuit_multi_control(c, "X", + ccx_controls, 2, + ccx_target, 1, + NULL, 0) != 0) { + /* 处理错误 */ +} + +/* 三控制 H */ +uint32_t mch_controls[3] = {0, 1, 2}; +uint32_t mch_target[1] = {3}; + +if (circuit_multi_control(c, "H", + mch_controls, 3, + mch_target, 1, + NULL, 0) != 0) { + /* 处理错误 */ +} + +/* 单控制 RZ(theta),角度为符号表达式 */ +CParameter *theta = param_parse("theta"); +const struct CParameter *crz_params[1] = {theta}; +uint32_t crz_control[1] = {0}; +uint32_t crz_target[1] = {1}; + +if (circuit_multi_control(c, "RZ", + crz_control, 1, + crz_target, 1, + crz_params, 1) != 0) { + /* 处理错误 */ +} + +param_free(theta); +circuit_free(c); +``` + +--- + +## 与标准受控门的关系 + +常见受控门是 `circuit_multi_control` 以特定基础门调用的特例: + +| 标准门函数 | 等价调用 | +| --- | --- | +| `circuit_cx(ptr, c, t)` | `circuit_multi_control(ptr, "X", {c}, 1, {t}, 1, NULL, 0)` | +| `circuit_cy(ptr, c, t)` | `circuit_multi_control(ptr, "Y", {c}, 1, {t}, 1, NULL, 0)` | +| `circuit_cz(ptr, c, t)` | `circuit_multi_control(ptr, "Z", {c}, 1, {t}, 1, NULL, 0)` | +| `circuit_ccx(ptr, c1, c2, t)` | `circuit_multi_control(ptr, "X", {c1, c2}, 2, {t}, 1, NULL, 0)` | +| `circuit_crx(ptr, c, t, θ)` | `circuit_multi_control(ptr, "RX", {c}, 1, {t}, 1, {θ}, 1)` | +| `circuit_cry(ptr, c, t, θ)` | `circuit_multi_control(ptr, "RY", {c}, 1, {t}, 1, {θ}, 1)` | +| `circuit_crz(ptr, c, t, θ)` | `circuit_multi_control(ptr, "RZ", {c}, 1, {t}, 1, {θ}, 1)` | + +需要更多控制位或对任意标准门添加控制时,使用 `circuit_multi_control`;单控制/双控制的基础受控门直接调用[标准门](5_gate_standard.md)函数即可。 + +--- + +## 相关页面 + +- [标准门](5_gate_standard.md):基础门集合与各门参数个数。 +- [参数](3_parameter.md):`CParameter` 的创建与表达式语法。 diff --git a/docs/api/cn/c/0_circuit/8_gate_circuit_gate.md b/docs/api/cn/c/0_circuit/8_gate_circuit_gate.md new file mode 100644 index 00000000..65332c65 --- /dev/null +++ b/docs/api/cn/c/0_circuit/8_gate_circuit_gate.md @@ -0,0 +1,197 @@ +# 线路门 + +本页介绍 C API 中的线路门:把一段已构造的线路封装为可命名、可复用的复合门;错误码与内存管理的全局约定见 [Overview](../0_overview.md)。 + +--- + +## circuit_to_gate(ptr, name) + +把线路的一份冻结副本封装为命名复合门。传入线路被克隆,封装后仍可继续使用或释放。 + +参数: + +- `ptr` (`const struct CCircuit *`):源线路句柄。 +- `name` (`const char *`):复合门名称。 + +返回:新分配的 `struct CCircuitGate *`,用 `circuit_gate_free` 释放;输入为 NULL、名称含无效 UTF-8 或封装失败时返回 NULL。 + +封装时,源线路中的自由符号按其出现顺序构成复合门的调用签名,可通过 `circuit_gate_signature_params` 查询。 + +--- + +## circuit_gate_free(ptr) + +释放复合门句柄。允许传 NULL,无返回值。 + +--- + +## circuit_gate_name(ptr) + +返回复合门名称的新分配 C 字符串。 + +参数: + +- `ptr` (`const struct CCircuitGate *`):复合门句柄。 + +返回:`char *`,用 `cqlib_string_free` 释放;输入为 NULL 或出错时返回 NULL。 + +--- + +## circuit_gate_num_qubits(ptr) + +返回复合门应用时需要的量子比特数量。 + +参数: + +- `ptr` (`const struct CCircuitGate *`):复合门句柄。 + +返回:`uintptr_t` 比特数;输入为 NULL 时返回 0。 + +--- + +## circuit_gate_num_params(ptr) + +返回复合门调用签名中的位置参数个数。 + +参数: + +- `ptr` (`const struct CCircuitGate *`):复合门句柄。 + +返回:`uintptr_t` 参数个数;输入为 NULL 时返回 0。 + +--- + +## circuit_gate_signature_params_len(ptr) + +返回调用签名中位置参数名的个数,即两步式输出的第一步。 + +参数: + +- `ptr` (`const struct CCircuitGate *`):复合门句柄。 + +返回:`uintptr_t` 签名参数名个数;输入为 NULL 时返回 0。 + +--- + +## circuit_gate_signature_params(ptr, out, len) + +把调用签名的参数名拷贝到 `out`(两步式输出;先调用 `circuit_gate_signature_params_len` 查询长度)。 + +参数: + +- `ptr` (`const struct CCircuitGate *`):复合门句柄。 +- `out` (`char **`):输出缓冲区,调用者按长度分配;传 NULL 仅查询长度。 +- `len` (`uintptr_t`):`out` 可容纳的元素数。 + +返回:`uintptr_t` 签名参数名总数;每个写出的字符串由调用者用 `cqlib_string_free` 释放。 + +--- + +## circuit_circuit_gate(ptr, gate, qubits, qubits_len, params, params_len) + +向线路追加复合门,`params` 按位置绑定到门的调用签名:第 i 个参数替换签名中第 i 个符号名,替换同时进行。 + +参数: + +- `ptr` (`struct CCircuit *`):目标线路句柄。 +- `gate` (`const struct CCircuitGate *`):复合门句柄。 +- `qubits` (`const uint32_t *`):复合门在外层线路中作用的比特索引数组,按数组顺序映射到门定义的比特。 +- `qubits_len` (`uintptr_t`):`qubits` 的长度,须等于 `circuit_gate_num_qubits(gate)`。 +- `params` (`const struct CParameter *const *`):位置参数句柄数组;无参数门传 `NULL, 0`。 +- `params_len` (`uintptr_t`):`params` 的长度,须等于 `circuit_gate_num_params(gate)`。 + +返回:`int32_t`,`0` 成功;`-1` NULL 输入(长度非 0 时数组为 NULL);`-2` 比特越界;`-3` 比特或参数数量不匹配、应用失败。 + +--- + +## 示例:封装 Bell 线路为门并复用 + +```c +#include +#include "cqlib_c.h" + +int main(void) { + /* 1. 构造 Bell 子线路 */ + CCircuit *inner = circuit_new(2); + circuit_h(inner, 0); + circuit_cx(inner, 0, 1); + + /* 2. 封装为命名复合门;源线路被克隆,随后即可释放 */ + struct CCircuitGate *bell = circuit_to_gate(inner, "Bell"); + circuit_free(inner); + + if (bell == NULL) { + return 1; + } + + /* 3. 查询门元数据:num_qubits == 2,num_params == 0 */ + char *name = circuit_gate_name(bell); + printf("gate: %s, qubits: %zu, params: %zu\n", + name, + circuit_gate_num_qubits(bell), + circuit_gate_num_params(bell)); + cqlib_string_free(name); + + /* 4. 在外层线路中把 Bell 门复用两次 */ + CCircuit *outer = circuit_new(4); + uint32_t first[2] = {0, 1}; + uint32_t second[2] = {2, 3}; + + if (circuit_circuit_gate(outer, bell, first, 2, NULL, 0) != 0) { + /* 处理错误 */ + } + if (circuit_circuit_gate(outer, bell, second, 2, NULL, 0) != 0) { + /* 处理错误 */ + } + + circuit_gate_free(bell); + circuit_free(outer); + return 0; +} +``` + +带参数的复合门按签名绑定:下面的子线路使用符号 `theta`,封装后签名为 `["theta"]`,外层调用以 `alpha` 替换。 + +```c +#include +#include "cqlib_c.h" + +CCircuit *inner = circuit_new(1); +CParameter *theta = param_parse("theta"); + +circuit_rz_param(inner, 0, theta); +param_free(theta); + +struct CCircuitGate *rot = circuit_to_gate(inner, "RzBlock"); +circuit_free(inner); + +/* 签名查询:circuit_gate_signature_params_len(rot) == 1 */ +uintptr_t n = circuit_gate_signature_params_len(rot); +char **names = malloc(n * sizeof(char *)); +circuit_gate_signature_params(rot, names, n); /* names[0] == "theta" */ +for (uintptr_t i = 0; i < n; ++i) { + cqlib_string_free(names[i]); +} +free(names); + +/* 外层调用:以 alpha 绑定 theta */ +CCircuit *outer = circuit_new(1); +CParameter *alpha = param_parse("alpha"); +const struct CParameter *bind[1] = {alpha}; +uint32_t q[1] = {0}; + +circuit_circuit_gate(outer, rot, q, 1, bind, 1); + +param_free(alpha); +circuit_gate_free(rot); +circuit_free(outer); +``` + +--- + +## 相关页面 + +- [线路](1_circuit.md):线路的创建、追加与组合。 +- [参数](3_parameter.md):`CParameter` 的创建与释放。 +- [酉门](6_gate_unitary.md):矩阵定义的自定义酉门。 +- [线路转矩阵](13_circuit_to_matrix.md):复合门展开后整线路矩阵的计算。 diff --git a/docs/api/cn/c/0_circuit/9_classical_control_flow.md b/docs/api/cn/c/0_circuit/9_classical_control_flow.md new file mode 100644 index 00000000..84e586b6 --- /dev/null +++ b/docs/api/cn/c/0_circuit/9_classical_control_flow.md @@ -0,0 +1,708 @@ +# 经典变量与控制流 + +本页介绍 C API 中与经典数据和结构化控制流相关的函数:经典变量的分配与查询、经典表达式的构造与内省、测量声明、经典存储,以及基于回调的 `if` / `if-else` / `while` / `for` / `switch` 控制流。错误码与内存管理约定见 [Overview](../0_overview.md)。 + +--- + +## 类型标签与快照结构 + +经典数据的静态类型由 (tag, width) 二元组描述,通过 `CClassicalType` 快照传递: + +```c +typedef struct CClassicalType { + uint32_t tag; /* CQLIB_CLASSICAL_TYPE_* 之一 */ + uint32_t width; /* 位宽,Bit/Bool 恒为 1 */ +} CClassicalType; +``` + +| 类型标签 | 值 | 类型 | 说明 | +| --- | --- | --- | --- | +| `CQLIB_CLASSICAL_TYPE_BIT` | 0 | `Bit` | 单个 bit,取值 0 或 1,常用于单量子比特测量结果。 | +| `CQLIB_CLASSICAL_TYPE_BOOL` | 1 | `Bool` | 逻辑布尔值,用于 `if` / `while` 条件。 | +| `CQLIB_CLASSICAL_TYPE_UINT` | 2 | `UInt(width)` | 指定位宽的无符号整数。 | +| `CQLIB_CLASSICAL_TYPE_BIT_VEC` | 3 | `BitVec(width)` | 指定位宽的 bit 向量。 | + +表达式读取的不可变经典值以 `CClassicalValueInfo` 快照表示: + +```c +typedef struct CClassicalValueInfo { + uint32_t index; /* 线路内值表下标 */ + uint32_t tag; /* CQLIB_CLASSICAL_TYPE_* 之一 */ + uint32_t width; /* 位宽,Bit/Bool 恒为 1 */ +} CClassicalValueInfo; +``` + +表达式节点种类由 `classical_expr_kind` 写出的标签描述: + +| 节点标签 | 值 | 节点 | +| --- | --- | --- | +| `CQLIB_CLASSICAL_EXPR_VAR` | 0 | 变量读取 | +| `CQLIB_CLASSICAL_EXPR_VALUE` | 1 | 不可变值读取 | +| `CQLIB_CLASSICAL_EXPR_BOOL_LITERAL` | 2 | `Bool` 字面量 | +| `CQLIB_CLASSICAL_EXPR_BIT_LITERAL` | 3 | `Bit` 字面量 | +| `CQLIB_CLASSICAL_EXPR_UINT_LITERAL` | 4 | `UInt` 字面量 | +| `CQLIB_CLASSICAL_EXPR_BIT_VEC_LITERAL` | 5 | `BitVec` 字面量 | +| `CQLIB_CLASSICAL_EXPR_UNARY` | 6 | 一元运算(`not`) | +| `CQLIB_CLASSICAL_EXPR_BINARY` | 7 | 二元运算(`and` / `or` / `xor`) | +| `CQLIB_CLASSICAL_EXPR_COMPARE` | 8 | 比较(`eq` / `ne` / `lt` / `le` / `gt` / `ge`) | +| `CQLIB_CLASSICAL_EXPR_CAST` | 9 | 类型转换(`bit_to_bool` / `bit_vec_to_uint`) | +| `CQLIB_CLASSICAL_EXPR_SELECT` | 10 | 三元选择 | +| `CQLIB_CLASSICAL_EXPR_EXTRACT_BIT` | 11 | 单 bit 提取 | +| `CQLIB_CLASSICAL_EXPR_EXTRACT_BITS` | 12 | 连续 bit 区间提取 | +| `CQLIB_CLASSICAL_EXPR_CONCAT` | 13 | `BitVec` 拼接 | +| `CQLIB_CLASSICAL_EXPR_PACK_BITS` | 14 | `Bit` 打包 | + +--- + +## 经典变量 + +### circuit_var(ptr, ty_tag, width) + +在线路中分配一个可变经典变量,句柄归调用者所有。 + +- `ptr` (`CCircuit*`):目标线路句柄。 +- `ty_tag` (`uint32_t`):类型标签,取 `CQLIB_CLASSICAL_TYPE_*` 之一。 +- `width` (`uint32_t`):位宽,仅对 `UInt` / `BitVec` 生效(0 会被拒绝);对 `Bit` / `Bool` 忽略。 + +返回新分配的 `CClassicalVar*`;线路句柄为 NULL 或类型参数非法时返回 NULL。 + +### classical_var_free(ptr) + +释放经典变量句柄。 + +- `ptr` (`CClassicalVar*`):待释放句柄,允许传 NULL。 + +无返回值。 + +### classical_var_id(ptr) + +返回变量在线路内的稳定身份标识。 + +- `ptr` (`const CClassicalVar*`):变量句柄。 + +返回 `uint32_t` 身份 id;句柄为 NULL 时返回 `UINT32_MAX`。 + +### classical_var_index(ptr) + +返回变量在线路变量表中的下标。 + +- `ptr` (`const CClassicalVar*`):变量句柄。 + +返回 `uint32_t` 表下标;句柄为 NULL 时返回 `UINT32_MAX`。 + +### classical_var_ty(ptr, tag, width) + +写出变量的类型(标签 + 位宽)。 + +- `ptr` (`const CClassicalVar*`):变量句柄。 +- `tag` (`uint32_t*`):写出 `CQLIB_CLASSICAL_TYPE_*` 标签的出参。 +- `width` (`uint32_t*`):写出位宽的出参。 + +返回 0 表示成功;-1 表示指针为 NULL。 + +### classical_var_expr(ptr) + +创建一个读取该变量当前运行时值的表达式。 + +- `ptr` (`const CClassicalVar*`):变量句柄。 + +返回新分配的 `CClassicalExpr*`(用 `classical_expr_free` 释放);句柄为 NULL 时返回 NULL。 + +### circuit_classical_vars_len(ptr) + +返回线路已分配的经典变量数量。 + +- `ptr` (`const CCircuit*`):目标线路句柄。 + +返回变量数量;句柄为 NULL 时返回 0。 + +### circuit_classical_vars(ptr, buffer, len) + +按两步式把线路全部经典变量的类型快照拷入 `buffer`(先调用 `circuit_classical_vars_len` 获取长度)。 + +- `ptr` (`const CCircuit*`):目标线路句柄。 +- `buffer` (`CClassicalType*`):接收 (tag, width) 快照的数组。 +- `len` (`uintptr_t`):数组长度,必须等于 `circuit_classical_vars_len`。 + +返回 0 表示成功;-1 表示指针为 NULL;-8 表示 `len` 与变量数量不符。 + +### circuit_validate_classical_var(ptr, var) + +检查经典变量属于该线路且类型记录一致。 + +- `ptr` (`const CCircuit*`):目标线路句柄。 +- `var` (`const CClassicalVar*`):待检查的变量句柄。 + +返回 0 表示通过;-1 表示指针为 NULL;-3 表示校验失败。 + +### circuit_validate_classical_expr(ptr, expr) + +检查表达式读取的每个经典变量和经典值都属于该线路。 + +- `ptr` (`const CCircuit*`):目标线路句柄。 +- `expr` (`const CClassicalExpr*`):待检查的表达式句柄。 + +返回 0 表示通过;-1 表示指针为 NULL;-3 表示校验失败。 + +--- + +## 经典表达式构造器 + +所有构造器都返回新分配的 `CClassicalExpr*`(用 `classical_expr_free` 释放),失败时返回 NULL。 + +### classical_expr_free(ptr) + +释放经典表达式句柄。 + +- `ptr` (`CClassicalExpr*`):待释放句柄,允许传 NULL。 + +无返回值。 + +### classical_expr_bit_literal(value) + +创建 `Bit` 字面量表达式(false = 0,true = 1)。 + +- `value` (`bool`):字面量取值。 + +返回新表达式句柄。 + +### classical_expr_bool_literal(value) + +创建 `Bool` 字面量表达式。 + +- `value` (`bool`):字面量取值。 + +返回新表达式句柄。 + +### classical_expr_uint_literal(width, lo, hi) + +创建 `UInt(width)` 字面量。128 位数值以两个 64 位半字传递(`lo` 为最低有效位)。 + +- `width` (`uint32_t`):位宽。 +- `lo` (`uint64_t`):数值低 64 位。 +- `hi` (`uint64_t`):数值高 64 位。 + +返回新表达式句柄;位宽为 0、超过 128 或数值不能容纳于该位宽时返回 NULL。 + +### classical_expr_bit_vec_literal(width, lo, hi) + +创建 `BitVec(width)` 字面量。128 位按小端打包的数值以两个 64 位半字传递(`lo` 为最低有效位)。 + +- `width` (`uint32_t`):位宽。 +- `lo` (`uint64_t`):打包值低 64 位。 +- `hi` (`uint64_t`):打包值高 64 位。 + +返回新表达式句柄;位宽为 0、超过 128 或数值不能容纳于该位宽时返回 NULL。 + +### classical_type_zero_literal(ty_tag, width) + +创建某经典类型的零字面量。 + +- `ty_tag` (`uint32_t`):类型标签,取 `CQLIB_CLASSICAL_TYPE_*` 之一。 +- `width` (`uint32_t`):位宽,用于 `UInt` / `BitVec`。 + +返回新表达式句柄;类型参数非法或位宽超出 128 位字面量表示范围时返回 NULL。 + +### classical_type_one_literal(ty_tag, width) + +创建某经典类型的一字面量。 + +- `ty_tag` (`uint32_t`):类型标签,取 `CQLIB_CLASSICAL_TYPE_*` 之一。 +- `width` (`uint32_t`):位宽,用于 `UInt` / `BitVec`。 + +返回新表达式句柄;类型参数非法或位宽超出 128 位字面量表示范围时返回 NULL。 + +### classical_expr_var(var) + +创建读取 `var` 当前运行时值的表达式。 + +- `var` (`const CClassicalVar*`):变量句柄。 + +返回新表达式句柄;句柄为 NULL 时返回 NULL。 + +### classical_expr_not(expr) + +创建 `Bool` 或 `Bit` 操作数上的 `not` 表达式。 + +- `expr` (`const CClassicalExpr*`):操作数。 + +返回新表达式句柄;输入为 NULL 或操作数类型非法时返回 NULL。 + +### classical_expr_and(lhs, rhs) + +创建 `and` 表达式,要求两侧同为 `Bool` 或同为 `Bit`。 + +- `lhs` (`const CClassicalExpr*`):左操作数。 +- `rhs` (`const CClassicalExpr*`):右操作数。 + +返回新表达式句柄;输入为 NULL 或类型不匹配时返回 NULL。 + +### classical_expr_or(lhs, rhs) + +创建 `or` 表达式,要求两侧同为 `Bool` 或同为 `Bit`。 + +- `lhs` (`const CClassicalExpr*`):左操作数。 +- `rhs` (`const CClassicalExpr*`):右操作数。 + +返回新表达式句柄;输入为 NULL 或类型不匹配时返回 NULL。 + +### classical_expr_xor(lhs, rhs) + +创建 `xor` 表达式,要求两侧同为 `Bool` 或同为 `Bit`。 + +- `lhs` (`const CClassicalExpr*`):左操作数。 +- `rhs` (`const CClassicalExpr*`):右操作数。 + +返回新表达式句柄;输入为 NULL 或类型不匹配时返回 NULL。 + +### classical_expr_eq(lhs, rhs) + +创建相等比较,产出 `Bool`;要求两侧类型相同。 + +- `lhs` (`const CClassicalExpr*`):左操作数。 +- `rhs` (`const CClassicalExpr*`):右操作数。 + +返回新表达式句柄;输入为 NULL 或类型不匹配时返回 NULL。 + +### classical_expr_ne(lhs, rhs) + +创建不等比较,产出 `Bool`;要求两侧类型相同。 + +- `lhs` (`const CClassicalExpr*`):左操作数。 +- `rhs` (`const CClassicalExpr*`):右操作数。 + +返回新表达式句柄;输入为 NULL 或类型不匹配时返回 NULL。 + +### classical_expr_lt(lhs, rhs) + +创建无符号小于比较,产出 `Bool`;要求两侧为位宽一致的 `UInt`。 + +- `lhs` (`const CClassicalExpr*`):左操作数。 +- `rhs` (`const CClassicalExpr*`):右操作数。 + +返回新表达式句柄;输入为 NULL 或操作数类型非法时返回 NULL。 + +### classical_expr_le(lhs, rhs) + +创建无符号小于等于比较,产出 `Bool`;要求两侧为位宽一致的 `UInt`。 + +- `lhs` (`const CClassicalExpr*`):左操作数。 +- `rhs` (`const CClassicalExpr*`):右操作数。 + +返回新表达式句柄;输入为 NULL 或类型非法时返回 NULL。 + +### classical_expr_gt(lhs, rhs) + +创建无符号大于比较,产出 `Bool`;要求两侧为位宽一致的 `UInt`。 + +- `lhs` (`const CClassicalExpr*`):左操作数。 +- `rhs` (`const CClassicalExpr*`):右操作数。 + +返回新表达式句柄;输入为 NULL 或操作数类型非法时返回 NULL。 + +### classical_expr_ge(lhs, rhs) + +创建无符号大于等于比较,产出 `Bool`;要求两侧为位宽一致的 `UInt`。 + +- `lhs` (`const CClassicalExpr*`):左操作数。 +- `rhs` (`const CClassicalExpr*`):右操作数。 + +返回新表达式句柄;输入为 NULL 或类型非法时返回 NULL。 + +### classical_expr_select(condition, then_expr, else_expr) + +创建三元选择表达式 `condition ? then_expr : else_expr`。条件必须是 `Bool`,两个分支类型必须相同。 + +- `condition` (`const CClassicalExpr*`):条件表达式。 +- `then_expr` (`const CClassicalExpr*`):条件成立时取值的表达式。 +- `else_expr` (`const CClassicalExpr*`):条件不成立时取值的表达式。 + +返回新表达式句柄;输入为 NULL 或操作数类型非法时返回 NULL。 + +### classical_expr_extract_bit(value, index) + +从 `UInt` 或 `BitVec` 表达式中提取单个 bit(下标 0 为最低有效位),产出 `Bit`。 + +- `value` (`const CClassicalExpr*`):被提取的表达式。 +- `index` (`uint32_t`):bit 下标。 + +返回新表达式句柄;输入为 NULL、操作数非整数类型或下标越界时返回 NULL。 + +### classical_expr_extract_bits(value, offset, width) + +从 `UInt` 或 `BitVec` 表达式中提取连续 bit 区间 `[offset, offset + width)`,产出 `BitVec(width)`。偏移 0 从最低有效位开始。 + +- `value` (`const CClassicalExpr*`):被提取的表达式。 +- `offset` (`uint32_t`):起始偏移。 +- `width` (`uint32_t`):区间宽度。 + +返回新表达式句柄;输入为 NULL 或区间非法时返回 NULL。 + +### classical_expr_pack_bits(bits, len) + +把一组 `Bit` 表达式打包为 `BitVec`;第一个 bit 成为输出 bit 下标 0。 + +- `bits` (`const CClassicalExpr* const*`):表达式句柄数组。 +- `len` (`uintptr_t`):数组长度。 + +返回新表达式句柄;出现 NULL 元素、空输入或操作数不是 `Bit` 时返回 NULL。 + +### classical_expr_concat(parts, len) + +把一组 `Bit` / `BitVec` 表达式拼接为更宽的 `BitVec`;第一段占据输出的最低有效位。 + +- `parts` (`const CClassicalExpr* const*`):表达式句柄数组。 +- `len` (`uintptr_t`):数组长度。 + +返回新表达式句柄;出现 NULL 元素、空输入或段类型非法时返回 NULL。 + +### classical_expr_bit_to_bool(expr) + +把 `Bit` 表达式显式转换为 `Bool`。 + +- `expr` (`const CClassicalExpr*`):待转换表达式。 + +返回新表达式句柄;输入为 NULL 或操作数不是 `Bit` 时返回 NULL。 + +### classical_expr_bit_vec_to_uint(expr) + +把 `BitVec` 表达式显式转换为等位宽的小端 `UInt`。 + +- `expr` (`const CClassicalExpr*`):待转换表达式。 + +返回新表达式句柄;输入为 NULL 或操作数不是 `BitVec` 时返回 NULL。 + +### classical_expr_to_bool(expr) + +把 `Bit` 表达式转换为 `Bool`(`classical_expr_bit_to_bool` 的别名)。 + +- `expr` (`const CClassicalExpr*`):待转换表达式。 + +返回新表达式句柄;输入为 NULL 或操作数不是 `Bit` 时返回 NULL。 + +### classical_expr_to_uint(expr) + +把 `BitVec` 表达式转换为小端 `UInt`(`classical_expr_bit_vec_to_uint` 的别名)。 + +- `expr` (`const CClassicalExpr*`):待转换表达式。 + +返回新表达式句柄;输入为 NULL 或操作数不是 `BitVec` 时返回 NULL。 + +### classical_expr_simplified(ptr) + +返回表达式的结构化简副本。化简消除由字面量驱动的冗余,不会对运行时读取求值。 + +- `ptr` (`const CClassicalExpr*`):原表达式句柄。 + +返回新分配的化简副本;输入为 NULL 时返回 NULL。 + +--- + +## 表达式查询 + +### classical_expr_kind(ptr, kind) + +写出表达式节点种类标签(`CQLIB_CLASSICAL_EXPR_*` 之一)。 + +- `ptr` (`const CClassicalExpr*`):表达式句柄。 +- `kind` (`uint32_t*`):接收标签的出参。 + +返回 0 表示成功;-1 表示指针为 NULL。 + +### classical_expr_ty(ptr, tag, width) + +写出表达式的静态类型(标签 + 位宽)。 + +- `ptr` (`const CClassicalExpr*`):表达式句柄。 +- `tag` (`uint32_t*`):写出 `CQLIB_CLASSICAL_TYPE_*` 标签的出参。 +- `width` (`uint32_t*`):写出位宽的出参。 + +返回 0 表示成功;-1 表示指针为 NULL。 + +### classical_expr_is_bit_true(ptr) + +判断表达式是否为 `Bit` 字面量 `true`(1)。 + +- `ptr` (`const CClassicalExpr*`):表达式句柄。 + +返回 `1` 是;`0` 否;`-1` NULL。 + +### classical_expr_is_bit_false(ptr) + +判断表达式是否为 `Bit` 字面量 `false`(0)。 + +- `ptr` (`const CClassicalExpr*`):表达式句柄。 + +返回 `1` 是;`0` 否;`-1` NULL。 + +### classical_expr_is_bool_true(ptr) + +判断表达式是否为 `Bool` 字面量 `true`。 + +- `ptr` (`const CClassicalExpr*`):表达式句柄。 + +返回 `1` 是;`0` 否;`-1` NULL。 + +### classical_expr_is_bool_false(ptr) + +判断表达式是否为 `Bool` 字面量 `false`。 + +- `ptr` (`const CClassicalExpr*`):表达式句柄。 + +返回 `1` 是;`0` 否;`-1` NULL。 + +### classical_expr_values_len(ptr) + +返回表达式读取的不同不可变经典值的数量。 + +- `ptr` (`const CClassicalExpr*`):表达式句柄。 + +返回数量;句柄为 NULL 时返回 0。 + +### classical_expr_values(ptr, buffer, len) + +按两步式把表达式读取的全部不可变经典值快照 (index, tag, width) 拷入 `buffer`(先调用 `classical_expr_values_len` 获取长度)。 + +- `ptr` (`const CClassicalExpr*`):表达式句柄。 +- `buffer` (`CClassicalValueInfo*`):接收快照的数组。 +- `len` (`uintptr_t`):数组长度,必须等于 `classical_expr_values_len`。 + +返回 0 表示成功;-1 表示指针为 NULL;-8 表示 `len` 与数量不符。 + +### classical_expr_vars_len(ptr) + +返回表达式读取的不同经典变量的数量。 + +- `ptr` (`const CClassicalExpr*`):表达式句柄。 + +返回数量;句柄为 NULL 时返回 0。 + +### classical_expr_vars(ptr, buffer, len) + +按两步式把表达式读取的全部经典变量的自有句柄填入 `buffer`,按 id 升序排列(先调用 `classical_expr_vars_len` 获取长度)。每个返回的句柄必须用 `classical_var_free` 释放。 + +- `ptr` (`const CClassicalExpr*`):表达式句柄。 +- `buffer` (`CClassicalVar**`):接收自有句柄的数组。 +- `len` (`uintptr_t`):数组长度,必须等于 `classical_expr_vars_len`。 + +返回 0 表示成功;-1 表示指针为 NULL;-8 表示 `len` 与数量不符。 + +### classical_expr_remap_classical_ids(ptr, old_var_ids, new_var_ids, var_len, old_value_indices, new_value_indices, value_len) + +返回把线路内经典 id 重映射后的表达式副本。`old_var_ids[i]` 把该 id 的变量映射到 `new_var_ids[i]`,`old_value_indices[i]` 把该下标的不可变值映射到 `new_value_indices[i]`。旧 id 未在表达式中出现的表项会被忽略,但表达式实际读取的每个变量和值都必须被覆盖,否则重映射失败。 + +- `ptr` (`const CClassicalExpr*`):原表达式句柄。 +- `old_var_ids` (`const uint32_t*`):旧变量 id 数组。 +- `new_var_ids` (`const uint32_t*`):新变量 id 数组,按下标配对。 +- `var_len` (`uintptr_t`):变量映射数组长度。 +- `old_value_indices` (`const uint32_t*`):旧不可变值下标数组。 +- `new_value_indices` (`const uint32_t*`):新值下标数组,按下标配对。 +- `value_len` (`uintptr_t`):值映射数组长度。 + +返回新表达式句柄(用 `classical_expr_free` 释放);输入为 NULL(含长度非零时的 NULL 数组)或映射缺失时返回 NULL。 + +--- + +## 测量声明 + +### circuit_measure_bits(ptr, qubits, len) + +测量多个量子比特并返回读取不可变 `BitVec` 结果的表达式。第一个量子比特对应 bit 下标 0(最低有效位)。 + +- `ptr` (`CCircuit*`):目标线路句柄。 +- `qubits` (`const uint32_t*`):量子比特 id 数组。 +- `len` (`uintptr_t`):量子比特数量。 + +返回新分配的 `CClassicalExpr*`;指针为 NULL、列表为空或量子比特越界时返回 NULL。 + +### circuit_measure_into(ptr, qubit, target) + +测量单个量子比特并把结果存入 `target`(`Bit` 类型变量),返回读取该不可变测量值的表达式。 + +- `ptr` (`CCircuit*`):目标线路句柄。 +- `qubit` (`uint32_t`):被测量子比特 id。 +- `target` (`const CClassicalVar*`):接收结果的 `Bit` 变量句柄。 + +返回新分配的 `CClassicalExpr*`;指针为 NULL、量子比特越界或目标类型错误时返回 NULL。 + +### circuit_measure_bits_into(ptr, qubits, len, target) + +测量多个量子比特并把 `BitVec` 结果存入 `target`。目标位宽必须等于量子比特数量。 + +- `ptr` (`CCircuit*`):目标线路句柄。 +- `qubits` (`const uint32_t*`):量子比特 id 数组。 +- `len` (`uintptr_t`):量子比特数量。 +- `target` (`const CClassicalVar*`):接收结果的 `BitVec` 变量句柄。 + +返回读取不可变测量值的 `CClassicalExpr*`;出错时返回 NULL。 + +--- + +## 经典存储 + +### circuit_store(ptr, target, value) + +把 `value` 的运行时值存入可变变量 `target`。表达式类型必须与目标类型一致。 + +- `ptr` (`CCircuit*`):目标线路句柄。 +- `target` (`const CClassicalVar*`):写入目标变量。 +- `value` (`const CClassicalExpr*`):提供值的表达式。 + +返回 0 表示成功;-1 表示指针为 NULL;-3 表示校验失败。 + +--- + +## 控制流 + +控制流函数通过 C 回调构造分支体和循环体: + +```c +typedef int32_t (*CqlibBodyFn)(struct CCircuit *circuit, void *user_data); +``` + +回调契约: + +- `circuit` 与传给控制流函数的 `CCircuit*` 是同一个指针,仅在回调期间有效;分支体操作必须通过它用常规线路函数(`circuit_h`、`circuit_x`、`circuit_measure`、嵌套控制流构造器等)追加。 +- `user_data` 原样透传,绑定层不会解引用,可为 NULL。 +- 返回 0 表示成功;任何非零返回都会中止整个控制流操作:回调追加的全部操作被回滚,该值原样传播给控制流函数的调用者。直接返回门函数的错误码(如 `return circuit_x(circuit, 0);`)是惯用写法。 + +### circuit_if(ptr, condition, body_fn, user_data) + +向线路追加一个由 `Bool` 经典表达式控制的 `if` 操作。`body_fn` 被调用一次,其追加的全部操作构成 then 分支体。 + +- `ptr` (`CCircuit*`):目标线路句柄。 +- `condition` (`const CClassicalExpr*`):条件表达式。 +- `body_fn` (`CqlibBodyFn`):构造分支体的回调。 +- `user_data` (`void*`):传给回调的用户指针,可为 NULL。 + +返回 0 表示成功;-1 表示参数为 NULL;-3 表示条件校验失败(例如非 `Bool` 类型或句柄来自其他线路);否则为 `body_fn` 返回的非零状态码(回滚后传播)。 + +### circuit_if_else(ptr, condition, then_fn, else_fn, user_data) + +向线路追加一个由 `Bool` 经典表达式控制的 `if` / `else` 操作。`then_fn` 先执行、`else_fn` 后执行,各自追加的操作成为对应分支体。 + +- `ptr` (`CCircuit*`):目标线路句柄。 +- `condition` (`const CClassicalExpr*`):条件表达式。 +- `then_fn` (`CqlibBodyFn`):构造 then 分支体的回调。 +- `else_fn` (`CqlibBodyFn`):构造 else 分支体的回调。 +- `user_data` (`void*`):传给两个回调的用户指针,可为 NULL。 + +返回 0 表示成功;-1 表示参数为 NULL;-3 表示校验失败;否则为任一回调返回的非零状态码(回滚后传播)。 + +### circuit_while(ptr, condition, body_fn, user_data) + +向线路追加一个由 `Bool` 经典表达式控制的 `while` 循环。条件在运行时反复求值。`circuit_break_loop` / `circuit_continue_loop` 可以在回调中调用,但只能作为分支体的最后一个操作(终端控制转移)。 + +- `ptr` (`CCircuit*`):目标线路句柄。 +- `condition` (`const CClassicalExpr*`):循环条件表达式。 +- `body_fn` (`CqlibBodyFn`):构造循环体的回调。 +- `user_data` (`void*`):传给回调的用户指针,可为 NULL。 + +返回 0 表示成功;-1 表示参数为 NULL;-3 表示校验失败;否则为 `body_fn` 返回的非零状态码(回滚后传播)。 + +### circuit_for_uint(ptr, var, start, stop, step, body_fn, user_data) + +向线路追加无符号运行时区间循环,区间为半开 `[start, stop)`。`var` 必须是本线路的 `UInt` 变量;`start`、`stop`、`step` 必须是同位宽的 `UInt` 表达式。循环体内部需要读取循环变量时,可用 `classical_expr_var(var)` 重建其表达式。 + +- `ptr` (`CCircuit*`):目标线路句柄。 +- `var` (`const CClassicalVar*`):循环变量。 +- `start` (`const CClassicalExpr*`):起点表达式。 +- `stop` (`const CClassicalExpr*`):终点表达式(不含)。 +- `step` (`const CClassicalExpr*`):步长表达式。 +- `body_fn` (`CqlibBodyFn`):构造循环体的回调。 +- `user_data` (`void*`):传给回调的用户指针,可为 NULL。 + +返回 0 表示成功;-1 表示参数为 NULL;-3 表示校验失败(类型不匹配或外部句柄);否则为 `body_fn` 返回的非零状态码(回滚后传播)。 + +### circuit_switch(ptr, target, case_values_lo, case_values_hi, case_bodies, case_count, default_body, user_data) + +向线路追加基于 `UInt` 表达式的精确值匹配 `switch` 操作。case 值为 128 位,拆成两个 64 位半字:`value = lo | (hi << 64)`。每个值必须能被目标位宽表示且至多出现一次;`default_body` 可为 NULL(无默认分支)。所有回调接收同一个 `user_data`。 + +- `ptr` (`CCircuit*`):目标线路句柄。 +- `target` (`const CClassicalExpr*`):`UInt` 目标表达式。 +- `case_values_lo` (`const uint64_t*`):各 case 值的低 64 位。 +- `case_values_hi` (`const uint64_t*`):各 case 值的高 64 位。 +- `case_bodies` (`const CqlibBodyFn*`):与 case 值一一对应的回调数组。 +- `case_count` (`uintptr_t`):case 数量。 +- `default_body` (`CqlibBodyFn`):默认分支回调,可为 NULL。 +- `user_data` (`void*`):传给所有回调的用户指针。 + +返回 0 表示成功;-1 表示参数为 NULL(含 `case_count > 0` 时 case 体为 NULL);-3 表示校验失败(目标非 `UInt`、case 值重复或越界);否则为回调返回的第一个非零状态码(回滚后传播)。 + +### circuit_break_loop(ptr) + +向最近的包围循环或 switch 体追加 `break`。仅可在 `circuit_while`、`circuit_for_uint` 或 `circuit_switch` 的体回调(或嵌套于其中的体)内调用,且只能作为该体的最后一个操作。 + +- `ptr` (`CCircuit*`):目标线路句柄。 + +返回 0 表示成功;-1 表示线路为 NULL;-3 表示没有激活的包围循环或 switch 体,或该转移不是终端转移。 + +### circuit_continue_loop(ptr) + +向最近的包围循环体追加 `continue`。仅可在 `circuit_while` 或 `circuit_for_uint` 的体回调(或嵌套于其中的体)内调用,且只能作为该体的最后一个操作。 + +- `ptr` (`CCircuit*`):目标线路句柄。 + +返回 0 表示成功;-1 表示线路为 NULL;-3 表示没有激活的包围循环体,或该转移不是终端转移。 + +### circuit_append_control(ptr, source, index) + +把 `source` 操作列表中下标为 `index` 的控制流操作经验证后的副本追加到 `ptr`。`ptr` 与 `source` 可以是同一线路(复制自身的某个控制操作)。跨线路复制要求该操作是自包含的(不引用 `source` 的经典句柄)。 + +- `ptr` (`CCircuit*`):目标线路句柄。 +- `source` (`const CCircuit*`):来源线路句柄。 +- `index` (`uintptr_t`):来源操作列表中的下标。 + +返回 0 表示成功;-1 表示参数为 NULL;-8 表示 `index` 越界或该位置不是控制流操作;-3 表示副本校验失败(外部经典句柄、非法 `break` / `continue` 作用域或未知量子比特)。 + +--- + +## 完整示例 + +声明经典变量 → 构建布尔表达式 → `if` / `else` 分支: + +```c +#include +#include "cqlib_c.h" + +static int32_t then_body(struct CCircuit *circuit, void *user_data) { + (void)user_data; + return circuit_x(circuit, 1); /* 条件成立:对 qubit 1 施加 X */ +} + +static int32_t else_body(struct CCircuit *circuit, void *user_data) { + (void)user_data; + return circuit_z(circuit, 1); /* 条件不成立:对 qubit 1 施加 Z */ +} + +int main(void) { + struct CCircuit *circuit = circuit_new(2); + if (circuit == NULL) { + return 1; + } + + /* 1. 声明一个 Bool 经典变量 */ + struct CClassicalVar *flag = circuit_var(circuit, CQLIB_CLASSICAL_TYPE_BOOL, 0); + + /* 2. 构建布尔表达式:flag == true */ + struct CClassicalExpr *flag_read = classical_expr_var(flag); + struct CClassicalExpr *true_lit = classical_expr_bool_literal(true); + struct CClassicalExpr *cond = classical_expr_eq(flag_read, true_lit); + + /* 3. if / else 分支 */ + int32_t status = circuit_if_else(circuit, cond, then_body, else_body, NULL); + if (status != 0) { + fprintf(stderr, "circuit_if_else failed: %d\n", status); + } + + /* 释放表达式与变量句柄 */ + classical_expr_free(cond); + classical_expr_free(true_lit); + classical_expr_free(flag_read); + classical_var_free(flag); + circuit_free(circuit); + return status; +} +``` + +线路构造函数(`circuit_new` / `circuit_free`)见 [Circuit](1_circuit.md),标准门函数(`circuit_x` / `circuit_z`)见 [标准门](5_gate_standard.md)。 diff --git a/docs/api/cn/c/0_overview.md b/docs/api/cn/c/0_overview.md new file mode 100644 index 00000000..959a1949 --- /dev/null +++ b/docs/api/cn/c/0_overview.md @@ -0,0 +1,386 @@ +# C API 概览 + +## 欢迎查阅 Cqlib C API 参考手册! + +Cqlib C 绑定(`binding-c`)以纯 C ABI 提供量子线路构造、符号参数、IR 转换、设备建模、量子信息、编译优化、可视化与错误缓解等能力。全部接口遵循同一风格:自由函数 + 不透明句柄(opaque handle)+ `int32_t` 错误码,头文件为自动生成的 [crates/binding-c/include/cqlib_c.h](../../../../crates/binding-c/include/cqlib_c.h)。 + +--- + +## 文档导航 + +### 量子电路(circuit) + +- [Overview](0_circuit/0_overview.md) +- [Circuit](0_circuit/1_circuit.md) +- [量子比特](0_circuit/2_qubit.md) +- [符号参数](0_circuit/3_parameter.md) +- [操作与指令](0_circuit/4_operation_instruction.md) +- [标准门](0_circuit/5_gate_standard.md) +- [自定义酉门](0_circuit/6_gate_unitary.md) +- [多控制门](0_circuit/7_gate_mc_gate.md) +- [复合门](0_circuit/8_gate_circuit_gate.md) +- [经典数据与控制流](0_circuit/9_classical_control_flow.md) +- [符号矩阵](0_circuit/10_symbolic_matrix.md) +- [Ansatz](0_circuit/11_ansatz.md) +- [控制流图](0_circuit/12_cfg.md) +- [线路转矩阵](0_circuit/13_circuit_to_matrix.md) +- [线路 DAG](0_circuit/14_circuit_dag.md) + +### 中间表达(ir) + +- [Overview](1_ir/0_overview.md) +- [QCIS](1_ir/1_qcis.md) +- [OpenQASM 2.0](1_ir/2_qasm2.md) +- [OpenQASM 3.0](1_ir/3_qasm3.md) + +### 设备模块(device) + +- [Overview](2_device/0_overview.md) +- [拓扑](2_device/1_topology.md) +- [设备与噪声属性](2_device/2_properties_device.md) +- [布局](2_device/3_layout.md) +- [噪声](2_device/4_noise.md) +- [执行结果](2_device/5_result.md) +- [量子比特标识](2_device/6_qubits.md) + +### 量子信息(qis) + +- [Overview](3_qis/0_overview.md) +- [态矢量](3_qis/1_statevector.md) +- [密度矩阵](3_qis/2_density_matrix.md) +- [噪声密度矩阵](3_qis/3_density_matrix_noise.md) +- [稳定器](3_qis/4_stabilizer.md) +- [经典态](3_qis/5_classical_state.md) +- [Pauli](3_qis/6_pauli.md) +- [哈密顿量](3_qis/7_hamiltonian.md) +- [Pauli 演化](3_qis/8_evolution.md) +- [度量与熵](3_qis/9_metrics_entropy.md) + +### 编译优化(compile) + +- [Overview](4_compile/0_overview.md) +- [编译器](4_compile/1_compiler.md) +- [线路变换](4_compile/2_transform.md) +- [布局](4_compile/3_layout.md) +- [路由](4_compile/4_routing.md) +- [SABRE](4_compile/5_sabre.md) +- [分解与重综合](4_compile/6_decompose_resynthesis.md) +- [知识规则](4_compile/7_knowledge.md) +- [资源管理](4_compile/8_resource.md) +- [对易检查](4_compile/9_commutation.md) + +### 可视化(visualization) + +- [Overview](5_visualization/0_overview.md) +- [文本线路图](5_visualization/1_draw_text.md) +- [SVG 线路图](5_visualization/2_draw_figure.md) +- [状态图](5_visualization/3_state_plots.md) +- [结果图](5_visualization/4_result_plots.md) +- [落盘与输出](5_visualization/5_render_to_file.md) +- [Visual IR](5_visualization/6_visual_ir.md) + +### 错误缓解(error_mitigation) + +- [Overview](6_error_mitigation/0_overview.md) +- [零噪声外推](6_error_mitigation/1_zne.md) +- [虚拟蒸馏](6_error_mitigation/2_virtual_distillation.md) +- [统一流水线](6_error_mitigation/3_unified.md) + +--- + +## 快速上手:Bell 态 + +下面的完整示例构造两比特 Bell 线路,精确模拟后用两步式接口读取概率分布,并释放全部资源: + +```c +#include +#include +#include + +int main(void) { + /* 1. 构造 Bell 线路:H(0) + CX(0, 1) */ + CCircuit *qc = circuit_new(2); + if (qc == NULL) { + return 1; + } + if (circuit_h(qc, 0) != 0 || circuit_cx(qc, 0, 1) != 0) { + circuit_free(qc); + return 1; + } + + /* 2. 精确模拟 */ + CStatevector *sv = statevector_from_circuit(qc); + if (sv == NULL) { + circuit_free(qc); + return 1; + } + + /* 3. 两步式输出:先查长度,再填充缓冲 */ + uintptr_t len = statevector_probabilities_len(sv); + double *probs = malloc(len * sizeof(double)); + if (probs == NULL || statevector_probabilities(sv, probs, len) != 0) { + free(probs); + statevector_free(sv); + circuit_free(qc); + return 1; + } + /* probs = {0.5, 0.0, 0.0, 0.5} */ + printf("P(00) = %f, P(11) = %f\n", probs[0], probs[3]); + + /* 4. 释放(顺序与获取顺序相反) */ + free(probs); + statevector_free(sv); + circuit_free(qc); + return 0; +} +``` + +### 编译与链接 + +Windows(MinGW gcc,gnu 目标): + +```bash +cargo build --release -p binding-c --target x86_64-pc-windows-gnu + +gcc -Icrates/binding-c/include main.c -Ltarget/x86_64-pc-windows-gnu/release -lbinding_c -lntdll -lws2_32 -lbcrypt -luserenv -ladvapi32 -o bell +``` + +运行时需要能定位 `binding_c.dll`:将 `target/x86_64-pc-windows-gnu/release` 的绝对路径加入 `PATH`,或把 DLL 复制到可执行文件旁。macOS 与 Linux 的链接方式、测试工程与更多示例见 [crates/binding-c/README.md](../../../../crates/binding-c/README.md)。 + +--- + +## 错误码 + +返回 `int32_t` 的接口用 `0` 表示成功,负值表示错误: + +| 值 | 名称 | 典型场景 | +| --- | --- | --- | +| `0` | Ok | 成功。 | +| `-1` | NullPtr | 传入 NULL 句柄、NULL 数组或非法 C 字符串。 | +| `-2` | QubitOutOfBounds | 量子比特编号超出线路或设备范围。 | +| `-3` | CircuitError | 线路结构错误:验证失败、门作用比特数量或参数数量不匹配、同一操作重复使用比特、不可逆、作用比特重复等。 | +| `-4` | ParseError | 解析错误:未知门名、非法表达式语法、非法 UTF-8、非法 IR 文本。 | +| `-5` | IoError | 文件读写失败。 | +| `-6` | CompilerError | 编译流程错误。 | +| `-7` | SimulationError | 模拟错误:非 Clifford 门进入稳定器模拟、尚未运行等。 | +| `-8` | InvalidParam | 非法参数:概率越界、缓冲区过小、非法标签值等。 | + +返回指针的接口用 `NULL` 表示失败;`param_evaluate` 求值失败(含符号未绑定)返回 `0.0`;`circuit_depth` 返回负值表示错误。布尔类查询返回 `1`/`0`,`-1` 通常表示 NULL 输入。 + +--- + +## 内存所有权与释放 + +1. **构造与变换返回堆句柄**:`circuit_new`、`circuit_from_qubits`、`circuit_inverse`、`circuit_decompose`、`circuit_assign_params`、`param_parse`、`circuit_to_gate` 等返回新分配的不透明句柄,调用方用对应 `*_free` 释放(`circuit_free`、`param_free`、`circuit_gate_free` 等)。所有 `*_free` 都允许传 `NULL`。 +2. **字符串用 `cqlib_string_free` 释放**:凡是返回 `char*` 的接口(`operation_name`、`value_operation_name`、`circuit_gate_name`、`circuit_symbols` 写出的每个元素等),都用 `cqlib_string_free` 释放。 +3. **数组采用两步式输出**:先调用 `*_len` 接口查询数量并分配缓冲,再调用填充接口。缓冲不足时的行为分两类:多数填充接口返回负错误码(通常 `-8`);部分接口(如 `circuit_qubits`)只返回总数而不拷贝。 +4. **嵌套列表配套 free**:`circuit_parameters`(每项是新分配的 `CParameter*`,用 `param_free` 逐个释放)、`circuit_cfg_block_operations`(每项用 `operation_free` 释放)等写出的每个元素都是独立句柄,必须逐个释放。 +5. **显式接管所有权的接口**:`circuit_to_gate` 克隆输入线路生成复合门句柄;`circuit_index` 返回的 `CValueOperation*` 是独立于后续线路变更的只读快照(用 `value_operation_free` 释放);`CParameterValue.tag == PARAMETER_VALUE_TAG_PARAM` 时其中的 `param` 字段是新分配的 `CParameter*`(用 `param_free` 释放)。这些对象的所有权完全归调用方。 + +--- + +## 常量标签 + +头文件通过 `#define` 导出以下标签常量,均为稳定 ABI 的一部分。 + +### 参数存储标签 + +| 常量 | 值 | 说明 | +| --- | --- | --- | +| `CIRCUIT_PARAM_TAG_FIXED` | 0 | `CCircuitParam.tag`:固定数值,存放在 `value` 字段。 | +| `CIRCUIT_PARAM_TAG_INDEX` | 1 | `CCircuitParam.tag`:线路参数表驻留表达式,`index` 字段存放表下标。 | +| `PARAMETER_VALUE_TAG_FIXED` | 0 | `CParameterValue.tag`:固定数值,存放在 `value` 字段,`param` 为 NULL。 | +| `PARAMETER_VALUE_TAG_PARAM` | 1 | `CParameterValue.tag`:符号参数,`param` 指向新分配的 `CParameter*`。 | +| `OPERATION_PARAM_FIXED` | 0 | `operation_params` 写出的参数标签:固定数值。 | +| `OPERATION_PARAM_INDEX` | 1 | `operation_params` 写出的参数标签:所属线路参数表下标,需结合线路解析。 | + +### Ansatz 拓扑与演化策略 + +| 常量 | 值 | 说明 | +| --- | --- | --- | +| `ENTANGLEMENT_LINEAR` | 0 | TwoLocal 纠缠层拓扑:线性最近邻链。 | +| `ENTANGLEMENT_CIRCULAR` | 1 | 线性链加首尾环绕边。 | +| `ENTANGLEMENT_FULL` | 2 | 全连接比特对。 | +| `ENTANGLEMENT_CUSTOM` | 3 | 自定义显式控制/目标 id 对数组。 | +| `EVOLUTION_STRATEGY_EXACT` | 0 | QAOA 演化策略:逐项精确演化。 | +| `EVOLUTION_STRATEGY_AUTO` | 1 | 自动在精确与 Trotter 之间选择。 | +| `EVOLUTION_STRATEGY_TROTTER` | 2 | 显式 Trotter 乘积公式。 | +| `TROTTER_FIRST_ORDER` | 0 | 一阶乘积公式。 | +| `TROTTER_SECOND_ORDER` | 1 | 二阶乘积公式。 | +| `TROTTER_RANDOMIZED` | 2 | 随机化一阶公式。 | +| `TROTTER_MODE_FIRST_ORDER` | 0 | Trotter-Suzuki 分解模式:一阶 Lie-Trotter。 | +| `TROTTER_MODE_SECOND_ORDER` | 1 | Trotter-Suzuki 分解模式:二阶 Strang 分裂。 | + +### 控制流图标签 + +| 常量 | 值 | 说明 | +| --- | --- | --- | +| `CFG_FLOW_TRUE_BRANCH` | 1 | `if`/`while`/`for` 头的真出口边。 | +| `CFG_FLOW_FALSE_BRANCH` | 2 | 假出口边。 | +| `CFG_FLOW_UNCONDITIONAL` | 3 | 无条件跳转 / 结构化汇合边。 | +| `CFG_FLOW_CASE` | 4 | switch 精确匹配 case 边(128 位值写在 case 两半)。 | +| `CFG_FLOW_DEFAULT_CASE` | 5 | switch default 边。 | +| `CFG_FLOW_BREAK` | 6 | 结构化 `break` 边。 | +| `CFG_FLOW_CONTINUE` | 7 | 结构化 `continue` 边。 | +| `CFG_TERMINATOR_BRANCH` | 1 | 布尔分支头终结器。 | +| `CFG_TERMINATOR_FOR_LOOP` | 2 | 无符号区间循环头终结器。 | +| `CFG_TERMINATOR_SWITCH` | 3 | 精确匹配多路分支头终结器。 | +| `CFG_TERMINATOR_JUMP` | 4 | 无条件跳转终结器。 | +| `CFG_TERMINATOR_BREAK` | 5 | 结构化 `break` 终结器。 | +| `CFG_TERMINATOR_CONTINUE` | 6 | 结构化 `continue` 终结器。 | +| `CFG_TERMINATOR_RETURN` | 7 | 执行结束终结器。 | +| `CFG_REGION_IF` | 1 | 结构化条件区域。 | +| `CFG_REGION_WHILE` | 2 | 结构化 while 循环区域。 | +| `CFG_REGION_FOR` | 3 | 结构化区间循环区域。 | +| `CFG_REGION_SWITCH` | 4 | 结构化精确匹配 switch 区域。 | + +### 经典类型与表达式节点 + +| 常量 | 值 | 说明 | +| --- | --- | --- | +| `CQLIB_CLASSICAL_TYPE_BIT` | 0 | 经典类型 `Bit`。 | +| `CQLIB_CLASSICAL_TYPE_BOOL` | 1 | 经典类型 `Bool`。 | +| `CQLIB_CLASSICAL_TYPE_UINT` | 2 | 经典类型 `UInt`。 | +| `CQLIB_CLASSICAL_TYPE_BIT_VEC` | 3 | 经典类型 `BitVec`。 | + +`classical_expr_kind` 写出的节点类型标签:`CQLIB_CLASSICAL_EXPR_VAR`(0)、`_VALUE`(1)、`_BOOL_LITERAL`(2)、`_BIT_LITERAL`(3)、`_UINT_LITERAL`(4)、`_BIT_VEC_LITERAL`(5)、`_UNARY`(6)、`_BINARY`(7)、`_COMPARE`(8)、`_CAST`(9)、`_SELECT`(10)、`_EXTRACT_BIT`(11)、`_EXTRACT_BITS`(12)、`_CONCAT`(13)、`_PACK_BITS`(14)。 + +### DAG 线标签 + +| 常量 | 值 | 说明 | +| --- | --- | --- | +| `DAG_WIRE_QUBIT` | 0 | 量子比特时间线,负载为比特 id。 | +| `DAG_WIRE_CLASSICAL_VAR` | 1 | 可变经典存储线,负载为变量 id。 | +| `DAG_WIRE_CLASSICAL_VALUE` | 2 | 不可变经典值线,负载为值 id。 | +| `DAG_WIRE_GLOBAL_ORDER` | 3 | 全局顺序资源线,无负载。 | + +### 编译配置标签 + +| 常量 | 值 | 说明 | +| --- | --- | --- | +| `COMPILE_MODE_NORMAL` | 0 | 普通编译模式。 | +| `COMPILE_MODE_ENHANCED` | 1 | 增强编译模式。 | +| `COMPILE_TARGET_LOGICAL` | 0 | 逻辑比特目标。 | +| `COMPILE_TARGET_BASIS` | 1 | 基门集目标。 | +| `COMPILE_TARGET_DEVICE` | 2 | 设备目标。 | +| `COMPILE_TARGET_TOPOLOGY_BASIS` | 3 | 拓扑基门集目标。 | + +### 编译 pass 标签 + +| 常量 | 值 | 说明 | +| --- | --- | --- | +| `REWRITE_MODE_OPTIMIZE` | 0 | 知识重写模式:优化。 | +| `REWRITE_MODE_LOWERING` | 1 | 知识重写模式:向目标门集降低。 | +| `LAYOUT_OBJECTIVE_TOPOLOGY_ONLY` | 0 | 布局目标:仅拓扑(距离 + 方向失配)。 | +| `LAYOUT_OBJECTIVE_FIDELITY_AWARE` | 1 | 布局目标:保真度感知(叠加错误率项)。 | +| `LAYOUT_OBJECTIVE_AUTO` | 2 | 布局目标:由设备校准数据自动选择。 | +| `VF2_EDGE_REQUIREMENT_POSITIVE_INTERACTIONS` | 0 | VF2 边要求:仅正权重交互。 | +| `VF2_EDGE_REQUIREMENT_ALL_INTERACTIONS` | 1 | VF2 边要求:全部交互。 | +| `SABRE_MAX_LOOKAHEAD_WEIGHTS` | 8 | `CSabreConfigC.lookahead_weights` 数组的最大长度。 | +| `TWO_QUBIT_BASIS_PAULI_ROTATIONS` | 0 | 双比特综合目标基:Pauli 旋转。 | +| `TWO_QUBIT_BASIS_CX` | 1 | 双比特综合目标基:CX。 | +| `TWO_QUBIT_BASIS_CY` | 2 | 双比特综合目标基:CY。 | +| `TWO_QUBIT_BASIS_CZ` | 3 | 双比特综合目标基:CZ。 | +| `TWO_QUBIT_BASIS_RZZ` | 4 | 双比特综合目标基:RZZ。 | +| `KAK_FACTOR_K1L` | 0 | KAK 分解局域因子:第一个局域门(左)。 | +| `KAK_FACTOR_K1R` | 1 | KAK 分解局域因子:第一个局域门(右)。 | +| `KAK_FACTOR_K2L` | 2 | KAK 分解局域因子:第二个局域门(左)。 | +| `KAK_FACTOR_K2R` | 3 | KAK 分解局域因子:第二个局域门(右)。 | + +### 知识规则与资源标签 + +| 常量 | 值 | 说明 | +| --- | --- | --- | +| `RULE_KIND_SIMPLIFY` | 0 | 知识规则类别:化简。 | +| `RULE_KIND_CANCEL` | 1 | 知识规则类别:消相。 | +| `RULE_KIND_MERGE` | 2 | 知识规则类别:合并。 | +| `RULE_KIND_COMMUTE` | 3 | 知识规则类别:对易换位。 | +| `RULE_KIND_DECOMPOSE` | 4 | 知识规则类别:分解。 | +| `RULE_KIND_CANONICALIZE` | 5 | 知识规则类别:规范化。 | +| `RULE_KIND_HARDWARE_NATIVE` | 6 | 知识规则类别:硬件原生。 | +| `RULE_KIND_OTHER` | 7 | 知识规则类别:其他。 | +| `RESOURCE_REQUIREMENT_CLEAN_ZERO` | 0 | 资源需求:\|0⟩ 辅助比特(干净比特)。 | +| `RESOURCE_REQUIREMENT_DIRTY` | 1 | 资源需求:脏比特(任意态辅助比特)。 | + +### 对易检查标签 + +| 常量 | 值 | 说明 | +| --- | --- | --- | +| `COMMUTATION_RESULT_UNPROVEN` | 0 | 未能证明对易。 | +| `COMMUTATION_RESULT_EXACT` | 1 | 严格对易。 | +| `COMMUTATION_RESULT_UP_TO_GLOBAL_PHASE` | 2 | 在全局相位意义下对易。 | + +### 噪声通道标签 + +| 常量 | 值 | 说明 | +| --- | --- | --- | +| `NOISE_BIT_FLIP` | 0 | 单比特翻转通道,参数为翻转概率 p。 | +| `NOISE_PHASE_FLIP` | 1 | 相位翻转通道,参数为翻转概率 p。 | +| `NOISE_DEPOLARIZING` | 2 | 去极化通道,参数为去极化参数 p。 | +| `NOISE_AMPLITUDE_DAMPING` | 3 | 幅度阻尼通道,参数为阻尼系数 gamma。 | +| `NOISE_PHASE_DAMPING` | 4 | 相位阻尼通道,参数为散射概率。 | +| `NOISE_PAULI` | 5 | 一般单比特 Pauli 通道,参数为 px/py/pz(各自非负,和 ≤ 1)。 | +| `NOISE_TWO_DEPOLARIZING` | 0 | 双比特去极化通道。 | +| `NOISE_TWO_INDEPENDENT` | 1 | 双比特独立通道(q0/q1 各带一个单比特通道)。 | +| `NOISE_TWO_CORRELATED_PAULI` | 2 | 关联 Pauli 通道(概率 p 加 `NOISE_PAULI_OP_*` 算符)。 | +| `NOISE_PAULI_OP_I` / `_X` / `_Y` / `_Z` | 0 / 1 / 2 / 3 | 关联 Pauli 通道作用在 q0/q1 上的 Pauli 算符。 | + +### 执行状态标签 + +| 常量 | 值 | 说明 | +| --- | --- | --- | +| `EXECUTION_STATUS_QUEUED` | 0 | 任务已提交排队。 | +| `EXECUTION_STATUS_RUNNING` | 1 | 任务正在运行。 | +| `EXECUTION_STATUS_COMPLETED` | 2 | 任务成功完成。 | +| `EXECUTION_STATUS_FAILED` | 3 | 任务失败(错误码与消息另行查询)。 | +| `EXECUTION_STATUS_CANCELLED` | 4 | 任务已取消。 | + +### 错误缓解标签 + +| 常量 | 值 | 说明 | +| --- | --- | --- | +| `MITIGATION_ZNE` | 0 | 缓解方法:零噪声外推(附加参数为折叠层数组)。 | +| `MITIGATION_VIRTUAL_DISTILLATION` | 1 | 缓解方法:虚拟蒸馏(附加参数为副本数)。 | +| `PROCESS_ZNE_POLYNOMIAL` | 0 | 缓解流程:ZNE + 多项式外推。 | +| `PROCESS_ZNE_EXPONENTIAL` | 1 | 缓解流程:ZNE + 指数外推。 | +| `PROCESS_VIRTUAL_DISTILLATION` | 2 | 缓解流程:虚拟蒸馏。 | +| `ZNE_EXTRAPOLATE_POLYNOMIAL` | 0 | `zne_extrapolate` 外推方法:多项式。 | +| `ZNE_EXTRAPOLATE_EXPONENTIAL` | 1 | `zne_extrapolate` 外推方法:指数。 | + +### Pauli 算符标签 + +| 常量 | 值 | 说明 | +| --- | --- | --- | +| `PAULI_I` | 0 | 单位算符 I。 | +| `PAULI_X` | 1 | Pauli X。 | +| `PAULI_Y` | 2 | Pauli Y。 | +| `PAULI_Z` | 3 | Pauli Z。 | + +### 参数显示模式标签 + +| 常量 | 值 | 说明 | +| --- | --- | --- | +| `PARAM_MODE_NUMERIC` | 0 | 数值显示模式。 | +| `PARAM_MODE_SYMBOLIC` | 1 | 符号显示模式。 | +| `PARAM_MODE_SYMBOLIC_WITH_VALUE` | 2 | 符号加数值显示模式。 | +| `PARAM_MODE_PI_FRACTION_PREFERRED` | 3 | 优先 π 分数显示模式。 | + +--- + +## 线程与 Complex64 布局 + +### 线程与并发 + +- 无全局初始化或清理调用:加载动态库(Windows 为 `binding_c.dll`,Linux 为 `libbinding_c.so`,macOS 为 `libbinding_c.dylib`)后直接调用接口。 +- 句柄是堆上独立对象,所有权归调用方;多线程共享同一句柄时,访问的串行化由调用方负责。不同句柄之间相互独立,可各自使用。 + +### Complex64 布局 + +```c +typedef struct Complex64 { + double re; + double im; +} Complex64; +``` + +复数以 `(re, im)` 交错的连续双精度对表示。矩阵类接口(`circuit_to_matrix`、`operation_matrix` 等)按行主序输出交错双精度数组,缓冲区需容纳 `2 × 元素个数` 个 `double`。 diff --git a/docs/api/cn/c/1_ir/0_overview.md b/docs/api/cn/c/1_ir/0_overview.md new file mode 100644 index 00000000..fed3beb3 --- /dev/null +++ b/docs/api/cn/c/1_ir/0_overview.md @@ -0,0 +1,61 @@ +# 中间表达(C) + +C 绑定的 IR 接口在 `CCircuit` 与三种线路文本格式(QCIS、OpenQASM 2.0、OpenQASM 3.0)之间双向转换。错误码、字符串与句柄的释放约定遵循 [Overview](../0_overview.md)。 + +--- + +## 四函数约定 + +每种格式提供同一组四个入口,前缀 `` 分别为 `qcis`、`qasm2`、`qasm3`: + +| 动作 | 函数 | 返回 | +| --- | --- | --- | +| 字符串 → `CCircuit` | `_loads(source)` | 成功返回新建的 `CCircuit*`,失败返回 NULL | +| 文件 → `CCircuit` | `_load(path)` | 成功返回新建的 `CCircuit*`,失败返回 NULL | +| `CCircuit` → 字符串 | `_dumps(circuit)` | 成功返回堆上 C 字符串,失败返回 NULL | +| `CCircuit` → 文件 | `_dump(circuit, path)` | 成功返回 0,失败返回负错误码 | + +- `loads` / `load` 得到的线路用 `circuit_free` 释放(见 [Circuit](../0_circuit/1_circuit.md))。 +- `dumps` 返回的字符串用 `cqlib_string_free` 释放。 +- `dump` 返回 `int32_t` 错误码:空指针为 -1,路径不是合法 UTF-8 为 -8,导出或写入失败为 -5。 +- 解析方向不修改任何输入;`loads(dumps(circuit))` 得到的线路与原线路在操作序列上等价。 +- 也可以由一种格式读入、另一种格式写出,完成格式转换,例如把 OpenQASM 2.0 文本写成 QCIS 文本。 + +--- + +## 页面导航 + +| 页面 | 内容 | +| --- | --- | +| [QCIS](1_qcis.md) | `qcis_loads` / `qcis_load` / `qcis_dumps` / `qcis_dump` 与 QCIS 原生门集。 | +| [OpenQASM 2.0](2_qasm2.md) | `qasm2_loads` / `qasm2_load` / `qasm2_dumps` / `qasm2_dump` 与支持的 2.0 子集。 | +| [OpenQASM 3.0](3_qasm3.md) | `qasm3_loads` / `qasm3_load` / `qasm3_dumps` / `qasm3_dump` 与支持的 3.0 子集。 | + +--- + +## 示例 + +```c +#include +#include "cqlib_c.h" + +/* 字符串 -> 线路 -> 字符串 */ +struct CCircuit *c = qcis_loads("H Q0\nCZ Q0 Q1\n"); +if (c == NULL) { + /* 解析失败 */ +} + +char *text = qcis_dumps(c); +if (text != NULL) { + printf("%s", text); /* "H Q0\nCZ Q0 Q1\n" */ + cqlib_string_free(text); +} + +circuit_free(c); + +/* 格式转换:QCIS 文本写出为 OpenQASM 2.0 文本 */ +struct CCircuit *c2 = qcis_loads("X Q0\nY Q1\n"); +char *qasm = qasm2_dumps(c2); /* 含 "OPENQASM 2.0;" 头 */ +cqlib_string_free(qasm); +circuit_free(c2); +``` diff --git a/docs/api/cn/c/1_ir/1_qcis.md b/docs/api/cn/c/1_ir/1_qcis.md new file mode 100644 index 00000000..91c787d0 --- /dev/null +++ b/docs/api/cn/c/1_ir/1_qcis.md @@ -0,0 +1,117 @@ +# QCIS(C) + +C 绑定提供 QCIS 文本与 `CCircuit` 之间的四个转换入口。错误码、字符串与句柄的释放约定遵循 [Overview](../0_overview.md)。 + +--- + +## 接口一览 + +| 函数 | 方向 | +| --- | --- | +| `qcis_loads` | QCIS 字符串 → `CCircuit` | +| `qcis_load` | QCIS 文件 → `CCircuit` | +| `qcis_dumps` | `CCircuit` → QCIS 字符串 | +| `qcis_dump` | `CCircuit` → QCIS 文件 | + +--- + +## 解析 + +### qcis_loads(source) + +从 QCIS 字符串解析线路,得到新的 `CCircuit*`。 + +参数: + +- `source` (`const char*`):QCIS 文本,NUL 结尾。 + +返回:成功返回新建的 `CCircuit*`(用 `circuit_free` 释放);`source` 为 NULL、不是合法 UTF-8 或解析失败时返回 NULL。 + +解析失败的原因包括:比特写法不是 `Q`、`Q` 后编号无法解析、门的比特数或参数个数与规格不符、参数表达式为空、门名不在 QCIS 指令集内、空行或没有有效内容。 + +### qcis_load(path) + +从文件读取 QCIS 文本并解析,得到新的 `CCircuit*`。 + +参数: + +- `path` (`const char*`):文件路径,NUL 结尾。 + +返回:成功返回新建的 `CCircuit*`;`path` 为 NULL、不是合法 UTF-8、读取失败或解析失败时返回 NULL。 + +--- + +## 导出 + +### qcis_dumps(circuit) + +把线路导出为 QCIS 字符串。 + +参数: + +- `circuit` (`const struct CCircuit*`):待导出的线路。 + +返回:成功返回堆上 C 字符串(用 `cqlib_string_free` 释放);`circuit` 为 NULL 或导出失败时返回 NULL。 + +导出失败的原因包括:线路中含有 QCIS 原生门集之外的门(标准恒等门、全局相位、多控制门、自定义门与酉门)、经典存储、经典控制流、无法求值的符号参数,或延迟刻度不是非负整数。测量(`M`)与屏障(`B`)可以导出。 + +### qcis_dump(circuit, path) + +把线路导出为 QCIS 字符串并写入文件。 + +参数: + +- `circuit` (`const struct CCircuit*`):待导出的线路。 +- `path` (`const char*`):目标文件路径。 + +返回:0 成功;-1 任一指针为 NULL;-8 路径不是合法 UTF-8;-5 导出或写入失败。 + +--- + +## QCIS 指令集 + +QCIS 解析与导出仅支持原生门集: + +- 单比特门:`H`, `S`, `SD`, `T`, `TD`, `X`, `X2P`, `X2M`, `Y`, `Y2P`, `Y2M`, `Z` +- 含参单比特门:`RX`, `RXY`, `RY`, `RZ`, `U`, `XY`, `XY2P`, `XY2M`, `PHASE` +- 多比特门:`RXX`, `RYY`, `RZX`, `RZZ`, `SWAP`, `CX`, `CCX`, `CY`, `CZ`, `CRX`, `CRY`, `CRZ`, `FSIM` + +除门之外,QCIS 文本还表示测量(`M`)、屏障(`B`)与延迟(`I Qn t`,`t` 为延迟刻度数,单位 0.5 ns,必须是非负整数)。 + +解析时 `SDG`、`TDG`、`Barrier` 分别作为 `SD`、`TD`、`B` 的等价写法接受;导出时统一写成 `SD`、`TD`、`B`。标准恒等门与全局相位不在原生门集内,QCIS 的 `I` 一律表示延迟。遇到无法表达的门时,应先分解到 QCIS 门集后再导出。 + +--- + +## 示例 + +```c +#include +#include "cqlib_c.h" + +/* 解析:比特写作 Q,参数支持表达式 */ +const char *src = + "RX Q0 1.0\n" + "RY Q1 pi/2\n" + "X Q0\n" + "CZ Q0 Q1\n" + "M Q0 Q1\n"; + +struct CCircuit *c = qcis_loads(src); +if (c == NULL) { + /* 解析失败 */ +} + +uintptr_t n = circuit_num_qubits(c); /* 2 */ + +/* 回写 */ +char *text = qcis_dumps(c); +if (text != NULL) { + printf("%s", text); + cqlib_string_free(text); +} + +/* 写入文件 */ +int32_t rc = qcis_dump(c, "out.qcis"); /* 0 成功,-5 写入失败 */ + +circuit_free(c); +``` diff --git a/docs/api/cn/c/1_ir/2_qasm2.md b/docs/api/cn/c/1_ir/2_qasm2.md new file mode 100644 index 00000000..185ec7f0 --- /dev/null +++ b/docs/api/cn/c/1_ir/2_qasm2.md @@ -0,0 +1,120 @@ +# OpenQASM 2.0(C) + +C 绑定提供 OpenQASM 2.0 文本与 `CCircuit` 之间的四个转换入口。错误码、字符串与句柄的释放约定遵循 [Overview](../0_overview.md)。 + +--- + +## 接口一览 + +| 函数 | 方向 | +| --- | --- | +| `qasm2_loads` | OpenQASM 2.0 字符串 → `CCircuit` | +| `qasm2_load` | OpenQASM 2.0 文件 → `CCircuit` | +| `qasm2_dumps` | `CCircuit` → OpenQASM 2.0 字符串 | +| `qasm2_dump` | `CCircuit` → OpenQASM 2.0 文件 | + +--- + +## 解析 + +### qasm2_loads(source) + +从 OpenQASM 2.0 字符串解析线路,得到新的 `CCircuit*`。 + +参数: + +- `source` (`const char*`):OpenQASM 2.0 文本,NUL 结尾。 + +返回:成功返回新建的 `CCircuit*`(用 `circuit_free` 释放);`source` 为 NULL、不是合法 UTF-8 或解析失败时返回 NULL。 + +解析失败的原因包括:语法错误、AST 到线路的转换失败、引用未定义的量子或经典寄存器、引用未定义的门、重定义 qelib1 声明的门名、参数个数或比特数不符、门展开超深或相互依赖成环、参数表达式求值失败、版本声明不是 2.0、重复声明、`include` 成环,以及被调用的 `opaque` 门。 + +`include` 引入的外部文件不会被解析;文件入口 `qasm2_load` 才按文件所在目录解析相对 `include`。 + +### qasm2_load(path) + +从文件读取 OpenQASM 2.0 文本并解析,得到新的 `CCircuit*`。 + +参数: + +- `path` (`const char*`):文件路径,NUL 结尾。 + +返回:成功返回新建的 `CCircuit*`;`path` 为 NULL、不是合法 UTF-8、读取失败或解析失败时返回 NULL。 + +--- + +## 导出 + +### qasm2_dumps(circuit) + +把线路导出为 OpenQASM 2.0 字符串。输出含自动生成注释、`OPENQASM 2.0;`、`include "qelib1.inc";` 头部与 `qreg`/`creg` 声明。 + +参数: + +- `circuit` (`const struct CCircuit*`):待导出的线路。 + +返回:成功返回堆上 C 字符串(用 `cqlib_string_free` 释放);`circuit` 为 NULL 或导出失败时返回 NULL。 + +导出失败的原因包括:内容生成失败、`gate` 定义体内出现测量或复位、比特下标非法、无法表示为 `creg` 的经典类型、经典数据操作或经典控制无法表达,以及不同的自定义门使用了同一个 OpenQASM 门名。 + +### qasm2_dump(circuit, path) + +把线路导出为 OpenQASM 2.0 字符串并写入文件。 + +参数: + +- `circuit` (`const struct CCircuit*`):待导出的线路。 +- `path` (`const char*`):目标文件路径。 + +返回:0 成功;-1 任一指针为 NULL;-8 路径不是合法 UTF-8;-5 导出或写入失败。 + +--- + +## 支持的 OpenQASM 2.0 子集 + +- 标准门与参数门,含 `ch`/`cu1`/`cu3` 与 `sx`/`sxdg`/`crx`/`cry`/`crz`/`rxx`/`ryy`/`rzz`/`fsim` +- 测量、屏障、复位等指令 +- `qreg`、`creg`、`opaque` 声明,以及 `if (creg == 整数) qop;` 条件语句 +- `CircuitGate` 导出为门定义;只带矩阵的酉门导出为 `opaque` 声明 +- 未直接覆盖的扩展门以 `gate` 定义形式输出(如 `crx`/`cry`/`rzz`/`rxx`/`ryy`/`rzx`) +- `include "qelib1.inc";` 按内建引入处理,不要求本地存在该文件 + +导出会按 OpenQASM 2.0 的表达能力取舍,下列结构会失败: + +- 经典控制中的 `else` 分支、循环、`switch` 与嵌套控制流 +- 无法表示为 `creg` 的经典类型,例如 `bool` 与无符号整数 +- `gate` 定义体内的测量与复位 +- 逐位条件(如 `if (c[0] == 1)`)与条件屏障 + +`rzx` 只出现在导出侧的扩展门定义中,加载不接受;全局相位不写成语句,只作为注释输出,重新加载时不会恢复。 + +--- + +## 示例 + +```c +#include +#include "cqlib_c.h" + +const char *src = + "OPENQASM 2.0;\n" + "include \"qelib1.inc\";\n" + "qreg q[2];\n" + "h q[0];\n" + "cx q[0],q[1];\n"; + +struct CCircuit *c = qasm2_loads(src); +if (c == NULL) { + /* 解析失败 */ +} + +char *out = qasm2_dumps(c); +if (out != NULL) { + printf("%s", out); /* 含 "OPENQASM 2.0;" */ + cqlib_string_free(out); +} + +int32_t rc = qasm2_dump(c, "out.qasm"); /* 0 成功 */ + +circuit_free(c); +``` diff --git a/docs/api/cn/c/1_ir/3_qasm3.md b/docs/api/cn/c/1_ir/3_qasm3.md new file mode 100644 index 00000000..58715594 --- /dev/null +++ b/docs/api/cn/c/1_ir/3_qasm3.md @@ -0,0 +1,139 @@ +# OpenQASM 3.0(C) + +C 绑定提供 OpenQASM 3.0 文本与 `CCircuit` 之间的四个转换入口。错误码、字符串与句柄的释放约定遵循 [Overview](../0_overview.md)。 + +--- + +## 接口一览 + +| 函数 | 方向 | +| --- | --- | +| `qasm3_loads` | OpenQASM 3.0 字符串 → `CCircuit` | +| `qasm3_load` | OpenQASM 3.0 文件 → `CCircuit` | +| `qasm3_dumps` | `CCircuit` → OpenQASM 3.0 字符串 | +| `qasm3_dump` | `CCircuit` → OpenQASM 3.0 文件 | + +--- + +## 解析 + +### qasm3_loads(source) + +从 OpenQASM 3.0 字符串解析线路,得到新的 `CCircuit*`。 + +参数: + +- `source` (`const char*`):OpenQASM 3.0 文本,NUL 结尾。 + +返回:成功返回新建的 `CCircuit*`(用 `circuit_free` 释放);`source` 为 NULL、不是合法 UTF-8 或解析失败时返回 NULL。 + +解析失败的原因包括:语法错误、语义错误、转换失败、不支持的特性、未定义的符号或门、类型错误、参数个数或比特数不符、门展开超深或相互依赖成环。 + +### qasm3_load(path) + +从文件读取 OpenQASM 3.0 文本并解析,得到新的 `CCircuit*`。相对路径的 `include` 从输入文件所在目录解析。 + +参数: + +- `path` (`const char*`):文件路径,NUL 结尾。 + +返回:成功返回新建的 `CCircuit*`;`path` 为 NULL、不是合法 UTF-8、读取失败或解析失败时返回 NULL。 + +--- + +## 导出 + +### qasm3_dumps(circuit) + +把线路导出为 OpenQASM 3.0 字符串。输出依次为 `OPENQASM 3.0;`、`include "stdgates.inc";`、扩展门定义、自定义门定义、量子与经典声明、主线路操作。 + +参数: + +- `circuit` (`const struct CCircuit*`):待导出的线路。 + +返回:成功返回堆上 C 字符串(用 `cqlib_string_free` 释放);`circuit` 为 NULL 或导出失败时返回 NULL。 + +导出失败的原因包括:无法表示的指令(如 `delay`、`XY`、`RXY`)、无法表示的经典存储、无法表示的控制流(如 `while`、`for`、`break` / `continue`)、门定义体内包含测量,以及同名门定义的签名冲突。 + +### qasm3_dump(circuit, path) + +把线路导出为 OpenQASM 3.0 字符串并写入文件。 + +参数: + +- `circuit` (`const struct CCircuit*`):待导出的线路。 +- `path` (`const char*`):目标文件路径。 + +返回:0 成功;-1 任一指针为 NULL;-8 路径不是合法 UTF-8;-5 导出或写入失败。 + +--- + +## 支持的 OpenQASM 3.0 子集 + +- 头部 `OPENQASM 3;` 与 `OPENQASM 3.0;`,以及 `include "stdgates.inc";`(使用内置标准库,不要求磁盘文件) +- 标量量子比特与一维量子寄存器声明 +- 经典声明 `bit`、`bit[n]`、`bool`、`uint[n]`,以及角度与浮点输入声明 +- 映射到标准门的标准门与扩展门 +- 自定义门定义与调用 +- 测量、`reset`、`barrier`、全局相位 +- 控制流 `if` / `else`、静态 `for`、常量分支 `switch` + +以下特性遇到时给出明确错误,不做部分降级: + +- 定时与脉冲:`delay`、`box`、`cal`、`defcal` +- 子程序与外部调用:`def` 子程序、`extern` +- 硬件相关:硬件量子比特、`output` 声明,以及除角度与浮点以外的 `input` 声明 +- 其它语句:`alias`、`pragma`、注解、旧式声明 +- 门修饰符:`negctrl`、`pow`;`ctrl` 与 `inv` 仅在目标门存在对应实现时可用 +- 部分标准门:`ch`、`cp`、`cu`、`cswap` +- 控制流:运行时条件的 `while` 会被语义检查拒绝;`for` 只支持范围为常量的静态展开形式 +- 复杂经典算术、复杂左值切片、多维索引,以及作用域内的量子比特声明 + +导出侧另有约束:不导出 `delay`、仅矩阵形式的酉门、写入标量 `bit` 变量的普通赋值、门定义体内的测量,以及 `while`、`for`、`break`、`continue` 等控制流和 `XY`、`RXY` 两种标准门。 + +--- + +## 往返行为 + +导出结果可被重新加载。规范化规则: + +- 头部固定为 `OPENQASM 3.0;` 与 `include "stdgates.inc";`,与输入中的头部写法无关。 +- 顶层量子比特统一导出为 `qubit[n] q`,单比特情形同样写作 `qubit[1] q`。 +- 用户经典变量按 `c0`、`c1`… 命名,不可变的测量值按 `v0`、`v1`… 命名。 +- 测量紧接兼容的存储时折叠为赋值形式,如 `c0 = measure q;`。 +- 独立测量写入自动生成的 `bit[k] meas` 寄存器,如 `meas[0] = measure q[0];`。 +- 非连续或重排的寄存器测量拆分为逐位赋值,如 `c0[0] = measure q[2];`。 +- `stdgates.inc` 已提供的门直接调用;扩展门在主线路前生成一次 `gate` 定义,调用处仍使用其名称。 +- 全局相位以 `gphase(...);` 语句导出,重新加载后进入线路的全局相位。 +- 自动生成的名称会避开线路中已有的门名与寄存器名。 + +--- + +## 示例 + +```c +#include +#include "cqlib_c.h" + +const char *src = + "OPENQASM 3;\n" + "include \"stdgates.inc\";\n" + "qubit[2] q;\n" + "h q[0];\n" + "cx q[0], q[1];\n"; + +struct CCircuit *c = qasm3_loads(src); +if (c == NULL) { + /* 解析失败 */ +} + +char *out = qasm3_dumps(c); +if (out != NULL) { + printf("%s", out); /* 头部规范化为 "OPENQASM 3.0;" */ + cqlib_string_free(out); +} + +int32_t rc = qasm3_dump(c, "out.qasm"); /* 0 成功 */ + +circuit_free(c); +``` diff --git a/docs/api/cn/c/2_device/0_overview.md b/docs/api/cn/c/2_device/0_overview.md new file mode 100644 index 00000000..d2544676 --- /dev/null +++ b/docs/api/cn/c/2_device/0_overview.md @@ -0,0 +1,71 @@ +# 设备(C) + +C 绑定的设备接口以不透明句柄(`CTopology`、`CDevice`、`CLayout`、`CNoiseModel`、`CExecutionResult`)加自由函数的形式描述量子设备:连接拓扑、校准属性、逻辑到物理的映射、噪声模型与任务执行结果。错误码、字符串与句柄的释放约定遵循 [Overview](../0_overview.md)。 + +--- + +## 概念关系 + +- `CDevice` 聚合一张 `CTopology` 与一组属性默认值:拓扑描述物理比特与耦合构成的有向图;设备在其上叠加设备名、原生门集、不可用比特、校准时间与设备级默认属性。查询比特属性时局部值优先,缺失时回退默认值。 +- `CLayout` 是逻辑比特与物理比特之间的双向映射,用于路由过程;`layout_bind` / `layout_unbind` / `layout_swap_physical` 是移动逻辑比特的基本操作。 +- `CNoiseModel` 独立于设备构建,按「门 + 比特」组合登记噪声信道与读出误差。 +- `CExecutionResult` 承载一次任务的执行结果:状态机(排队 → 运行 → 完成 / 失败 / 取消)、测量计数与概率分布。 + +--- + +## 页面导航 + +| 页面 | 内容 | +| --- | --- | +| [Topology](1_topology.md) | `CTopology`:拓扑构造、比特与耦合编辑、连接性与度查询。 | +| [Device / Properties](2_properties_device.md) | `CDevice`:构造工厂、原生门集、校验、误差属性、默认值与属性结构体。 | +| [Layout](3_layout.md) | `CLayout`:逻辑↔物理映射的构造、绑定与交换。 | +| [NoiseModel](4_noise.md) | `CNoiseModel`:单比特、双比特与读出噪声的登记和查询。 | +| [ExecutionResult](5_result.md) | `CExecutionResult` 与 `CCountsList`:任务状态机、计数与概率。 | +| [量子比特标识](6_qubits.md) | `CLogicalQubit` / `CPhysicalQubit`:设备侧强类型比特标识及其与 `CQubit` 的互转。 | + +--- + +## 共同约定 + +- 所有句柄用对应的 `*_free` 函数释放,传 NULL 均合法:`topology_free`、`device_free`、`layout_free`、`noise_model_free`、`execution_result_free`、`counts_list_free`。 +- `device_topology` 返回拓扑的克隆副本,用 `topology_free` 释放。 +- 返回 `char*` 的接口(如 `device_name`、`counts_list_get_key`)用 `cqlib_string_free` 释放。 +- 数组输出采用两步式:先调用 `*_len` 函数取总数,分配缓冲区后再调用填充函数(如 `device_usable_qubits_len` + `device_usable_qubits`),填充函数同样返回总数。 + +--- + +## 示例 + +```c +#include +#include "cqlib_c.h" + +int main(void) { + /* 直线拓扑设备:0 -> 1 -> 2 */ + struct CDevice *dev = device_line("demo", 3); + if (dev == NULL) { + return 1; + } + + char *name = device_name(dev); /* "demo" */ + uintptr_t usable = device_num_qubits(dev); /* 3 */ + cqlib_string_free(name); + + /* 拓扑克隆副本 */ + struct CTopology *topo = device_topology(dev); + int32_t directed = topology_supports_directed_coupling(topo, 0, 1); /* 1 */ + int32_t reverse = topology_supports_directed_coupling(topo, 1, 0); /* 0 */ + topology_free(topo); + + /* 设备级默认值与回退查询 */ + device_set_default_t1(dev, 40.0); + double t1 = 0.0; + device_get_t1(dev, 0, &t1); /* 0(成功),t1 == 40.0 */ + + device_free(dev); + return 0; +} +``` + +编译器接口以 `CDevice` 与 `CLayout` 作为设备目标的输入,见 [Compiler](../4_compile/1_compiler.md)。 diff --git a/docs/api/cn/c/2_device/1_topology.md b/docs/api/cn/c/2_device/1_topology.md new file mode 100644 index 00000000..6800e3ff --- /dev/null +++ b/docs/api/cn/c/2_device/1_topology.md @@ -0,0 +1,232 @@ +# Topology(C) + +`CTopology` 是硬件连接图的不透明句柄:物理比特与它们之间的有向耦合。错误码、字符串与句柄的释放约定遵循 [Overview](../0_overview.md);本页所有接口均在 `cqlib_c.h` 中声明。 + +`CTopology` 句柄有两个来源:`topology_line` 从比特 ID 数组按直线形态构造;`device_topology` 返回设备拓扑的克隆副本(见 [Device / Properties](2_properties_device.md))。耦合是有向的:`(control, target)` 上有耦合不代表反方向也有。 + +--- + +## 构造与释放 + +### topology_line(qubits, num_qubits) + +按给定顺序构造一条有向直线 `qubits[0] -> qubits[1] -> ...`,耦合名为空串。 + +参数: + +- `qubits` (`const uint32_t*`):物理比特 ID 数组。 +- `num_qubits` (`uintptr_t`):数组长度。 + +返回:成功返回新建的 `CTopology*`;指针为 NULL 或构造失败时返回 NULL。 + +### topology_free(ptr) + +释放 `CTopology`,允许传 NULL。 + +参数: + +- `ptr` (`struct CTopology*`):待释放的句柄。 + +### topology_num_qubits(ptr) + +返回拓扑中的比特数;`ptr` 为 NULL 时返回 0。 + +参数: + +- `ptr` (`const struct CTopology*`):拓扑句柄。 + +### topology_num_couplings(ptr) + +返回拓扑中的耦合边数;`ptr` 为 NULL 时返回 0。 + +参数: + +- `ptr` (`const struct CTopology*`):拓扑句柄。 + +--- + +## 比特与耦合编辑 + +### topology_add_qubits(ptr, qubits, len) + +向拓扑添加一组比特。 + +参数: + +- `ptr` (`struct CTopology*`):拓扑句柄。 +- `qubits` (`const uint32_t*`):比特 ID 数组。 +- `len` (`uintptr_t`):数组长度。 + +返回:0 成功;-1 空指针;-8 比特已存在于拓扑中或数组内重复。 + +### topology_remove_qubits(ptr, qubits, len) + +移除一组比特及所有触及它们的耦合。 + +参数: + +- `ptr` (`struct CTopology*`):拓扑句柄。 +- `qubits` (`const uint32_t*`):比特 ID 数组。 +- `len` (`uintptr_t`):数组长度。 + +返回:0 成功;-1 空指针;-2 比特不在拓扑中;-8 数组内重复。 + +### topology_add_couplings(ptr, edges, num_edges, names) + +添加一组有向耦合边。`edges` 指向 `2 * num_edges` 个 u32,按 `(control, target)` 对连续排列;`names` 可选地指向 `num_edges` 个 C 字符串(NULL 条目或 NULL 数组表示该耦合不带名字)。 + +参数: + +- `ptr` (`struct CTopology*`):拓扑句柄。 +- `edges` (`const uint32_t*`):耦合边数组。 +- `num_edges` (`uintptr_t`):边的数量。 +- `names` (`const char *const*`):耦合名数组,可为 NULL。 + +返回:0 成功;-1 空指针;-4 名字不是合法 UTF-8;-2 端点比特不在拓扑中;-8 重复或自耦合。 + +### topology_remove_couplings(ptr, edges, num_edges) + +移除一组有向耦合边。`edges` 布局与 `topology_add_couplings` 相同。 + +参数: + +- `ptr` (`struct CTopology*`):拓扑句柄。 +- `edges` (`const uint32_t*`):耦合边数组。 +- `num_edges` (`uintptr_t`):边的数量。 + +返回:0 成功;-1 空指针;-2 比特或耦合不在拓扑中;-8 耦合在数组内重复。 + +--- + +## 查询与遍历 + +### topology_contains_qubit(ptr, qubit) + +判断比特是否属于拓扑。 + +参数: + +- `ptr` (`const struct CTopology*`):拓扑句柄。 +- `qubit` (`uint32_t`):比特 ID。 + +返回:1 属于;0 不属于;-1 空指针。 + +### topology_get_coupling_name(ptr, control, target) + +返回有向耦合 `control -> target` 的名字;耦合存在但未命名时返回空字符串。 + +参数: + +- `ptr` (`const struct CTopology*`):拓扑句柄。 +- `control` (`uint32_t`):控制比特 ID。 +- `target` (`uint32_t`):目标比特 ID。 + +返回:成功返回堆上 C 字符串(用 `cqlib_string_free` 释放);输入为 NULL 或耦合不存在时返回 NULL。 + +### topology_qubits_len(ptr) / topology_qubits(ptr, out, len) + +两步式输出拓扑中的全部比特 ID。 + +- `topology_qubits_len(ptr)` 返回比特总数,`ptr` 为 NULL 时返回 0。 +- `topology_qubits(ptr, out, len)` 把比特 ID 拷贝到 `out`,返回总数。 + +### topology_neighbors_undirected_len(ptr, qubit) / topology_neighbors_undirected(ptr, qubit, out, len) + +两步式输出比特的无向邻居(后继与前驱去重,升序排列)。比特不在拓扑中或 `ptr` 为 NULL 时长度为 0。 + +参数: + +- `qubit` (`uint32_t`):比特 ID。 + +### topology_predecessors_len(ptr, qubit) / topology_predecessors(ptr, qubit, out, len) + +两步式输出比特的前驱(有耦合指向它的比特,升序排列)。 + +### topology_successors_len(ptr, qubit) / topology_successors(ptr, qubit, out, len) + +两步式输出比特的后继(经出边可达的比特,升序排列)。 + +### topology_undirected_edges_len(ptr) / topology_undirected_edges(ptr, out, len) + +两步式输出去重后的无向耦合对:双向耦合折叠为一对,按 `(low, high)` 升序排列。`out` 需容纳 `2 * len` 个 u32,返回对的总数。 + +--- + +## 度与支持性 + +### topology_in_degree(ptr, qubit) + +返回比特的入度(指向它的耦合数);`ptr` 为 NULL 或比特不在拓扑中时返回 0。 + +### topology_out_degree(ptr, qubit) + +返回比特的出度(从它出发的耦合数);`ptr` 为 NULL 或比特不在拓扑中时返回 0。 + +### topology_supports_directed_coupling(ptr, control, target) + +判断有向耦合 `control -> target` 是否存在。 + +返回:1 存在;0 不存在;-1 空指针。 + +### topology_supports_coupling_either_direction(ptr, a, b) + +判断两个比特之间是否存在任一方向的耦合。 + +返回:1 存在;0 不存在;-1 空指针。 + +--- + +## 示例 + +```c +#include +#include "cqlib_c.h" + +int main(void) { + uint32_t ids[3] = {0, 1, 2}; + struct CTopology *topo = topology_line(ids, 3); + if (topo == NULL) { + return 1; + } + + /* 3 个比特、2 条有向耦合:0->1、1->2 */ + printf("qubits=%llu couplings=%llu\n", + (unsigned long long)topology_num_qubits(topo), + (unsigned long long)topology_num_couplings(topo)); + + /* 连接性 */ + int32_t fwd = topology_supports_directed_coupling(topo, 1, 2); /* 1 */ + int32_t bwd = topology_supports_directed_coupling(topo, 2, 1); /* 0 */ + int32_t either = topology_supports_coupling_either_direction(topo, 2, 1); /* 1 */ + + /* 遍历比特 1 的邻居与度 */ + uintptr_t succ_len = topology_successors_len(topo, 1); /* 1 */ + uint32_t succ[1]; + topology_successors(topo, 1, succ, succ_len); /* {2} */ + + uintptr_t nb_len = topology_neighbors_undirected_len(topo, 1); /* 2 */ + uint32_t nb[2]; + topology_neighbors_undirected(topo, 1, nb, nb_len); /* {0, 2} */ + + uintptr_t in_deg = topology_in_degree(topo, 1); /* 1 */ + uintptr_t out_deg = topology_out_degree(topo, 1); /* 1 */ + + /* 添加并移除比特与耦合 */ + uint32_t extra[1] = {3}; + if (topology_add_qubits(topo, extra, 1) == 0) { + uint32_t edge[2] = {2, 3}; + const char *names[1] = {"CX"}; + topology_add_couplings(topo, edge, 1, names); + topology_remove_couplings(topo, edge, 1); + topology_remove_qubits(topo, extra, 1); + } + + /* 无向边 */ + uintptr_t pairs = topology_undirected_edges_len(topo); /* 2 */ + uint32_t buf[4]; + topology_undirected_edges(topo, buf, pairs); /* {0,1, 1,2} */ + + topology_free(topo); + return 0; +} +``` diff --git a/docs/api/cn/c/2_device/2_properties_device.md b/docs/api/cn/c/2_device/2_properties_device.md new file mode 100644 index 00000000..94533709 --- /dev/null +++ b/docs/api/cn/c/2_device/2_properties_device.md @@ -0,0 +1,647 @@ +# Device / Properties(C) + +`CDevice` 是设备的不透明句柄:在拓扑之上聚合设备名、原生门集、比特集合、校准时间与属性默认值,并提供校验和误差查询。错误码、字符串与句柄的释放约定遵循 [Overview](../0_overview.md)。 + +查询比特属性时局部值优先:`device_get_t1` 等接口先读取该比特记录的属性,取不到时回退设备级默认值。比特或边一旦记录了自己的原生指令列表,该列表就是此处能力的完整覆盖,不会回写设备级默认门集。 + +--- + +## 属性结构体 + +### CQubitProp + +单个物理比特的属性快照,由 `device_qubit_properties` 写出: + +| 字段 | 类型 | 含义 | +| --- | --- | --- | +| `readout_error` | `double` | 读出误差率,[0, 1]。 | +| `t1` | `double` | T1 弛豫时间(μs);未设置时为 NaN。 | +| `t2` | `double` | T2 去相干时间(μs);未设置时为 NaN。 | +| `prob_meas0_prep1` | `double` | P(制备 1 测得 0);未设置时为 NaN。 | +| `prob_meas1_prep0` | `double` | P(制备 0 测得 1);未设置时为 NaN。 | +| `frequency` | `double` | 频率(GHz);未设置时为 NaN。 | +| `num_native_instructions` | `uintptr_t` | 该比特记录的原生指令数,用作 `device_qubit_prop_native_instruction` 的下标上界。 | + +### CNativeInstruction + +单条原生指令能力快照,由 `device_qubit_prop_native_instruction` / `device_edge_prop_native_instruction` 写出: + +| 字段 | 类型 | 含义 | +| --- | --- | --- | +| `name` | `char*` | 堆上标准门名,用 `cqlib_string_free` 释放。 | +| `error_rate` | `double` | 误差率,[0, 1]。 | +| `length` | `double` | 时长(ns);未设置时为 NaN。 | + +### CEdgeProp + +单条有向耦合边的属性快照,由 `device_edge_properties` 写出: + +| 字段 | 类型 | 含义 | +| --- | --- | --- | +| `num_native_instructions` | `uintptr_t` | 该边记录的原生指令数,用作 `device_edge_prop_native_instruction` 的下标上界。 | + +### CQubitPropInput + +`device_add_qubit_properties` 的输入。可选的 `double` 字段用 NaN 表示未设置;原生指令以长度为 `num_native_instructions` 的并行数组给出(门名、误差率、时长),`native_lengths` 可为 NULL 表示全部时长未设置: + +| 字段 | 类型 | 含义 | +| --- | --- | --- | +| `readout_error` | `double` | 读出误差率,[0, 1]。 | +| `t1` | `double` | T1 弛豫时间(μs);NaN = 未设置。 | +| `t2` | `double` | T2 去相干时间(μs);NaN = 未设置。 | +| `prob_meas0_prep1` | `double` | P(制备 1 测得 0);NaN = 未设置。 | +| `prob_meas1_prep0` | `double` | P(制备 0 测得 1);NaN = 未设置。 | +| `frequency` | `double` | 频率(GHz);NaN = 未设置。 | +| `native_gate_names` | `const char *const*` | 标准门名数组,无指令时为 NULL。 | +| `native_error_rates` | `const double*` | 误差率数组,无指令时为 NULL。 | +| `native_lengths` | `const double*` | 时长数组(ns,NaN = 未设置),可为 NULL。 | +| `num_native_instructions` | `uintptr_t` | 原生指令数。 | + +### CEdgePropInput + +`device_add_edge_properties` 的输入,字段与 `CQubitPropInput` 的原生指令部分一致,且只接受双比特标准门: + +| 字段 | 类型 | 含义 | +| --- | --- | --- | +| `native_gate_names` | `const char *const*` | 标准门名数组,无指令时为 NULL。 | +| `native_error_rates` | `const double*` | 误差率数组,无指令时为 NULL。 | +| `native_lengths` | `const double*` | 时长数组(ns,NaN = 未设置),可为 NULL。 | +| `num_native_instructions` | `uintptr_t` | 原生指令数。 | + +--- + +## 构造 + +### device_new(name, num_qubits) + +创建一个含 `num_qubits` 个孤立物理比特、无耦合的设备;自定义拓扑用 `device_from_edges`。 + +参数: + +- `name` (`const char*`):设备名。 +- `num_qubits` (`uint32_t`):物理比特数。 + +返回:成功返回新建的 `CDevice*`;失败返回 NULL。 + +### device_line(name, num_qubits) + +创建有向直线拓扑 `0 -> 1 -> ... -> n-1` 的设备。 + +### device_bidirectional_line(name, num_qubits) + +创建双向直线拓扑的设备。 + +### device_ring(name, num_qubits) + +创建双向环形拓扑的设备。 + +### device_grid(name, rows, cols) + +创建双向网格拓扑的设备,比特按行优先编号。 + +参数: + +- `rows` (`uint32_t`):行数。 +- `cols` (`uint32_t`):列数。 + +### device_star(name, num_qubits, center) + +创建以 `center` 为中心的双向星形拓扑设备。 + +参数: + +- `num_qubits` (`uint32_t`):比特总数。 +- `center` (`uint32_t`):中心比特 ID。 + +### device_from_edges(name, num_qubits, edges, num_edges) + +由显式的有向边创建设备。`edges` 指向 `2 * num_edges` 个 u32,按 `(control, target)` 对连续排列。 + +参数: + +- `num_qubits` (`uint32_t`):物理比特数。 +- `edges` (`const uint32_t*`):耦合边数组。 +- `num_edges` (`uintptr_t`):边的数量。 + +返回:成功返回新建的 `CDevice*`;失败返回 NULL。 + +### device_line_from_qubits(name, qubits, num_qubits) + +按给定顺序把比特 ID 数组连成有向直线,创建设备。 + +参数: + +- `qubits` (`const uint32_t*`):物理比特 ID 数组。 +- `num_qubits` (`uintptr_t`):数组长度。 + +返回:成功返回新建的 `CDevice*`;失败返回 NULL。 + +### device_free(ptr) + +释放 `CDevice`,允许传 NULL。 + +参数: + +- `ptr` (`struct CDevice*`):待释放的句柄。 + +--- + +## 基本信息与原生门 + +### device_name(ptr) + +返回设备名。 + +返回:成功返回堆上 C 字符串(用 `cqlib_string_free` 释放);失败返回 NULL。 + +### device_num_qubits(ptr) + +返回可用物理比特数;`ptr` 为 NULL 时返回 0。 + +### device_native_gates(ptr) + +返回原生门名列表。 + +返回:成功返回逗号分隔的堆上 C 字符串(如 `"H,CX"`),用 `cqlib_string_free` 释放;失败或门集为空时返回 NULL。 + +### device_with_native_gates(ptr, gate_names) + +用 `gate_names`(逗号分隔,如 `"H,CX,RZ"`)替换设备的原生门集。 + +参数: + +- `gate_names` (`const char*`):门名列表。 + +返回:0 成功;-4 任一门名未知;其他负值为错误码。 + +### device_topology(ptr) + +返回设备拓扑的克隆副本,用 `topology_free` 释放。 + +返回:成功返回新建的 `CTopology*`;失败返回 NULL。 + +--- + +## 校验 + +### device_validate_circuit(ptr, circuit) + +校验线路是否与设备兼容。 + +参数: + +- `circuit` (`const struct CCircuit*`):待校验的线路。 + +返回:0 兼容;-3 校验失败;其他负值为错误码。 + +### device_validate_operation(ptr, operation) + +在物理比特 ID 空间中校验一个来自 `circuit_index` 的已解析操作快照。 + +参数: + +- `operation` (`const struct CValueOperation*`):操作快照。 + +返回:0 合法;-3 设备拒绝该操作;-1 空指针。 + +### device_validate_value_operation(ptr, operation) + +在物理比特 ID 空间中校验一个来自 `circuit_index` 的值级操作快照;控制流体会被递归校验。 + +参数: + +- `operation` (`const struct CValueOperation*`):操作快照。 + +返回:0 合法;-3 设备拒绝该操作;-1 空指针。 + +### device_supports_native_instruction(ptr, gate_name, qargs, num_qargs) + +判断标准门 `gate_name` 能否在给定有序物理比特上原生执行。 + +参数: + +- `gate_name` (`const char*`):标准门名。 +- `qargs` (`const uint32_t*`):物理比特 ID 数组;`GPhase` 允许空数组。 +- `num_qargs` (`uintptr_t`):数组长度。 + +返回:1 支持;0 不支持;-1 空指针;-4 门名未知。校准数据不影响该能力查询。 + +--- + +## 误差与相干属性 + +### device_get_t1(ptr, qubit, out) + +返回比特的 T1 弛豫时间(μs),局部属性优先,缺失时回退设备默认值。 + +参数: + +- `qubit` (`uint32_t`):物理比特 ID。 +- `out` (`double*`):写出结果。 + +返回:0 并写入 `*out`;-8 该比特无 T1 数据。 + +### device_get_t2(ptr, qubit, out) + +返回比特的 T2 去相干时间(μs),回退规则同上。返回:0 并写入 `*out`;-8 未设置。 + +### device_get_readout_error(ptr, qubit, out) + +返回比特的读出误差率,回退规则同上。返回:0 并写入 `*out`;-8 未设置。 + +### device_single_qubit_error(ptr, gate_name, qubit, out) + +返回单比特标准门 `gate_name` 在 `qubit` 上的误差率,局部原生指令属性优先,缺失时回退设备默认单比特误差率。 + +参数: + +- `gate_name` (`const char*`):标准门名。 +- `qubit` (`uint32_t`):物理比特 ID。 +- `out` (`double*`):写出结果。 + +返回:0 并写入 `*out`;-4 门名未知;-8 该比特不支持此门。 + +### device_two_qubit_error(ptr, gate_name, control, target, out) + +返回双比特标准门 `gate_name` 在有向耦合 `control -> target` 上的误差率,回退规则同上。 + +返回:0 并写入 `*out`;-4 门名未知;-8 该耦合不支持此门。 + +### device_edge_error(ptr, control, target, out) + +返回有向耦合 `control -> target` 的方向性误差:该边上标定最好的原生双比特误差,缺失时取设备默认值。 + +返回:0 并写入 `*out`;-8 该有向耦合不存在。 + +### device_calibration_time(ptr, out_unix_ms) + +返回系统标定时间戳(Unix 纪元起的毫秒数)。 + +参数: + +- `out_unix_ms` (`int64_t*`):写出结果。 + +返回:0 并写入 `*out`;-8 未设置。 + +--- + +## 比特属性的读取与设置 + +以下 getter 读取单个比特记录的属性值;与 `device_get_t1` 不同,频率与制备-测量概率的 getter 没有设备级默认值回退。setter 只修改该比特已记录属性(例如通过 `device_add_qubit_properties` 写入)中的单个字段,其余字段全部保留,也不会创建属性记录。带下标的变体寻址原生指令列表,上界为 `device_qubit_properties` 快照中的 `num_native_instructions`。未设置的可选值以 -8 报告,并向 `*out` 写入 NaN。 + +### device_get_frequency(ptr, qubit, out) + +返回比特记录的频率(GHz),无默认值回退。 + +参数: + +- `qubit` (`uint32_t`):物理比特 ID。 +- `out` (`double*`):写出结果。 + +返回:0 并写入 `*out`;-8 该比特没有记录属性或未设置频率。 + +### device_get_prob_meas0_prep1(ptr, qubit, out) + +返回比特记录的 P(制备 1 测得 0),无默认值回退。 + +返回:0 并写入 `*out`;-8 该比特没有记录属性或未设置该概率。 + +### device_get_prob_meas1_prep0(ptr, qubit, out) + +返回比特记录的 P(制备 0 测得 1),无默认值回退。 + +返回:0 并写入 `*out`;-8 该比特没有记录属性或未设置该概率。 + +### device_get_error_rate(ptr, qubit, index, out) + +返回比特记录的下标为 `index` 的原生指令误差率。 + +参数: + +- `index` (`uintptr_t`):原生指令下标。 +- `out` (`double*`):写出结果。 + +返回:0 并写入 `*out`;-8 该比特没有记录属性或下标越界。 + +### device_get_length(ptr, qubit, index, out) + +返回比特记录的下标为 `index` 的原生指令时长(ns)。 + +返回:0 并写入 `*out`;-8 该比特没有记录属性、下标越界或时长未设置。 + +### device_set_t1(ptr, qubit, t1) + +设置比特记录的 T1 弛豫时间(μs)。 + +返回:0 成功;-1 空指针;-8 该比特没有记录属性。 + +### device_set_t2(ptr, qubit, t2) + +设置比特记录的 T2 去相干时间(μs)。 + +返回:0 成功;-1 空指针;-8 该比特没有记录属性。 + +### device_set_frequency(ptr, qubit, frequency) + +设置比特记录的频率(GHz)。 + +返回:0 成功;-1 空指针;-8 该比特没有记录属性。 + +### device_set_prob_meas0_prep1(ptr, qubit, prob) + +设置比特记录的 P(制备 1 测得 0)。 + +返回:0 成功;-1 空指针;-8 该比特没有记录属性。 + +### device_set_prob_meas1_prep0(ptr, qubit, prob) + +设置比特记录的 P(制备 0 测得 1)。 + +返回:0 成功;-1 空指针;-8 该比特没有记录属性。 + +### device_set_error_rate(ptr, qubit, index, error_rate) + +设置比特记录的下标为 `index` 的原生指令误差率。 + +返回:0 成功;-1 空指针;-8 该比特没有记录属性或下标越界。 + +### device_set_length(ptr, qubit, index, length) + +设置比特记录的下标为 `index` 的原生指令时长(ns)。 + +返回:0 成功;-1 空指针;-8 该比特没有记录属性或下标越界。 + +### device_set_instruction(ptr, qubit, index, gate_name) + +替换比特记录的下标为 `index` 的原生指令所承载的标准门。编辑后的列表会重新校验,把单比特门改成双比特门(或反过来)会被拒绝。 + +参数: + +- `index` (`uintptr_t`):原生指令下标。 +- `gate_name` (`const char*`):替换后的标准门名。 + +返回:0 成功;-1 空指针;-4 门名未知;-8 该比特没有记录属性、下标越界或编辑后的列表未通过校验。 + +### device_set_native_instruction(ptr, qubit, gate_name, error_rate, length) + +向比特记录的属性追加一条原生单比特指令(标准门 `gate_name`,误差率 `error_rate`,时长 `length`(ns);`length` 为 NaN 表示未设置)。 + +参数: + +- `gate_name` (`const char*`):标准门名。 +- `error_rate` (`double`):误差率,[0, 1]。 +- `length` (`double`):时长(ns);NaN = 未设置。 + +返回:0 成功;-1 空指针;-4 门名未知;-8 该比特没有记录属性,或指令被拒绝(元数不符或标定值非法)。 + +--- + +## 默认值 + +### device_default_t1(ptr, out) + +返回设备级默认 T1 时间(μs)。返回:0 并写入 `*out`;-8 未设置默认值。 + +### device_default_t2(ptr, out) + +返回设备级默认 T2 时间(μs)。返回:0 并写入 `*out`;-8 未设置。 + +### device_default_readout_error(ptr, out) + +返回设备级默认读出误差率。返回:0 并写入 `*out`;-8 未设置。 + +### device_default_single_qubit_error(ptr, out) + +返回设备级默认单比特门误差率。返回:0 并写入 `*out`;-8 未设置。 + +### device_default_two_qubit_error(ptr, out) + +返回设备级默认双比特门误差率。返回:0 并写入 `*out`;-8 未设置。 + +### device_set_default_t1(ptr, t1) + +设置设备级默认 T1 时间(μs)。返回:0 成功;负值为错误码。 + +### device_set_default_t2(ptr, t2) + +设置设备级默认 T2 时间(μs)。返回:0 成功;负值为错误码。 + +### device_set_default_readout_error(ptr, error) + +设置设备级默认读出误差率。返回:0 成功;负值为错误码。 + +### device_set_default_single_qubit_error(ptr, error) + +设置设备级默认单比特门误差率。返回:0 成功;负值为错误码。 + +### device_set_default_two_qubit_error(ptr, error) + +设置设备级默认双比特门误差率。返回:0 成功;负值为错误码。 + +--- + +## 可用比特 + +### device_invalid_qubits_len(ptr) + +返回不可用(离线 / 故障)比特数;`ptr` 为 NULL 时返回 0。 + +### device_invalid_qubits(ptr, out, len) + +两步式输出不可用比特 ID,与 `device_invalid_qubits_len` 配对;返回总数。 + +### device_set_invalid_qubits(ptr, qubits, len) + +替换不可用比特集合。 + +参数: + +- `qubits` (`const uint32_t*`):比特 ID 数组。 +- `len` (`uintptr_t`):数组长度。 + +返回:0 成功;-1 空指针;-2 任一比特未在设备中登记(此时原集合保持不变)。 + +### device_is_usable_qubit(ptr, qubit) + +判断比特是否已登记且未标记为不可用。 + +返回:1 可用;0 不可用或未登记;-1 空指针。 + +### device_num_usable_qubits(ptr) + +返回可用比特数;`ptr` 为 NULL 时返回 0。 + +### device_usable_qubits_len(ptr) / device_usable_qubits(ptr, out, len) + +两步式输出可用比特 ID;填充函数返回总数。 + +### device_qubits_len(ptr) / device_qubits(ptr, out, len) + +两步式输出全部已登记的物理比特 ID(含不可用比特);填充函数返回总数。 + +--- + +## 比特与边属性 + +### device_qubit_properties(ptr, qubit, out) + +写出比特的属性快照。 + +参数: + +- `qubit` (`uint32_t`):物理比特 ID。 +- `out` (`struct CQubitProp*`):写出快照。 + +返回:0 成功;-8 该比特没有记录属性(快照中的可选字段为 NaN)。 + +### device_qubit_prop_native_instruction(ptr, qubit, index, out) + +写出该比特下标为 `index` 的原生指令。 + +参数: + +- `qubit` (`uint32_t`):物理比特 ID。 +- `index` (`uintptr_t`):指令下标,上界为快照中的 `num_native_instructions`。 +- `out` (`struct CNativeInstruction*`):写出快照;`name` 需用 `cqlib_string_free` 释放。 + +返回:0 成功;-8 该比特没有记录属性或下标越界。 + +### device_edge_properties(ptr, control, target, out) + +写出有向耦合 `control -> target` 的属性快照。 + +返回:0 成功;-8 该边没有记录属性。 + +### device_edge_prop_native_instruction(ptr, control, target, index, out) + +写出该有向边下标为 `index` 的原生指令(双比特标准门)。 + +返回:0 成功;-8 该边没有记录属性或下标越界;`name` 需用 `cqlib_string_free` 释放。 + +### device_add_qubit_properties(ptr, qubit, input) + +记录(或替换)比特的属性。 + +参数: + +- `qubit` (`uint32_t`):物理比特 ID。 +- `input` (`const struct CQubitPropInput*`):属性输入。 + +返回:0 成功;-1 空指针;-2 比特未在设备中登记或不在其拓扑中;-4 原生门名未知;-8 原生指令标定值非法。 + +### device_add_edge_properties(ptr, control, target, input) + +记录(或替换)有向耦合 `control -> target` 的属性。 + +参数: + +- `input` (`const struct CEdgePropInput*`):属性输入。 + +返回:0 成功;-1 空指针;-2 该有向耦合不在设备拓扑中;-4 门名未知;-8 原生指令不是双比特标准门或标定值非法。 + +--- + +## 示例 + +```c +#include +#include "cqlib_c.h" + +int main(void) { + /* 直线拓扑:0 -> 1 -> 2 */ + struct CDevice *dev = device_line("mock_backend", 3); + if (dev == NULL) { + return 1; + } + + /* 设备级默认值 */ + device_set_default_t1(dev, 50.0); + device_set_default_t2(dev, 35.0); + device_set_default_readout_error(dev, 0.05); + + /* 比特 0 的局部属性:读出误差 0.02、T1 80、原生 H 指令 */ + struct CQubitPropInput input; + input.readout_error = 0.02; + input.t1 = 80.0; + input.t2 = 70.0; + input.prob_meas0_prep1 = 0.01; + input.prob_meas1_prep0 = 0.03; + input.frequency = 5.0; + + const char *names[1] = {"H"}; + const double rates[1] = {0.001}; + input.native_gate_names = names; + input.native_error_rates = rates; + input.native_lengths = NULL; + input.num_native_instructions = 1; + + int32_t rc = device_add_qubit_properties(dev, 0, &input); /* 0 */ + + /* 边 0 -> 1 的局部属性:原生 CX 指令 */ + struct CEdgePropInput einput; + const char *enames[1] = {"CX"}; + const double erates[1] = {0.02}; + einput.native_gate_names = enames; + einput.native_error_rates = erates; + einput.native_lengths = NULL; + einput.num_native_instructions = 1; + + rc = device_add_edge_properties(dev, 0, 1, &einput); /* 0 */ + + /* 查询:局部优先、默认值回退 */ + double t1_0 = 0.0, t1_2 = 0.0; + device_get_t1(dev, 0, &t1_0); /* 0,t1_0 == 80.0(局部) */ + device_get_t1(dev, 2, &t1_2); /* 0,t1_2 == 50.0(默认值) */ + + double sq_err = 0.0, tq_err = 0.0, edge_err = 0.0; + device_single_qubit_error(dev, "H", 0, &sq_err); /* 0,0.001 */ + device_two_qubit_error(dev, "CX", 0, 1, &tq_err); /* 0,0.02 */ + device_edge_error(dev, 0, 1, &edge_err); /* 0,0.02 */ + + /* 比特属性 getter(无默认值回退) */ + double freq = 0.0, pm01 = 0.0, pm10 = 0.0, rate = 0.0, len = 0.0; + device_get_frequency(dev, 0, &freq); /* 0,freq == 5.0 */ + device_get_prob_meas0_prep1(dev, 0, &pm01); /* 0,pm01 == 0.01 */ + device_get_prob_meas1_prep0(dev, 0, &pm10); /* 0,pm10 == 0.03 */ + device_get_error_rate(dev, 0, 0, &rate); /* 0,rate == 0.001 */ + device_get_length(dev, 0, 0, &len); /* -8:时长未设置,len == NaN */ + + /* 比特属性 setter:每次改一个字段,其余保留 */ + device_set_t1(dev, 0, 85.0); /* 0 */ + device_set_frequency(dev, 0, 5.1); /* 0 */ + device_get_frequency(dev, 0, &freq); /* 0,freq == 5.1 */ + device_set_error_rate(dev, 0, 0, 0.002); /* 0 */ + device_set_length(dev, 0, 0, 50.0); /* 0 */ + device_set_t1(dev, 2, 1.0); /* -8:比特 2 没有记录属性 */ + + /* 原生指令编辑 */ + device_set_instruction(dev, 0, 0, "X"); /* 0:H -> X,标定值保留 */ + device_set_native_instruction(dev, 0, "Z", 0.003, 80.0); /* 0:追加 */ + + /* 属性快照与原生指令 */ + struct CQubitProp prop; + if (device_qubit_properties(dev, 0, &prop) == 0) { + for (uintptr_t i = 0; i < prop.num_native_instructions; i++) { + struct CNativeInstruction ni; + if (device_qubit_prop_native_instruction(dev, 0, i, &ni) == 0) { + printf("%s rate=%f\n", ni.name, ni.error_rate); + cqlib_string_free(ni.name); + } + } + } + + /* 不可用比特 */ + uint32_t bad[1] = {2}; + device_set_invalid_qubits(dev, bad, 1); + int32_t usable = device_is_usable_qubit(dev, 2); /* 0 */ + uintptr_t n_usable = device_num_usable_qubits(dev); /* 2 */ + + /* 原生门集 */ + rc = device_with_native_gates(dev, "H,CX,RZ"); /* 0 */ + char *gates = device_native_gates(dev); /* "H,CX,RZ" */ + cqlib_string_free(gates); + + device_free(dev); + return 0; +} +``` diff --git a/docs/api/cn/c/2_device/3_layout.md b/docs/api/cn/c/2_device/3_layout.md new file mode 100644 index 00000000..2c35143a --- /dev/null +++ b/docs/api/cn/c/2_device/3_layout.md @@ -0,0 +1,236 @@ +# Layout(C) + +`CLayout` 是逻辑比特到物理比特映射的不透明句柄,维护路由过程中的逻辑↔物理双向映射;未承载逻辑比特的物理比特是空闲物理比特。错误码、字符串与句柄的释放约定遵循 [Overview](../0_overview.md)。 + +--- + +## 构造与释放 + +### layout_new(logical, num_logical, physical, num_physical) + +创建一个布局:`logical` 数组中的逻辑比特按顺序映射到 `physical` 数组中的物理比特。 + +参数: + +- `logical` (`const uint32_t*`):逻辑比特 ID 数组。 +- `num_logical` (`uintptr_t`):逻辑比特数,必须非零。 +- `physical` (`const uint32_t*`):物理比特 ID 数组。 +- `num_physical` (`uintptr_t`):物理比特数,必须与 `num_logical` 相同。 + +返回:成功返回新建的 `CLayout*`;失败返回 NULL。 + +### layout_from_pairs(pairs, num_pairs, physical_count) + +由 `(logical, physical)` 对创建布局。`pairs` 指向 `2 * num_pairs` 个 u32,按 `(逻辑, 物理)` 对连续排列;`physical_count` 定义物理比特总数(`0..physical_count`),未被引用的物理比特保持空闲。 + +参数: + +- `pairs` (`const uint32_t*`):比特对数组。 +- `num_pairs` (`uintptr_t`):对数,必须非零。 +- `physical_count` (`uint32_t`):物理比特总数。 + +返回:成功返回新建的 `CLayout*`;失败返回 NULL。 + +### layout_free(ptr) + +释放 `CLayout`,允许传 NULL。 + +--- + +## 映射查询 + +### layout_get(ptr, logical) + +返回逻辑比特映射到的物理比特。 + +返回:已映射时返回物理比特 ID;未映射或出错时返回 `UINT32_MAX`。 + +### layout_get_logical(ptr, physical) + +返回物理比特承载的逻辑比特(`layout_get` 的反向查询)。 + +返回:已占用时返回逻辑比特 ID;物理比特空闲或未知时返回 `UINT32_MAX`,调用方需将其视为哨兵值。 + +### layout_is_physical_vacant(ptr, physical) + +判断物理比特是否属于布局且空闲。 + +返回:1 空闲;0 已占用或未知;-1 空指针。 + +### layout_num_logical(ptr) + +返回已映射的逻辑比特数;`ptr` 为 NULL 时返回 0。 + +### layout_num_physical(ptr) + +返回物理比特总数;`ptr` 为 NULL 时返回 0。 + +### layout_num_vacant_physical(ptr) + +返回空闲物理比特数;`ptr` 为 NULL 时返回 0。 + +--- + +## 绑定与交换 + +### layout_bind(ptr, logical, physical) + +把一个未映射的逻辑比特绑定到空闲物理比特上。 + +返回:0 成功;-1 空指针;-2 物理比特不属于该布局;-8 任一侧已参与映射。 + +### layout_unbind(ptr, logical, out_physical) + +解除逻辑比特的映射,并把释放的物理比特 ID 写到 `*out_physical`。 + +返回:0 成功;-1 空指针;-8 该逻辑比特未绑定。 + +### layout_swap_physical(ptr, phys_a, phys_b) + +交换两个物理比特上承载的逻辑比特;任一侧可为空闲比特(与空闲比特交换即把逻辑比特搬到空闲位置),这是路由过程中移动逻辑比特的基本操作。 + +返回:0 成功;-1 空指针;-2 任一物理比特不属于该布局。 + +--- + +## 整体读取 + +以下接口均为两步式:先取长度,再填充缓冲区,填充函数返回总数。 + +### layout_l2p_map_len(ptr) / layout_l2p_map(ptr, out, len) + +逻辑→物理映射按 `(logical, physical)` 对输出,按逻辑 ID 升序排列;`out` 需容纳 `2 * len` 个 u32。 + +### layout_p2l_map_len(ptr) / layout_p2l_map(ptr, out, len) + +物理→逻辑映射按 `(physical, logical)` 对输出,按物理 ID 升序排列;`out` 需容纳 `2 * len` 个 u32。 + +### layout_logical_qubits_len(ptr) / layout_logical_qubits(ptr, out, len) + +已映射的逻辑比特 ID,升序输出。 + +### layout_physical_qubits_len(ptr) / layout_physical_qubits(ptr, out, len) + +布局可用的全部物理比特 ID(含空闲比特),升序输出。 + +### layout_vacant_physical_qubits_len(ptr) / layout_vacant_physical_qubits(ptr, out, len) + +空闲物理比特 ID,升序输出。 + +--- + +## 布局结果辅助 + +以下接口读取 SABRE 布局产物:物理布局图的全对最短距离表,以及线路布局分析中每个逻辑比特的活动度权重。句柄来自编译侧布局 API([初始布局](../4_compile/3_layout.md)):`CPhysicalLayoutGraph` 来自 `physical_layout_graph_from_device`,`CCircuitLayoutAnalysis` 来自 `analyze_circuit_for_layout`(或 `layout_result_analysis`),`CPreparedSabreCircuit` 来自 `prepare_sabre_circuit`。 + +### layout_result_distances(ptr, out, len) + +把物理布局图的全对无向最短路径距离表复制到 `out`(两步式)。表按图的物理比特 ID 升序行优先排列(`n` 个比特共 `n * n` 项;轴顺序可用 `physical_layout_graph_physical_qubits` 查询)。不可达的比特对写入 `UINT32_MAX`。 + +参数: + +- `ptr` (`const struct CPhysicalLayoutGraph*`):物理布局图。 +- `out` (`uint32_t*`):接收距离表;传 NULL 仅查询长度。 +- `len` (`uintptr_t`):缓冲区容量(项数);值较小时只复制前 `len` 项。 + +返回:总项数(`n * n`);`ptr` 为 NULL 时返回 0。 + +### layout_result_logical_activity(ptr, out_qubits, out_activity, len) + +把每个逻辑比特的活动度权重(该比特所有关联交互权重之和)复制到平行数组,按逻辑 ID 升序(两步式):`out_qubits[i]` 接收逻辑比特 ID,`out_activity[i]` 接收对应活动度。任一数组为 NULL 仅当两者同时为 NULL(长度查询)时允许。 + +参数: + +- `ptr` (`const struct CCircuitLayoutAnalysis*`):线路布局分析。 +- `out_qubits` (`uint32_t*`):接收逻辑比特 ID。 +- `out_activity` (`double*`):接收活动度权重。 +- `len` (`uintptr_t`):缓冲区容量(项数);值较小时只复制前 `len` 项。 + +返回:总项数;`ptr` 为 NULL 时返回 0。 + +### layout_result_analysis(ptr) + +把 SABRE 预备线路中捕获的线路布局分析提取为独立的 `CCircuitLayoutAnalysis*`(与 `analyze_circuit_for_layout` 返回相同的句柄类型),用 `circuit_layout_analysis_free` 释放。 + +参数: + +- `ptr` (`const struct CPreparedSabreCircuit*`):SABRE 预备线路。 + +返回:成功返回新建的 `CCircuitLayoutAnalysis*`;输入为 NULL 时返回 NULL。 + +--- + +## 示例 + +```c +#include +#include "cqlib_c.h" + +int main(void) { + /* 逻辑 {0, 1} 按序映射到物理 {10, 11},物理总数 3(12 空闲) */ + uint32_t logical[2] = {0, 1}; + uint32_t physical[3] = {10, 11, 12}; + struct CLayout *layout = layout_new(logical, 2, physical, 3); + if (layout == NULL) { + return 1; + } + + uintptr_t n_logical = layout_num_logical(layout); /* 2 */ + uintptr_t n_physical = layout_num_physical(layout); /* 3 */ + uintptr_t n_vacant = layout_num_vacant_physical(layout); /* 1 */ + + uint32_t p = layout_get(layout, 0); /* 10 */ + uint32_t l = layout_get_logical(layout, 10); /* 0 */ + uint32_t vacant_l = layout_get_logical(layout, 12); /* UINT32_MAX(空闲) */ + int32_t is_vacant = layout_is_physical_vacant(layout, 12); /* 1 */ + + /* 整体读取 */ + uintptr_t pairs_len = layout_l2p_map_len(layout); /* 2 */ + uint32_t pairs[4]; + layout_l2p_map(layout, pairs, pairs_len); /* {0,10, 1,11} */ + + /* 交换物理 11 与 12:逻辑 1 移到 12 */ + layout_swap_physical(layout, 11, 12); + + /* 解绑逻辑 0:释放物理 10 */ + uint32_t freed = 0; + layout_unbind(layout, 0, &freed); /* 0,freed == 10 */ + + /* 重新绑定到空闲比特 11 */ + int32_t rc = layout_bind(layout, 0, 11); /* 0 */ + + /* 布局结果辅助 */ + struct CDevice *dev = device_line("mock_backend", 3); + struct CPhysicalLayoutGraph *graph = physical_layout_graph_from_device(dev); + uintptr_t n_dist = layout_result_distances(graph, NULL, 0); /* 9(3x3) */ + uint32_t dist[9]; + layout_result_distances(graph, dist, n_dist); + /* dist == {0,1,2, 1,0,1, 2,1,0},行优先 */ + + struct CCircuit *circuit = circuit_new(3); + circuit_cx(circuit, 0, 1); + circuit_cx(circuit, 0, 1); + circuit_cx(circuit, 1, 2); + + struct CCircuitLayoutAnalysis *analysis = analyze_circuit_for_layout(circuit); + uintptr_t n_act = layout_result_logical_activity(analysis, NULL, NULL, 0); /* 3 */ + uint32_t lq[3]; + double act[3]; + layout_result_logical_activity(analysis, lq, act, n_act); + /* lq == {0,1,2},act == {2.0, 3.0, 1.0} */ + circuit_layout_analysis_free(analysis); + + /* 复用 SABRE 预备线路中捕获的分析 */ + struct CPreparedSabreCircuit *prepared = prepare_sabre_circuit(circuit); + struct CCircuitLayoutAnalysis *reused = layout_result_analysis(prepared); + circuit_layout_analysis_free(reused); + prepared_sabre_circuit_free(prepared); + + circuit_free(circuit); + physical_layout_graph_free(graph); + device_free(dev); + + layout_free(layout); + return 0; +} +``` diff --git a/docs/api/cn/c/2_device/4_noise.md b/docs/api/cn/c/2_device/4_noise.md new file mode 100644 index 00000000..80789edc --- /dev/null +++ b/docs/api/cn/c/2_device/4_noise.md @@ -0,0 +1,254 @@ +# NoiseModel(C) + +`CNoiseModel` 是噪声模型的不透明句柄,按「标准门 + 比特」组合登记噪声信道,按比特登记读出误差;同一个门与比特组合可以登记多条信道。错误码、字符串与句柄的释放约定遵循 [Overview](../0_overview.md)。 + +--- + +## 通道常量 + +单比特通道标签(用于 `noise_model_add_single_qubit` 的 `noise_type` 与 `CSingleQubitNoise.tag`): + +| 常量 | 值 | 通道 | 参数含义 | +| --- | --- | --- | --- | +| `NOISE_BIT_FLIP` | 0 | 比特翻转 | 翻转概率 p | +| `NOISE_PHASE_FLIP` | 1 | 相位翻转 | 翻转概率 p | +| `NOISE_DEPOLARIZING` | 2 | 去极化 | 去极化参数 p | +| `NOISE_AMPLITUDE_DAMPING` | 3 | 振幅阻尼 | 阻尼参数 gamma | +| `NOISE_PHASE_DAMPING` | 4 | 相位阻尼 | 散射概率 | +| `NOISE_PAULI` | 5 | Pauli 信道 | 概率 `px`/`py`/`pz`(和 ≤ 1) | + +双比特通道标签(`NOISE_TWO_DEPOLARIZING` 用于 `noise_model_add_two_qubit`,全部用于 `CTwoQubitNoise.tag`): + +| 常量 | 值 | 通道 | 参数含义 | +| --- | --- | --- | --- | +| `NOISE_TWO_DEPOLARIZING` | 0 | 去极化 | 去极化参数 p | +| `NOISE_TWO_INDEPENDENT` | 1 | 独立信道组合 | q0/q1 各自的单比特信道 | +| `NOISE_TWO_CORRELATED_PAULI` | 2 | 关联 Pauli | 概率 p + `NOISE_PAULI_OP_*` 算子 | + +关联 Pauli 通道的算子标签(`CTwoQubitNoise.op_q0` / `op_q1`):`NOISE_PAULI_OP_I` = 0、`NOISE_PAULI_OP_X` = 1、`NOISE_PAULI_OP_Y` = 2、`NOISE_PAULI_OP_Z` = 3。 + +--- + +## 快照结构体 + +### CSingleQubitNoise + +单比特噪声信道快照,由 `noise_model_get_single_qubit_errors` 写出。`tag` 选取信道;`p` 承载标签 0–4 的参数,`px`/`py`/`pz` 承载标签 5 的 Pauli 概率;不适用的字段为零。 + +| 字段 | 类型 | 含义 | +| --- | --- | --- | +| `tag` | `uint8_t` | 单比特 `NOISE_*` 标签(0–5)。 | +| `p` | `double` | 标签 0–4 的信道概率。 | +| `px` | `double` | 标签 5 的 Pauli-X 概率。 | +| `py` | `double` | 标签 5 的 Pauli-Y 概率。 | +| `pz` | `double` | 标签 5 的 Pauli-Z 概率。 | + +### CTwoQubitNoise + +双比特噪声信道快照,由 `noise_model_get_two_qubit_errors` 写出。不适用的字段为零。 + +| 字段 | 类型 | 含义 | +| --- | --- | --- | +| `tag` | `uint8_t` | `NOISE_TWO_*` 标签。 | +| `p` | `double` | 去极化与关联 Pauli 标签的信道概率。 | +| `q0_noise` | `struct CSingleQubitNoise` | 独立标签下作用于 q0 的信道。 | +| `q1_noise` | `struct CSingleQubitNoise` | 独立标签下作用于 q1 的信道。 | +| `op_q0` | `uint8_t` | 关联标签下作用于 q0 的 Pauli 算子(`NOISE_PAULI_OP_*`)。 | +| `op_q1` | `uint8_t` | 关联标签下作用于 q1 的 Pauli 算子(`NOISE_PAULI_OP_*`)。 | + +--- + +## 构造与释放 + +### noise_model_new() + +创建一个空的噪声模型。 + +返回:新建的 `CNoiseModel*`。 + +### noise_model_free(ptr) + +释放 `CNoiseModel`,允许传 NULL。 + +--- + +## 登记信道 + +### noise_model_add_single_qubit(ptr, gate_name, qubit, noise_type, p) + +向标准门 `gate_name` 在 `qubit` 上登记一条单比特噪声信道。 + +参数: + +- `gate_name` (`const char*`):标准门名。 +- `qubit` (`uint32_t`):比特 ID。 +- `noise_type` (`uint8_t`):`NOISE_*` 单比特标签(0–4)。 +- `p` (`double`):信道参数。 + +返回:0 成功;-1 空指针;-4 门名未知或非法 UTF-8;-8 噪声标签未知或概率非法。 + +### noise_model_add_single_qubit_pauli(ptr, gate_name, qubit, px, py, pz) + +向标准门 `gate_name` 在 `qubit` 上登记一条一般单比特 Pauli 信道,X/Y/Z 概率为 `px`、`py`、`pz`(各自 ≥ 0,和 ≤ 1)。 + +参数: + +- `px` / `py` / `pz` (`double`):Pauli-X/Y/Z 概率。 + +返回:0 成功;-1 空指针;-4 门名未知;-8 概率非法。 + +### noise_model_add_two_qubit(ptr, gate_name, q0, q1, noise_type, p) + +向标准门 `gate_name` 在有序比特对 `(q0, q1)` 上登记一条双比特去极化信道。`noise_type` 只接受 `NOISE_TWO_DEPOLARIZING`(0)。 + +参数: + +- `q0` / `q1` (`uint32_t`):比特 ID。 +- `noise_type` (`uint8_t`):`NOISE_TWO_DEPOLARIZING`。 +- `p` (`double`):去极化参数。 + +返回:0 成功;-1 空指针;-4 门名未知;-8 比特相同或概率非法。 + +### noise_model_add_readout(ptr, qubit, p_0_given_1, p_1_given_0) + +为 `qubit` 登记非对称读出误差。 + +参数: + +- `qubit` (`uint32_t`):比特 ID。 +- `p_0_given_1` (`double`):P(真值为 1 测得 0)。 +- `p_1_given_0` (`double`):P(真值为 0 测得 1)。 + +返回:0 成功;-8 概率非法。 + +--- + +## 查询信道 + +### noise_model_get_single_qubit_errors_len(ptr, gate_name, qubit) + +返回标准门 `gate_name` 在 `qubit` 上登记的单比特信道数。 + +返回:总数;`ptr` 为 NULL、门名未知或无记录时为 0。 + +### noise_model_get_single_qubit_errors(ptr, gate_name, qubit, out, len) + +两步式输出上述信道,与 `noise_model_get_single_qubit_errors_len` 配对;每个条目是一个 `CSingleQubitNoise` 快照,有效字段取决于其 `tag`。返回总数。 + +### noise_model_get_two_qubit_errors_len(ptr, gate_name, q0, q1) + +返回标准门 `gate_name` 在有序比特对 `(q0, q1)` 上登记的双比特信道数。 + +返回:总数;`ptr` 为 NULL、门名未知、比特相同或无记录时为 0。 + +### noise_model_get_two_qubit_errors(ptr, gate_name, q0, q1, out, len) + +两步式输出上述信道,与 `noise_model_get_two_qubit_errors_len` 配对;每个条目是一个 `CTwoQubitNoise` 快照,有效字段取决于其 `tag`。返回总数。 + +### noise_model_get_readout_error(ptr, qubit, out_p_0_given_1, out_p_1_given_0) + +写出 `qubit` 登记的读出误差。 + +参数: + +- `out_p_0_given_1` (`double*`):写出 P(真值为 1 测得 0)。 +- `out_p_1_given_0` (`double*`):写出 P(真值为 0 测得 1)。 + +返回:0 成功;-1 空指针;-8 该比特没有登记读出误差。 + +--- + +## 信道有效性与 Kraus 算子 + +以下接口按 `(gate_name, qubit, index)` 键寻址一条已登记的单比特信道;`index` 的上界为 `noise_model_get_single_qubit_errors_len` 的返回值。Kraus 算子以 `Complex64` 的 re/im 对传输(`cqlib_c.h` 中定义的 `struct Complex64`,含 `re` / `im` 字段)。 + +### noise_model_is_valid(ptr, gate_name, qubit, index) + +报告标准门 `gate_name` 在 `qubit` 上、下标为 `index` 的单比特信道是否携带合法的噪声参数。 + +参数: + +- `gate_name` (`const char*`):标准门名。 +- `qubit` (`uint32_t`):比特 ID。 +- `index` (`uintptr_t`):信道下标。 + +返回:1 合法;0 不合法;-1 空指针;-4 门名未知;-8 该键/下标处没有信道。 + +### noise_model_to_kraus_len(ptr, gate_name, qubit, index) + +返回该信道全部 Kraus 算子的 `Complex64` 元素总数;每个算子贡献 `dim * dim` 个元素(单比特算子为 `2 * 2 = 4`)。 + +返回:总元素数;`ptr` 为 NULL、门名未知或键/下标无记录时返回 0。 + +### noise_model_to_kraus(ptr, gate_name, qubit, index, out, len) + +两步式输出 Kraus 算子,与 `noise_model_to_kraus_len` 配对:算子按顺序连续写出,每个算子按行优先排列为 `dim * dim` 个 `Complex64` re/im 对。 + +参数: + +- `out` (`Complex64*`):接收元素;传 NULL 仅查询长度。 +- `len` (`uintptr_t`):缓冲区容量(元素数);值较小时只复制前 `len` 个元素。 + +返回:总元素数;`ptr` 为 NULL、门名未知或键/下标无记录时返回 0。 + +--- + +## 示例 + +```c +#include +#include "cqlib_c.h" + +int main(void) { + struct CNoiseModel *nm = noise_model_new(); + + /* 单比特信道:X 门上的比特翻转 */ + int32_t rc = noise_model_add_single_qubit(nm, "X", 0, NOISE_BIT_FLIP, 0.005); + + /* 单比特 Pauli 信道 */ + rc = noise_model_add_single_qubit_pauli(nm, "X", 0, 0.001, 0.002, 0.003); + + /* 双比特去极化信道:CX 门 (0, 1) */ + rc = noise_model_add_two_qubit(nm, "CX", 0, 1, NOISE_TWO_DEPOLARIZING, 0.02); + + /* 读出误差 */ + rc = noise_model_add_readout(nm, 0, 0.02, 0.01); + + /* 查询单比特信道 */ + uintptr_t n = noise_model_get_single_qubit_errors_len(nm, "X", 0); /* 2 */ + struct CSingleQubitNoise chans[2]; + noise_model_get_single_qubit_errors(nm, "X", 0, chans, n); + /* chans[0]: tag == NOISE_BIT_FLIP, p == 0.005 + chans[1]: tag == NOISE_PAULI, px/py/pz == 0.001/0.002/0.003 */ + + /* 查询双比特信道 */ + uintptr_t m = noise_model_get_two_qubit_errors_len(nm, "CX", 0, 1); /* 1 */ + struct CTwoQubitNoise tchans[1]; + noise_model_get_two_qubit_errors(nm, "CX", 0, 1, tchans, m); + /* tchans[0]: tag == NOISE_TWO_DEPOLARIZING, p == 0.02 */ + + /* 查询读出误差 */ + double p01 = 0.0, p10 = 0.0; + rc = noise_model_get_readout_error(nm, 0, &p01, &p10); /* 0,0.02 / 0.01 */ + + /* 信道有效性与 Kraus 算子 */ + rc = noise_model_add_single_qubit(nm, "H", 0, NOISE_DEPOLARIZING, 0.1); + int32_t valid = noise_model_is_valid(nm, "H", 0, 0); /* 1 */ + uintptr_t klen = noise_model_to_kraus_len(nm, "H", 0, 0); /* 16:四个 2x2 算子 */ + Complex64 kraus[16]; + noise_model_to_kraus(nm, "H", 0, 0, kraus, klen); + /* kraus[0..4]:I*sqrt(0.9) 按行优先(kraus[0].re == sqrt(0.9)) + kraus[4..16]:X、Y、Z,缩放 sqrt(0.1/3) */ + + /* X@0 上的振幅阻尼,下标 2(比特翻转与 Pauli 之后) */ + rc = noise_model_add_single_qubit(nm, "X", 0, NOISE_AMPLITUDE_DAMPING, 0.25); + klen = noise_model_to_kraus_len(nm, "X", 0, 2); /* 8:两个 2x2 算子 */ + + /* 错误码 */ + int32_t absent = noise_model_is_valid(nm, "H", 1, 0); /* -8:该键处没有信道 */ + int32_t unknown = noise_model_is_valid(nm, "nope", 0, 0); /* -4:门名未知 */ + + noise_model_free(nm); + return 0; +} +``` diff --git a/docs/api/cn/c/2_device/5_result.md b/docs/api/cn/c/2_device/5_result.md new file mode 100644 index 00000000..a92e1878 --- /dev/null +++ b/docs/api/cn/c/2_device/5_result.md @@ -0,0 +1,304 @@ +# ExecutionResult(C) + +`CExecutionResult` 是任务执行结果的不透明句柄,记录一次任务的生命周期、测量计数与概率分布;`CCountsList` 是「比特串 → 权重」的键值列表。错误码、字符串与句柄的释放约定遵循 [Overview](../0_overview.md)。 + +`qubits` 数组的第 `i` 项对应结果键的第 `i` 位;比特串按最高位在左书写,与 `qubits` 顺序相反。 + +--- + +## 状态常量 + +`execution_result_status` 写出的标签: + +| 常量 | 值 | 含义 | +| --- | --- | --- | +| `EXECUTION_STATUS_QUEUED` | 0 | 任务已提交并排队。 | +| `EXECUTION_STATUS_RUNNING` | 1 | 任务正在运行。 | +| `EXECUTION_STATUS_COMPLETED` | 2 | 任务成功完成。 | +| `EXECUTION_STATUS_FAILED` | 3 | 任务失败(细节查询错误码与错误消息)。 | +| `EXECUTION_STATUS_CANCELLED` | 4 | 任务被用户取消。 | + +--- + +## 构造与释放 + +### execution_result_new(task_id, qubits, num_qubits, shots, backend) + +创建一个处于 Queued 状态的执行结果;创建时间戳取当前时间。 + +参数: + +- `task_id` (`const char*`):任务 ID。 +- `qubits` (`const uint32_t*`):被测比特 ID 数组,第 `i` 项对应结果键的第 `i` 位。 +- `num_qubits` (`uintptr_t`):被测比特数。 +- `shots` (`uintptr_t`):shot 数。 +- `backend` (`const char*`):后端名,可为 NULL。 + +返回:成功返回新建的 `CExecutionResult*`;失败返回 NULL。 + +### execution_result_from_counts(task_id, num_qubits, shots, bitstrings, counts, num_entries) + +由测量计数直接构造一个已完成(Completed)的结果,被测比特为 `0..num_qubits-1`。 + +参数: + +- `task_id` (`const char*`):任务 ID。 +- `num_qubits` (`uintptr_t`):被测比特数,每个比特串必须恰好有 `num_qubits` 个字符。 +- `shots` (`uintptr_t`):shot 数。 +- `bitstrings` (`const char *const*`):比特串数组,与 `counts` 平行。 +- `counts` (`const uint64_t*`):计数数组,与 `bitstrings` 平行。 +- `num_entries` (`uintptr_t`):条目数。 + +返回:成功返回新建的 `CExecutionResult*`;失败返回 NULL。 + +### execution_result_free(ptr) + +释放 `CExecutionResult`,允许传 NULL。 + +--- + +## 生命周期 + +### execution_result_start(ptr) + +把任务标记为运行中,并把当前时间记录为开始时间戳。 + +返回:0 成功;-1 空指针。 + +### execution_result_finish(ptr, bitstrings, counts, num_entries) + +把任务标记为已完成并替换测量计数;`bitstrings` 与 `counts` 为平行数组,重复的比特串会累加;完成时间戳取当前时间。 + +参数: + +- `bitstrings` (`const char *const*`):比特串数组。 +- `counts` (`const uint64_t*`):计数数组。 +- `num_entries` (`uintptr_t`):条目数。 + +返回:0 成功;-1 空指针;-4 比特串非法(长度或字符)。 + +### execution_result_fail(ptr, msg, code) + +把任务标记为失败,并记录错误消息与错误码。 + +参数: + +- `msg` (`const char*`):错误消息。 +- `code` (`int32_t`):错误码。 + +返回:0 成功;-1 空指针;-4 消息不是合法 UTF-8。 + +### execution_result_cancel(ptr) + +把任务标记为已取消。 + +返回:0 成功;-1 空指针。 + +--- + +## 状态与时间戳 + +### execution_result_status(ptr, out_tag, out_error_code) + +写出执行状态标签(`EXECUTION_STATUS_*` 之一)。状态为失败时,`*out_error_code` 收到保存的错误码(否则为 0);不需要错误码时可传 NULL。 + +参数: + +- `out_tag` (`uint8_t*`):写出状态标签。 +- `out_error_code` (`int32_t*`):写出错误码,可为 NULL。 + +返回:0 成功;-1 空指针。 + +### execution_result_is_success(ptr) + +返回执行是否成功完成。 + +返回:1 成功;0 否;-1 空指针。 + +### execution_result_is_terminal(ptr) + +返回执行是否已到达终态(已完成、失败或已取消);Queued 与 Running 不是终态。 + +返回:1 已到终态;0 否;-1 空指针。 + +### execution_result_error_message(ptr) + +返回失败状态保存的错误消息。 + +返回:成功返回堆上 C 字符串(用 `cqlib_string_free` 释放);输入为 NULL 或状态不是失败时返回 NULL。 + +### execution_result_task_id(ptr) + +返回任务 ID。 + +返回:成功返回堆上 C 字符串(用 `cqlib_string_free` 释放);失败返回 NULL。 + +### execution_result_backend(ptr) + +返回后端名。 + +返回:成功返回堆上 C 字符串(用 `cqlib_string_free` 释放);失败或未设置后端时返回 NULL。 + +### execution_result_created_at(ptr, out_ms) + +写出任务创建时间戳(Unix 纪元起的毫秒数)。 + +返回:0 并写入 `*out_ms`;-1 空指针。 + +### execution_result_started_at(ptr, out_ms) + +写出执行开始时间戳(Unix 纪元起的毫秒数)。 + +返回:0 并写入 `*out_ms`;-8 任务尚未开始。 + +### execution_result_finished_at(ptr, out_ms) + +写出执行完成时间戳(Unix 纪元起的毫秒数)。 + +返回:0 并写入 `*out_ms`;-8 任务尚未结束。 + +--- + +## 测量结果 + +### execution_result_shots(ptr) + +返回 shot 数;`ptr` 为 NULL 时返回 0。 + +### execution_result_num_qubits(ptr) + +返回被测比特数;`ptr` 为 NULL 时返回 0。 + +### execution_result_qubits_len(ptr) / execution_result_qubits(ptr, out, len) + +两步式输出被测比特 ID,第 `i` 项对应结果键的第 `i` 位;填充函数返回总数。 + +### execution_result_to_bitstring(ptr, outcome) + +把原始结果掩码格式化为按被测比特宽度书写的大端比特串:`outcome` 的第 `i` 位对应被测比特 `i`,超出宽度的高位被截断。 + +参数: + +- `outcome` (`uint64_t`):原始结果掩码。 + +返回:成功返回堆上 C 字符串(用 `cqlib_string_free` 释放);输入为 NULL 时返回 NULL。 + +### execution_result_counts(ptr) + +返回测量计数。 + +返回:成功返回新建的 `CCountsList*`(比特串为键、整数计数以 f64 为权重),用 `counts_list_free` 释放;失败返回 NULL。 + +### execution_result_probabilities(ptr) + +返回概率分布(需要时先按计数归一化计算)。 + +返回:成功返回新建的 `CCountsList*`(比特串为键、f64 概率为权重),用 `counts_list_free` 释放;失败返回 NULL。 + +### execution_result_calc_probabilities_len(ptr) / execution_result_calc_probabilities(ptr, out, len) + +两步式概率分布:`calc_probabilities_len` 计算分布(结果缓存在对象上)并返回条目数;`calc_probabilities` 重新计算并把概率复制到 `out`。条目按结果值升序排列,结果值的第 `i` 位对应被测比特 `i`(与 `execution_result_to_bitstring` 一致)。两者都接收可变句柄;`len` 较小时只复制前 `len` 项。填充函数返回总条目数;`ptr` 为 NULL 时返回 0。 + +--- + +## CCountsList + +`execution_result_counts` 与 `execution_result_probabilities` 的返回值:键为测量结果比特串,前者权重为计数,后者为概率。 + +### counts_list_len(ptr) + +返回列表条目数;`ptr` 为 NULL 时返回 0。 + +### counts_list_get_key(ptr, index) + +返回下标为 `index` 的比特串键。 + +返回:成功返回堆上 C 字符串(用 `cqlib_string_free` 释放);越界返回 NULL。 + +### counts_list_get_value(ptr, index) + +返回下标为 `index` 的权重(计数或概率);出错返回 NaN。 + +### counts_list_free(ptr) + +释放 `CCountsList`,允许传 NULL。 + +--- + +## 示例 + +```c +#include +#include "cqlib_c.h" + +int main(void) { + /* 路径一:由计数直接构造已完成的结果 */ + const char *bits[2] = {"00", "11"}; + uint64_t cnts[2] = {750, 250}; + + struct CExecutionResult *r = execution_result_from_counts( + "task-1", 2, 1000, bits, cnts, 2); + if (r == NULL) { + return 1; + } + + uint8_t status = 255; + int32_t err_code = 0; + execution_result_status(r, &status, &err_code); /* EXECUTION_STATUS_COMPLETED */ + + struct CCountsList *counts = execution_result_counts(r); + if (counts != NULL) { + for (uintptr_t i = 0; i < counts_list_len(counts); i++) { + char *key = counts_list_get_key(counts, i); + double w = counts_list_get_value(counts, i); + printf("%s: %f\n", key, w); /* "00": 750, "11": 250 */ + cqlib_string_free(key); + } + counts_list_free(counts); + } + + struct CCountsList *probs = execution_result_probabilities(r); + /* 同样遍历:权重为 0.75 / 0.25 */ + counts_list_free(probs); + + /* 状态标记、比特串与两步式概率缓冲区 */ + int32_t ok = execution_result_is_success(r); /* 1 */ + int32_t done = execution_result_is_terminal(r); /* 1 */ + + char *bs = execution_result_to_bitstring(r, 0b01); /* "01" */ + char *bs2 = execution_result_to_bitstring(r, 0b101); /* "11":第 2 位超出宽度,被截断 */ + cqlib_string_free(bs); + cqlib_string_free(bs2); + + uintptr_t n_probs = execution_result_calc_probabilities_len(r); /* 2 */ + double pvals[2]; + execution_result_calc_probabilities(r, pvals, n_probs); + /* 结果值升序:pvals[0] == 0.75("00"),pvals[1] == 0.25("11") */ + + execution_result_free(r); + + /* 路径二:完整生命周期 */ + uint32_t qubits[2] = {0, 1}; + struct CExecutionResult *r2 = execution_result_new( + "task-2", qubits, 2, 1000, "sim"); + if (r2 == NULL) { + return 1; + } + + execution_result_start(r2); /* Queued -> Running */ + execution_result_finish(r2, bits, cnts, 2); /* Running -> Completed */ + + char *task = execution_result_task_id(r2); /* "task-2" */ + char *backend = execution_result_backend(r2); /* "sim" */ + cqlib_string_free(task); + cqlib_string_free(backend); + + int64_t created = 0, started = 0, finished = 0; + execution_result_created_at(r2, &created); + execution_result_started_at(r2, &started); + execution_result_finished_at(r2, &finished); + + execution_result_free(r2); + return 0; +} +``` diff --git a/docs/api/cn/c/2_device/6_qubits.md b/docs/api/cn/c/2_device/6_qubits.md new file mode 100644 index 00000000..25e65566 --- /dev/null +++ b/docs/api/cn/c/2_device/6_qubits.md @@ -0,0 +1,214 @@ +# 量子比特标识(C) + +本页覆盖设备侧强类型比特标识 `CLogicalQubit` 与 `CPhysicalQubit` 的 C ABI:前者标识线路中的逻辑比特,后者标识设备上的物理比特。两个类型内部表示相同(一个 `uint32_t` 编号),但在类型层面互相区分;两者都只与 `CQubit` 互转,互相之间不能直接转换,编号相同也不代表是同一标识。错误码与字符串释放约定遵循 [Overview](../0_overview.md)。 + +--- + +## 结构体 + +两个类型都按值传递,无需释放: + +```c +typedef struct CLogicalQubit { + uint32_t id; +} CLogicalQubit; + +typedef struct CPhysicalQubit { + uint32_t id; +} CPhysicalQubit; +``` + +| 字段 | 类型 | 含义 | +| --- | --- | --- | +| `id` | `uint32_t` | 数值标识。 | + +--- + +## CLogicalQubit + +逻辑比特标识,用于线路中的逻辑比特。 + +### logical_qubit_new(id) + +```c +struct CLogicalQubit logical_qubit_new(uint32_t id); +``` + +从数值标识构造,按值返回。 + +- `id` (`uint32_t`):数值标识。 + +### logical_qubit_from_qubit(qubit) + +```c +struct CLogicalQubit logical_qubit_from_qubit(struct CQubit qubit); +``` + +包装既有的线路比特。 + +- `qubit` (`struct CQubit`):线路比特标识。 + +### logical_qubit_qubit(qubit) + +```c +struct CQubit logical_qubit_qubit(struct CLogicalQubit qubit); +``` + +返回底层线路比特。 + +- `qubit` (`struct CLogicalQubit`):逻辑比特标识。 + +### logical_qubit_id(qubit) + +```c +uint32_t logical_qubit_id(struct CLogicalQubit qubit); +``` + +返回数值标识。 + +- `qubit` (`struct CLogicalQubit`):逻辑比特标识。 + +### logical_qubit_equal(a, b) / logical_qubit_compare(a, b) + +```c +int32_t logical_qubit_equal(struct CLogicalQubit a, struct CLogicalQubit b); +int32_t logical_qubit_compare(struct CLogicalQubit a, struct CLogicalQubit b); +``` + +比较与排序基于数值标识:`equal` 返回 `1`(相同)或 `0`(不同);`compare` 返回 `-1`(`a < b`)、`0`(相等)或 `1`(`a > b`)。 + +- `a`、`b` (`struct CLogicalQubit`):待比较的标识。 + +### logical_qubit_to_string(qubit) + +```c +char *logical_qubit_to_string(struct CLogicalQubit qubit); +``` + +格式化为 `L`(如 `L3`)。返回的字符串用 [cqlib_string_free](../0_overview.md) 释放;分配失败时返回 NULL。 + +- `qubit` (`struct CLogicalQubit`):逻辑比特标识。 + +--- + +## CPhysicalQubit + +物理比特标识,用于设备上的比特位置。 + +### physical_qubit_new(id) + +```c +struct CPhysicalQubit physical_qubit_new(uint32_t id); +``` + +从数值标识构造,按值返回。 + +- `id` (`uint32_t`):数值标识。 + +### physical_qubit_from_qubit(qubit) + +```c +struct CPhysicalQubit physical_qubit_from_qubit(struct CQubit qubit); +``` + +包装既有比特形式的标识。 + +- `qubit` (`struct CQubit`):比特形式的标识。 + +### physical_qubit_qubit(qubit) + +```c +struct CQubit physical_qubit_qubit(struct CPhysicalQubit qubit); +``` + +返回底层比特标识。 + +- `qubit` (`struct CPhysicalQubit`):物理比特标识。 + +### physical_qubit_id(qubit) + +```c +uint32_t physical_qubit_id(struct CPhysicalQubit qubit); +``` + +返回数值标识。 + +- `qubit` (`struct CPhysicalQubit`):物理比特标识。 + +### physical_qubit_equal(a, b) / physical_qubit_compare(a, b) + +```c +int32_t physical_qubit_equal(struct CPhysicalQubit a, struct CPhysicalQubit b); +int32_t physical_qubit_compare(struct CPhysicalQubit a, struct CPhysicalQubit b); +``` + +比较与排序基于数值标识:`equal` 返回 `1`(相同)或 `0`(不同);`compare` 返回 `-1`(`a < b`)、`0`(相等)或 `1`(`a > b`)。 + +- `a`、`b` (`struct CPhysicalQubit`):待比较的标识。 + +### physical_qubit_to_string(qubit) + +```c +char *physical_qubit_to_string(struct CPhysicalQubit qubit); +``` + +格式化为 `P`(如 `P11`)。返回的字符串用 [cqlib_string_free](../0_overview.md) 释放;分配失败时返回 NULL。 + +- `qubit` (`struct CPhysicalQubit`):物理比特标识。 + +--- + +## 错误码 + +本页全部接口都不返回错误码:构造、访问与比较不失败;`*_to_string` 仅在分配失败时返回 NULL。 + +--- + +## 示例 + +```c +#include +#include +#include +#include + +int main(void) { + /* 线路比特包装为逻辑标识,并可还原 */ + CLogicalQubit logical = logical_qubit_from_qubit(qubit_new(3)); + assert(logical_qubit_id(logical) == 3u); + assert(qubit_id(logical_qubit_qubit(logical)) == 3u); + + /* 物理标识独立构造 */ + CPhysicalQubit physical = physical_qubit_new(100); + assert(physical_qubit_id(physical) == 100u); + + /* 比较:逻辑侧 L2 < L0 不成立,L0 < L2 成立 */ + assert(logical_qubit_compare(logical_qubit_new(0), logical_qubit_new(2)) == -1); + assert(physical_qubit_compare(physical, physical_qubit_new(101)) == -1); + + /* 字符串表示 */ + char *ltext = logical_qubit_to_string(logical); + assert(strcmp(ltext, "L3") == 0); + cqlib_string_free(ltext); + + char *ptext = physical_qubit_to_string(physical); + assert(strcmp(ptext, "P100") == 0); + cqlib_string_free(ptext); + + /* 布局场景:逻辑 1 -> 物理 102 的映射用编号登记与查询 */ + const uint32_t pairs[2] = {1u, 102u}; + CLayout *layout = layout_from_pairs(pairs, 1, 3); + assert(layout != NULL); + assert(layout_get(layout, 1) == physical_qubit_id(physical_qubit_new(102))); + layout_free(layout); + return 0; +} +``` + +--- + +## 相关页面 + +- [量子比特](../0_circuit/2_qubit.md):`CQubit` 线路比特标识与 `CQubit` 互转的入口。 +- [Layout](3_layout.md):逻辑↔物理映射的绑定与查询。 +- [Overview](../0_overview.md):错误码、内存所有权与全局约定。 diff --git a/docs/api/cn/c/3_qis/0_overview.md b/docs/api/cn/c/3_qis/0_overview.md new file mode 100644 index 00000000..18e0c9fe --- /dev/null +++ b/docs/api/cn/c/3_qis/0_overview.md @@ -0,0 +1,86 @@ +# QIS 量子信息(C) + +C 绑定的量子信息接口以不透明句柄(`CStatevector`、`CDensityMatrix`、`CDensityMatrixNoise`、`CStabilizerState` 等)加自由函数的形式提供本地量子模拟:态的构造与演化、门操作、概率与测量、期望值以及噪声仿真。错误码、句柄与字符串的释放约定遵循 [Overview](../0_overview.md)。 + +--- + +## 三个稠密态模拟器 + +| 模拟器 | 态表示 | 数据规模 | 噪声支持 | 读出噪声 | 适用场景 | +| --- | --- | --- | --- | --- | --- | +| [Statevector](1_statevector.md) | 纯态 | `2^N` 个复振幅 | 无 | 无 | 理想通用线路,内存占用最小 | +| [DensityMatrix](2_density_matrix.md) | 混合态 | `4^N` 个矩阵元 | Kraus 算符信道 | 无 | 混合态、量子信道与偏迹 | +| [DensityMatrixNoise](3_density_matrix_noise.md) | 混合态 | `4^N` 个矩阵元 | 完整噪声模型(门噪声自动施加) | 有 | 设备级含噪声仿真 | + +三个模拟器共享同一组门函数,命名规律为 `<模拟器>_apply_<门名>`: + +```c +statevector_apply_h(ptr, qubit); +density_matrix_apply_h(ptr, qubit); +density_matrix_noise_apply_h(ptr, qubit); +``` + +门集合覆盖:无参数单比特门 H、X、Y、Z、S、S†、T、T†、X2P、X2M、Y2P、Y2M;参数化单比特门 Rx、Ry、Rz、P、U、XY、XY2P、XY2M、RXY、GPhase;双比特门 CX、CY、CZ、SWAP、RXX、RYY、RZZ、RZX、CrX、CrY、CrZ、FSIM;三比特门 CCX。每个模拟器还提供按名称分派的 `apply_standard_gate` 族与整线路入口 `apply_circuit`;Statevector 与 DensityMatrixNoise 另有任意酉矩阵入口 `apply_unitary_gate`。 + +三个模拟器都与线路句柄对接:`*_from_circuit` 执行一条线路并返回演化后的态;`*_apply_circuit` 把线路原地作用到已有态上,线路的比特数必须与态一致。线路句柄见 [Circuit](../0_circuit/1_circuit.md)。 + +--- + +## 共享约定 + +- **句柄生命周期**:`*_new`、`*_from_circuit`、`*_from_state`、`*_maximally_mixed` 等构造接口返回堆分配句柄,用对应的 `*_free` 释放;所有 `*_free` 允许传 `NULL`;构造失败返回 `NULL`。 +- **错误码**:返回 `int32_t` 的接口用 `0` 表示成功、负值表示错误;测量接口 `*_measure` 用 `0`/`1` 表示测量结果 `|0>`/`|1>`,负值仍是错误。QIS 接口的典型错误码:`-1`(NULL 指针)、`-2`(比特编号越界)、`-3`(线路错误)、`-7`(不支持的操作)、`-8`(非法参数:非有限角度、缓冲区长度不符、未归一化的输入态、比特数不匹配、未知门名)。 +- **两步式数组输出**:概率与矩阵数据先调用 `*_len` 接口查询元素数量,分配缓冲后调用填充接口(如 `statevector_probabilities_len` + `statevector_probabilities`、`density_matrix_data_len` + `density_matrix_data`);`len` 与实际数量不符时填充接口返回 `-8`。 +- **字符串**:`*_measure_all` 返回大端比特串(最高位是比特 `N-1`,最低位是比特 `0`),用 `cqlib_string_free` 释放。 +- **Complex64**:`{ double re; double im; }`,振幅与矩阵元素按行主序、实部虚部交错排列。 +- **采样结果**:`*_sample_shots` 返回 `COutcomeList*`,每个元素是一个大端比特串 `char*`;用 `outcome_list_len` 取数量、`outcome_list_get` 取元素(用 `cqlib_string_free` 释放)、`outcome_list_free` 释放列表,详见 [ClassicalState](5_classical_state.md)。 +- **观测量**:期望值接口接受 `CHamiltonian*` 句柄,构造方式见 [Hamiltonian](7_hamiltonian.md),其 Pauli 串成分见 [Pauli](6_pauli.md)。 + +--- + +## 页面导航 + +| 页面 | 内容 | +| --- | --- | +| [Statevector](1_statevector.md) | 纯态模拟器:振幅访问、门操作、概率、测量、采样与期望值。 | +| [DensityMatrix](2_density_matrix.md) | 混合态模拟器:矩阵数据、物理性校验、Kraus 信道、偏迹与期望值。 | +| [DensityMatrixNoise](3_density_matrix_noise.md) | 噪声模拟器:门噪声自动施加、含读出噪声的概率分布。 | +| [StabilizerState](4_stabilizer.md) | 稳定子态模拟器,支持大规模 Clifford 线路。 | +| [ClassicalState](5_classical_state.md) | 运行期经典数据与 `COutcomeList`。 | +| [Pauli](6_pauli.md) | Pauli 算子、Pauli 串及其矩阵表示。 | +| [Hamiltonian](7_hamiltonian.md) | 哈密顿量与可观测量。 | +| [Evolution](8_evolution.md) | Pauli 串演化门 `circuit_pauli_evolution`。 | +| [Metrics / Entropy](9_metrics_entropy.md) | 保真度、纯度、迹距离、熵与纠缠度量。 | + +--- + +## 示例 + +```c +#include +#include "cqlib_c.h" + +int main(void) { + /* Run the same Bell circuit on both simulators */ + struct CStatevector *sv = statevector_new(2); + struct CDensityMatrix *dm = density_matrix_new(2); + + statevector_apply_h(sv, 0); + statevector_apply_cx(sv, 0, 1); + density_matrix_apply_h(dm, 0); + density_matrix_apply_cx(dm, 0, 1); + + /* Pure and mixed representations agree: P(00) = P(11) = 0.5 */ + double p_sv[4], p_dm[4]; + statevector_probabilities(sv, p_sv, 4); + density_matrix_probabilities(dm, p_dm, 4); + printf("P(00): sv=%.3f dm=%.3f\n", p_sv[0], p_dm[0]); + printf("P(11): sv=%.3f dm=%.3f\n", p_sv[3], p_dm[3]); + + statevector_free(sv); + density_matrix_free(dm); + return 0; +} +``` + +噪声模型句柄 `CNoiseModel` 的构造与噪声登记见 [NoiseModel](../2_device/4_noise.md)。 diff --git a/docs/api/cn/c/3_qis/1_statevector.md b/docs/api/cn/c/3_qis/1_statevector.md new file mode 100644 index 00000000..fface258 --- /dev/null +++ b/docs/api/cn/c/3_qis/1_statevector.md @@ -0,0 +1,433 @@ +# Statevector(态矢量) + +`CStatevector*` 是纯态模拟器句柄:量子态表示为 `2^N` 个复振幅,全部操作通过 `statevector_*` 自由函数完成。本页覆盖构造与线路对接、振幅访问、门操作、概率与测量以及期望值。错误码与内存约定见 [Overview](../0_overview.md),模块总览见 [QIS 概览](0_overview.md)。 + +--- + +## 构造与释放 + +### statevector_new(num_qubits) + +```c +struct CStatevector *statevector_new(uintptr_t num_qubits); +``` + +创建纯态模拟器,初始态为 `|0...0>`:首分量为 1,其余为 0。 + +- `num_qubits` (`uintptr_t`):量子比特数量。 + +返回:新分配的 `CStatevector*`,用 `statevector_free` 释放。 + +### statevector_from_circuit(circuit) + +```c +struct CStatevector *statevector_from_circuit(const struct CCircuit *circuit); +``` + +执行线路并返回演化后的态,输入线路不被修改。线路先被分解为基础门再逐条执行;测量声明不参与状态演化。 + +- `circuit` (`const CCircuit*`):线路句柄,见 [Circuit](../0_circuit/1_circuit.md)。 + +返回:新分配的 `CStatevector*`;`circuit` 为 NULL 或执行失败返回 `NULL`。 + +### statevector_from_state(num_qubits, initial_state, len) + +```c +struct CStatevector *statevector_from_state(uintptr_t num_qubits, + const Complex64 *initial_state, + uintptr_t len); +``` + +用给定振幅数组构造态,要求 `len` 等于 `2^num_qubits` 且已归一化;下标 `i` 的分量对应计算基态 `|i>`,比特 0 对应最低位。 + +- `num_qubits` (`uintptr_t`):量子比特数量。 +- `initial_state` (`const Complex64*`):长度为 `2^num_qubits` 的振幅数组。 +- `len` (`uintptr_t`):数组长度。 + +返回:新分配的 `CStatevector*`;输入为 NULL、长度不符或未归一化时返回 `NULL`。 + +### statevector_free(ptr) + +```c +void statevector_free(struct CStatevector *ptr); +``` + +释放句柄。允许传 `NULL`。 + +- `ptr` (`CStatevector*`):态矢量句柄。 + +### statevector_num_qubits(ptr) + +```c +uintptr_t statevector_num_qubits(const struct CStatevector *ptr); +``` + +返回量子比特数量。 + +- `ptr` (`const CStatevector*`):态矢量句柄。 + +返回:比特数量;`NULL` 时返回 0。 + +### statevector_apply_circuit(ptr, circuit) + +```c +int32_t statevector_apply_circuit(struct CStatevector *ptr, const struct CCircuit *circuit); +``` + +把线路原地作用到当前态上,线路的比特数必须与态一致。 + +- `ptr` (`CStatevector*`):态矢量句柄。 +- `circuit` (`const CCircuit*`):线路句柄。 + +返回:`0` 成功;`-1` NULL 指针;`-3` 线路错误(门没有矩阵表示、符号参数未解析等);`-7` 线路含不支持的操作;`-8` 线路比特数与态不一致。 + +--- + +## 振幅访问 + +### statevector_data_len(ptr) + +```c +uintptr_t statevector_data_len(const struct CStatevector *ptr); +``` + +返回复振幅数量(`2^N`),用于为 `statevector_data` 分配缓冲。 + +- `ptr` (`const CStatevector*`):态矢量句柄。 + +返回:振幅数量;`NULL` 时返回 0。 + +### statevector_data(ptr, buffer, len) + +```c +int32_t statevector_data(const struct CStatevector *ptr, Complex64 *buffer, uintptr_t len); +``` + +把全部振幅按行主序拷贝进 `buffer`,共 `2^N` 个 `Complex64` 值。 + +- `ptr` (`const CStatevector*`):态矢量句柄。 +- `buffer` (`Complex64*`):输出缓冲。 +- `len` (`uintptr_t`):缓冲长度,必须等于 `statevector_data_len`。 + +返回:`0` 成功;`-1` NULL 指针;`-8` `len` 与实际数量不符。 + +```c +uintptr_t n = statevector_data_len(sv); +Complex64 *amps = malloc(n * sizeof(Complex64)); +if (statevector_data(sv, amps, n) == 0) { + /* amps[i] is the amplitude of |i> */ +} +free(amps); +``` + +### statevector_data_mut(ptr, buffer, len) + +```c +int32_t statevector_data_mut(struct CStatevector *ptr, const Complex64 *buffer, uintptr_t len); +``` + +用 `buffer` 中的 `len` 个 `Complex64` 值覆盖态矢量的全部振幅,是 `statevector_data` 的可写对应版本。 + +- `ptr` (`CStatevector*`):态矢量句柄。 +- `buffer` (`const Complex64*`):`2^N` 个振幅的输入数组。 +- `len` (`uintptr_t`):数组长度,必须等于 `statevector_data_len`。 + +返回:`0` 成功;`-1` NULL 指针;`-8` `len` 与实际数量不符。 + +直接写入振幅可能破坏归一化;调用方有责任保证写入的是合法态矢量——模拟器会按写入的数据原样继续演化。 + +--- + +## 门操作 + +所有门函数返回 `int32_t`:`0` 成功;`-1` 句柄为 NULL;`-2` 比特编号越界;参数化门的角度取值为 NaN 或 Inf 时返回 `-8`。单比特门统一签名为: + +```c +int32_t statevector_apply_h(struct CStatevector *ptr, uint32_t qubit); +``` + +### 无参数单比特门 + +| 函数 | 门 | 说明 | +| --- | --- | --- | +| `statevector_apply_h(ptr, qubit)` | H | Hadamard 门。 | +| `statevector_apply_x(ptr, qubit)` | X | Pauli-X,比特翻转。 | +| `statevector_apply_y(ptr, qubit)` | Y | Pauli-Y。 | +| `statevector_apply_z(ptr, qubit)` | Z | Pauli-Z,相位翻转。 | +| `statevector_apply_s(ptr, qubit)` | S | S 门(√Z)。 | +| `statevector_apply_sdg(ptr, qubit)` | S† | S 的逆。 | +| `statevector_apply_t(ptr, qubit)` | T | T 门(√S)。 | +| `statevector_apply_tdg(ptr, qubit)` | T† | T 的逆。 | +| `statevector_apply_x2p(ptr, qubit)` | X2P | `Rx(π/2)`。 | +| `statevector_apply_x2m(ptr, qubit)` | X2M | `Rx(-π/2)`。 | +| `statevector_apply_y2p(ptr, qubit)` | Y2P | `Ry(π/2)`。 | +| `statevector_apply_y2m(ptr, qubit)` | Y2M | `Ry(-π/2)`。 | + +### 参数化单比特门 + +| 函数 | 门 | 说明 | +| --- | --- | --- | +| `statevector_apply_rx(ptr, qubit, theta)` | Rx(θ) | 绕 X 轴旋转 `θ`。 | +| `statevector_apply_ry(ptr, qubit, theta)` | Ry(θ) | 绕 Y 轴旋转 `θ`。 | +| `statevector_apply_rz(ptr, qubit, theta)` | Rz(θ) | 绕 Z 轴旋转 `θ`。 | +| `statevector_apply_phase(ptr, qubit, theta)` | P(θ) | 相位门,给 `\|1>` 分量乘 `e^(iθ)`。 | +| `statevector_apply_u(ptr, qubit, theta, phi, lambda)` | U(θ, φ, λ) | 通用单比特门。 | +| `statevector_apply_xy(ptr, qubit, theta)` | XY(θ) | XY 门。 | +| `statevector_apply_xy2p(ptr, qubit, theta)` | XY2P(θ) | XY2P 门。 | +| `statevector_apply_xy2m(ptr, qubit, theta)` | XY2M(θ) | XY2M 门。 | +| `statevector_apply_rxy(ptr, qubit, theta, phi)` | RXY(θ, φ) | 绕 XY 平面内方位角为 `φ` 的轴旋转 `θ`。 | +| `statevector_apply_gphase(ptr, phi)` | GPhase(φ) | 给整个态乘全局相位 `e^(iφ)`;签名无 qubit 参数。 | + +GPhase 签名: + +```c +int32_t statevector_apply_gphase(struct CStatevector *ptr, double phi); +``` + +### 双比特门 + +统一签名 `int32_t statevector_apply_(struct CStatevector *ptr, uint32_t q0, uint32_t q1, ...)`,受控门的两个比特参数名为 `control` / `target`。 + +| 函数 | 门 | 说明 | +| --- | --- | --- | +| `statevector_apply_cx(ptr, control, target)` | CX | 受控 X。 | +| `statevector_apply_cy(ptr, control, target)` | CY | 受控 Y。 | +| `statevector_apply_cz(ptr, q0, q1)` | CZ | 受控 Z,两参数地位对称。 | +| `statevector_apply_swap(ptr, q0, q1)` | SWAP | 交换两个比特。 | +| `statevector_apply_rxx(ptr, q0, q1, theta)` | RXX(θ) | `exp(-i·θ/2·X⊗X)`。 | +| `statevector_apply_ryy(ptr, q0, q1, theta)` | RYY(θ) | `exp(-i·θ/2·Y⊗Y)`。 | +| `statevector_apply_rzz(ptr, q0, q1, theta)` | RZZ(θ) | `exp(-i·θ/2·Z⊗Z)`。 | +| `statevector_apply_rzx(ptr, q0, q1, theta)` | RZX(θ) | `exp(-i·θ/2·Z⊗X)`。 | +| `statevector_apply_crx(ptr, control, target, theta)` | CrX(θ) | 受控 `Rx(θ)`。 | +| `statevector_apply_cry(ptr, control, target, theta)` | CrY(θ) | 受控 `Ry(θ)`。 | +| `statevector_apply_crz(ptr, control, target, theta)` | CrZ(θ) | 受控 `Rz(θ)`。 | +| `statevector_apply_fsim(ptr, q0, q1, theta, phi)` | FSIM | 费米子模拟门,`theta` 为 iSWAP 角,`phi` 为受控相位角。 | + +### 三比特门 + +| 函数 | 门 | 说明 | +| --- | --- | --- | +| `statevector_apply_ccx(ptr, c0, c1, target)` | CCX | Toffoli 门,两个控制位同时为 `\|1>` 时翻转目标位。 | + +### 通用门入口 + +#### statevector_apply_standard_gate(ptr, gate_name, qubits, num_target_qubits, params, num_params) + +```c +int32_t statevector_apply_standard_gate(struct CStatevector *ptr, + const char *gate_name, + const uint32_t *qubits, + uintptr_t num_target_qubits, + const double *params, + uintptr_t num_params); +``` + +按名称施加标准门(如 `"H"`、`"CX"`、`"RZZ"`、`"FSIM"`),分派到对应的专用实现。 + +- `ptr` (`CStatevector*`):态矢量句柄。 +- `gate_name` (`const char*`):NUL 结尾的标准门名。 +- `qubits` (`const uint32_t*`):目标比特数组,长度为 `num_target_qubits`。 +- `num_target_qubits` (`uintptr_t`):目标比特数量,必须与门的要求一致。 +- `params` (`const double*`):门参数数组;`num_params` 为 0 时可为 NULL。 +- `num_params` (`uintptr_t`):参数数量,必须与门的要求一致。 + +返回:`0` 成功;`-1` NULL 指针;`-2` 比特越界;`-8` 未知门名、门名不是合法 UTF-8、比特数或参数个数与门不符、参数值非有限。 + +#### statevector_apply_unitary_gate(ptr, qubits, num_target_qubits, matrix, dim) + +```c +int32_t statevector_apply_unitary_gate(struct CStatevector *ptr, + const uint32_t *qubits, + uintptr_t num_target_qubits, + const Complex64 *matrix, + uintptr_t dim); +``` + +把任意酉矩阵作用到指定比特上。 + +- `ptr` (`CStatevector*`):态矢量句柄。 +- `qubits` (`const uint32_t*`):目标比特数组,不得重复。 +- `num_target_qubits` (`uintptr_t`):目标比特数量。 +- `matrix` (`const Complex64*`):行主序 `dim × dim` 矩阵。 +- `dim` (`uintptr_t`):矩阵维度,必须等于 `2^num_target_qubits`。 + +返回:`0` 成功;`-1` NULL 指针;`-2` 比特越界;`-8` `num_target_qubits` 或 `dim` 为 0、维度不匹配或比特重复。 + +#### statevector_apply_pauli_rotation(ptr, pauli, theta) + +```c +int32_t statevector_apply_pauli_rotation(struct CStatevector *ptr, + const struct CPauliString *pauli, + double theta); +``` + +原地作用 Pauli 串旋转 `exp(-i·θ/2·P)`,不做门分解。Pauli 串必须覆盖整个寄存器且为 Hermitian,构造方式见 [Pauli](6_pauli.md)。 + +- `ptr` (`CStatevector*`):态矢量句柄。 +- `pauli` (`const CPauliString*`):Pauli 串句柄。 +- `theta` (`double`):旋转角。 + +返回:`0` 成功;`-1` NULL 指针;`-8` `theta` 非有限或 Pauli 串比特数与态不一致。 + +--- + +## 概率与测量 + +### statevector_probabilities_len(ptr) + +```c +uintptr_t statevector_probabilities_len(const struct CStatevector *ptr); +``` + +返回计算基态概率分布的长度(`2^N`)。 + +- `ptr` (`const CStatevector*`):态矢量句柄。 + +返回:概率数量;`NULL` 时返回 0。 + +### statevector_probabilities(ptr, buffer, len) + +```c +int32_t statevector_probabilities(const struct CStatevector *ptr, double *buffer, uintptr_t len); +``` + +把全部计算基态上的测量概率(各振幅模的平方)拷贝进 `buffer`。 + +- `ptr` (`const CStatevector*`):态矢量句柄。 +- `buffer` (`double*`):输出缓冲。 +- `len` (`uintptr_t`):缓冲长度,必须等于 `statevector_probabilities_len`。 + +返回:`0` 成功;`-1` NULL 指针;`-8` `len` 与实际数量不符。 + +### statevector_measure(ptr, qubit) + +```c +int32_t statevector_measure(struct CStatevector *ptr, uint32_t qubit); +``` + +在 Z 基下测量指定比特并使态坍缩,坍缩后态在对应子空间内重新归一化。该操作是破坏性的。 + +- `ptr` (`CStatevector*`):态矢量句柄。 +- `qubit` (`uint32_t`):被测比特。 + +返回:`0` 表示结果 `|0>`,`1` 表示结果 `|1>`;`-1` NULL;`-2` 比特越界。 + +### statevector_measure_all(ptr) + +```c +char *statevector_measure_all(struct CStatevector *ptr); +``` + +按 `0..num_qubits` 顺序逐比特测量并使态坍缩,返回大端比特串(MSB = 比特 `N-1`,LSB = 比特 `0`)。 + +- `ptr` (`CStatevector*`):态矢量句柄。 + +返回:堆分配的 C 字符串,用 `cqlib_string_free` 释放;失败返回 `NULL`。 + +### statevector_sample_shots(ptr, shots) + +```c +struct COutcomeList *statevector_sample_shots(const struct CStatevector *ptr, uintptr_t shots); +``` + +按当前概率分布并行采样 `shots` 次独立测量结果,不修改态。返回的列表用 `outcome_list_len` 取数量、`outcome_list_get` 读取每个大端比特串(用 `cqlib_string_free` 释放)、`outcome_list_free` 释放列表,详见 [ClassicalState](5_classical_state.md)。 + +- `ptr` (`const CStatevector*`):态矢量句柄。 +- `shots` (`uintptr_t`):采样次数。 + +返回:新分配的 `COutcomeList*`;失败返回 `NULL`。 + +### statevector_reset(ptr, qubit) + +```c +int32_t statevector_reset(struct CStatevector *ptr, uint32_t qubit); +``` + +把指定比特重置为 `|0>`:先做一次 Z 基测量,结果为 `|1>` 时补一个 X 修正。 + +- `ptr` (`CStatevector*`):态矢量句柄。 +- `qubit` (`uint32_t`):目标比特。 + +返回:`0` 成功;`-1` NULL;`-2` 比特越界。 + +--- + +## 期望值 + +### statevector_expectation(ptr, observable, out) + +```c +int32_t statevector_expectation(const struct CStatevector *ptr, + const struct CHamiltonian *observable, + double *out); +``` + +计算 `⟨ψ|H|ψ⟩` 并写入 `out`。 + +- `ptr` (`const CStatevector*`):态矢量句柄。 +- `observable` (`const CHamiltonian*`):可观测量句柄,见 [Hamiltonian](7_hamiltonian.md)。 +- `out` (`double*`):输出指针。 + +返回:`0` 成功;`-1` NULL 指针;`-8` 观测量的比特数与态不一致。 + +```c +/* of the Bell state equals 1 */ +struct CPauliString *zz = pauli_string_parse("ZZ"); +struct CHamiltonian *h = hamiltonian_from_pauli(zz); /* takes ownership of zz */ +double exp = 0.0; +statevector_expectation(sv, h, &exp); /* exp ≈ 1.0 */ +hamiltonian_free(h); +``` + +--- + +## 示例 + +完整流程:制备 Bell 态、读概率、测量、采样、求期望值。 + +```c +#include +#include +#include "cqlib_c.h" + +int main(void) { + /* Bell state (|00> + |11>)/sqrt(2) */ + struct CStatevector *sv = statevector_new(2); + if (sv == NULL) return 1; + statevector_apply_h(sv, 0); + statevector_apply_cx(sv, 0, 1); + + /* Probabilities: P(00) = P(11) = 0.5 */ + uintptr_t n = statevector_probabilities_len(sv); /* 4 */ + double *probs = malloc(n * sizeof(double)); + statevector_probabilities(sv, probs, n); + printf("P(00)=%.3f P(11)=%.3f\n", probs[0], probs[3]); + free(probs); + + /* Sample 100 shots: only "00" and "11" appear */ + struct COutcomeList *shots = statevector_sample_shots(sv, 100); + for (uintptr_t i = 0; i < outcome_list_len(shots); i++) { + char *bits = outcome_list_get(shots, i); + printf("%s ", bits); + cqlib_string_free(bits); + } + printf("\n"); + outcome_list_free(shots); + + /* Expectation = 1 */ + struct CPauliString *zz = pauli_string_parse("ZZ"); + struct CHamiltonian *h = hamiltonian_from_pauli(zz); + double exp = 0.0; + statevector_expectation(sv, h, &exp); + printf("=%.3f\n", exp); + hamiltonian_free(h); + + /* Destructive full measurement collapses the state */ + char *bits = statevector_measure_all(sv); + printf("measured: %s\n", bits); /* "00" or "11" */ + cqlib_string_free(bits); + + statevector_free(sv); + return 0; +} +``` diff --git a/docs/api/cn/c/3_qis/2_density_matrix.md b/docs/api/cn/c/3_qis/2_density_matrix.md new file mode 100644 index 00000000..b0037fcf --- /dev/null +++ b/docs/api/cn/c/3_qis/2_density_matrix.md @@ -0,0 +1,520 @@ +# DensityMatrix(密度矩阵) + +`CDensityMatrix*` 是混合态模拟器句柄:量子态表示为 `2^N × 2^N` 的密度矩阵(行主序 `4^N` 个矩阵元),可以描述纯态、混合态以及量子信道作用后的结果。本页覆盖构造与线路对接、矩阵数据与物理性校验、门操作、量子信道与偏迹、概率与测量以及期望值。错误码与内存约定见 [Overview](../0_overview.md),模块总览见 [QIS 概览](0_overview.md)。 + +--- + +## 构造与释放 + +### density_matrix_new(num_qubits) + +```c +struct CDensityMatrix *density_matrix_new(uintptr_t num_qubits); +``` + +创建混合态模拟器,初始态为纯态 `|0...0><0...0|`。 + +- `num_qubits` (`uintptr_t`):量子比特数量。 + +返回:新分配的 `CDensityMatrix*`,用 `density_matrix_free` 释放。 + +### density_matrix_maximally_mixed(num_qubits) + +```c +struct CDensityMatrix *density_matrix_maximally_mixed(uintptr_t num_qubits); +``` + +构造最大混合态 `I / 2^N`:各计算基态等概率且无相干项。 + +- `num_qubits` (`uintptr_t`):量子比特数量。 + +返回:新分配的 `CDensityMatrix*`,用 `density_matrix_free` 释放。 + +### density_matrix_zeros(num_qubits) + +```c +struct CDensityMatrix *density_matrix_zeros(uintptr_t num_qubits); +``` + +创建全零的密度矩阵。它不是合法的物理态(迹为 0),适合作为 Kraus 信道等运算的累加起点。 + +- `num_qubits` (`uintptr_t`):量子比特数量。 + +返回:新分配的 `CDensityMatrix*`,用 `density_matrix_free` 释放;矩阵尺寸超出可寻址范围时返回 `NULL`。 + +### density_matrix_from_circuit(circuit) + +```c +struct CDensityMatrix *density_matrix_from_circuit(const struct CCircuit *circuit); +``` + +执行线路并返回演化后的密度矩阵,输入线路不被修改。线路先被分解为基础门再逐条执行;测量声明不参与状态演化。 + +- `circuit` (`const CCircuit*`):线路句柄,见 [Circuit](../0_circuit/1_circuit.md)。 + +返回:新分配的 `CDensityMatrix*`;`circuit` 为 NULL 或执行失败返回 `NULL`。 + +### density_matrix_from_state(num_qubits, initial_state, len) + +```c +struct CDensityMatrix *density_matrix_from_state(uintptr_t num_qubits, + const Complex64 *initial_state, + uintptr_t len); +``` + +由归一化纯态振幅做外积 `ρ = |ψ><ψ|` 构造密度矩阵;要求 `len` 等于 `2^num_qubits`。 + +- `num_qubits` (`uintptr_t`):量子比特数量。 +- `initial_state` (`const Complex64*`):长度为 `2^num_qubits` 的归一化振幅数组。 +- `len` (`uintptr_t`):数组长度。 + +返回:新分配的 `CDensityMatrix*`;输入为 NULL、长度不符或未归一化时返回 `NULL`。 + +### density_matrix_free(ptr) + +```c +void density_matrix_free(struct CDensityMatrix *ptr); +``` + +释放句柄。允许传 `NULL`。 + +- `ptr` (`CDensityMatrix*`):密度矩阵句柄。 + +### density_matrix_num_qubits(ptr) + +```c +uintptr_t density_matrix_num_qubits(const struct CDensityMatrix *ptr); +``` + +返回量子比特数量。 + +- `ptr` (`const CDensityMatrix*`):密度矩阵句柄。 + +返回:比特数量;`NULL` 时返回 0。 + +### density_matrix_apply_circuit(ptr, circuit) + +```c +int32_t density_matrix_apply_circuit(struct CDensityMatrix *ptr, const struct CCircuit *circuit); +``` + +把线路原地作用到当前密度矩阵上,线路的比特数必须与态一致。 + +- `ptr` (`CDensityMatrix*`):密度矩阵句柄。 +- `circuit` (`const CCircuit*`):线路句柄。 + +返回:`0` 成功;`-1` NULL 指针;`-3` 线路错误;`-7` 线路含不支持的操作;`-8` 线路比特数与态不一致。 + +--- + +## 矩阵数据与物理性 + +### density_matrix_data_len(ptr) + +```c +uintptr_t density_matrix_data_len(const struct CDensityMatrix *ptr); +``` + +返回矩阵元数量(`4^N`),用于为 `density_matrix_data` 分配缓冲。 + +- `ptr` (`const CDensityMatrix*`):密度矩阵句柄。 + +返回:矩阵元数量;`NULL` 时返回 0。 + +### density_matrix_data(ptr, buffer, len) + +```c +int32_t density_matrix_data(const struct CDensityMatrix *ptr, Complex64 *buffer, uintptr_t len); +``` + +把展平的行主序矩阵元拷贝进 `buffer`,共 `4^N` 个 `Complex64` 值;第 `r·2^N + c` 个元素是矩阵第 `r` 行第 `c` 列。 + +- `ptr` (`const CDensityMatrix*`):密度矩阵句柄。 +- `buffer` (`Complex64*`):输出缓冲。 +- `len` (`uintptr_t`):缓冲长度,必须等于 `density_matrix_data_len`。 + +返回:`0` 成功;`-1` NULL 指针;`-8` `len` 与实际数量不符。 + +### density_matrix_trace(ptr, out_re, out_im) + +```c +int32_t density_matrix_trace(const struct CDensityMatrix *ptr, double *out_re, double *out_im); +``` + +计算迹并把实部、虚部分别写入 `out_re` / `out_im`;合法物理态的迹等于 `1.0`。 + +- `ptr` (`const CDensityMatrix*`):密度矩阵句柄。 +- `out_re` (`double*`):迹的实部输出。 +- `out_im` (`double*`):迹的虚部输出。 + +返回:`0` 成功;`-1` 任一指针为 NULL。 + +### density_matrix_is_hermitian(ptr, tol, out) + +```c +int32_t density_matrix_is_hermitian(const struct CDensityMatrix *ptr, double tol, bool *out); +``` + +在容差 `tol` 内判断矩阵是否满足 `ρ = ρ†`,结果写入 `out`。 + +- `ptr` (`const CDensityMatrix*`):密度矩阵句柄。 +- `tol` (`double`):容差,必须为有限值。 +- `out` (`bool*`):结果输出。 + +返回:`0` 成功;`-1` NULL 指针;`-8` `tol` 非有限。 + +### density_matrix_is_positive_semidefinite(ptr, tol, out) + +```c +int32_t density_matrix_is_positive_semidefinite(const struct CDensityMatrix *ptr, double tol, bool *out); +``` + +在容差 `tol` 内判断矩阵是否半正定(全部特征值满足 `λ >= -tol`),结果写入 `out`。 + +- `ptr` (`const CDensityMatrix*`):密度矩阵句柄。 +- `tol` (`double`):容差,必须为有限值。 +- `out` (`bool*`):结果输出。 + +返回:`0` 成功;`-1` NULL 指针;`-8` `tol` 非有限。 + +### density_matrix_validate_physical(ptr, tol) + +```c +int32_t density_matrix_validate_physical(const struct CDensityMatrix *ptr, double tol); +``` + +在容差 `tol` 内依次校验密度矩阵的物理性约束:Hermitian(`ρ = ρ†`)、半正定、迹为 1。矩阵只被读取,不做修改。 + +- `ptr` (`const CDensityMatrix*`):密度矩阵句柄。 +- `tol` (`double`):容差,必须为有限值。 + +返回:`0` 态为物理态;`-1` 句柄为 NULL;`-7` 非 Hermitian 或非半正定;`-8` `tol` 非有限或迹不等于 `1`。 + +--- + +## 门操作 + +所有门函数返回 `int32_t`:`0` 成功;`-1` 句柄为 NULL;`-2` 比特编号越界;参数化门的角度取值为 NaN 或 Inf 时返回 `-8`。单比特门统一签名为: + +```c +int32_t density_matrix_apply_h(struct CDensityMatrix *ptr, uint32_t qubit); +``` + +### 无参数单比特门 + +| 函数 | 门 | 说明 | +| --- | --- | --- | +| `density_matrix_apply_h(ptr, qubit)` | H | Hadamard 门。 | +| `density_matrix_apply_x(ptr, qubit)` | X | Pauli-X,比特翻转。 | +| `density_matrix_apply_y(ptr, qubit)` | Y | Pauli-Y。 | +| `density_matrix_apply_z(ptr, qubit)` | Z | Pauli-Z,相位翻转。 | +| `density_matrix_apply_s(ptr, qubit)` | S | S 门(√Z)。 | +| `density_matrix_apply_sdg(ptr, qubit)` | S† | S 的逆。 | +| `density_matrix_apply_t(ptr, qubit)` | T | T 门(√S)。 | +| `density_matrix_apply_tdg(ptr, qubit)` | T† | T 的逆。 | +| `density_matrix_apply_x2p(ptr, qubit)` | X2P | `Rx(π/2)`。 | +| `density_matrix_apply_x2m(ptr, qubit)` | X2M | `Rx(-π/2)`。 | +| `density_matrix_apply_y2p(ptr, qubit)` | Y2P | `Ry(π/2)`。 | +| `density_matrix_apply_y2m(ptr, qubit)` | Y2M | `Ry(-π/2)`。 | + +### 参数化单比特门 + +| 函数 | 门 | 说明 | +| --- | --- | --- | +| `density_matrix_apply_rx(ptr, qubit, theta)` | Rx(θ) | 绕 X 轴旋转 `θ`。 | +| `density_matrix_apply_ry(ptr, qubit, theta)` | Ry(θ) | 绕 Y 轴旋转 `θ`。 | +| `density_matrix_apply_rz(ptr, qubit, theta)` | Rz(θ) | 绕 Z 轴旋转 `θ`。 | +| `density_matrix_apply_phase(ptr, qubit, theta)` | P(θ) | 相位门,给 `\|1>` 分量乘 `e^(iθ)`。 | +| `density_matrix_apply_u(ptr, qubit, theta, phi, lambda)` | U(θ, φ, λ) | 通用单比特门。 | +| `density_matrix_apply_xy(ptr, qubit, theta)` | XY(θ) | XY 门。 | +| `density_matrix_apply_xy2p(ptr, qubit, theta)` | XY2P(θ) | XY2P 门。 | +| `density_matrix_apply_xy2m(ptr, qubit, theta)` | XY2M(θ) | XY2M 门。 | +| `density_matrix_apply_rxy(ptr, qubit, theta, phi)` | RXY(θ, φ) | 绕 XY 平面内方位角为 `φ` 的轴旋转 `θ`。 | +| `density_matrix_apply_gphase(ptr, phi)` | GPhase(φ) | 全局相位。密度矩阵在全局相位下不变,调用恒返回 `0` 且不改变状态;签名无 qubit 参数。 | + +GPhase 签名: + +```c +int32_t density_matrix_apply_gphase(struct CDensityMatrix *ptr, double phi); +``` + +### 双比特门 + +统一签名 `int32_t density_matrix_apply_(struct CDensityMatrix *ptr, uint32_t q0, uint32_t q1, ...)`,受控门的两个比特参数名为 `control` / `target`。 + +| 函数 | 门 | 说明 | +| --- | --- | --- | +| `density_matrix_apply_cx(ptr, control, target)` | CX | 受控 X。 | +| `density_matrix_apply_cy(ptr, control, target)` | CY | 受控 Y。 | +| `density_matrix_apply_cz(ptr, q0, q1)` | CZ | 受控 Z,两参数地位对称。 | +| `density_matrix_apply_swap(ptr, q0, q1)` | SWAP | 交换两个比特。 | +| `density_matrix_apply_rxx(ptr, q0, q1, theta)` | RXX(θ) | `exp(-i·θ/2·X⊗X)`。 | +| `density_matrix_apply_ryy(ptr, q0, q1, theta)` | RYY(θ) | `exp(-i·θ/2·Y⊗Y)`。 | +| `density_matrix_apply_rzz(ptr, q0, q1, theta)` | RZZ(θ) | `exp(-i·θ/2·Z⊗Z)`。 | +| `density_matrix_apply_rzx(ptr, q0, q1, theta)` | RZX(θ) | `exp(-i·θ/2·Z⊗X)`。 | +| `density_matrix_apply_crx(ptr, control, target, theta)` | CrX(θ) | 受控 `Rx(θ)`。 | +| `density_matrix_apply_cry(ptr, control, target, theta)` | CrY(θ) | 受控 `Ry(θ)`。 | +| `density_matrix_apply_crz(ptr, control, target, theta)` | CrZ(θ) | 受控 `Rz(θ)`。 | +| `density_matrix_apply_fsim(ptr, q0, q1, theta, phi)` | FSIM | 费米子模拟门,`theta` 为 iSWAP 角,`phi` 为受控相位角。 | + +### 三比特门 + +| 函数 | 门 | 说明 | +| --- | --- | --- | +| `density_matrix_apply_ccx(ptr, c0, c1, target)` | CCX | Toffoli 门,两个控制位同时为 `\|1>` 时翻转目标位。 | + +### 通用门入口 + +#### density_matrix_apply_standard_gate(ptr, gate_name, qubits, num_target_qubits, params, num_params) + +```c +int32_t density_matrix_apply_standard_gate(struct CDensityMatrix *ptr, + const char *gate_name, + const uint32_t *qubits, + uintptr_t num_target_qubits, + const double *params, + uintptr_t num_params); +``` + +按名称施加标准门(如 `"H"`、`"CX"`、`"RZZ"`、`"FSIM"`),分派到对应的专用实现。 + +- `ptr` (`CDensityMatrix*`):密度矩阵句柄。 +- `gate_name` (`const char*`):NUL 结尾的标准门名。 +- `qubits` (`const uint32_t*`):目标比特数组,长度为 `num_target_qubits`。 +- `num_target_qubits` (`uintptr_t`):目标比特数量,必须与门的要求一致。 +- `params` (`const double*`):门参数数组;`num_params` 为 0 时可为 NULL。 +- `num_params` (`uintptr_t`):参数数量,必须与门的要求一致。 + +返回:`0` 成功;`-1` NULL 指针;`-2` 比特越界;`-8` 未知门名、门名不是合法 UTF-8、比特数或参数个数与门不符、参数值非有限。 + +--- + +## 量子信道与偏迹 + +### density_matrix_apply_kraus(ptr, ops, num_ops, op_dim, qubits, num_target_qubits) + +```c +int32_t density_matrix_apply_kraus(struct CDensityMatrix *ptr, + const Complex64 *ops, + uintptr_t num_ops, + uintptr_t op_dim, + const uint32_t *qubits, + uintptr_t num_target_qubits); +``` + +作用由 Kraus 算符给出的量子信道,演化为 `ρ → Σ_k K_k ρ K_k†`。 + +- `ptr` (`CDensityMatrix*`):密度矩阵句柄。 +- `ops` (`const Complex64*`):展平的算符数组,共 `num_ops × op_dim × op_dim` 个行主序 `Complex64` 值,每个算符依次存放。 +- `num_ops` (`uintptr_t`):算符数量。 +- `op_dim` (`uintptr_t`):单个算符的维度,等于 `2^num_target_qubits`。 +- `qubits` (`const uint32_t*`):目标比特数组。 +- `num_target_qubits` (`uintptr_t`):目标比特数量。 + +返回:`0` 成功;`-1` NULL 指针;`-2` 比特越界;`-8` `num_ops`、`op_dim` 或 `num_target_qubits` 为 0,或算符尺寸与比特数不符。 + +```c +/* Bit-flip channel with p = 0.3 on qubit 0 */ +double p = 0.3, s0 = sqrt(1.0 - p), s1 = sqrt(p); +Complex64 ops[8] = { + {s0, 0}, {0, 0}, {0, 0}, {s0, 0}, /* K0 = sqrt(1-p) * I */ + {0, 0}, {s1, 0}, {s1, 0}, {0, 0}, /* K1 = sqrt(p) * X */ +}; +uint32_t q = 0; +density_matrix_apply_kraus(dm, ops, 2, 2, &q, 1); +``` + +### density_matrix_partial_trace(ptr, keep_qubits, num_keep) + +```c +struct CDensityMatrix *density_matrix_partial_trace(const struct CDensityMatrix *ptr, + const uint32_t *keep_qubits, + uintptr_t num_keep); +``` + +对 `keep_qubits` 之外的比特求偏迹,返回比特数为 `num_keep` 的新密度矩阵。 + +- `ptr` (`const CDensityMatrix*`):密度矩阵句柄。 +- `keep_qubits` (`const uint32_t*`):保留比特数组;`num_keep` 为 0 时可为 NULL。 +- `num_keep` (`uintptr_t`):保留比特数量。 + +返回:新分配的 `CDensityMatrix*`,用 `density_matrix_free` 释放;失败返回 `NULL`。 + +--- + +## 概率与测量 + +### density_matrix_probabilities_len(ptr) + +```c +uintptr_t density_matrix_probabilities_len(const struct CDensityMatrix *ptr); +``` + +返回计算基态概率分布的长度(`2^N`)。 + +- `ptr` (`const CDensityMatrix*`):密度矩阵句柄。 + +返回:概率数量;`NULL` 时返回 0。 + +### density_matrix_probabilities(ptr, buffer, len) + +```c +int32_t density_matrix_probabilities(const struct CDensityMatrix *ptr, + double *buffer, + uintptr_t len); +``` + +把密度矩阵对角元(即各计算基态的测量概率)拷贝进 `buffer`。 + +- `ptr` (`const CDensityMatrix*`):密度矩阵句柄。 +- `buffer` (`double*`):输出缓冲。 +- `len` (`uintptr_t`):缓冲长度,必须等于 `density_matrix_probabilities_len`。 + +返回:`0` 成功;`-1` NULL 指针;`-8` `len` 与实际数量不符。 + +### density_matrix_measure(ptr, qubit) + +```c +int32_t density_matrix_measure(struct CDensityMatrix *ptr, uint32_t qubit); +``` + +在 Z 基下测量指定比特并使密度矩阵坍缩:`ρ' = Π_b ρ Π_b / Tr(Π_b ρ)`。 + +- `ptr` (`CDensityMatrix*`):密度矩阵句柄。 +- `qubit` (`uint32_t`):被测比特。 + +返回:`0` 表示结果 `|0>`,`1` 表示结果 `|1>`;`-1` NULL;`-2` 比特越界。 + +### density_matrix_measure_all(ptr) + +```c +char *density_matrix_measure_all(struct CDensityMatrix *ptr); +``` + +按 `0..num_qubits` 顺序逐比特测量并使态坍缩,返回大端比特串(MSB = 比特 `N-1`,LSB = 比特 `0`)。 + +- `ptr` (`CDensityMatrix*`):密度矩阵句柄。 + +返回:堆分配的 C 字符串,用 `cqlib_string_free` 释放;失败返回 `NULL`。 + +### density_matrix_sample_shots(ptr, shots) + +```c +struct COutcomeList *density_matrix_sample_shots(const struct CDensityMatrix *ptr, uintptr_t shots); +``` + +按当前概率分布并行采样 `shots` 次独立测量结果,不修改态。返回的列表用 `outcome_list_len` 取数量、`outcome_list_get` 读取每个大端比特串(用 `cqlib_string_free` 释放)、`outcome_list_free` 释放列表,详见 [ClassicalState](5_classical_state.md)。 + +- `ptr` (`const CDensityMatrix*`):密度矩阵句柄。 +- `shots` (`uintptr_t`):采样次数。 + +返回:新分配的 `COutcomeList*`;失败返回 `NULL`。 + +### density_matrix_reset(ptr, qubit) + +```c +int32_t density_matrix_reset(struct CDensityMatrix *ptr, uint32_t qubit); +``` + +把指定比特重置为 `|0>`。 + +- `ptr` (`CDensityMatrix*`):密度矩阵句柄。 +- `qubit` (`uint32_t`):目标比特。 + +返回:`0` 成功;`-1` NULL;`-2` 比特越界。 + +--- + +## 期望值 + +### density_matrix_expectation(ptr, observable, out) + +```c +int32_t density_matrix_expectation(const struct CDensityMatrix *ptr, + const struct CHamiltonian *observable, + double *out); +``` + +计算 `Tr(ρ·H)` 并写入 `out`。 + +- `ptr` (`const CDensityMatrix*`):密度矩阵句柄。 +- `observable` (`const CHamiltonian*`):可观测量句柄,见 [Hamiltonian](7_hamiltonian.md)。 +- `out` (`double*`):输出指针。 + +返回:`0` 成功;`-1` NULL 指针;`-8` 观测量的比特数与态不一致。 + +--- + +## 示例 + +### 1. Bell 态、矩阵数据与概率 + +```c +#include +#include +#include "cqlib_c.h" + +int main(void) { + struct CDensityMatrix *dm = density_matrix_new(2); + density_matrix_apply_h(dm, 0); + density_matrix_apply_cx(dm, 0, 1); + + /* Probabilities: P(00) = P(11) = 0.5 */ + double probs[4]; + density_matrix_probabilities(dm, probs, 4); + printf("P(00)=%.3f P(11)=%.3f\n", probs[0], probs[3]); + + /* Row-major matrix data: 16 Complex64 elements */ + uintptr_t n = density_matrix_data_len(dm); /* 16 */ + Complex64 *data = malloc(n * sizeof(Complex64)); + density_matrix_data(dm, data, n); + /* data[0] is rho[0][0], data[3] is rho[0][3], data[15] is rho[3][3] */ + printf("rho[3][3].re=%.3f\n", data[15].re); + free(data); + + density_matrix_free(dm); + return 0; +} +``` + +### 2. 最大混合态、迹与 Hermitian 校验 + +```c +struct CDensityMatrix *dm = density_matrix_maximally_mixed(2); +/* 4^2 = 16 matrix elements, trace = 1, Hermitian */ +double tr_re, tr_im; +density_matrix_trace(dm, &tr_re, &tr_im); /* tr_re = 1.0, tr_im = 0.0 */ +bool herm = false; +density_matrix_is_hermitian(dm, 1e-10, &herm); /* herm = true */ +density_matrix_free(dm); +``` + +### 3. 偏迹 + +```c +struct CDensityMatrix *dm = density_matrix_new(2); +density_matrix_apply_h(dm, 0); +density_matrix_apply_cx(dm, 0, 1); + +/* Trace out qubit 1: qubit 0 becomes maximally mixed */ +uint32_t keep[1] = {0}; +struct CDensityMatrix *reduced = density_matrix_partial_trace(dm, keep, 1); +/* density_matrix_num_qubits(reduced) == 1, P(0) = P(1) = 0.5 */ +density_matrix_free(reduced); +density_matrix_free(dm); +``` + +### 4. 期望值 + +```c +struct CDensityMatrix *dm = density_matrix_new(2); +density_matrix_apply_h(dm, 0); +density_matrix_apply_cx(dm, 0, 1); + +struct CPauliString *zz = pauli_string_parse("ZZ"); +struct CHamiltonian *h = hamiltonian_from_pauli(zz); /* takes ownership of zz */ +double exp = 0.0; +density_matrix_expectation(dm, h, &exp); /* exp ≈ 1.0 */ +hamiltonian_free(h); +density_matrix_free(dm); +``` diff --git a/docs/api/cn/c/3_qis/3_density_matrix_noise.md b/docs/api/cn/c/3_qis/3_density_matrix_noise.md new file mode 100644 index 00000000..03ddac33 --- /dev/null +++ b/docs/api/cn/c/3_qis/3_density_matrix_noise.md @@ -0,0 +1,398 @@ +# DensityMatrixNoise(噪声密度矩阵) + +`CDensityMatrixNoise*` 是带噪声模型的混合态模拟器句柄:在密度矩阵模拟器之上叠加 `CNoiseModel`,每个门施加后按噪声模型查询同一门、同一比特上的噪声条目并施加相应信道;读出误差只改变报告出来的概率分布。本页覆盖构造与线路对接、门操作、概率与读出噪声、测量与采样以及期望值。错误码与内存约定见 [Overview](../0_overview.md),模块总览见 [QIS 概览](0_overview.md)。 + +--- + +## 构造与释放 + +### density_matrix_noise_new(num_qubits, noise_model) + +```c +struct CDensityMatrixNoise *density_matrix_noise_new(uintptr_t num_qubits, + const struct CNoiseModel *noise_model); +``` + +创建噪声模拟器,初始态为 `|0...0><0...0|`;`noise_model` 传 NULL 进行理想(无噪声)仿真。 + +- `num_qubits` (`uintptr_t`):量子比特数量。 +- `noise_model` (`const CNoiseModel*`):噪声模型句柄,可为 NULL;构造时被克隆,之后模型句柄可独立释放。 + +返回:新分配的 `CDensityMatrixNoise*`,用 `density_matrix_noise_free` 释放。 + +### density_matrix_noise_from_circuit(circuit, noise_model) + +```c +struct CDensityMatrixNoise *density_matrix_noise_from_circuit(const struct CCircuit *circuit, + const struct CNoiseModel *noise_model); +``` + +执行线路并返回噪声模拟器:线路先被分解为基础门,噪声在每个门之后施加;`noise_model` 传 NULL 进行理想仿真。 + +- `circuit` (`const CCircuit*`):线路句柄,见 [Circuit](../0_circuit/1_circuit.md)。 +- `noise_model` (`const CNoiseModel*`):噪声模型句柄,可为 NULL。 + +返回:新分配的 `CDensityMatrixNoise*`;`circuit` 为 NULL 或执行失败返回 `NULL`。 + +### density_matrix_noise_free(ptr) + +```c +void density_matrix_noise_free(struct CDensityMatrixNoise *ptr); +``` + +释放句柄。允许传 `NULL`。 + +- `ptr` (`CDensityMatrixNoise*`):噪声模拟器句柄。 + +### density_matrix_noise_num_qubits(ptr) + +```c +uintptr_t density_matrix_noise_num_qubits(const struct CDensityMatrixNoise *ptr); +``` + +返回量子比特数量。 + +- `ptr` (`const CDensityMatrixNoise*`):噪声模拟器句柄。 + +返回:比特数量;`NULL` 时返回 0。 + +### density_matrix_noise_apply_circuit(ptr, circuit) + +```c +int32_t density_matrix_noise_apply_circuit(struct CDensityMatrixNoise *ptr, + const struct CCircuit *circuit); +``` + +把线路原地作用到当前模拟器上:线路先被分解,每个门施加后注入噪声模型中匹配的门噪声。 + +- `ptr` (`CDensityMatrixNoise*`):噪声模拟器句柄。 +- `circuit` (`const CCircuit*`):线路句柄。 + +返回:`0` 成功;`-1` NULL 指针;`-3` 线路错误;`-7` 线路含不支持的操作;`-8` 线路比特数与模拟器不一致。 + +--- + +## 门操作 + +所有门函数返回 `int32_t`:`0` 成功;`-1` 句柄为 NULL;`-2` 比特编号越界;参数化门的角度取值为 NaN 或 Inf 时返回 `-8`。每个门施加后按噪声模型注入匹配的门噪声;未配置噪声模型或没有匹配条目时只施加理想门。单比特门统一签名为: + +```c +int32_t density_matrix_noise_apply_h(struct CDensityMatrixNoise *ptr, uint32_t qubit); +``` + +### 无参数单比特门 + +| 函数 | 门 | 说明 | +| --- | --- | --- | +| `density_matrix_noise_apply_h(ptr, qubit)` | H | Hadamard 门。 | +| `density_matrix_noise_apply_x(ptr, qubit)` | X | Pauli-X,比特翻转。 | +| `density_matrix_noise_apply_y(ptr, qubit)` | Y | Pauli-Y。 | +| `density_matrix_noise_apply_z(ptr, qubit)` | Z | Pauli-Z,相位翻转。 | +| `density_matrix_noise_apply_s(ptr, qubit)` | S | S 门(√Z)。 | +| `density_matrix_noise_apply_sdg(ptr, qubit)` | S† | S 的逆。 | +| `density_matrix_noise_apply_t(ptr, qubit)` | T | T 门(√S)。 | +| `density_matrix_noise_apply_tdg(ptr, qubit)` | T† | T 的逆。 | +| `density_matrix_noise_apply_x2p(ptr, qubit)` | X2P | `Rx(π/2)`。 | +| `density_matrix_noise_apply_x2m(ptr, qubit)` | X2M | `Rx(-π/2)`。 | +| `density_matrix_noise_apply_y2p(ptr, qubit)` | Y2P | `Ry(π/2)`。 | +| `density_matrix_noise_apply_y2m(ptr, qubit)` | Y2M | `Ry(-π/2)`。 | + +### 参数化单比特门 + +| 函数 | 门 | 说明 | +| --- | --- | --- | +| `density_matrix_noise_apply_rx(ptr, qubit, theta)` | Rx(θ) | 绕 X 轴旋转 `θ`。 | +| `density_matrix_noise_apply_ry(ptr, qubit, theta)` | Ry(θ) | 绕 Y 轴旋转 `θ`。 | +| `density_matrix_noise_apply_rz(ptr, qubit, theta)` | Rz(θ) | 绕 Z 轴旋转 `θ`。 | +| `density_matrix_noise_apply_phase(ptr, qubit, theta)` | P(θ) | 相位门,给 `\|1>` 分量乘 `e^(iθ)`。 | +| `density_matrix_noise_apply_u(ptr, qubit, theta, phi, lambda)` | U(θ, φ, λ) | 通用单比特门。 | +| `density_matrix_noise_apply_xy(ptr, qubit, theta)` | XY(θ) | XY 门。 | +| `density_matrix_noise_apply_xy2p(ptr, qubit, theta)` | XY2P(θ) | XY2P 门。 | +| `density_matrix_noise_apply_xy2m(ptr, qubit, theta)` | XY2M(θ) | XY2M 门。 | +| `density_matrix_noise_apply_rxy(ptr, qubit, theta, phi)` | RXY(θ, φ) | 绕 XY 平面内方位角为 `φ` 的轴旋转 `θ`。 | +| `density_matrix_noise_apply_gphase(ptr, phi)` | GPhase(φ) | 给整个态乘全局相位 `e^(iφ)`;签名无 qubit 参数。 | + +GPhase 签名: + +```c +int32_t density_matrix_noise_apply_gphase(struct CDensityMatrixNoise *ptr, double phi); +``` + +### 双比特门 + +统一签名 `int32_t density_matrix_noise_apply_(struct CDensityMatrixNoise *ptr, uint32_t q0, uint32_t q1, ...)`,受控门的两个比特参数名为 `control` / `target`。 + +| 函数 | 门 | 说明 | +| --- | --- | --- | +| `density_matrix_noise_apply_cx(ptr, control, target)` | CX | 受控 X。 | +| `density_matrix_noise_apply_cy(ptr, control, target)` | CY | 受控 Y。 | +| `density_matrix_noise_apply_cz(ptr, q0, q1)` | CZ | 受控 Z,两参数地位对称。 | +| `density_matrix_noise_apply_swap(ptr, q0, q1)` | SWAP | 交换两个比特。 | +| `density_matrix_noise_apply_rxx(ptr, q0, q1, theta)` | RXX(θ) | `exp(-i·θ/2·X⊗X)`。 | +| `density_matrix_noise_apply_ryy(ptr, q0, q1, theta)` | RYY(θ) | `exp(-i·θ/2·Y⊗Y)`。 | +| `density_matrix_noise_apply_rzz(ptr, q0, q1, theta)` | RZZ(θ) | `exp(-i·θ/2·Z⊗Z)`。 | +| `density_matrix_noise_apply_rzx(ptr, q0, q1, theta)` | RZX(θ) | `exp(-i·θ/2·Z⊗X)`。 | +| `density_matrix_noise_apply_crx(ptr, control, target, theta)` | CrX(θ) | 受控 `Rx(θ)`。 | +| `density_matrix_noise_apply_cry(ptr, control, target, theta)` | CrY(θ) | 受控 `Ry(θ)`。 | +| `density_matrix_noise_apply_crz(ptr, control, target, theta)` | CrZ(θ) | 受控 `Rz(θ)`。 | +| `density_matrix_noise_apply_fsim(ptr, q0, q1, theta, phi)` | FSIM | 费米子模拟门,`theta` 为 iSWAP 角,`phi` 为受控相位角。 | + +### 三比特门 + +| 函数 | 门 | 说明 | +| --- | --- | --- | +| `density_matrix_noise_apply_ccx(ptr, c0, c1, target)` | CCX | Toffoli 门,两个控制位同时为 `\|1>` 时翻转目标位。 | + +### 通用门入口 + +#### density_matrix_noise_apply_standard_gate_noise(ptr, gate_name, qubits, qubit_len, params, param_len) + +```c +int32_t density_matrix_noise_apply_standard_gate_noise(struct CDensityMatrixNoise *ptr, + const char *gate_name, + const uint32_t *qubits, + uintptr_t qubit_len, + const double *params, + uintptr_t param_len); +``` + +按名称施加标准门(如 `"H"`、`"CX"`、`"RZZ"`、`"GPhase"`、`"FSIM"`):先施加理想门,再施加噪声模型中匹配的门噪声。 + +- `ptr` (`CDensityMatrixNoise*`):噪声模拟器句柄。 +- `gate_name` (`const char*`):NUL 结尾的标准门名。 +- `qubits` (`const uint32_t*`):目标比特数组,长度为 `qubit_len`;`qubit_len` 为 0 时可为 NULL。 +- `qubit_len` (`uintptr_t`):目标比特数量,必须与门的要求一致。 +- `params` (`const double*`):门参数数组;`param_len` 为 0 时可为 NULL。 +- `param_len` (`uintptr_t`):参数数量,必须与门的要求一致。 + +返回:`0` 成功;`-1` NULL 指针;`-2` 比特越界;`-4` 门名不是合法 UTF-8;`-8` 未知门名、比特数或参数个数与门不符、参数值非有限。 + +#### density_matrix_noise_apply_unitary_gate(ptr, qubits, qubit_len, matrix, matrix_dim) + +```c +int32_t density_matrix_noise_apply_unitary_gate(struct CDensityMatrixNoise *ptr, + const uint32_t *qubits, + uintptr_t qubit_len, + const Complex64 *matrix, + uintptr_t matrix_dim); +``` + +按 `ρ → U ρ U†` 作用任意酉矩阵。通用酉门没有对应的标准门类型,施加时不注入门噪声;需要噪声建模时使用具体的门函数。 + +- `ptr` (`CDensityMatrixNoise*`):噪声模拟器句柄。 +- `qubits` (`const uint32_t*`):目标比特数组,不得重复。 +- `qubit_len` (`uintptr_t`):目标比特数量。 +- `matrix` (`const Complex64*`):行主序 `matrix_dim × matrix_dim` 矩阵。 +- `matrix_dim` (`uintptr_t`):矩阵维度,必须等于 `2^qubit_len`。 + +返回:`0` 成功;`-1` NULL 指针;`-2` 比特越界;`-8` `qubit_len` 或 `matrix_dim` 为 0、维度不匹配或比特重复。 + +--- + +## 概率与读出噪声 + +模拟器把「量子态的噪声」与「读出的噪声」分开表达:门噪声作用于密度矩阵本身;读出误差只改变报告出来的概率分布。 + +### density_matrix_noise_probabilities_len(ptr) + +```c +uintptr_t density_matrix_noise_probabilities_len(const struct CDensityMatrixNoise *ptr); +``` + +返回计算基态概率分布的长度(`2^N`)。 + +- `ptr` (`const CDensityMatrixNoise*`):噪声模拟器句柄。 + +返回:概率数量;`NULL` 时返回 0。 + +### density_matrix_noise_probabilities(ptr, buffer, len) + +```c +int32_t density_matrix_noise_probabilities(const struct CDensityMatrixNoise *ptr, + double *buffer, + uintptr_t len); +``` + +把理想概率分布(密度矩阵对角元,含已施加的门噪声、不含读出误差)拷贝进 `buffer`。 + +- `ptr` (`const CDensityMatrixNoise*`):噪声模拟器句柄。 +- `buffer` (`double*`):输出缓冲。 +- `len` (`uintptr_t`):缓冲长度,必须等于 `density_matrix_noise_probabilities_len`。 + +返回:`0` 成功;`-1` NULL 指针;`-8` `len` 与实际数量不符。 + +### density_matrix_noise_probabilities_with_readout_len(ptr) + +```c +uintptr_t density_matrix_noise_probabilities_with_readout_len(const struct CDensityMatrixNoise *ptr); +``` + +返回含读出误差概率分布的缓冲长度(`2^N`);缓冲大小与传给 `density_matrix_noise_probabilities_with_readout` 的比特选择无关。 + +- `ptr` (`const CDensityMatrixNoise*`):噪声模拟器句柄。 + +返回:概率数量;`NULL` 时返回 0。 + +### density_matrix_noise_probabilities_with_readout(ptr, buffer, len, qubits, qubit_len) + +```c +int32_t density_matrix_noise_probabilities_with_readout(const struct CDensityMatrixNoise *ptr, + double *buffer, + uintptr_t len, + const uint32_t *qubits, + uintptr_t qubit_len); +``` + +在理想概率分布的基础上,对 `qubits` 列出的比特施加噪声模型中登记的读出误差,结果拷贝进 `buffer`。 + +- `ptr` (`const CDensityMatrixNoise*`):噪声模拟器句柄。 +- `buffer` (`double*`):输出缓冲。 +- `len` (`uintptr_t`):缓冲长度,必须等于 `density_matrix_noise_probabilities_with_readout_len`。 +- `qubits` (`const uint32_t*`):施加读出误差的比特数组;`qubit_len` 为 0 时可为 NULL,此时结果与 `density_matrix_noise_probabilities` 一致。 +- `qubit_len` (`uintptr_t`):比特数量。 + +返回:`0` 成功;`-1` NULL 指针;`-2` 比特越界;`-8` `len` 与实际数量不符。 + +--- + +## 测量、重置与采样 + +### density_matrix_noise_measure(ptr, qubit) + +```c +int32_t density_matrix_noise_measure(struct CDensityMatrixNoise *ptr, uint32_t qubit); +``` + +在 Z 基下测量指定比特并使底层密度矩阵坍缩;测量操作本身不施加噪声。 + +- `ptr` (`CDensityMatrixNoise*`):噪声模拟器句柄。 +- `qubit` (`uint32_t`):被测比特。 + +返回:`0` 表示结果 `|0>`,`1` 表示结果 `|1>`;`-1` NULL;`-2` 比特越界。 + +### density_matrix_noise_measure_all(ptr) + +```c +char *density_matrix_noise_measure_all(struct CDensityMatrixNoise *ptr); +``` + +按 `0..num_qubits` 顺序逐比特测量并使态坍缩,返回大端比特串(MSB = 比特 `N-1`,LSB = 比特 `0`)。 + +- `ptr` (`CDensityMatrixNoise*`):噪声模拟器句柄。 + +返回:堆分配的 C 字符串,用 `cqlib_string_free` 释放;失败返回 `NULL`。 + +### density_matrix_noise_reset(ptr, qubit) + +```c +int32_t density_matrix_noise_reset(struct CDensityMatrixNoise *ptr, uint32_t qubit); +``` + +把指定比特重置为 `|0>`;重置不施加读出误差。 + +- `ptr` (`CDensityMatrixNoise*`):噪声模拟器句柄。 +- `qubit` (`uint32_t`):目标比特。 + +返回:`0` 成功;`-1` NULL;`-2` 比特越界。 + +### density_matrix_noise_sample_shots(ptr, shots) + +```c +struct COutcomeList *density_matrix_noise_sample_shots(const struct CDensityMatrixNoise *ptr, + uintptr_t shots); +``` + +按当前概率分布并行采样 `shots` 次独立测量结果,不修改态;采样的是量子态本身的分布,不含读出误差。返回的列表用 `outcome_list_len` 取数量、`outcome_list_get` 读取每个大端比特串(用 `cqlib_string_free` 释放)、`outcome_list_free` 释放列表,详见 [ClassicalState](5_classical_state.md)。 + +- `ptr` (`const CDensityMatrixNoise*`):噪声模拟器句柄。 +- `shots` (`uintptr_t`):采样次数。 + +返回:新分配的 `COutcomeList*`;失败返回 `NULL`。 + +--- + +## 期望值 + +### density_matrix_noise_expectation(ptr, observable, out) + +```c +int32_t density_matrix_noise_expectation(const struct CDensityMatrixNoise *ptr, + const struct CHamiltonian *observable, + double *out); +``` + +计算 ` = Tr(H·ρ)` 并写入 `out`;结果含已施加的门噪声,不含读出误差(读出误差不影响量子态)。 + +- `ptr` (`const CDensityMatrixNoise*`):噪声模拟器句柄。 +- `observable` (`const CHamiltonian*`):可观测量句柄,见 [Hamiltonian](7_hamiltonian.md)。 +- `out` (`double*`):输出指针。 + +返回:`0` 成功;`-1` NULL 指针;`-8` 观测量的比特数与模拟器不一致。 + +--- + +## 噪声模型的配置 + +噪声模型句柄 `CNoiseModel` 由设备模块提供,构造与登记接口包括 `noise_model_new`、`noise_model_add_single_qubit`(按 `NOISE_BIT_FLIP` 等标签选择信道)、`noise_model_add_single_qubit_pauli`、`noise_model_add_two_qubit`(`NOISE_TWO_DEPOLARIZING`)与 `noise_model_add_readout`(非对称读出误差 `p_0_given_1` / `p_1_given_0`),详见 [NoiseModel](../2_device/4_noise.md)。 + +--- + +## 示例 + +### 1. 含门噪声与读出噪声的 Bell 态 + +```c +#include +#include "cqlib_c.h" + +int main(void) { + /* 5% depolarizing on CX(0,1), 10% readout error on qubit 0 */ + struct CNoiseModel *model = noise_model_new(); + noise_model_add_two_qubit(model, "CX", 0, 1, NOISE_TWO_DEPOLARIZING, 0.05); + noise_model_add_readout(model, 0, 0.0, 0.1); + + struct CDensityMatrixNoise *sim = density_matrix_noise_new(2, model); + density_matrix_noise_apply_h(sim, 0); + density_matrix_noise_apply_cx(sim, 0, 1); + + /* Ideal distribution: gate noise only, no readout error */ + double ideal[4]; + density_matrix_noise_probabilities(sim, ideal, 4); + printf("ideal P(00)=%.3f P(11)=%.3f\n", ideal[0], ideal[3]); + + /* With readout error applied to qubit 0 */ + double with_ro[4]; + uint32_t measured[1] = {0}; + density_matrix_noise_probabilities_with_readout(sim, with_ro, 4, measured, 1); + printf("readout P(00)=%.3f P(11)=%.3f\n", with_ro[0], with_ro[3]); + + density_matrix_noise_free(sim); + noise_model_free(model); + return 0; +} +``` + +### 2. 由线路构造并求期望值 + +```c +struct CCircuit *qc = circuit_new(2); +circuit_h(qc, 0); +circuit_cx(qc, 0, 1); + +struct CNoiseModel *model = noise_model_new(); +noise_model_add_two_qubit(model, "CX", 0, 1, NOISE_TWO_DEPOLARIZING, 0.05); + +struct CDensityMatrixNoise *sim = density_matrix_noise_from_circuit(qc, model); + +/* stays close to 1 under weak depolarizing noise */ +struct CPauliString *zz = pauli_string_parse("ZZ"); +struct CHamiltonian *h = hamiltonian_from_pauli(zz); /* takes ownership of zz */ +double exp = 0.0; +density_matrix_noise_expectation(sim, h, &exp); +printf("=%.3f\n", exp); +hamiltonian_free(h); + +density_matrix_noise_free(sim); +noise_model_free(model); +circuit_free(qc); +``` diff --git a/docs/api/cn/c/3_qis/4_stabilizer.md b/docs/api/cn/c/3_qis/4_stabilizer.md new file mode 100644 index 00000000..080a61b5 --- /dev/null +++ b/docs/api/cn/c/3_qis/4_stabilizer.md @@ -0,0 +1,249 @@ +# 稳定器(C) + +`CStabilizerState` 是稳定子态模拟器的不透明句柄:用一组相互对易的 Pauli 算子(稳定子生成元)描述量子态,只能表示 Clifford 线路可达的稳定子态,可描述的比特规模远大于稠密表示;遇到任意角旋转、`T` 门等非 Clifford 操作时返回错误。错误码、字符串与句柄的释放约定遵循 [Overview](../0_overview.md)。 + +--- + +## 构造与释放 + +### stabilizer_new(num_qubits) + +构造 `|0...0>` 稳定子态,初始生成元为每个比特的 destabilizer `X_i` 与 stabilizer `Z_i`,相位均为 `+1`。 + +参数: + +- `num_qubits` (`uintptr_t`):量子比特数。 + +返回:成功返回新建的 `CStabilizerState*`;`num_qubits` 为 0 时返回 NULL。 + +### stabilizer_free(ptr) + +释放稳定子态句柄,允许传 NULL。 + +### stabilizer_from_circuit(circuit) + +执行 Clifford 线路并返回最终稳定子态。线路末尾的测量声明被当作输出声明忽略,不使态坍缩;屏障与延迟无副作用。 + +参数: + +- `circuit` (`const CCircuit*`):Clifford 线路,可含 `I`、`H`、`X`、`Y`、`Z`、`S`、`SDG`、`X2P`、`X2M`、`Y2P`、`Y2M`、`CX`、`CY`、`CZ`、`SWAP` 与 `Reset`。 + +返回:成功返回新建的 `CStabilizerState*`;NULL 输入、线路含非 Clifford 门或执行失败时返回 NULL。 + +### stabilizer_run_circuit(circuit) + +从 `|0...0>` 出发执行 Clifford 线路并返回最终态。与 `stabilizer_from_circuit` 不同,该入口会执行 `MeasureBit` / `MeasureBits` 操作,返回的态已经坍缩。 + +参数: + +- `circuit` (`const CCircuit*`):Clifford 线路。 + +返回:成功返回已坍缩的 `CStabilizerState*`;NULL 输入或执行失败时返回 NULL。 + +--- + +## 线路作用 + +### stabilizer_apply_circuit(ptr, circuit) + +把 Clifford 线路原地作用到当前态上。 + +参数: + +- `ptr` (`CStabilizerState*`):稳定子态句柄。 +- `circuit` (`const CCircuit*`):Clifford 线路,比特数必须与态一致。 + +返回:0 成功;-1 空指针;-3 线路含无法执行的门或未解析的符号参数;-7 含控制流等不支持的操作;-8 线路比特数与态不一致。 + +--- + +## 单比特 Clifford 门 + +以下入口把对应门原地作用到 `qubit` 上,门名列给出每个入口的语义: + +| 门名 | 函数 | 说明 | +| --- | --- | --- | +| `H` | `stabilizer_apply_h(ptr, qubit)` | Hadamard 门。 | +| `X` | `stabilizer_apply_x(ptr, qubit)` | Pauli-X。 | +| `Y` | `stabilizer_apply_y(ptr, qubit)` | Pauli-Y。 | +| `Z` | `stabilizer_apply_z(ptr, qubit)` | Pauli-Z。 | +| `S` | `stabilizer_apply_s(ptr, qubit)` | S 门。 | +| `SDG` | `stabilizer_apply_sdg(ptr, qubit)` | S† 门。 | +| `X2P` | `stabilizer_apply_x2p(ptr, qubit)` | √X,即 Rx(π/2)。 | +| `X2M` | `stabilizer_apply_x2m(ptr, qubit)` | √X†,即 Rx(-π/2)。 | +| `Y2P` | `stabilizer_apply_y2p(ptr, qubit)` | √Y,即 Ry(π/2)。 | +| `Y2M` | `stabilizer_apply_y2m(ptr, qubit)` | √Y†,即 Ry(-π/2)。 | + +参数: + +- `ptr` (`CStabilizerState*`):稳定子态句柄。 +- `qubit` (`uint32_t`):目标比特下标。 + +返回:0 成功;-1 空指针;-2 比特下标越界;-3 门无法执行。 + +--- + +## 双比特 Clifford 门 + +| 门名 | 函数 | 说明 | +| --- | --- | --- | +| `CX` | `stabilizer_apply_cx(ptr, control, target)` | 受控 X 门。 | +| `CY` | `stabilizer_apply_cy(ptr, control, target)` | 受控 Y 门。 | +| `CZ` | `stabilizer_apply_cz(ptr, q0, q1)` | 受控 Z 门。 | +| `SWAP` | `stabilizer_apply_swap(ptr, q0, q1)` | 交换两个比特。 | + +参数: + +- `ptr` (`CStabilizerState*`):稳定子态句柄。 +- `control` / `target` / `q0` / `q1` (`uint32_t`):目标比特下标。 + +返回:0 成功;-1 空指针;-2 比特下标越界;-8 两个比特相同。 + +### stabilizer_apply_standard_gate(ptr, gate_name, qubits, num_target_qubits, params, num_params) + +按名称作用 Clifford 标准门(如 `"H"`、`"CX"`、`"SWAP"`)。 + +参数: + +- `ptr` (`CStabilizerState*`):稳定子态句柄。 +- `gate_name` (`const char*`):标准门名称。 +- `qubits` (`const uint32_t*`):目标比特下标数组,共 `num_target_qubits` 个。 +- `num_target_qubits` (`uintptr_t`):目标比特数。 +- `params` (`const double*`):门参数数组,共 `num_params` 个;`num_params` 为 0 时允许传 NULL。 +- `num_params` (`uintptr_t`):门参数个数。 + +返回:0 成功;-1 空指针;-2 比特越界;-8 未知门名或参数个数不符;-7 非 Clifford 门。 + +--- + +## 测量、重置与采样 + +### stabilizer_measure(ptr, qubit) + +在 Z 基下测量 `qubit` 并使态坍缩。 + +返回:测量结果 `0` 或 `1`;-1 空指针;-2 比特下标越界。 + +### stabilizer_measure_all(ptr) + +按顺序测量所有比特并使态坍缩。 + +返回:大端位串(最左字符是比特 N-1,最右字符是比特 0),用 `cqlib_string_free` 释放;NULL 输入返回 NULL。 + +### stabilizer_reset(ptr, qubit) + +把 `qubit` 重置为 `|0>`(一次 Z 基测量,结果为 `|1>` 时补一个 X 修正)。 + +返回:0 成功;-1 空指针;-2 比特下标越界。 + +### stabilizer_sample_shots(ptr, shots) + +并行采样 `shots` 次独立测量结果;同一初始态总是产生相同的样本集合。 + +参数: + +- `ptr` (`const CStabilizerState*`):稳定子态句柄。 +- `shots` (`uintptr_t`):采样次数。 + +返回:`COutcomeList*`,用 `outcome_list_free` 释放;NULL 输入返回 NULL。位串遍历见 [经典态](5_classical_state.md)。 + +--- + +## 概率分布 + +### stabilizer_probabilities_len(ptr) / stabilizer_probabilities(ptr, buffer, len) + +两步式读取全部计算基态上的概率分布:`*_len` 返回长度 `2^N`(NULL 返回 0),填充接口把分布拷入 `buffer`,下标 `i` 对应二进制表示为 `i` 的基态(比特 0 为最低位)。该接口只适用于小规模系统。 + +返回(填充接口):0 成功;-1 空指针;-8 `len` 与 `2^N` 不符或系统规模超限。 + +### stabilizer_probability_of(ptr, bits, len, out) + +返回指定计算基态的测量概率:与稳定子态相容的位串返回 `1/2^k`(`k` 为测量结果随机的比特数),不相容时返回 0。该调用不修改态。 + +参数: + +- `ptr` (`const CStabilizerState*`):稳定子态句柄。 +- `bits` (`const bool*`):`len` 个布尔值,`bits[0]` 是比特 0。 +- `len` (`uintptr_t`):必须等于比特数。 +- `out` (`double*`):概率输出。 + +返回:0 成功;-1 空指针;-8 长度不符。 + +--- + +## 生成元与 Pauli 期望值 + +### stabilizer_num_qubits(ptr) + +返回比特数;NULL 返回 0。 + +### stabilizer_get_stabilizers_len(ptr) / stabilizer_get_stabilizer(ptr, index) + +读取稳定子生成元:`*_len` 返回生成元个数(恒为 N),`get_stabilizer` 返回第 `index` 个生成元的 `"+XYZ"` 形式 Pauli 标签(比特序约定见 [Pauli](6_pauli.md)),用 `cqlib_string_free` 释放;越界返回 NULL。 + +### stabilizer_get_destabilizers_len(ptr) / stabilizer_get_destabilizer(ptr, index) + +读取 destabilizer 生成元:`*_len` 返回生成元个数(恒为 N),`get_destabilizer` 返回第 `index` 个生成元的 Pauli 标签,格式与 `stabilizer_get_stabilizer` 相同,用 `cqlib_string_free` 释放;越界返回 NULL。 + +### stabilizer_pauli_expectation(ptr, pauli, out) + +计算 Pauli 期望值 ⟨P⟩:`1` 表示 `P` 属于稳定子群、`-1` 表示属于取负的稳定子群、`0` 表示两者皆非。该判定覆盖生成元之积,不只针对单个生成元。 + +参数: + +- `ptr` (`const CStabilizerState*`):稳定子态句柄。 +- `pauli` (`const CPauliString*`):Pauli 串,比特数必须与态一致。 +- `out` (`int32_t*`):期望值输出(-1、0 或 +1)。 + +返回:0 成功;-1 空指针;-8 Pauli 串比特数与态不一致。 + +### stabilizer_to_stim_format(ptr) + +按 Stim 兼容文本格式导出生成元表(每行一个 `"+XYZ"` Pauli 标签)。 + +返回:C 字符串,用 `cqlib_string_free` 释放;NULL 输入返回 NULL。 + +--- + +## 示例 + +```c +#include +#include "cqlib_c.h" + +int main(void) { + /* 1. Bell 态:H(0) + CX(0, 1) */ + struct CStabilizerState *s = stabilizer_new(2); + if (s == NULL) { + return 1; + } + stabilizer_apply_h(s, 0); + stabilizer_apply_cx(s, 0, 1); + + /* 2. 读取生成元标签("+XY" 形式)与 Pauli 期望值 */ + char *stab = stabilizer_get_stabilizer(s, 0); + struct CPauliString *zz = pauli_string_parse("ZZ"); + int32_t e_zz = 0; + stabilizer_pauli_expectation(s, zz, &e_zz); /* 1:ZZ 是 Bell 态稳定子 */ + cqlib_string_free(stab); + pauli_string_free(zz); + + /* 3. 指定基态概率:|00⟩ 与 |11⟩ 各占一半 */ + bool bits[2] = {false, false}; + double p00 = 0.0; + stabilizer_probability_of(s, bits, 2, &p00); /* 0.5 */ + + /* 4. 采样 500 次并遍历 OutcomeList */ + struct COutcomeList *shots = stabilizer_sample_shots(s, 500); + uintptr_t n = outcome_list_len(shots); /* 500 */ + for (uintptr_t i = 0; i < n; i++) { + char *bs = outcome_list_get(shots, i); /* "00" 或 "11" */ + cqlib_string_free(bs); + } + outcome_list_free(shots); + + stabilizer_free(s); + return 0; +} +``` diff --git a/docs/api/cn/c/3_qis/5_classical_state.md b/docs/api/cn/c/3_qis/5_classical_state.md new file mode 100644 index 00000000..b8f45e70 --- /dev/null +++ b/docs/api/cn/c/3_qis/5_classical_state.md @@ -0,0 +1,164 @@ +# 经典态(C) + +经典态页面覆盖三类句柄:测量声明 `CMeasurement`(把测量比特与结果比特序绑定在一起)、测量产生的不可变经典值 `CClassicalValue`,以及采样结果列表 `COutcomeList`。错误码、字符串与句柄的释放约定遵循 [Overview](../0_overview.md)。 + +--- + +## 测量声明 + +### measurement_new(value_index, ty_tag, ty_width, qubits, len) + +由原始组件创建独立测量声明:`qubits[0]` 成为结果比特 0(最低有效位),`qubits[1]` 成为结果比特 1,依此类推。 + +该构造面向自包含的比特序工具(`measurement_project`、`measurement_project_basis`、`measurement_check_qubits`):其内部经典值使用全新的线路身份构建,不属于任何线路,由此派生的表达式会被 `circuit_validate_classical_expr` 拒绝;线路集成的测量用 `circuit_measure` / `circuit_measure_bits` 等构造(见 [经典数据与控制流](../0_circuit/9_classical_control_flow.md))。 + +参数: + +- `value_index` (`uint32_t`):值表下标。 +- `ty_tag` (`uint32_t`):结果类型标签,取 `CQLIB_CLASSICAL_TYPE_*` 之一(`Bit` 与 `BitVec` 是直接的测量目标类型)。 +- `ty_width` (`uint32_t`):结果类型宽度。 +- `qubits` (`const uint32_t*`):被测比特下标数组,按结果比特序排列。 +- `len` (`uintptr_t`):被测比特数。 + +返回:成功返回新建的 `CMeasurement*`;`qubits` 为 NULL、列表为空或类型标签非法时返回 NULL。 + +### measurement_free(ptr) + +释放测量声明句柄,允许传 NULL。 + +### measurement_value(ptr) + +返回该测量产生的不可变经典值。 + +返回:新建的 `CClassicalValue*`(用 `classical_value_free` 释放);NULL 输入返回 NULL。 + +### measurement_expr(ptr) + +创建读取该测量不可变值的表达式。 + +返回:新建的 `CClassicalExpr*`(用 `classical_expr_free` 释放);NULL 输入返回 NULL。 + +### measurement_qubits_len(ptr) / measurement_qubits(ptr, buffer, len) + +两步式读取被测比特下标(按结果比特序):`*_len` 返回个数(NULL 返回 0),填充接口把下标拷入 `buffer`。 + +返回(填充接口):0 成功;-1 空指针;-8 `len` 与 `measurement_qubits_len` 不符。 + +### measurement_width(ptr) + +返回被测比特数(即结果位数);NULL 返回 0。 + +### measurement_ty(ptr, tag, width) + +把结果类型写到出参。 + +返回:0 成功(`*tag` 为 `CQLIB_CLASSICAL_TYPE_*` 之一,`*width` 为类型宽度);-1 空指针。 + +### measurement_check_qubits(ptr, num_qubits) + +校验全部被测比特对 `num_qubits` 比特的态均有效。 + +返回:0 成功;-1 空指针;-2 存在越界比特下标。 + +### measurement_project(ptr, bitstring) + +把全寄存器测量结果投影到该测量的比特序上:`bitstring` 是覆盖整个寄存器的大端 `'0'`/`'1'` 串(最左字符为最高比特,与 `statevector_measure_all` 输出一致);若 `qubits[i]` 在结果中为 1,则结果比特 `i` 置 1。 + +返回:`measurement_width` 位的大端位串,用 `cqlib_string_free` 释放;NULL 输入、非法 UTF-8 或非法位串返回 NULL。 + +### measurement_project_basis(ptr, basis) + +把计算基下标投影到该测量的比特序上:若 `basis` 的比特 `qubits[i]` 为 1,则结果比特 `i` 置 1。 + +返回:`measurement_width` 位的大端位串,用 `cqlib_string_free` 释放;NULL 输入返回 NULL。 + +--- + +## 经典值 + +### classical_value_free(ptr) + +释放经典值句柄,允许传 NULL。 + +### classical_value_index(ptr) + +返回线路局部值表下标;NULL 返回 `UINT32_MAX`。 + +### classical_value_ty(ptr, tag, width) + +把值类型写到出参。 + +返回:0 成功(`*tag` 为 `CQLIB_CLASSICAL_TYPE_*` 之一,`*width` 为类型宽度);-1 空指针。 + +### classical_value_expr(ptr) + +创建读取该不可变运行期值的表达式。 + +返回:新建的 `CClassicalExpr*`(用 `classical_expr_free` 释放);NULL 输入返回 NULL。 + +--- + +## 采样结果列表 + +`*_sample_shots` 类接口(见 [态矢量](1_statevector.md)、[稳定器](4_stabilizer.md) 等)返回 `COutcomeList*`:每个元素是大端二进制位串,最左字符对应比特 N-1、最右字符对应比特 0。 + +### outcome_list_free(ptr) + +释放采样结果列表,允许传 NULL。 + +### outcome_list_len(ptr) + +返回位串个数;NULL 返回 0。 + +### outcome_list_get(ptr, index) + +返回下标 `index` 处的位串。 + +返回:堆上 C 字符串,用 `cqlib_string_free` 释放;越界返回 NULL。 + +--- + +## 示例 + +测量声明 → 模拟采样 → 遍历 OutcomeList 的完整流程: + +```c +#include +#include "cqlib_c.h" + +int main(void) { + /* 1. 构造 Bell 线路并声明测量 */ + struct CCircuit *qc = circuit_new(2); + circuit_h(qc, 0); + circuit_cx(qc, 0, 1); + if (circuit_measure(qc, 0) != 0) { + circuit_free(qc); + return 1; + } + + /* 2. 精确模拟后采样 100 次 */ + struct CStatevector *sv = statevector_from_circuit(qc); + struct COutcomeList *shots = statevector_sample_shots(sv, 100); + + /* 3. 遍历 OutcomeList:位串为大端(最左字符是比特 N-1) */ + uintptr_t n = outcome_list_len(shots); /* 100 */ + for (uintptr_t i = 0; i < n; i++) { + char *bs = outcome_list_get(shots, i); /* "00" 或 "11" */ + printf("%s\n", bs); + cqlib_string_free(bs); + } + + outcome_list_free(shots); + statevector_free(sv); + circuit_free(qc); + + /* 4. 独立测量声明:qubits[0] 为结果比特 0,全寄存器投影 */ + uint32_t qubits[2] = {1, 0}; + struct CMeasurement *m = + measurement_new(0, CQLIB_CLASSICAL_TYPE_BIT_VEC, 2, qubits, 2); + char *proj = measurement_project(m, "01"); /* "10" */ + cqlib_string_free(proj); + measurement_free(m); + return 0; +} +``` diff --git a/docs/api/cn/c/3_qis/6_pauli.md b/docs/api/cn/c/3_qis/6_pauli.md new file mode 100644 index 00000000..5079672e --- /dev/null +++ b/docs/api/cn/c/3_qis/6_pauli.md @@ -0,0 +1,206 @@ +# Pauli(C) + +`CPauliString` 是 Pauli 串的不透明句柄:N 个比特上各带一个 `I`/`X`/`Y`/`Z` 算子并附一个相位。单比特算子用 `PAULI_*` 常量表示,串的文本格式为 `[+|-][i|j]<算子序列>`。错误码、字符串与句柄的释放约定遵循 [Overview](../0_overview.md)。 + +--- + +## 常量与格式约定 + +单比特 Pauli 算子标签: + +| 常量 | 值 | 算子 | +| --- | --- | --- | +| `PAULI_I` | 0 | 单位算符 I。 | +| `PAULI_X` | 1 | Pauli X。 | +| `PAULI_Y` | 2 | Pauli Y。 | +| `PAULI_Z` | 3 | Pauli Z。 | + +文本格式与比特序: + +- 格式为 `[+|-][i|j]<算子序列>`,算子为 `I`、`X`、`Y`、`Z`;相位可取 `+`、`-`、`+i`(可写作 `+j`)、`-i`(可写作 `-j`),省略相位时默认 `+`。 +- 比特序与阅读顺序相反:**字符串的第一个字符对应最高比特下标**。例如 `"XZI"` 解析后比特 2 为 `X`、比特 1 为 `Z`、比特 0 为 `I`;`pauli_string_to_string` 输出时沿用同一约定(如 `"+XZI"`)。 + +--- + +## 单比特算子乘法 + +### pauli_mul_with_phase(left, right, out_pauli, out_phase) + +```c +int32_t pauli_mul_with_phase(uint8_t left, uint8_t right, uint8_t *out_pauli, Complex64 *out_phase); +``` + +将两个单比特 Pauli 算子按 `left * right` 相乘,把结果算子标签(`PAULI_I/X/Y/Z` 之一)写入 `*out_pauli`,把 Pauli 群相位因子(`1`、`i`、`-1`、`-i` 之一)写入 `*out_phase`;例如 `X * Z = -iY`,`Z * X = iY`。 + +返回:`0` 成功;`-1` 输出指针为 NULL;`-8` 输入标签不是 `PAULI_I/X/Y/Z` 之一。 + +--- + +## 构造与释放 + +### pauli_string_new(num_qubits) + +构造全 `I` 的 Pauli 串。 + +参数: + +- `num_qubits` (`uintptr_t`):量子比特数。 + +返回:成功返回新建的 `CPauliString*`;`num_qubits` 为 0 时返回 NULL。 + +### pauli_string_free(ptr) + +释放 Pauli 串句柄,允许传 NULL。 + +### pauli_string_parse(source) + +按 `[+|-][i|j]<算子序列>` 格式解析 C 字符串(如 `"XYZ"`、`"+ZZI"`、`"-iZII"`)。 + +返回:成功返回新建的 `CPauliString*`;NULL 输入、非法 UTF-8 或解析失败时返回 NULL。 + +--- + +## 算子读写 + +### pauli_string_set_pauli(ptr, idx, pauli) + +把比特 `idx` 上的算子设为 `pauli`(`PAULI_*` 之一)。 + +返回:0 成功;-1 空指针;-2 `idx` 越界;-8 非法算子标签。 + +### pauli_string_get_pauli(ptr, idx) + +读取比特 `idx` 上的算子。 + +返回:`PAULI_I`/`PAULI_X`/`PAULI_Y`/`PAULI_Z` 之一;NULL 输入或越界返回 255(`0xFF`)。 + +### pauli_string_try_get_pauli(ptr, idx, out) + +非 panic 版本的算子读取:把算子标签写到 `*out`。 + +返回:0 成功;-1 空指针;-2 越界。 + +### pauli_string_try_set_pauli(ptr, idx, pauli) + +非 panic 版本的算子写入:`pauli` 必须是 `PAULI_*` 之一。 + +返回:0 成功;-1 空指针;-2 越界;-8 非法算子标签。 + +--- + +## 属性查询 + +### pauli_string_num_qubits(ptr) + +返回 Pauli 串作用的比特数;NULL 返回 0。 + +### pauli_string_to_string(ptr) + +把 Pauli 串格式化为 `"+XYZ"` 形式的 C 字符串(首个字符对应最高比特)。 + +返回:堆上字符串,用 `cqlib_string_free` 释放;NULL 输入返回 NULL。 + +### pauli_string_x_mask(ptr) + +返回 X 分量位掩码:比特 `i` 带 `X` 或 `Y` 算子时位 `i` 置 1;NULL 返回 0。 + +### pauli_string_z_mask(ptr) + +返回 Z 分量位掩码:比特 `i` 带 `Z` 或 `Y` 算子时位 `i` 置 1;NULL 返回 0。 + +### pauli_string_y_phase(ptr, out) + +计算串中 n 个 `Y` 算子贡献的相位因子 `i^n`。 + +返回:0 成功(复数值写入 `*out`);-1 空指针。 + +### pauli_string_support_len(ptr) / pauli_string_support(ptr, buffer, len) + +两步式读取支集:`*_len` 返回非单位算子的比特个数(NULL 返回 0),填充接口把支集下标(升序)作为 `uint32_t` 拷入 `buffer`。 + +返回(填充接口):0 成功;-1 空指针;-8 `len` 与 `pauli_string_support_len` 不符。 + +--- + +## 对易与矩阵 + +### pauli_string_commutes_with(ptr, other) + +判断两个 Pauli 串是否对易。 + +返回:1 对易;0 不对易;-1 空指针;-8 两者比特数不同。 + +### pauli_string_matrix_len(ptr) + +返回稠密矩阵的边长 `2^N`;NULL 返回 0。 + +### pauli_string_matrix(ptr, buffer, len) + +把 `2^N × 2^N` 稠密矩阵按行主序拷入 `buffer`(`Complex64` 值)。`len` 必须等于 `pauli_string_matrix_len(ptr) * pauli_string_matrix_len(ptr)`。 + +返回:0 成功;-1 空指针;-8 `len` 与矩阵元素数不符。 + +--- + +## 期望值 + +### pauli_string_expectation(ptr, bitstrings, probs, len, out) + +在计算基概率分布上计算 Pauli 串的期望值 `⟨P⟩ = Σ_s p(s)·⟨s|P|s⟩`,分布以并行数组给出:`bitstrings[i]` 与 `probs[i]` 一一对应。 + +约定: + +- 每个比特串采用小端约定(最右字符对应比特 0),且必须恰好为 `num_qubits` 个 `'0'`/`'1'` 字符。 +- 串中含 `X` 或 `Y` 算子时结果恒为 `0.0`。 +- 串中仅含 `I` 与 `Z` 时按公式 `phase × Σ_s p(s) × (-1)^(Σ_i z[i]·s[i])` 求值,其中 `phase` 为串的全局相位,`z[i]` 为比特 `i` 的 Z 分量。 + +参数: + +- `ptr` (`const struct CPauliString*`):Pauli 串句柄。 +- `bitstrings` (`const char* const*`):NUL 结尾比特串数组;`len` 为 0 时可为 NULL。 +- `probs` (`const double*`):与 `bitstrings` 平行的概率数组;`len` 为 0 时可为 NULL。 +- `len` (`uintptr_t`):分布条目数量。 +- `out` (`double*`):输出指针。 + +返回:0 成功;-1 `ptr` 或 `out` 为 NULL,或 `len > 0` 时任一数组为 NULL;-4 比特串不是合法 UTF-8;-7 比特串长度不符或含 `'0'`/`'1'` 之外的字符;-8 比特串数组含 NULL 元素。 + +--- + +## 示例 + +```c +#include +#include +#include "cqlib_c.h" + +int main(void) { + /* 1. 解析:首字符对应最高比特,"XZI" -> 比特2=X、比特1=Z、比特0=I */ + struct CPauliString *ps = pauli_string_parse("XZI"); + uintptr_t n = pauli_string_num_qubits(ps); /* 3 */ + uint8_t p2 = pauli_string_get_pauli(ps, 2); /* PAULI_X (1) */ + uint8_t p0 = pauli_string_get_pauli(ps, 0); /* PAULI_I (0) */ + + /* 2. 掩码与支集 */ + uintptr_t x_mask = pauli_string_x_mask(ps); /* bit 2 -> 0b100 */ + uintptr_t z_mask = pauli_string_z_mask(ps); /* bit 1 -> 0b010 */ + uintptr_t support_len = pauli_string_support_len(ps); /* 2 */ + uint32_t support[2]; + pauli_string_support(ps, support, support_len); /* {1, 2} */ + + /* 3. 文本输出与对易判断 */ + char *text = pauli_string_to_string(ps); /* "+XZI" */ + struct CPauliString *zz = pauli_string_parse("ZZI"); + int32_t commutes = pauli_string_commutes_with(ps, zz); /* 0(比特 2 上 X 与 Z 反对易) */ + + /* 4. 两步式读取 8x8 稠密矩阵 */ + uintptr_t dim = pauli_string_matrix_len(ps); /* 8 */ + Complex64 *matrix = malloc(dim * dim * sizeof(Complex64)); + pauli_string_matrix(ps, matrix, dim * dim); + free(matrix); + + cqlib_string_free(text); + pauli_string_free(zz); + pauli_string_free(ps); + return 0; +} +``` diff --git a/docs/api/cn/c/3_qis/7_hamiltonian.md b/docs/api/cn/c/3_qis/7_hamiltonian.md new file mode 100644 index 00000000..ad066c5f --- /dev/null +++ b/docs/api/cn/c/3_qis/7_hamiltonian.md @@ -0,0 +1,282 @@ +# 哈密顿量(C) + +`CHamiltonian` 是可观测量(Hamiltonian)的不透明句柄:若干带复系数的 Pauli 串项之和,支持化简、缩放、稠密矩阵输出与时间演化线路构造。错误码、字符串与句柄的释放约定遵循 [Overview](../0_overview.md)。 + +--- + +## 常量 + +Trotter-Suzuki 分解模式标签(`hamiltonian_to_trotter_circuit` 与 `hamiltonian_to_evolution_circuit` 的 `mode` 参数): + +| 常量 | 值 | 模式 | +| --- | --- | --- | +| `TROTTER_MODE_FIRST_ORDER` | 0 | 一阶 Lie-Trotter。 | +| `TROTTER_MODE_SECOND_ORDER` | 1 | 二阶 Strang 分裂。 | + +--- + +## 构造与释放 + +### hamiltonian_new(num_qubits) + +创建作用在 `num_qubits` 比特上的空 Hamiltonian(无任何 Pauli 项)。 + +参数: + +- `num_qubits` (`uintptr_t`):量子比特数。 + +返回:新建的 `CHamiltonian*`。 + +### hamiltonian_free(ptr) + +释放 Hamiltonian 句柄,允许传 NULL。 + +### hamiltonian_from_pauli(pauli) + +由单个 Pauli 串构造 Hamiltonian(系数为 1.0)。 + +**所有权约定**:该接口接管入参 `CPauliString*` 的所有权并释放它;调用之后不得再使用或释放原句柄。 + +参数: + +- `pauli` (`CPauliString*`):Pauli 串句柄,所有权被接管。 + +返回:成功返回新建的 `CHamiltonian*`;NULL 输入返回 NULL。 + +### hamiltonian_add_term(ptr, pauli, coeff_re, coeff_im) + +添加一个带复系数的 Pauli 项。 + +**所有权约定**:Pauli 串被克隆,入参句柄不被消耗,调用方继续负责释放。 + +参数: + +- `ptr` (`CHamiltonian*`):Hamiltonian 句柄。 +- `pauli` (`const CPauliString*`):Pauli 串句柄(克隆使用)。 +- `coeff_re` (`double`):系数实部。 +- `coeff_im` (`double`):系数虚部。 + +返回:0 成功;-1 空指针;-8 Pauli 串比特数与 Hamiltonian 不一致。 + +### hamiltonian_simplify(ptr) + +原地化简:合并 Pauli 串相同的项并移除系数近似为零的项。 + +返回:0 成功;-1 空指针。 + +### hamiltonian_scale(ptr, factor_re, factor_im) + +原地把全部系数乘以一个复因子。 + +参数: + +- `factor_re` (`double`):因子实部。 +- `factor_im` (`double`):因子虚部。 + +返回:0 成功;-1 空指针;-8 因子非有限值(NaN 或无穷)。 + +--- + +## 查询 + +### hamiltonian_num_qubits(ptr) + +返回比特数;NULL 返回 0。 + +### hamiltonian_num_terms(ptr) + +返回 Pauli 项数;NULL 返回 0。 + +### hamiltonian_all_terms_commute(ptr) + +判断全部 Pauli 项是否两两对易。全部对易时,时间演化可精确分解为逐项 Pauli 旋转。 + +返回:1 全部对易;0 存在不对易项;-1 空指针。 + +--- + +## 稠密矩阵输出 + +### hamiltonian_to_matrix_len(ptr) / hamiltonian_to_matrix(ptr, buffer, len) + +两步式读取稠密矩阵:`*_len` 返回边长 `2^N`(NULL 返回 0),填充接口把 `2^N × 2^N` 矩阵按行主序拷入 `buffer`(`Complex64` 值)。`len` 必须等于边长的平方。 + +返回(填充接口):0 成功;-1 空指针;-8 `len` 与矩阵元素数不符。 + +--- + +## 期望值与方差 + +### CProbMeasurement + +`hamiltonian_expectation_probs` 使用的测量描述符:一个被测基及其观测到的结果分布。 + +```c +typedef struct CProbMeasurement { + const struct CPauliString *basis; /* 借用,不消耗 */ + const char *const *states; /* len 个计算基位串 */ + const double *probs; /* len 个概率,与 states 按下标配对 */ + uintptr_t len; /* states 与 probs 的条目数 */ +} CProbMeasurement; +``` + +- `basis` (`const CPauliString*`):作为测量基的 Pauli 串;借用,不消耗,由调用方负责释放。 +- `states` (`const char* const*`):`len` 个计算基位串组成的数组,每个量子比特一个 `'0'`/`'1'` 字符,大端序(量子比特 0 是最后一个字符)。 +- `probs` (`const double*`):`len` 个观测概率组成的数组,与 `states` 按下标配对。 +- `len` (`uintptr_t`):`states` 与 `probs` 的条目数。 + +### hamiltonian_expectation_probs(ptr, measurements, measurements_len, out) + +由测量结果概率计算 Hamiltonian 的期望值。`measurements` 每个条目对应一个被测基;Hamiltonian 的每一项在第一个与之兼容的被测基中求值——兼容指被测基在该项的所有非恒等因子上与之一致。结果写入 `out`。分布只需覆盖观测到的结果,未列出的结果贡献为零。 + +```c +int32_t hamiltonian_expectation_probs(const struct CHamiltonian *ptr, + const struct CProbMeasurement *measurements, + uintptr_t measurements_len, + double *out); +``` + +参数: + +- `ptr` (`const CHamiltonian*`):Hamiltonian 句柄。 +- `measurements` (`const CProbMeasurement*`):`measurements_len` 个测量描述符组成的数组。 +- `measurements_len` (`uintptr_t`):`measurements` 的条目数;`0` 表示未提供任何被测基。 +- `out` (`double*`):接收期望值。 + +错误码: + +| 值 | 场景 | +| --- | --- | +| `0` | 成功。 | +| `-1` | `ptr` 或 `out` 为 NULL;`measurements` 非零条目数时为 NULL;或条目内 `basis` 为 NULL,或 `len` 非零时 `states`/`probs`/位串为 NULL。 | +| `-4` | `states` 中的位串不是有效 UTF-8。 | +| `-7` | 某一项没有兼容的测量基、位串格式非法或长度与 Hamiltonian 比特数不符,或其他核心错误。 | + +### hamiltonian_variance_statevector(ptr, sv, out) + +计算 Hamiltonian 在态矢量下的方差 `Var(H) = - ^2`,写入 `out`。 + +```c +int32_t hamiltonian_variance_statevector(const struct CHamiltonian *ptr, + const struct CStatevector *sv, + double *out); +``` + +参数: + +- `ptr` (`const CHamiltonian*`):Hamiltonian 句柄。 +- `sv` (`const CStatevector*`):输入态矢量,比特数必须与 Hamiltonian 一致。 +- `out` (`double*`):接收方差。 + +错误码: + +| 值 | 场景 | +| --- | --- | +| `0` | 成功。 | +| `-1` | `ptr`、`sv` 或 `out` 为 NULL。 | +| `-7` | Hamiltonian 含非厄米项,或其他模拟错误。 | +| `-8` | 态矢量比特数与 Hamiltonian 不一致,或其他非法参数。 | + +--- + +## 时间演化线路 + +### hamiltonian_to_trotter_circuit(ptr, time, steps, mode) + +把 Hamiltonian 转换为 Trotter 化时间演化线路,逼近 `U(t) = exp(-i H t)`。 + +参数: + +- `ptr` (`const CHamiltonian*`):Hamiltonian 句柄。 +- `time` (`double`):演化时间,必须为有限值。 +- `steps` (`uintptr_t`):Trotter 步数。 +- `mode` (`uint32_t`):`TROTTER_MODE_FIRST_ORDER` 或 `TROTTER_MODE_SECOND_ORDER`。 + +返回:成功返回新建的 `CCircuit*`(用 `circuit_free` 释放);空指针、非有限时间、非法模式或构造失败时返回 NULL。 + +### hamiltonian_to_evolution_circuit(ptr, time, steps, mode) + +把 Hamiltonian 转换为时间演化线路:全部项相互对易时使用精确的单趟分解,否则回退到 Trotter 近似(`mode`、`steps`)。 + +参数: + +- `ptr` (`const CHamiltonian*`):Hamiltonian 句柄。 +- `time` (`double`):演化时间,必须为有限值。 +- `steps` (`uintptr_t`):Trotter 步数。 +- `mode` (`uint32_t`):`TROTTER_MODE_FIRST_ORDER` 或 `TROTTER_MODE_SECOND_ORDER`。 + +返回:成功返回新建的 `CCircuit*`(用 `circuit_free` 释放);空指针、非有限时间、非法模式或构造失败时返回 NULL。 + +--- + +## 示例 + +```c +#include +#include +#include "cqlib_c.h" + +int main(void) { + /* 1. 构造 H = 0.5 * ZI + 0.5 * IZ("ZI":比特1=Z、比特0=I) */ + struct CHamiltonian *ham = hamiltonian_new(2); + struct CPauliString *zi = pauli_string_parse("ZI"); + struct CPauliString *iz = pauli_string_parse("IZ"); + hamiltonian_add_term(ham, zi, 0.5, 0.0); /* 克隆,句柄保留 */ + hamiltonian_add_term(ham, iz, 0.5, 0.0); + pauli_string_free(zi); + pauli_string_free(iz); + + /* 2. 查询与化简 */ + uintptr_t nq = hamiltonian_num_qubits(ham); /* 2 */ + uintptr_t nt = hamiltonian_num_terms(ham); /* 2 */ + int32_t commute = hamiltonian_all_terms_commute(ham); /* 1(两单项对易) */ + hamiltonian_simplify(ham); + + /* 3. 两步式读取 4x4 稠密矩阵 */ + uintptr_t dim = hamiltonian_to_matrix_len(ham); /* 4 */ + Complex64 *matrix = malloc(dim * dim * sizeof(Complex64)); + hamiltonian_to_matrix(ham, matrix, dim * dim); + free(matrix); + + /* 4. 时间演化线路(对易时精确分解) */ + struct CCircuit *evo = + hamiltonian_to_evolution_circuit(ham, 0.1, 1, TROTTER_MODE_FIRST_ORDER); + if (evo != NULL) { + circuit_free(evo); + } + + /* 5. 由单个 Pauli 串构造(接管所有权) */ + struct CPauliString *zz = pauli_string_parse("ZZ"); + struct CHamiltonian *from_pauli = hamiltonian_from_pauli(zz); /* zz 已被接管 */ + + /* 6. 由测量结果概率计算期望值,并计算态矢量方差 + (H = Z⊗Z 作用于贝尔态,P(00) = P(11) = 0.5) */ + struct CStatevector *bell = statevector_new(2); + statevector_apply_h(bell, 0); + statevector_apply_cx(bell, 0, 1); + + struct CPauliString *basis = pauli_string_parse("ZZ"); + const char *states[4] = {"00", "01", "10", "11"}; + double probs[4] = {0.5, 0.0, 0.0, 0.5}; + struct CProbMeasurement measurement; + measurement.basis = basis; /* 借用,不消耗 */ + measurement.states = states; + measurement.probs = probs; + measurement.len = 4; + + double expectation = 0.0; + hamiltonian_expectation_probs(from_pauli, &measurement, 1, &expectation); /* 1.0 */ + + double variance = 0.0; + hamiltonian_variance_statevector(from_pauli, bell, &variance); /* 0.0(Z⊗Z 的本征态) */ + + pauli_string_free(basis); + statevector_free(bell); + + hamiltonian_free(from_pauli); + hamiltonian_free(ham); + return 0; +} +``` + +哈密顿量作为可观测量用于误差缓解流程(见 [零噪声外推](../6_error_mitigation/1_zne.md)、[虚拟蒸馏](../6_error_mitigation/2_virtual_distillation.md)),其期望值可直接由态矢量模拟器计算(见 [态矢量](1_statevector.md))。 diff --git a/docs/api/cn/c/3_qis/8_evolution.md b/docs/api/cn/c/3_qis/8_evolution.md new file mode 100644 index 00000000..9933959a --- /dev/null +++ b/docs/api/cn/c/3_qis/8_evolution.md @@ -0,0 +1,94 @@ +# Pauli 演化(C) + +本页介绍把单条 Pauli 串 `P` 指数化为演化算符 `e^(-i·θ/2·P)` 并追加到线路的接口。整条哈密顿量的时间演化线路(Trotter 分解,以及精确分解与 Trotter 近似间的自动选择)由 [Hamiltonian](7_hamiltonian.md) 页的 `hamiltonian_to_trotter_circuit` 与 `hamiltonian_to_evolution_circuit` 提供。错误码、句柄与字符串的释放约定遵循 [Overview](../0_overview.md)。 + +--- + +## circuit_pauli_evolution(circuit, pauli, angle, qubits, qubits_len) + +把演化算符 `e^(-i·θ/2·P)` 追加到线路末尾,`θ` 即 `angle`。 + +```c +int32_t circuit_pauli_evolution(struct CCircuit *circuit, + const struct CPauliString *pauli, + double angle, + const uint32_t *qubits, + uintptr_t qubits_len); +``` + +生成的门序列: + +1. **相位校验**:Pauli 串的相位必须为 ±1(Hermitian),相位为 ±i 时无法作为酉演化的生成元,返回 `-3`。 +2. **相位吸收**:把 ±1 相位吸收进旋转角,得到有效角 `θ_eff = θ · phase`。 +3. **基变换**:对每个非恒等比特施加把分量转到 Z 基的门——`X` 用 `H`,`Y` 先用 `S†` 再用 `H`,`Z` 不需要变换。 +4. **CNOT 链**:按非恒等比特下标递增的顺序施加 `CX`,逐级累积奇偶性。 +5. **核心旋转**:在最后一个非恒等比特上施加 `RZ(θ_eff)`。 +6. **逆序 CNOT 链与逆基变换**,把比特恢复到原来的基。 + +当 Pauli 串全为恒等算子时,演化退化为全局相位 `e^(-i·θ/2)`,不产生任何门,相位记在线路的全局相位上。 + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `circuit` | `CCircuit*` | 目标线路句柄,演化门追加到其末尾。 | +| `pauli` | `const CPauliString*` | 要指数化的 Pauli 串 `P`,构造方式见 [Pauli](6_pauli.md)。 | +| `angle` | `double` | 旋转角 `θ`(固定浮点值)。 | +| `qubits` | `const uint32_t*` | 作用位置数组,按 Pauli 串比特顺序排列。 | +| `qubits_len` | `uintptr_t` | 作用位置数量,必须等于 `pauli_string_num_qubits(pauli)`。 | + +返回值(错误码): + +| 返回值 | 场景 | +| --- | --- | +| `0` | 成功。 | +| `-1` | `circuit` 或 `pauli` 为 NULL;或 `qubits_len > 0` 而 `qubits` 为 NULL。 | +| `-2` | `qubits` 中存在越界的比特编号。 | +| `-3` | 位置数量与 Pauli 串比特数不一致,或 Pauli 串相位为 ±i。 | +| `-8` | `angle` 为非有限值(NaN 或无穷)。 | + +--- + +## 示例 + +```c +#include +#include +#include "cqlib_c.h" + +int main(void) { + /* Append e^(-i * pi/4 * XZ): basis changes, a CNOT ladder, + one RZ, and the inverse ladder. */ + CCircuit* circuit = circuit_new(2); + CPauliString* pauli = pauli_string_parse("XZ"); + assert(pauli != NULL); + + const uint32_t qubits[2] = {0, 1}; + assert(circuit_pauli_evolution(circuit, pauli, 1.5707963267948966, qubits, 2) == 0); + assert(circuit_num_operations(circuit) > 0); + + /* NULL arguments are rejected. */ + assert(circuit_pauli_evolution(NULL, pauli, 1.0, qubits, 2) == -1); + + /* Out-of-bounds qubit index. */ + const uint32_t bad[2] = {0, 5}; + assert(circuit_pauli_evolution(circuit, pauli, 1.0, bad, 2) == -2); + + /* Position-count mismatch: a 3-qubit Pauli string on 2 positions. */ + CPauliString* big = pauli_string_parse("XXX"); + assert(circuit_pauli_evolution(circuit, big, 1.0, qubits, 2) == -3); + + pauli_string_free(big); + pauli_string_free(pauli); + circuit_free(circuit); + return 0; +} +``` + +--- + +## 相关页面 + +- [QIS 概览](0_overview.md):模块总览与共享约定。 +- [Pauli](6_pauli.md):Pauli 串的字符串解析与相位取值。 +- [Hamiltonian](7_hamiltonian.md):整条哈密顿量的 Trotter 演化与精确演化入口。 diff --git a/docs/api/cn/c/3_qis/9_metrics_entropy.md b/docs/api/cn/c/3_qis/9_metrics_entropy.md new file mode 100644 index 00000000..356f072f --- /dev/null +++ b/docs/api/cn/c/3_qis/9_metrics_entropy.md @@ -0,0 +1,190 @@ +# 度量与熵(C) + +本页覆盖量子信息度量(纯度、保真度、迹距离)与熵及纠缠度量(Von Neumann 熵、Renyi 熵、纠缠熵、负性、并发度、生成纠缠度),输入为态矢量 `CStatevector` 或密度矩阵 `CDensityMatrix` 句柄。错误码、字符串与句柄的释放约定遵循 [Overview](../0_overview.md)。 + +--- + +## 共同约定 + +- 所有函数返回 `int32_t`:0 成功(数值写入 `out`),-1 空指针,其余负值为模拟层错误(典型 -8:维度不符、非归一化输入、非双比特输入)。 +- 子系统参数(`qubits`/`subsys_a`/`sys_a` + `len`):`len` 为 0 表示空子系统,合法;`len > 0` 且指针为 NULL 返回 -1。 +- `density_matrix_partial_transpose` 返回新句柄;其余函数通过 out 指针写出数值。 + +--- + +## 纯度与保真度 + +### statevector_purity(sv, out) + +计算态矢量的纯度范数(归一化纯态为 1.0)。 + +返回:0 成功;-1 空指针。 + +### density_matrix_purity(dm, out) + +计算密度矩阵的纯度 `Tr(rho^2)`。 + +返回:0 成功;-1 空指针。 + +### statevector_fidelity(sv1, sv2, out) + +计算两个态矢量之间的态保真度 `||^2`。 + +返回:0 成功;-1 空指针。 + +### statevector_fidelity_pure_mixed(sv, dm, out) + +计算纯态与混合态之间的保真度 ``。 + +返回:0 成功;-1 空指针。 + +### density_matrix_fidelity(dm1, dm2, out) + +计算两个密度矩阵之间的态保真度 `(Tr sqrt(sqrt(rho) sigma sqrt(rho)))^2`。 + +返回:0 成功;-1 空指针。 + +--- + +## 迹距离 + +### statevector_trace_distance(sv1, sv2, out) + +计算两个纯态之间的迹距离 `sqrt(1 - ||^2)`。 + +返回:0 成功;-1 空指针。 + +### density_matrix_trace_distance(dm1, dm2, out) + +计算两个密度矩阵之间的迹距离 `1/2 Tr|rho - sigma|`。 + +返回:0 成功;-1 空指针。 + +--- + +## 熵 + +### density_matrix_entropy(dm, out) + +计算密度矩阵的 Von Neumann 熵 `-Tr(rho log2 rho)`(单位比特)。 + +返回:0 成功;-1 空指针。 + +### density_matrix_linear_entropy(dm, out) + +计算线性熵 `S_L = 1 - Tr(rho^2)`。 + +返回:0 成功;-1 空指针。 + +### density_matrix_renyi_entropy(dm, alpha, out) + +计算阶数 `alpha` 的 Renyi 熵(以 2 为底)。`alpha` 必须为正的有限值;接近 1 时回退到 Von Neumann 熵。 + +参数: + +- `alpha` (`double`):Renyi 阶数。 + +返回:0 成功;-1 空指针;-8 `alpha` 非有限值或非正。 + +--- + +## 纠缠度量 + +### statevector_entanglement_entropy(sv, subsys_a, len, out) + +计算纯态相对子系统 A 的二分纠缠熵(单位比特):`subsys_a` 为 `len` 个比特下标。 + +返回:0 成功;-1 空指针;-8 比特下标或维度非法。 + +### statevector_entanglement_entropy_pure(sv, subsys_a, len, out) + +计算态矢量相对子系统 A 的纯态二分纠缠熵(以 2 为底,单位比特):`subsys_a` 为 `len` 个比特下标;Bell 态取子系统 `{0}` 时结果为 `1.0`。 + +返回:0 成功;-1 空指针;-8 比特下标或维度非法。 + +### density_matrix_negativity(dm, subsys_a, len, out) + +计算二分密度矩阵的负性纠缠度量:对子系统 A 转置后求迹范数。 + +返回:0 成功;-1 空指针;-8 比特下标或维度非法。 + +### density_matrix_logarithmic_negativity(dm, sys_a, len, out) + +计算对数负性 `log2 ||rho^{T_A}||_1`(子系统 A 由 `sys_a` 指定)。 + +返回:0 成功;-1 空指针;-8 比特下标或维度非法。 + +### density_matrix_concurrence(dm, out) + +计算双比特密度矩阵的并发度:可分离态为 0,最大纠缠态为 1。 + +返回:0 成功;-1 空指针;-8 输入不是双比特系统。 + +### density_matrix_entanglement_of_formation(dm, out) + +计算双比特密度矩阵的生成纠缠度(单位比特),由并发度导出。 + +返回:0 成功;-1 空指针;-8 输入不是双比特系统。 + +--- + +## 部分转置 + +### density_matrix_partial_transpose(dm, qubits, len) + +对 `qubits`(`len` 个下标)执行部分转置,返回新密度矩阵。 + +返回:成功返回新建的 `CDensityMatrix*`(用 `density_matrix_free` 释放);空指针(含 `len > 0` 且数组为 NULL)或失败返回 NULL。 + +--- + +## 示例 + +```c +#include +#include "cqlib_c.h" + +int main(void) { + /* 1. Bell 态与其密度矩阵 */ + struct CCircuit *qc = circuit_new(2); + circuit_h(qc, 0); + circuit_cx(qc, 0, 1); + struct CStatevector *sv = statevector_from_circuit(qc); + struct CDensityMatrix *dm = density_matrix_from_circuit(qc); + + /* 2. 纯度与保真度 */ + double purity = 0.0; + density_matrix_purity(dm, &purity); /* 1.0:纯态 */ + + struct CCircuit *qc2 = circuit_new(2); + circuit_h(qc2, 0); + circuit_cx(qc2, 0, 1); + struct CStatevector *sv2 = statevector_from_circuit(qc2); + double fid = 0.0; + statevector_fidelity(sv, sv2, &fid); /* 1.0:同一状态 */ + + /* 3. Von Neumann 熵与纠缠熵 */ + double entropy = 0.0; + density_matrix_entropy(dm, &entropy); /* 0.0:纯态 */ + + uint32_t subsystem[1] = {0}; + double entanglement = 0.0; + statevector_entanglement_entropy(sv, subsystem, 1, &entanglement); /* 1.0 比特 */ + + /* 4. 部分转置与负性 */ + double negativity = 0.0; + density_matrix_negativity(dm, subsystem, 1, &negativity); /* 0.5 */ + + struct CDensityMatrix *pt = density_matrix_partial_transpose(dm, subsystem, 1); + if (pt != NULL) { + density_matrix_free(pt); + } + + statevector_free(sv2); + circuit_free(qc2); + density_matrix_free(dm); + statevector_free(sv); + circuit_free(qc); + return 0; +} +``` diff --git a/docs/api/cn/c/4_compile/0_overview.md b/docs/api/cn/c/4_compile/0_overview.md new file mode 100644 index 00000000..993d95d7 --- /dev/null +++ b/docs/api/cn/c/4_compile/0_overview.md @@ -0,0 +1,111 @@ +# 编译优化(C) + +编译模块以自由函数 + 不透明句柄(`CCompileResult`)的形式提供线路优化工作流:按 `CompileConfigC` 配置运行逻辑编译,或针对 `CDevice` 设备目标运行路由与原生门降级,并把优化后的线路、变更标记与逐步骤报告写入编译结果。错误码、字符串与句柄的释放约定遵循 [Overview](../0_overview.md)。 + +--- + +## 概念关系 + +- `compile` 按值接收 `CompileConfigC`:`mode` 选择普通或增强模式,`target` 选择编译目标;非 `Logical` 目标按逻辑编译处理。 +- `compile_with_device` 是设备目标入口:线路在设备拓扑上路由,并降级到设备的有序原生门集,使结果线路始终被该设备接受;可选的初始布局与随机种子控制映射过程。 +- `CCompileResult` 承载优化后的线路(深拷贝取出)、整体变更标记、编译模式与逐步骤报告;设备目标编译额外记录初始与最终布局(`CLayout`,见 [Layout](../2_device/3_layout.md))。 + +--- + +## 页面导航 + +| 页面 | 内容 | +| --- | --- | +| [Compiler](1_compiler.md) | `compile` / `compile_with_device` 与 `CCompileResult` 的构造、结果读取、步骤报告与布局读取。 | +| [Transform](2_transform.md) | 独立变换 pass:结构分析 `circuit_analyze`、规范化 `canonicalize_circuit`、知识重写 `rewrite_circuit` 与 `transform_*` 封装。 | +| [Initial Layout](3_layout.md) | 初始布局算法 `trivial_layout` / `greedy_layout` / `vf2_perfect_layout` / `sabre_layout`、线路交互分析、物理布局图、SABRE 预备对象与布局结果读取。 | +| [Routing](4_routing.md) | 路由入口 `route_with_layout` / `route_sabre`、`CRoutedCircuit` 路由结果与布局可达性校验。 | +| [SABRE Configuration](5_sabre.md) | `SabreConfigC` 配置族与已知初始布局条件下的 `sabre_route` 路由。 | +| [Decompose / Resynthesis](6_decompose_resynthesis.md) | 定义展开、酉门合成、多控门分解与双比特块重综合。 | +| [Knowledge](7_knowledge.md) | 知识规则的构造、校验与结构匹配。 | +| [Resource](8_resource.md) | Ancilla 资源策略、上限与租借管理。 | +| [Commutation](9_commutation.md) | 操作间对易性的保守检查。 | + +--- + +## CompileConfigC + +`compile` 的输入配置,按值传递: + +```c +typedef struct CompileConfigC { + uint8_t mode; /* One of COMPILE_MODE_*. */ + uint8_t target; /* One of COMPILE_TARGET_*. */ + uint8_t allow_dirty_ancilla; /* Reserved for future use. */ + uint8_t allow_clean_ancilla; /* Reserved for future use. */ + uint8_t _reserved[4]; /* Reserved padding. */ +} CompileConfigC; +``` + +| 字段 | 类型 | 含义 | +| --- | --- | --- | +| `mode` | `uint8_t` | 编译模式,取 `COMPILE_MODE_*` 之一。 | +| `target` | `uint8_t` | 编译目标,取 `COMPILE_TARGET_*` 之一;`Logical` 目标只读取 `mode`。 | +| `allow_dirty_ancilla` | `uint8_t` | 保留字段(资源策略标志),当前不参与编译。 | +| `allow_clean_ancilla` | `uint8_t` | 保留字段(资源策略标志),当前不参与编译。 | +| `_reserved[4]` | `uint8_t[4]` | 填充字段,保持结构体布局稳定,勿写入非零值。 | + +`Logical` 目标只使用 `mode`;其余目标值按逻辑编译处理。全部字段保留在结构体中以便后续无 ABI 破坏地扩展。 + +--- + +## 常量 + +编译模式标签(`CompileConfigC.mode` 与 `compile_with_device` 的 `mode` 参数): + +| 常量 | 值 | 模式 | +| --- | --- | --- | +| `COMPILE_MODE_NORMAL` | 0 | 普通编译模式。 | +| `COMPILE_MODE_ENHANCED` | 1 | 增强编译模式。 | + +编译目标标签(`CompileConfigC.target`): + +| 常量 | 值 | 目标 | +| --- | --- | --- | +| `COMPILE_TARGET_LOGICAL` | 0 | 逻辑比特目标。 | +| `COMPILE_TARGET_BASIS` | 1 | 基门集目标。 | +| `COMPILE_TARGET_DEVICE` | 2 | 设备目标。 | +| `COMPILE_TARGET_TOPOLOGY_BASIS` | 3 | 拓扑基门集目标。 | + +--- + +## 示例 + +```c +#include +#include "cqlib_c.h" + +int main(void) { + struct CCircuit *qc = circuit_new(2); + circuit_h(qc, 0); + circuit_cx(qc, 0, 1); + + /* 增强模式逻辑编译 */ + struct CompileConfigC config = {0}; + config.mode = COMPILE_MODE_ENHANCED; + config.target = COMPILE_TARGET_LOGICAL; + + struct CCompileResult *result = compile(qc, config); + if (result == NULL) { + circuit_free(qc); + return 1; + } + + int32_t changed = compile_result_changed(result); /* 1:工作流改动了线路 */ + struct CCircuit *optimized = compile_result_circuit(result); + if (optimized != NULL) { + circuit_free(optimized); /* 深拷贝,独立于 result 释放 */ + } + + compile_result_free(result); + circuit_free(qc); + return 0; +} +``` + +设备目标编译(含路由与布局读取)的完整流程见 [Compiler](1_compiler.md)。 diff --git a/docs/api/cn/c/4_compile/1_compiler.md b/docs/api/cn/c/4_compile/1_compiler.md new file mode 100644 index 00000000..4b1805b4 --- /dev/null +++ b/docs/api/cn/c/4_compile/1_compiler.md @@ -0,0 +1,344 @@ +# 编译器(C) + +本页覆盖编译入口 `compile` / `compile_with_device` 与编译结果句柄 `CCompileResult` 的全部读取接口:优化线路、变更标记、编译模式、步骤报告与设备布局。错误码、字符串与句柄的释放约定遵循 [Overview](../0_overview.md);配置结构与 `COMPILE_*` 常量见 [Overview](0_overview.md)。 + +--- + +## 编译入口 + +### compile(circuit, config) + +按 `CompileConfigC` 配置运行编译工作流:`config.mode` 选择普通或增强模式;`config.target` 选择 `Logical` 目标,其他目标值按逻辑编译处理。 + +参数: + +- `circuit` (`const CCircuit*`):输入线路。 +- `config` (`struct CompileConfigC`):编译配置(按值传递)。 + +返回:成功返回新建的 `CCompileResult*`;空指针或编译失败返回 NULL。 + +### compile_with_device(circuit, mode, device, initial_layout, seed) + +针对具体设备目标运行编译工作流:线路在设备拓扑上路由,并降级到设备的有序原生门集,结果线路始终被该设备接受。 + +参数: + +- `circuit` (`const CCircuit*`):输入线路。 +- `mode` (`uint8_t`):编译模式,取 `COMPILE_MODE_*` 之一。 +- `device` (`const CDevice*`):目标设备(构造方式见 [设备与噪声属性](../2_device/2_properties_device.md))。 +- `initial_layout` (`const CLayout*`):可选的初始逻辑→物理映射;NULL 时由工作流自行选择。 +- `seed` (`int64_t`):设备布局/路由的确定性种子;负值表示无种子(使用启发式默认值)。 + +返回:成功返回新建的 `CCompileResult*`;空指针或编译失败返回 NULL。 + +### compile_result_free(ptr) + +释放编译结果句柄,允许传 NULL。 + +--- + +## 结果读取 + +### compile_result_circuit(ptr) + +取出结果中保存的优化线路。返回的是深拷贝,可与 `result` 独立释放。 + +返回:成功返回新建的 `CCircuit*`(用 `circuit_free` 释放);空指针或失败返回 NULL。 + +### compile_result_changed(ptr) + +判断工作流是否改动了线路。 + +返回:1 有改动;0 无改动;-1 空指针。 + +### compile_result_mode(ptr) + +返回编译模式。 + +返回:`0`(Normal)、`1`(Enhanced)或 -1(空指针)。 + +### compile_result_initial_layout(ptr) + +读取设备目标编译记录的初始逻辑→物理布局。 + +返回:成功返回新建的 `CLayout*`(用 `layout_free` 释放);结果不含设备元数据(例如逻辑目标编译)或空指针输入时返回 NULL。 + +### compile_result_final_layout(ptr) + +读取设备目标编译记录的最终逻辑→物理布局。 + +返回:成功返回新建的 `CLayout*`(用 `layout_free` 释放);结果不含设备元数据(例如逻辑目标编译)或空指针输入时返回 NULL。 + +--- + +## 步骤报告 + +### compile_result_num_steps(ptr) + +返回结果中的步骤报告数;NULL 返回 0。 + +### compile_result_step_name(ptr, index) + +返回第 `index` 个步骤的名称。 + +返回:堆上 C 字符串,用 `cqlib_string_free` 释放;越界返回 NULL。 + +### compile_result_step_changed(ptr, index) + +判断第 `index` 个步骤是否改动了线路。 + +返回:1 有改动;0 无改动;-1 出错。 + +### compile_result_step_reason(ptr, index) + +返回第 `index` 个步骤的可选跳过/配置说明。 + +返回:调用方拥有的堆上 C 字符串,用 `cqlib_string_free` 释放。该步骤记录没有原因、`ptr` 为 NULL 或 `index` 越界时返回 NULL。 + +### compile_result_step(ptr, name, out_changed, out_skipped) + +按名称查找第一个匹配的步骤报告,并把该步骤的原始 `changed` 与 `skipped` 标志写到出参。 + +参数: + +- `name` (`const char*`):步骤名称。 +- `out_changed` (`int32_t*`):changed 标志输出。 +- `out_skipped` (`int32_t*`):skipped 标志输出。 + +返回:1 找到匹配步骤;0 无匹配;-1 空指针或非法 UTF-8。 + +--- + +## 可复用工作流(CompilerWorkflow) + +`CCompilerWorkflow` 是可复用的编译工作流句柄:按 `CompileConfigC` 一次构建后,可在多条线路上重复执行,输入线路不会被修改。工作流结果与 `compile` 的结果使用同一套读取接口(见上文"结果读取"与"步骤报告")。 + +### compiler_workflow_new(config) + +```c +struct CCompilerWorkflow *compiler_workflow_new(struct CompileConfigC config); +``` + +由 C 配置创建可复用编译工作流。`config.mode` 选择普通或增强模式;`config.target` 仅支持 `COMPILE_TARGET_LOGICAL`,其他目标值返回 NULL。目标准备在构造时即时校验,非法配置返回 NULL。 + +- `config` (`struct CompileConfigC`):编译配置,按值传递(字段说明见 [Overview](0_overview.md))。 + +返回:成功返回新建的 `CCompilerWorkflow*`(用 `compiler_workflow_free` 释放);`config.mode` 非法、`config.target` 非 `COMPILE_TARGET_LOGICAL` 或配置校验失败返回 NULL。 + +### compiler_workflow_free(ptr) + +```c +void compiler_workflow_free(struct CCompilerWorkflow *ptr); +``` + +释放工作流句柄。允许传 NULL。 + +### compiler_workflow_run(workflow, circuit) + +```c +struct CCompileResult *compiler_workflow_run(const struct CCompilerWorkflow *workflow, + const struct CCircuit *circuit); +``` + +在工作流上运行一条线路。工作流可在不同线路上反复调用,输入线路不被修改。 + +- `workflow` (`const CCompilerWorkflow*`):工作流句柄。 +- `circuit` (`const CCircuit*`):输入线路。 + +返回:成功返回新建的 `CCompileResult*`(用 `compile_result_free` 释放);任一参数为 NULL 或编译失败返回 NULL。 + +--- + +## 设备编译元数据(DeviceCompilationMetadata) + +设备目标编译(`compile_with_device`)在结果中记录设备元数据:初始与最终布局(与 `compile_result_initial_layout` / `compile_result_final_layout` 读取同一份数据),以及虚拟输出置换——原始输出比特到重写后输出比特的对应关系,用于把测量结果映射回原始比特序。逻辑目标编译不携带设备元数据。 + +### compile_result_has_device_metadata(ptr) + +```c +int32_t compile_result_has_device_metadata(const struct CCompileResult *ptr); +``` + +判断结果是否携带设备编译元数据(即设备目标编译)。 + +返回:`1` 有设备元数据;`0` 没有(例如逻辑目标编译);`-1` 空指针。 + +### compile_result_device_metadata_initial_layout(ptr) + +```c +struct CLayout *compile_result_device_metadata_initial_layout(const struct CCompileResult *ptr); +``` + +读取设备目标编译记录的初始逻辑→物理布局(路由开始前)。 + +返回:成功返回新建的 `CLayout*`(用 `layout_free` 释放);结果不含设备元数据或空指针输入时返回 NULL。 + +### compile_result_device_metadata_final_layout(ptr) + +```c +struct CLayout *compile_result_device_metadata_final_layout(const struct CCompileResult *ptr); +``` + +读取设备目标编译记录的最终逻辑→物理布局(全部路由 SWAP 之后)。 + +返回:成功返回新建的 `CLayout*`(用 `layout_free` 释放);结果不含设备元数据或空指针输入时返回 NULL。 + +### compile_result_device_metadata_permutation_len(ptr) + +```c +uintptr_t compile_result_device_metadata_permutation_len(const struct CCompileResult *ptr); +``` + +返回虚拟输出置换的条目数,用于为 `compile_result_device_metadata_permutation` 分配缓冲。 + +返回:置换条目数;空指针输入或结果不含设备元数据时返回 0。 + +### compile_result_device_metadata_permutation(ptr, original, rewritten, len) + +```c +int32_t compile_result_device_metadata_permutation(const struct CCompileResult *ptr, + uint32_t *original, + uint32_t *rewritten, + uintptr_t len); +``` + +两步式读取虚拟输出置换:按原始输出升序把 qubit ID 对拷入 `original` 与 `rewritten`。下标 `i` 记录"原始输出 `original[i]` 的量子态由重写后输出 `rewritten[i]` 承载";`len` 必须等于 `compile_result_device_metadata_permutation_len(ptr)`。 + +- `ptr` (`const CCompileResult*`):编译结果句柄。 +- `original` (`uint32_t*`):原始输出比特 ID 缓冲。 +- `rewritten` (`uint32_t*`):重写后输出比特 ID 缓冲。 +- `len` (`uintptr_t`):条目数。 + +返回:`0` 成功(含无设备元数据且 `len = 0`);`-1` 空指针(`ptr` 为 NULL,或 `len > 0` 而任一缓冲为 NULL);`-8` `len` 与置换条目数不符(含无设备元数据但 `len > 0`)。 + +--- + +## 示例:可复用工作流与设备元数据 + +```c +#include +#include +#include +#include "cqlib_c.h" + +int main(void) { + struct CCircuit *qc = circuit_new(2); + circuit_h(qc, 0); + circuit_cx(qc, 0, 1); + + /* Enhanced-mode logical workflow, built once */ + struct CompileConfigC config; + memset(&config, 0, sizeof(config)); + config.mode = COMPILE_MODE_ENHANCED; + config.target = COMPILE_TARGET_LOGICAL; + struct CCompilerWorkflow *workflow = compiler_workflow_new(config); + if (workflow == NULL) { + circuit_free(qc); + return 1; + } + + /* Run the workflow; it is reusable across circuits */ + struct CCompileResult *result = compiler_workflow_run(workflow, qc); + if (result == NULL) { + compiler_workflow_free(workflow); + circuit_free(qc); + return 1; + } + struct CCompileResult *second = compiler_workflow_run(workflow, qc); + if (second != NULL) { + compile_result_free(second); /* independent result handle */ + } + + /* A logical-target compile carries no device metadata */ + int32_t has_meta = compile_result_has_device_metadata(result); /* 0 */ + struct CLayout *initial = compile_result_device_metadata_initial_layout(result); + if (initial != NULL) { + layout_free(initial); + } + uintptr_t perm_len = compile_result_device_metadata_permutation_len(result); + uint32_t *original = malloc(perm_len * sizeof(uint32_t)); + uint32_t *rewritten = malloc(perm_len * sizeof(uint32_t)); + int32_t rc = compile_result_device_metadata_permutation( + result, original, rewritten, perm_len); /* 0: len == 0 */ + free(original); + free(rewritten); + + compile_result_free(result); + compiler_workflow_free(workflow); + circuit_free(qc); + return 0; +} +``` + +设备目标编译后 `compile_result_has_device_metadata` 返回 1,初始/最终布局与输出置换可用;对应流程见下文"示例"一节的 `compile_with_device` 用法。 + +--- + +## 示例 + +编译线路并读取步骤报告(设备目标流程): + +```c +#include +#include "cqlib_c.h" + +int main(void) { + /* 1. 输入线路与直线拓扑设备 0 - 1 - 2 */ + struct CCircuit *qc = circuit_new(3); + circuit_h(qc, 0); + circuit_cx(qc, 0, 2); /* 非相邻耦合,需要路由 */ + + struct CDevice *dev = device_line("demo", 3); + if (dev == NULL) { + circuit_free(qc); + return 1; + } + + /* 2. 设备目标编译:无初始布局、无种子 */ + struct CCompileResult *result = + compile_with_device(qc, COMPILE_MODE_ENHANCED, dev, NULL, -1); + if (result == NULL) { + device_free(dev); + circuit_free(qc); + return 1; + } + + /* 3. 读取优化线路与整体变更标记 */ + int32_t changed = compile_result_changed(result); /* 1:路由改动了线路 */ + struct CCircuit *optimized = compile_result_circuit(result); + if (optimized != NULL) { + circuit_free(optimized); + } + + /* 4. 遍历步骤报告 */ + uintptr_t steps = compile_result_num_steps(result); + for (uintptr_t i = 0; i < steps; i++) { + char *name = compile_result_step_name(result, i); + int32_t step_changed = compile_result_step_changed(result, i); + printf("step %s: changed=%d\n", name, step_changed); + cqlib_string_free(name); + } + + /* 5. 按名称查找步骤并读取原始标志(步骤名形如 "route.sabre"、"validate.device") */ + int32_t flag_changed = 0, flag_skipped = 0; + int32_t found = compile_result_step(result, "route.sabre", + &flag_changed, &flag_skipped); + + /* 6. 设备编译元数据:初始与最终布局 */ + struct CLayout *initial = compile_result_initial_layout(result); + struct CLayout *final = compile_result_final_layout(result); + if (initial != NULL) { + layout_free(initial); + } + if (final != NULL) { + layout_free(final); + } + + compile_result_free(result); + device_free(dev); + circuit_free(qc); + return 0; +} +``` + +逻辑目标编译(`CompileConfigC` 按值传参)的示例见 [Overview](0_overview.md);布局句柄的读取接口见 [Layout](../2_device/3_layout.md)。 diff --git a/docs/api/cn/c/4_compile/2_transform.md b/docs/api/cn/c/4_compile/2_transform.md new file mode 100644 index 00000000..79d82c8f --- /dev/null +++ b/docs/api/cn/c/4_compile/2_transform.md @@ -0,0 +1,856 @@ +# 线路变换(C) + +本页覆盖编译 transform 阶段的 C ABI:结构分析快照 `CCircuitAnalysis`、句柄式配置 `CTransformConfig` 与 `CCanonicalizeConfig`、规范化 `canonicalize_circuit` / `canonicalize_circuit_with_config`、知识规则重写 `rewrite_circuit`,以及五个独立 pass 封装 `transform_*`。全部接口不修改输入线路:分析快照写入调用方提供的结构体,变换结果以新句柄或新线路返回。错误码、字符串与句柄的释放约定遵循 [Overview](../0_overview.md)。 + +--- + +## 常量 + +重写模式标签(`CRewriteConfig.mode`): + +| 常量 | 值 | 模式 | +| --- | --- | --- | +| `REWRITE_MODE_OPTIMIZE` | 0 | 保守优化(生产默认):接受的替换必须严格改善局部代价。 | +| `REWRITE_MODE_LOWERING` | 1 | 显式降级:分解与硬件原生规则可以局部扩张。 | + +重写规则类别标签(`transform_config_with_enabled_kinds` 与 `transform_config_enabled_kinds` 使用): + +| 常量 | 值 | 类别 | +| --- | --- | --- | +| `REWRITE_KIND_SIMPLIFY` | 0 | 代数化简。 | +| `REWRITE_KIND_CANCEL` | 1 | 互逆 / 重复抵消。 | +| `REWRITE_KIND_MERGE` | 2 | 相邻门合并。 | +| `REWRITE_KIND_COMMUTE` | 3 | 显式对易。 | +| `REWRITE_KIND_DECOMPOSE` | 4 | 分解 / 降级。 | +| `REWRITE_KIND_CANONICALIZE` | 5 | 规范表示。 | +| `REWRITE_KIND_HARDWARE_NATIVE` | 6 | 硬件原生改写。 | +| `REWRITE_KIND_OTHER` | 7 | 其他。 | + +--- + +## 重写配置 + +`CRewriteConfig` 是知识重写的输入配置,按值传递: + +```c +typedef struct CRewriteConfig { + uint8_t mode; /* One of REWRITE_MODE_*. */ + uint8_t max_rounds; /* Fixpoint round limit. */ + uint8_t recurse_control_flow; /* 1 recurses into control-flow bodies. */ + uint8_t skip_labeled_ops; /* 1 skips labeled operations. */ + uint8_t preserve_two_qubit_connectivity; /* 1 preserves undirected 2q connectivity. */ + uint8_t _pad[3]; /* Reserved padding. */ + uintptr_t max_window_ops; /* Matching-window operation limit. */ + uintptr_t max_pattern_len; /* Maximum matched pattern length. */ + const char *const *target_gate_names; /* Borrowed target-basis gate names (NULL = none). */ + uintptr_t target_gate_names_len; /* Number of entries in target_gate_names. */ +} CRewriteConfig; +``` + +| 字段 | 类型 | 含义 | +| --- | --- | --- | +| `mode` | `uint8_t` | 规则集基线,取 `REWRITE_MODE_*` 之一:`OPTIMIZE` 使用生产默认规则,`LOWERING` 追加分解与硬件原生规则。 | +| `max_rounds` | `uint8_t` | 不动点迭代轮数上限。 | +| `recurse_control_flow` | `uint8_t` | 1 时递归进入控制流块内部,0 时不递归。 | +| `skip_labeled_ops` | `uint8_t` | 1 时跳过带标签的操作,0 时不跳过。 | +| `preserve_two_qubit_connectivity` | `uint8_t` | 1 时保持无向二比特连接性(重写不得拆散相邻二比特对),0 时不要求。 | +| `max_window_ops` | `uintptr_t` | 规则匹配窗口内的操作数上限。 | +| `max_pattern_len` | `uintptr_t` | 单条规则可匹配的最大模式长度。 | +| `target_gate_names` | `const char *const *` | 借用的目标基标准门名数组(每个元素为 NUL 结尾字符串);未配置目标基时为 NULL。通过 `rewrite_config_with_target_instructions` 设置。 | +| `target_gate_names_len` | `uintptr_t` | `target_gate_names` 的条目数。 | +| `_pad[3]` | — | 填充字段,保持结构体布局稳定,勿写入非零值。 | + +除 `mode` 外的字段在基线值之上覆盖:以 `rewrite_config_default()` / `rewrite_config_lowering()` 为起点,再修改需要的字段。`target_gate_names` 数组及其字符串是借用而非拷贝:必须保持有效,直到消费该配置的 `rewrite_circuit` / `transform_knowledge_rewrite` 调用结束。 + +### rewrite_config_default() + +```c +struct CRewriteConfig rewrite_config_default(void); +``` + +按值返回生产优化配置(`REWRITE_MODE_OPTIMIZE`):`max_rounds = 8`、`recurse_control_flow = 1`、`skip_labeled_ops = 1`、`preserve_two_qubit_connectivity = 0`、`max_window_ops = 16`、`max_pattern_len = 8`。 + +### rewrite_config_lowering() + +```c +struct CRewriteConfig rewrite_config_lowering(void); +``` + +按值返回显式降级配置:与 `rewrite_config_default()` 相同的字段值,但 `mode = REWRITE_MODE_LOWERING`,规则集追加分解与硬件原生规则。 + +### rewrite_config_with_target_instructions(config, gate_names, len) + +```c +int32_t rewrite_config_with_target_instructions(struct CRewriteConfig *config, + const char *const *gate_names, + uintptr_t len); +``` + +把按值重写配置限制到显式的标准门目标指令基,指令基由 `len` 个门名解析而来(如 `"H"`、`"CX"`)。门名数组是借用的(见上方字段表)并在设置时立即校验;空指令基被拒绝,配置保持不变。目标基只在 `config` 使用 `REWRITE_MODE_LOWERING` 时生效。 + +- `config` (`CRewriteConfig*`):重写配置,就地修改。 +- `gate_names` (`const char *const *`):`len` 个 NUL 结尾标准门名组成的数组。 +- `len` (`uintptr_t`):`gate_names` 的条目数。 + +返回:`0` 成功。 + +| 错误码 | 名称 | 场景 | +| --- | --- | --- | +| `-1` | NullPtr | `config` 为 NULL,或 `len` 大于 0 但 `gate_names` 为 NULL 或含 NULL 条目。 | +| `-4` | ParseError | 某个条目不是合法 UTF-8 或门名未知。 | +| `-6` | CompilerError | 核心拒绝该指令基。 | +| `-8` | InvalidParam | 指令基为空(`len` 为 0);配置保持不变。 | + +--- + +## 变换配置句柄 + +`CTransformConfig` 是知识重写 / 变换配置核心对象的透明句柄,其按值 C 形式为 `CRewriteConfig`。与按值结构体不同,它分配在堆上并就地修改:每个 `transform_config_with_*` 设置器直接改写句柄内的配置并返回 `int32_t` 状态码——设置器不可链式调用。规则类别字段使用[常量](#常量)一节的 `REWRITE_KIND_*` 标签。 + +### transform_config_default() + +```c +struct CTransformConfig *transform_config_default(void); +``` + +创建生产(保守优化)默认的变换配置:`mode = REWRITE_MODE_OPTIMIZE`、`max_rounds = 8`、`max_window_ops = 16`、`max_pattern_len = 8`、启用控制流递归与带标签操作跳过、启用的规则类别为 `SIMPLIFY`、`CANCEL`、`MERGE`、`CANONICALIZE`,未设置目标指令基组。 + +返回:成功返回新建的 `CTransformConfig*`(用 `transform_config_free` 释放);分配失败返回 NULL。 + +### transform_config_lowering() + +```c +struct CTransformConfig *transform_config_lowering(void); +``` + +创建显式知识降级默认的变换配置:与 `transform_config_default()` 相同,但 `mode = REWRITE_MODE_LOWERING`,启用的规则集放宽为六个类别(追加 `DECOMPOSE` 与 `HARDWARE_NATIVE`)。 + +返回:成功返回新建的 `CTransformConfig*`(用 `transform_config_free` 释放);分配失败返回 NULL。 + +### transform_config_free(ptr) + +```c +void transform_config_free(struct CTransformConfig *ptr); +``` + +释放变换配置句柄,允许传 NULL。 + +### transform_config_with_mode(ptr, mode) + +```c +int32_t transform_config_with_mode(struct CTransformConfig *ptr, uint8_t mode); +``` + +就地设置重写模式;`mode` 必须是 `REWRITE_MODE_*` 之一。 + +返回:`0` 成功。 + +| 错误码 | 名称 | 场景 | +| --- | --- | --- | +| `-1` | NullPtr | `ptr` 为 NULL。 | +| `-8` | InvalidParam | `mode` 不是合法的 `REWRITE_MODE_*` 标签。 | + +### transform_config_with_max_rounds(ptr, max_rounds) + +```c +int32_t transform_config_with_max_rounds(struct CTransformConfig *ptr, uint8_t max_rounds); +``` + +就地设置不动点迭代轮数上限。 + +返回:`0` 成功;`-1`(`NullPtr`)`ptr` 为 NULL。 + +### transform_config_with_max_window_ops(ptr, max_window_ops) + +```c +int32_t transform_config_with_max_window_ops(struct CTransformConfig *ptr, + uintptr_t max_window_ops); +``` + +就地设置匹配窗口内的操作数上限。 + +返回:`0` 成功;`-1`(`NullPtr`)`ptr` 为 NULL。 + +### transform_config_with_max_pattern_len(ptr, max_pattern_len) + +```c +int32_t transform_config_with_max_pattern_len(struct CTransformConfig *ptr, + uintptr_t max_pattern_len); +``` + +就地设置单条匹配模式的最大长度。 + +返回:`0` 成功;`-1`(`NullPtr`)`ptr` 为 NULL。 + +### transform_config_with_enabled_kinds(ptr, kinds, len) + +```c +int32_t transform_config_with_enabled_kinds(struct CTransformConfig *ptr, + const uint8_t *kinds, + uintptr_t len); +``` + +就地替换启用的规则类别:`kinds` 指向 `len` 个 `REWRITE_KIND_*` 标签(`len` 为 0 时可为 NULL,表示关闭全部规则类别)。调用被拒绝时配置保持不变。 + +返回:`0` 成功。 + +| 错误码 | 名称 | 场景 | +| --- | --- | --- | +| `-1` | NullPtr | `ptr` 为 NULL,或 `len` 大于 0 但 `kinds` 为 NULL。 | +| `-8` | InvalidParam | `kinds` 中存在不是合法 `REWRITE_KIND_*` 的标签。 | + +### transform_config_with_target_instructions(ptr, gate_names, len) + +```c +int32_t transform_config_with_target_instructions(struct CTransformConfig *ptr, + const char *const *gate_names, + uintptr_t len); +``` + +把配置限制到显式的标准门目标指令基组,基组由 `len` 个门名解析(如 `"H"`、`"CX"`)。空基组被拒绝;调用被拒绝时配置保持不变。 + +返回:`0` 成功。 + +| 错误码 | 名称 | 场景 | +| --- | --- | --- | +| `-1` | NullPtr | `ptr` 为 NULL,或 `len` 大于 0 但 `gate_names` 为 NULL 或含 NULL 条目。 | +| `-4` | ParseError | 某条目非法 UTF-8 或未知门名。 | +| `-6` | CompilerError | 核心拒绝该基组。 | +| `-8` | InvalidParam | 基组为空(`len` 为 0)。 | + +### transform_config_mode(ptr) + +```c +int32_t transform_config_mode(const struct CTransformConfig *ptr); +``` + +返回配置的 `REWRITE_MODE_*` 模式标签。 + +返回:模式标签;NULL 输入返回 `-1`。 + +### transform_config_max_rounds(ptr) + +```c +uint8_t transform_config_max_rounds(const struct CTransformConfig *ptr); +``` + +返回配置的不动点迭代轮数上限。 + +返回:轮数上限;NULL 输入返回 0。 + +### transform_config_max_window_ops(ptr) + +```c +uintptr_t transform_config_max_window_ops(const struct CTransformConfig *ptr); +``` + +返回配置的匹配窗口操作数上限。 + +返回:操作数上限;NULL 输入返回 0。 + +### transform_config_max_pattern_len(ptr) + +```c +uintptr_t transform_config_max_pattern_len(const struct CTransformConfig *ptr); +``` + +返回配置的最大匹配模式长度。 + +返回:模式长度上限;NULL 输入返回 0。 + +### transform_config_recurse_control_flow(ptr) + +```c +int32_t transform_config_recurse_control_flow(const struct CTransformConfig *ptr); +``` + +判断配置是否递归进入控制流块内部。 + +返回:`1` 启用;`0` 未启用;`-1` NULL 输入。 + +### transform_config_skip_labeled_ops(ptr) + +```c +int32_t transform_config_skip_labeled_ops(const struct CTransformConfig *ptr); +``` + +判断配置是否跳过带标签的操作。 + +返回:`1` 启用;`0` 未启用;`-1` NULL 输入。 + +### transform_config_enabled_kinds_len(ptr) / transform_config_enabled_kinds(ptr, out, len) + +```c +uintptr_t transform_config_enabled_kinds_len(const struct CTransformConfig *ptr); +uintptr_t transform_config_enabled_kinds(const struct CTransformConfig *ptr, + uint8_t *out, + uintptr_t len); +``` + +两步式读取启用的规则类别(第一步:`*_len` 返回总数;第二步:填充接口把 `REWRITE_KIND_*` 标签拷入 `out`,实际拷贝 `min(总数, len)` 个)。`out` 为 NULL 时填充接口只返回总数、不拷贝。 + +返回(两者一致):启用的类别总数;NULL 输入返回 0。 + +### transform_config_target_instruction_basis_len(ptr) / transform_config_target_instruction_basis(ptr, out, len) + +```c +uintptr_t transform_config_target_instruction_basis_len(const struct CTransformConfig *ptr); +uintptr_t transform_config_target_instruction_basis(const struct CTransformConfig *ptr, + char **out, + uintptr_t len); +``` + +两步式读取配置的目标基组指令名(未设置目标基组时为 0)。填充接口把门名拷入 `out`,实际拷贝 `min(总数, len)` 个;每个写入条目都是新建的 C 字符串,用 `cqlib_string_free` 释放。`out` 为 NULL 时只返回总数、不拷贝。 + +返回(两者一致):基组指令总数;NULL 输入或未设置目标基组返回 0。 + +--- + +## 结构分析 + +`CCircuitAnalysis` 是线路特征快照,全部标志位取 0 或 1: + +```c +typedef struct CCircuitAnalysis { + uint8_t has_classical_data; + uint8_t has_classical_control; + uint8_t has_measurement; + uint8_t has_classical_values; + uint8_t has_classical_vars; + uint8_t has_runtime_classical; + uint8_t needs_classical_handle_preservation; + uint8_t has_circuit_gate_definitions; + uint8_t has_unitary_circuit_definitions; + uint8_t has_unitary_gates; + uint8_t has_mc_gates; +} CCircuitAnalysis; +``` + +| 字段 | 含义 | +| --- | --- | +| `has_classical_data` | 线路含经典数据操作。 | +| `has_classical_control` | 线路含经典控制流。 | +| `has_measurement` | 线路含测量操作。 | +| `has_classical_values` | 线路注册经典值。 | +| `has_classical_vars` | 线路注册经典变量。 | +| `has_runtime_classical` | 线路使用任意运行时经典构造。 | +| `needs_classical_handle_preservation` | 重写必须为该线路保持经典句柄。 | +| `has_circuit_gate_definitions` | 线路含未展开的线路型自定义门定义。 | +| `has_unitary_circuit_definitions` | 线路含带线路体的酉定义。 | +| `has_unitary_gates` | 线路含矩阵型酉门。 | +| `has_mc_gates` | 线路含多控门。 | + +### circuit_analyze(circuit, out) + +```c +int32_t circuit_analyze(const struct CCircuit *circuit, struct CCircuitAnalysis *out); +``` + +分析线路并把结构特征快照写入 `*out`。 + +- `circuit` (`const CCircuit*`):输入线路。 +- `out` (`CCircuitAnalysis*`):快照输出。 + +返回:`0` 成功;`-1` 任一参数为 NULL。 + +--- + +## 规范化 + +规范化 pass 统一指令形式、折叠全局相位并消除空操作,结果由 `CCanonicalizeResult` 句柄承载。`canonicalize_circuit` 以生产默认配置执行;`canonicalize_circuit_with_config` 使用调用方构造的 `CCanonicalizeConfig` 句柄。 + +### canonicalize_circuit(circuit) + +```c +struct CCanonicalizeResult *canonicalize_circuit(const struct CCircuit *circuit); +``` + +以生产默认配置规范化线路,输入线路不被修改。 + +- `circuit` (`const CCircuit*`):输入线路。 + +返回:成功返回新建的 `CCanonicalizeResult*`(用 `canonicalize_result_free` 释放);NULL 输入或执行失败返回 NULL。 + +### canonicalize_circuit_with_config(circuit, config) + +```c +struct CCanonicalizeResult *canonicalize_circuit_with_config(const struct CCircuit *circuit, + const struct CCanonicalizeConfig *config); +``` + +按给定配置规范化线路;输入线路不被修改,配置也不被消耗——配置仍归调用方所有,可重复使用。 + +- `circuit` (`const CCircuit*`):输入线路。 +- `config` (`const CCanonicalizeConfig*`):规范化配置句柄(见下文)。 + +返回:成功返回新建的 `CCanonicalizeResult*`(用 `canonicalize_result_free` 释放);NULL 输入或执行失败返回 NULL。 + +### canonicalize_result_free(ptr) + +```c +void canonicalize_result_free(struct CCanonicalizeResult *ptr); +``` + +释放规范化结果句柄。允许传 NULL。 + +### canonicalize_result_circuit(ptr) + +```c +struct CCircuit *canonicalize_result_circuit(const struct CCanonicalizeResult *ptr); +``` + +取出规范化后的线路(深拷贝,与 `ptr` 独立)。 + +返回:成功返回新建的 `CCircuit*`(用 `circuit_free` 释放);NULL 输入或失败返回 NULL。 + +### canonicalize_result_changed(ptr) + +```c +int32_t canonicalize_result_changed(const struct CCanonicalizeResult *ptr); +``` + +判断规范化是否改变了线路表示。 + +返回:`1` 有改动;`0` 无改动;`-1` 空指针。 + +### canonicalize_result_rounds(ptr) + +```c +uint8_t canonicalize_result_rounds(const struct CCanonicalizeResult *ptr); +``` + +返回实际执行的规范化轮数。 + +返回:轮数;NULL 输入返回 0。 + +### canonicalize_config_default() + +```c +struct CCanonicalizeConfig *canonicalize_config_default(void); +``` + +创建生产默认的规范化配置:轮数上限 8,且全部可选行为启用(控制流递归、`GPhase` 折叠、指令形式规范化、严格空操作消除、barrier 规范化)。 + +返回:成功返回新建的 `CCanonicalizeConfig*`(用 `canonicalize_config_free` 释放);分配失败返回 NULL。 + +### canonicalize_config_free(ptr) + +```c +void canonicalize_config_free(struct CCanonicalizeConfig *ptr); +``` + +释放规范化配置句柄,允许传 NULL。 + +### canonicalize_config_with_round_limit(ptr, round_limit) + +```c +int32_t canonicalize_config_with_round_limit(struct CCanonicalizeConfig *ptr, uint8_t round_limit); +``` + +就地设置规范化轮数上限。 + +返回:`0` 成功;`-1`(`NullPtr`)`ptr` 为 NULL。 + +### canonicalize_config_with_recurse_control_flow(ptr, enabled) + +```c +int32_t canonicalize_config_with_recurse_control_flow(struct CCanonicalizeConfig *ptr, + int32_t enabled); +``` + +就地设置是否递归规范化控制流块内部(`enabled` 非 0)或不递归(`0`)。 + +返回:`0` 成功;`-1`(`NullPtr`)`ptr` 为 NULL。 + +### canonicalize_config_with_fold_gphase(ptr, enabled) + +```c +int32_t canonicalize_config_with_fold_gphase(struct CCanonicalizeConfig *ptr, int32_t enabled); +``` + +就地设置是否把 `GPhase` 操作折叠进作用域局部相位。 + +返回:`0` 成功;`-1`(`NullPtr`)`ptr` 为 NULL。 + +### canonicalize_config_with_canonicalize_instruction_form(ptr, enabled) + +```c +int32_t canonicalize_config_with_canonicalize_instruction_form(struct CCanonicalizeConfig *ptr, + int32_t enabled); +``` + +就地设置是否把 `McGate` 形式折叠为已有的标准门。 + +返回:`0` 成功;`-1`(`NullPtr`)`ptr` 为 NULL。 + +### canonicalize_config_with_drop_noops(ptr, enabled) + +```c +int32_t canonicalize_config_with_drop_noops(struct CCanonicalizeConfig *ptr, int32_t enabled); +``` + +就地设置是否执行严格空操作消除。 + +返回:`0` 成功;`-1`(`NullPtr`)`ptr` 为 NULL。 + +### canonicalize_config_with_canonicalize_barriers(ptr, enabled) + +```c +int32_t canonicalize_config_with_canonicalize_barriers(struct CCanonicalizeConfig *ptr, + int32_t enabled); +``` + +就地设置是否规范化 barrier 作用域并合并相邻 barrier。 + +返回:`0` 成功;`-1`(`NullPtr`)`ptr` 为 NULL。 + +### canonicalize_config_round_limit(ptr) + +```c +uint8_t canonicalize_config_round_limit(const struct CCanonicalizeConfig *ptr); +``` + +返回配置的轮数上限。 + +返回:轮数上限;NULL 输入返回 0。 + +### canonicalize_config_recurse_control_flow(ptr) + +```c +int32_t canonicalize_config_recurse_control_flow(const struct CCanonicalizeConfig *ptr); +``` + +判断是否启用控制流递归。 + +返回:`1` 启用;`0` 未启用;`-1` NULL 输入。 + +### canonicalize_config_fold_gphase(ptr) + +```c +int32_t canonicalize_config_fold_gphase(const struct CCanonicalizeConfig *ptr); +``` + +判断是否启用 `GPhase` 折叠。 + +返回:`1` 启用;`0` 未启用;`-1` NULL 输入。 + +### canonicalize_config_canonicalize_instruction_form(ptr) + +```c +int32_t canonicalize_config_canonicalize_instruction_form(const struct CCanonicalizeConfig *ptr); +``` + +判断是否启用指令形式规范化。 + +返回:`1` 启用;`0` 未启用;`-1` NULL 输入。 + +### canonicalize_config_drop_noops(ptr) + +```c +int32_t canonicalize_config_drop_noops(const struct CCanonicalizeConfig *ptr); +``` + +判断是否启用严格空操作消除。 + +返回:`1` 启用;`0` 未启用;`-1` NULL 输入。 + +### canonicalize_config_canonicalize_barriers(ptr) + +```c +int32_t canonicalize_config_canonicalize_barriers(const struct CCanonicalizeConfig *ptr); +``` + +判断是否启用 barrier 规范化。 + +返回:`1` 启用;`0` 未启用;`-1` NULL 输入。 + +--- + +## 知识重写 + +知识重写 pass 基于知识规则做模式化简与抵消,是编译管线逻辑优化的核心。结果由 `CKnowledgeRewriteResult` 句柄承载。 + +### rewrite_circuit(circuit, config) + +```c +struct CKnowledgeRewriteResult *rewrite_circuit(const struct CCircuit *circuit, + struct CRewriteConfig config); +``` + +按给定配置对线路执行知识重写,输入线路不被修改。 + +- `circuit` (`const CCircuit*`):输入线路。 +- `config` (`struct CRewriteConfig`):重写配置,按值传递。 + +返回:成功返回新建的 `CKnowledgeRewriteResult*`(用 `knowledge_rewrite_result_free` 释放);NULL 输入、`config.mode` 或目标指令基非法、执行失败返回 NULL。 + +### knowledge_rewrite_result_free(ptr) + +```c +void knowledge_rewrite_result_free(struct CKnowledgeRewriteResult *ptr); +``` + +释放知识重写结果句柄。允许传 NULL。 + +### knowledge_rewrite_result_circuit(ptr) + +```c +struct CCircuit *knowledge_rewrite_result_circuit(const struct CKnowledgeRewriteResult *ptr); +``` + +取出重写后的线路(深拷贝,与 `ptr` 独立)。 + +返回:成功返回新建的 `CCircuit*`(用 `circuit_free` 释放);NULL 输入或失败返回 NULL。 + +### knowledge_rewrite_result_changed(ptr) + +```c +int32_t knowledge_rewrite_result_changed(const struct CKnowledgeRewriteResult *ptr); +``` + +判断重写是否改变了线路表示。 + +返回:`1` 有改动;`0` 无改动;`-1` 空指针。 + +### knowledge_rewrite_result_stats(ptr, out) + +```c +int32_t knowledge_rewrite_result_stats(const struct CKnowledgeRewriteResult *ptr, + struct CKnowledgeRewriteStats *out); +``` + +把重写运行统计快照写入 `*out`。`CKnowledgeRewriteStats` 布局如下: + +| 字段 | 类型 | 含义 | +| --- | --- | --- | +| `rounds_executed` | `uint8_t` | 实际执行的不动点轮数。 | +| `reached_fixpoint` | `uint8_t` | 在 `max_rounds` 内观测到稳定轮时为 1。 | +| `rules_applied` | `uintptr_t` | 发射到重建序列中的规则补丁数。 | +| `changed_sequences` | `uintptr_t` | 补丁集非空的操作序列数。 | + +- `ptr` (`const CKnowledgeRewriteResult*`):重写结果句柄。 +- `out` (`CKnowledgeRewriteStats*`):统计快照输出。 + +返回:`0` 成功;`-1` 任一参数为 NULL。 + +### knowledge_rewrite_result_diagnostics(ptr, out) + +```c +int32_t knowledge_rewrite_result_diagnostics(const struct CKnowledgeRewriteResult *ptr, + struct CKnowledgeRewriteDiagnostics *out); +``` + +把重写性能诊断快照写入 `*out`。`CKnowledgeRewriteDiagnostics` 布局如下: + +| 字段 | 类型 | 含义 | +| --- | --- | --- | +| `direct_reuses` | `uintptr_t` | 被可复用会话不动点证明跳过的重写阶段数。 | +| `dirty_anchors` | `uintptr_t` | 密度回退前由已验证脏区间提供的锚点数。 | +| `full_scan_fallbacks` | `uintptr_t` | 被保守提升为全量扫描的增量执行次数。 | +| `condition_cache_hits` | `uintptr_t` | 复用的缓存规则条件结果数。 | +| `condition_cache_misses` | `uintptr_t` | 本地缓存未命中的可缓存规则条件绑定数。 | +| `symbolic_fallbacks` | `uintptr_t` | 由保守符号路径求值的条件数。 | + +- `ptr` (`const CKnowledgeRewriteResult*`):重写结果句柄。 +- `out` (`CKnowledgeRewriteDiagnostics*`):诊断快照输出。 + +返回:`0` 成功;`-1` 任一参数为 NULL。 + +--- + +## 独立 pass 封装 + +`transform_*` 系列是单个 transform pass 的便捷封装:直接返回变换后的线路,省去中间结果句柄。全部接口输入线路不被修改,输出用 `circuit_free` 释放。 + +### transform_canonicalize(circuit) + +```c +struct CCircuit *transform_canonicalize(const struct CCircuit *circuit); +``` + +以生产默认配置规范化线路并返回重建后的线路(等价于 `canonicalize_circuit` 后再取出线路)。 + +返回:成功返回新建的 `CCircuit*`;NULL 输入或执行失败返回 NULL。 + +### transform_knowledge_rewrite(circuit, config) + +```c +struct CCircuit *transform_knowledge_rewrite(const struct CCircuit *circuit, + struct CRewriteConfig config); +``` + +按给定配置执行知识重写并返回重建后的线路。 + +- `circuit` (`const CCircuit*`):输入线路。 +- `config` (`struct CRewriteConfig`):重写配置,按值传递。 + +返回:成功返回新建的 `CCircuit*`;NULL 输入、`config.mode` 或目标指令基非法、执行失败返回 NULL。 + +### transform_optimize_one_qubit_runs(circuit) + +```c +struct CCircuit *transform_optimize_one_qubit_runs(const struct CCircuit *circuit); +``` + +通过精确综合融合固定数值的单比特连续门串(目标中立的逻辑代价),产物操作数不会多于输入。 + +返回:成功返回新建的 `CCircuit*`;NULL 输入或执行失败返回 NULL。 + +### transform_commutative_cancellation(circuit) + +```c +struct CCircuit *transform_commutative_cancellation(const struct CCircuit *circuit); +``` + +基于全局对易性分析抵消自反门对(如相邻的 `H·H`、`CX·CX`)。 + +返回:成功返回新建的 `CCircuit*`;NULL 输入或执行失败返回 NULL。 + +### transform_lower_to_routing_basis(circuit) + +```c +struct CCircuit *transform_lower_to_routing_basis(const struct CCircuit *circuit); +``` + +把线路降级到路由门集:超出 SABRE 门元数约束的门(如 `CCX`)被展开为至多两比特的操作,为路由阶段做准备。 + +返回:成功返回新建的 `CCircuit*`;NULL 输入或执行失败返回 NULL。 + +--- + +## 示例 + +结构分析、规范化与知识重写: + +```c +#include +#include +#include "cqlib_c.h" + +int main(void) { + /* 1. Structural analysis of a Bell-pair circuit */ + struct CCircuit *bell = circuit_new(2); + circuit_h(bell, 0); + circuit_cx(bell, 0, 1); + + struct CCircuitAnalysis analysis; + memset(&analysis, 0, sizeof(analysis)); + if (circuit_analyze(bell, &analysis) != 0) { + circuit_free(bell); + return 1; + } + /* analysis.has_classical_control == 0, analysis.has_unitary_gates == 0 */ + + /* 2. Canonicalize and read the result handle */ + struct CCanonicalizeResult *canon = canonicalize_circuit(bell); + if (canon == NULL) { + circuit_free(bell); + return 1; + } + uint8_t rounds = canonicalize_result_rounds(canon); /* >= 1 */ + struct CCircuit *canonical = canonicalize_result_circuit(canon); + if (canonical != NULL) { + circuit_free(canonical); /* deep clone, independent of canon */ + } + canonicalize_result_free(canon); + + /* 3. Knowledge rewrite with the production configuration */ + struct CRewriteConfig config = rewrite_config_default(); + struct CKnowledgeRewriteResult *rewritten = rewrite_circuit(bell, config); + if (rewritten != NULL) { + int32_t changed = knowledge_rewrite_result_changed(rewritten); + struct CKnowledgeRewriteStats stats; + struct CKnowledgeRewriteDiagnostics diagnostics; + memset(&stats, 0, sizeof(stats)); + memset(&diagnostics, 0, sizeof(diagnostics)); + if (knowledge_rewrite_result_stats(rewritten, &stats) == 0) { + printf("rewrite: rounds=%u rules=%llu\n", + stats.rounds_executed, (unsigned long long)stats.rules_applied); + } + knowledge_rewrite_result_diagnostics(rewritten, &diagnostics); + knowledge_rewrite_result_free(rewritten); + } + circuit_free(bell); + + /* 4. Standalone pass wrappers return rebuilt circuits directly */ + struct CCircuit *runs = circuit_new(1); + circuit_h(runs, 0); + circuit_t(runs, 0); + circuit_tdg(runs, 0); + struct CCircuit *fused = transform_optimize_one_qubit_runs(runs); + if (fused != NULL) { + circuit_free(fused); + } + + struct CCircuit *lowered = transform_lower_to_routing_basis(runs); + if (lowered != NULL) { + circuit_free(lowered); + } + circuit_free(runs); + return 0; +} +``` + +带配置的规范化与变换配置句柄: + +```c +#include +#include +#include "cqlib_c.h" + +int main(void) { + /* 1. Build a canonicalize config and tighten the round limit */ + struct CCanonicalizeConfig *config = canonicalize_config_default(); + if (config == NULL) { + return 1; + } + /* canonicalize_config_round_limit(config) == 8, all optional behaviors on */ + canonicalize_config_with_round_limit(config, 2); + + struct CCircuit *circuit = circuit_new(2); + circuit_h(circuit, 0); + circuit_cx(circuit, 0, 1); + circuit_t(circuit, 1); + + /* 2. Canonicalize under the config; the config stays owned by the caller */ + struct CCanonicalizeResult *result = canonicalize_circuit_with_config(circuit, config); + if (result != NULL) { + struct CCircuit *canonical = canonicalize_result_circuit(result); + /* canonicalize_result_rounds(result) <= 2 */ + circuit_free(canonical); + canonicalize_result_free(result); + } + + /* 3. Reuse the same config with a tighter limit */ + canonicalize_config_with_round_limit(config, 1); + struct CCanonicalizeResult *limited = canonicalize_circuit_with_config(circuit, config); + /* canonicalize_result_rounds(limited) <= 1 */ + canonicalize_result_free(limited); + canonicalize_config_free(config); + circuit_free(circuit); + + /* 4. Transform config: inspect defaults, then restrict in place */ + struct CTransformConfig *tconfig = transform_config_default(); + if (tconfig == NULL) { + return 1; + } + /* transform_config_mode(tconfig) == REWRITE_MODE_OPTIMIZE, + transform_config_enabled_kinds_len(tconfig) == 4 */ + const uint8_t kinds[2] = {REWRITE_KIND_CANCEL, REWRITE_KIND_DECOMPOSE}; + transform_config_with_enabled_kinds(tconfig, kinds, 2); + + const char *const gates[2] = {"H", "CX"}; + if (transform_config_with_target_instructions(tconfig, gates, 2) == 0) { + char *basis[2] = {NULL, NULL}; + uintptr_t total = transform_config_target_instruction_basis(tconfig, basis, 2); + for (uintptr_t i = 0; i < total && i < 2; i++) { + cqlib_string_free(basis[i]); /* each entry: newly allocated */ + } + } + transform_config_with_mode(tconfig, 99); /* -8, config unchanged */ + transform_config_free(tconfig); + return 0; +} +``` + +单比特门串经 `transform_optimize_one_qubit_runs` 融合后操作数不增;端到端编排这些 pass 的编译入口见 [编译器](1_compiler.md),布局与路由见 [初始布局](3_layout.md)、[路由](4_routing.md)。 diff --git a/docs/api/cn/c/4_compile/3_layout.md b/docs/api/cn/c/4_compile/3_layout.md new file mode 100644 index 00000000..accb650f --- /dev/null +++ b/docs/api/cn/c/4_compile/3_layout.md @@ -0,0 +1,773 @@ +# 初始布局(C) + +本页覆盖初始布局算法与布局分析工具的 C ABI:四个布局入口(`trivial_layout` / `greedy_layout` / `vf2_perfect_layout` / `sabre_layout`)、线路交互分析与物理布局图查询、SABRE 预备对象与预计算布局管线,以及布局结果 `CLayoutResult` 的读取接口。布局把逻辑比特映射到物理比特,为路由阶段提供起点。错误码、字符串与句柄的释放约定遵循 [Overview](../0_overview.md);映射句柄 `CLayout` 的构造与读写接口见 [Layout](../2_device/3_layout.md)。 + +--- + +## 常量 + +布局目标标签(各布局入口与 `score_layout` 的 `objective` 参数): + +| 常量 | 值 | 目标 | +| --- | --- | --- | +| `LAYOUT_OBJECTIVE_TOPOLOGY_ONLY` | 0 | 纯拓扑目标:距离加方向失配加权。 | +| `LAYOUT_OBJECTIVE_FIDELITY_AWARE` | 1 | 保真度感知目标:在拓扑项之上追加错误率项。 | +| `LAYOUT_OBJECTIVE_AUTO` | 2 | 由设备的校准数据自动选择:物理图含校准数据时为保真度感知目标,否则回退纯拓扑目标。 | +| `LAYOUT_OBJECTIVE_FIDELITY_REQUIRED` | 3 | 强制保真度:与保真度感知相同,但物理图不含可用校准数据时报错。 | + +VF2 边要求标签(`CVf2LayoutConfig.edge_requirement`): + +| 常量 | 值 | 要求 | +| --- | --- | --- | +| `VF2_EDGE_REQUIREMENT_POSITIVE_INTERACTIONS` | 0 | 只要求正权重交互边匹配。 | +| `VF2_EDGE_REQUIREMENT_ALL_INTERACTIONS` | 1 | 要求全部交互边匹配。 | + +--- + +## 数据结构 + +### CLayoutScore + +布局打分快照,由 `score_layout` 与 `layout_result_score` 写入: + +```c +typedef struct CLayoutScore { + double total; /* Weighted total according to the objective. */ + double distance; /* Raw weighted-distance component. */ + double direction; /* Raw direction-mismatch component. */ + double two_qubit_error; /* Unweighted effective two-qubit placement cost. */ + double readout_error; /* Raw readout error component. */ + uint8_t used_fidelity; /* 1 when the objective used fidelity terms. */ + uint8_t _reserved[7]; /* Reserved padding. */ +} CLayoutScore; +``` + +| 字段 | 含义 | +| --- | --- | +| `total` | 按目标加权后的总得分,越小越好。 | +| `distance` | 原始加权距离分量。 | +| `direction` | 原始方向失配分量。 | +| `two_qubit_error` | 未加权的有效二比特放置代价。 | +| `readout_error` | 原始读出错误分量。 | +| `used_fidelity` | 目标使用了保真度项时为 1。 | + +### CInteraction + +一条逻辑比特对交互快照,由 `circuit_layout_analysis_interaction` 写入: + +| 字段 | 类型 | 含义 | +| --- | --- | --- | +| `left` | `uint32_t` | 按升序排列的较小逻辑比特端点。 | +| `right` | `uint32_t` | 较大逻辑比特端点。 | +| `weight` | `double` | 该无序比特对的总交互权重。 | +| `directed_weight_left_to_right` | `double` | 按 `left -> right` 操作顺序观测到的权重。 | +| `directed_weight_right_to_left` | `double` | 按 `right -> left` 操作顺序观测到的权重。 | +| `first_seen_order` | `uintptr_t` | 在操作顺序中的首次出现序号。 | + +### CVf2LayoutConfig + +VF2 精确布局配置: + +```c +typedef struct CVf2LayoutConfig { + uintptr_t candidate_limit; /* Max complete perfect candidates to score. */ + uintptr_t call_limit; /* Max partial mapping extensions ((uintptr_t)-1 = none). */ + uint8_t edge_requirement; /* One of VF2_EDGE_REQUIREMENT_*. */ + uint8_t _reserved[7]; /* Reserved padding. */ +} CVf2LayoutConfig; +``` + +| 字段 | 类型 | 含义 | +| --- | --- | --- | +| `candidate_limit` | `uintptr_t` | 参与打分的完整完美候选映射数量上限。 | +| `call_limit` | `uintptr_t` | 尝试的部分映射扩展次数上限;`(uintptr_t)-1` 表示不设上限。 | +| `edge_requirement` | `uint8_t` | 取 `VF2_EDGE_REQUIREMENT_*` 之一。 | +| `_reserved[7]` | `uint8_t[7]` | 填充字段,保持结构体布局稳定,勿写入非零值。 | + +### vf2_layout_config_default() + +```c +struct CVf2LayoutConfig vf2_layout_config_default(void); +``` + +按值返回默认 VF2 配置:`candidate_limit = 10`、`call_limit = (uintptr_t)-1`(无上限)、`edge_requirement = VF2_EDGE_REQUIREMENT_POSITIVE_INTERACTIONS`。 + +--- + +## 布局入口 + +四个入口均不修改输入线路,返回的 `CLayoutResult*` 用 `layout_result_free` 释放。`objective` 取 `LAYOUT_OBJECTIVE_*` 之一;`LAYOUT_OBJECTIVE_AUTO` 依据设备的校准数据解析(物理图由设备派生),`LAYOUT_OBJECTIVE_FIDELITY_REQUIRED` 则要求目标必须携带可用校准数据,否则所有入口均返回 NULL。 + +### trivial_layout(circuit, device, objective) + +```c +struct CLayoutResult *trivial_layout(const struct CCircuit *circuit, + const struct CDevice *device, + uint8_t objective); +``` + +平凡布局:按现有顺序把逻辑比特映射到可用物理比特(逻辑 i 映射到第 i 个可用物理比特)。 + +- `circuit` (`const CCircuit*`):输入线路。 +- `device` (`const CDevice*`):目标设备(构造方式见 [设备与噪声属性](../2_device/2_properties_device.md))。 +- `objective` (`uint8_t`):布局目标标签。 + +返回:成功返回新建的 `CLayoutResult*`;任一参数为 NULL、`objective` 标签未知或可用物理比特不足时返回 NULL。 + +### greedy_layout(circuit, device, objective) + +```c +struct CLayoutResult *greedy_layout(const struct CCircuit *circuit, + const struct CDevice *device, + uint8_t objective); +``` + +贪心布局:按交互强度顺序放置逻辑比特,优先满足强交互对。 + +参数与返回值同 `trivial_layout`:成功返回新建的 `CLayoutResult*`;任一参数为 NULL、`objective` 标签未知或执行失败返回 NULL。 + +### vf2_perfect_layout(circuit, device, objective, config) + +```c +struct CLayoutResult *vf2_perfect_layout(const struct CCircuit *circuit, + const struct CDevice *device, + uint8_t objective, + const struct CVf2LayoutConfig *config); +``` + +用非诱导 VF2++ 匹配搜索完美初始布局:当线路交互图能精确嵌入设备拓扑时,每个正权重交互都落在相邻物理边上。 + +- `circuit` (`const CCircuit*`):输入线路。 +- `device` (`const CDevice*`):目标设备。 +- `objective` (`uint8_t`):布局目标标签。 +- `config` (`const CVf2LayoutConfig*`):VF2 配置;传 NULL 使用默认配置。 + +返回:成功返回新建的 `CLayoutResult*`;不存在完美映射、任一指针参数为 NULL、`objective` 标签未知或 `config->edge_requirement` 非法时返回 NULL。 + +### sabre_layout(circuit, device, objective, config) + +```c +struct CLayoutResult *sabre_layout(const struct CCircuit *circuit, + const struct CDevice *device, + uint8_t objective, + const struct SabreConfigC *config); +``` + +用 SABRE 布局细化选择初始布局:多个候选布局经前向/后向细化与打分后择优。 + +- `circuit` (`const CCircuit*`):输入线路。 +- `device` (`const CDevice*`):目标设备。 +- `objective` (`uint8_t`):布局目标标签。 +- `config` (`const SabreConfigC*`):SABRE 配置;传 NULL 使用默认配置,字段说明见 [SABRE 配置](5_sabre.md)。 + +返回:成功返回新建的 `CLayoutResult*`;任一指针参数为 NULL、`objective` 标签未知或配置非法时返回 NULL。 + +--- + +## 线路布局分析 + +`analyze_circuit_for_layout` 预计算线路的加权逻辑交互,供预计算布局入口复用。 + +### analyze_circuit_for_layout(circuit) + +```c +struct CCircuitLayoutAnalysis *analyze_circuit_for_layout(const struct CCircuit *circuit); +``` + +分析线路的布局特征(逻辑比特表与加权交互图)。 + +- `circuit` (`const CCircuit*`):输入线路。 + +返回:成功返回新建的 `CCircuitLayoutAnalysis*`(用 `circuit_layout_analysis_free` 释放);NULL 输入或失败返回 NULL。 + +### circuit_layout_analysis_free(ptr) + +```c +void circuit_layout_analysis_free(struct CCircuitLayoutAnalysis *ptr); +``` + +释放线路布局分析句柄。允许传 NULL。 + +### circuit_layout_analysis_num_logical(ptr) + +```c +uintptr_t circuit_layout_analysis_num_logical(const struct CCircuitLayoutAnalysis *ptr); +``` + +返回分析中的逻辑比特数。 + +返回:逻辑比特数;NULL 输入返回 0。 + +### circuit_layout_analysis_logical_qubits(ptr, out, len) + +```c +uintptr_t circuit_layout_analysis_logical_qubits(const struct CCircuitLayoutAnalysis *ptr, + uint32_t *out, + uintptr_t len); +``` + +两步式读取:按源线路顺序把逻辑比特 ID 拷入 `out`。先以 `len = 0`、`out = NULL` 调用查总数,再分配缓冲后以 `len` 等于总数调用填充。 + +- `ptr` (`const CCircuitLayoutAnalysis*`):分析句柄。 +- `out` (`uint32_t*`):输出缓冲。 +- `len` (`uintptr_t`):缓冲容量。 + +返回:逻辑比特总数;NULL 输入返回 0。 + +### circuit_layout_analysis_interactions_len(ptr) + +```c +uintptr_t circuit_layout_analysis_interactions_len(const struct CCircuitLayoutAnalysis *ptr); +``` + +返回加权逻辑交互条数。 + +返回:交互条数;NULL 输入返回 0。 + +### circuit_layout_analysis_interaction(ptr, index, out) + +```c +int32_t circuit_layout_analysis_interaction(const struct CCircuitLayoutAnalysis *ptr, + uintptr_t index, + struct CInteraction *out); +``` + +把第 `index` 条交互快照写入 `*out`(字段见上文 `CInteraction` 表)。 + +- `ptr` (`const CCircuitLayoutAnalysis*`):分析句柄。 +- `index` (`uintptr_t`):交互下标,必须小于 `circuit_layout_analysis_interactions_len`。 +- `out` (`CInteraction*`):交互快照输出。 + +返回:`0` 成功;`-1` 任一参数为 NULL;`-8` `index` 越界。 + +--- + +## 物理布局图 + +`CPhysicalLayoutGraph` 是设备的编译器侧物理拓扑与校准视图:可用物理比特、最短距离、耦合方向与校准错误率。 + +### physical_layout_graph_from_device(device) + +```c +struct CPhysicalLayoutGraph *physical_layout_graph_from_device(const struct CDevice *device); +``` + +由设备构建物理布局图。 + +- `device` (`const CDevice*`):目标设备。 + +返回:成功返回新建的 `CPhysicalLayoutGraph*`(用 `physical_layout_graph_free` 释放);NULL 输入或失败返回 NULL。 + +### physical_layout_graph_free(ptr) + +```c +void physical_layout_graph_free(struct CPhysicalLayoutGraph *ptr); +``` + +释放物理布局图句柄。允许传 NULL。 + +### physical_layout_graph_num_physical(ptr) + +```c +uintptr_t physical_layout_graph_num_physical(const struct CPhysicalLayoutGraph *ptr); +``` + +返回可用物理比特数。 + +返回:可用物理比特数;NULL 输入返回 0。 + +### physical_layout_graph_physical_qubits(ptr, out, len) + +```c +uintptr_t physical_layout_graph_physical_qubits(const struct CPhysicalLayoutGraph *ptr, + uint32_t *out, + uintptr_t len); +``` + +两步式读取:按升序把可用物理比特 ID 拷入 `out`。先查总数(`physical_layout_graph_num_physical`),再分配缓冲填充。 + +- `ptr` (`const CPhysicalLayoutGraph*`):物理布局图句柄。 +- `out` (`uint32_t*`):输出缓冲。 +- `len` (`uintptr_t`):缓冲容量。 + +返回:可用物理比特总数;NULL 输入返回 0。 + +### physical_layout_graph_distance(ptr, a, b) + +```c +uint32_t physical_layout_graph_distance(const struct CPhysicalLayoutGraph *ptr, + uint32_t a, + uint32_t b); +``` + +返回两个物理比特之间的无向最短路径距离。 + +- `ptr` (`const CPhysicalLayoutGraph*`):物理布局图句柄。 +- `a`、`b` (`uint32_t`):物理比特 ID。 + +返回:最短距离;两比特不连通、未知或句柄为 NULL 时返回 `UINT32_MAX`。 + +### physical_layout_graph_is_adjacent_undirected(ptr, a, b) + +```c +int32_t physical_layout_graph_is_adjacent_undirected(const struct CPhysicalLayoutGraph *ptr, + uint32_t a, + uint32_t b); +``` + +判断两个物理比特是否无向相邻(直接耦合)。 + +返回:`1` 相邻;`0` 不相邻;`-1` 空指针。 + +### physical_layout_graph_readout_error(ptr, qubit, out) + +```c +int32_t physical_layout_graph_readout_error(const struct CPhysicalLayoutGraph *ptr, + uint32_t qubit, + double *out); +``` + +读取一个物理比特的读出错误率。 + +- `ptr` (`const CPhysicalLayoutGraph*`):物理布局图句柄。 +- `qubit` (`uint32_t`):物理比特 ID。 +- `out` (`double*`):错误率输出。 + +返回:`0` 成功(值写入 `*out`);`-1` 任一参数为 NULL;`-8` 该比特没有记录读出数据。 + +### physical_layout_graph_supports_directed_coupling(ptr, control, target) + +```c +int32_t physical_layout_graph_supports_directed_coupling(const struct CPhysicalLayoutGraph *ptr, + uint32_t control, + uint32_t target); +``` + +判断有向耦合 `control -> target` 是否存在。 + +- `control`、`target` (`uint32_t`):物理比特 ID。 + +返回:`1` 有向耦合存在;`0` 不存在;`-1` 空指针。 + +### physical_layout_graph_supports_two_qubit_gate_directed(ptr, gate_name, control, target) + +```c +int32_t physical_layout_graph_supports_two_qubit_gate_directed(const struct CPhysicalLayoutGraph *ptr, + const char *gate_name, + uint32_t control, + uint32_t target); +``` + +判断 `gate_name` 是否是有向边 `control -> target` 上的原生能力。 + +- `gate_name` (`const char*`):标准门名称(如 `"CX"`、`"SWAP"`)。 +- `control`、`target` (`uint32_t`):物理比特 ID。 + +返回:`1` 支持;`0` 不支持;`-1` 空指针;`-4` 门名称未知。 + +### physical_layout_graph_two_qubit_gate_error_directed(ptr, gate_name, control, target, out) + +```c +int32_t physical_layout_graph_two_qubit_gate_error_directed(const struct CPhysicalLayoutGraph *ptr, + const char *gate_name, + uint32_t control, + uint32_t target, + double *out); +``` + +读取 `gate_name` 在有向边 `control -> target` 上的校准错误率。 + +- `gate_name` (`const char*`):标准门名称。 +- `control`、`target` (`uint32_t`):物理比特 ID。 +- `out` (`double*`):错误率输出。 + +返回:`0` 成功(值写入 `*out`);`-1` 空指针;`-4` 门名称未知;`-8` 该门在此边上不受支持或未校准。 + +### physical_layout_graph_has_fidelity_data(ptr) / has_readout_error_data / has_two_qubit_error_data + +```c +int32_t physical_layout_graph_has_fidelity_data(const struct CPhysicalLayoutGraph *ptr); +int32_t physical_layout_graph_has_readout_error_data(const struct CPhysicalLayoutGraph *ptr); +int32_t physical_layout_graph_has_two_qubit_error_data(const struct CPhysicalLayoutGraph *ptr); +``` + +判断校准数据的可用性:任一读出或二比特校准数据 / 读出错误数据 / 二比特错误数据是否可用。`LAYOUT_OBJECTIVE_AUTO` 依据这些标志选择保真度感知或纯拓扑目标。 + +返回:`1` 数据可用;`0` 不可用;`-1` 空指针。 + +--- + +## SABRE 预备对象 + +预计算管线把 SABRE 需要的线路侧与设备侧数据一次构建、多次复用:`prepare_sabre_circuit` 准备线路侧分析,`prepare_sabre_device_target` 准备设备侧精确数据。 + +### prepare_sabre_circuit(circuit) + +```c +struct CPreparedSabreCircuit *prepare_sabre_circuit(const struct CCircuit *circuit); +``` + +准备 SABRE 使用的线路侧分析与依赖模型。 + +- `circuit` (`const CCircuit*`):输入线路。 + +返回:成功返回新建的 `CPreparedSabreCircuit*`(用 `prepared_sabre_circuit_free` 释放);NULL 输入或失败返回 NULL。 + +### prepared_sabre_circuit_free(ptr) + +```c +void prepared_sabre_circuit_free(struct CPreparedSabreCircuit *ptr); +``` + +释放预备线路句柄。允许传 NULL。 + +### prepared_sabre_circuit_logical_qubits_len(ptr) + +```c +uintptr_t prepared_sabre_circuit_logical_qubits_len(const struct CPreparedSabreCircuit *ptr); +``` + +返回预备线路的逻辑比特数。 + +返回:逻辑比特数;NULL 输入返回 0。 + +### prepared_sabre_circuit_logical_qubits(ptr, out, len) + +```c +uintptr_t prepared_sabre_circuit_logical_qubits(const struct CPreparedSabreCircuit *ptr, + uint32_t *out, + uintptr_t len); +``` + +两步式读取:按源线路顺序把预备线路的逻辑比特 ID 拷入 `out`。先查总数,再分配缓冲填充。 + +返回:逻辑比特总数;NULL 输入返回 0。 + +### prepare_sabre_device_target(prepared, device) + +```c +struct CPreparedSabreTarget *prepare_sabre_device_target(const struct CPreparedSabreCircuit *prepared, + const struct CDevice *device); +``` + +为预备线路准备精确设备原生的 SABRE 数据:包含原生操作可行性、方向与实现代价。 + +**前提**:设备必须声明路由能力,即满足以下条件之一——设备级原生门集非空(例如调用 `device_with_native_gates` 设置,见 [设备与噪声属性](../2_device/2_properties_device.md));至少一个可用物理比特的属性含原生指令;或至少一条可用耦合边的属性含原生指令。未声明路由能力的设备会让此调用返回 NULL。 + +- `prepared` (`const CPreparedSabreCircuit*`):预备线路句柄(`prepare_sabre_circuit` 的结果)。 +- `device` (`const CDevice*`):目标设备。 + +返回:成功返回新建的 `CPreparedSabreTarget*`(用 `prepared_sabre_target_free` 释放);任一参数为 NULL、设备未声明路由能力或失败返回 NULL。 + +### prepared_sabre_target_free(ptr) + +```c +void prepared_sabre_target_free(struct CPreparedSabreTarget *ptr); +``` + +释放预备目标句柄。允许传 NULL。 + +### prepared_sabre_target_physical(ptr) + +```c +struct CPhysicalLayoutGraph *prepared_sabre_target_physical(const struct CPreparedSabreTarget *ptr); +``` + +取出预备目标使用的物理布局图(深拷贝),用于布局目标打分。 + +返回:成功返回新建的 `CPhysicalLayoutGraph*`(用 `physical_layout_graph_free` 释放);NULL 输入返回 NULL。 + +--- + +## 预计算布局算法 + +预计算入口接受 `analyze_circuit_for_layout` 的分析与 `CPhysicalLayoutGraph`(或 SABRE 预备对象),跳过重复分析。 + +### trivial_layout_prepared(analysis, physical, objective) + +```c +struct CLayoutResult *trivial_layout_prepared(const struct CCircuitLayoutAnalysis *analysis, + const struct CPhysicalLayoutGraph *physical, + uint8_t objective); +``` + +在预计算分析与物理图上执行平凡布局。`LAYOUT_OBJECTIVE_AUTO` 依据传入物理图的校准数据解析。 + +- `analysis` (`const CCircuitLayoutAnalysis*`):线路布局分析。 +- `physical` (`const CPhysicalLayoutGraph*`):物理布局图。 +- `objective` (`uint8_t`):布局目标标签。 + +返回:成功返回新建的 `CLayoutResult*`;任一参数为 NULL、`objective` 标签未知或执行失败返回 NULL。 + +### greedy_layout_prepared(analysis, physical, objective) + +```c +struct CLayoutResult *greedy_layout_prepared(const struct CCircuitLayoutAnalysis *analysis, + const struct CPhysicalLayoutGraph *physical, + uint8_t objective); +``` + +在预计算分析与物理图上执行贪心布局。参数与返回值同 `trivial_layout_prepared`。 + +### vf2_perfect_layout_prepared(analysis, physical, objective, config) + +```c +struct CLayoutResult *vf2_perfect_layout_prepared(const struct CCircuitLayoutAnalysis *analysis, + const struct CPhysicalLayoutGraph *physical, + uint8_t objective, + const struct CVf2LayoutConfig *config); +``` + +在预计算分析与物理图上执行 VF2 精确布局;`config` 传 NULL 使用默认配置。 + +返回:成功返回新建的 `CLayoutResult*`;不存在完美映射、参数非法或执行失败返回 NULL。 + +### sabre_layout_prepared(prepared, prepared_target, objective, config) + +```c +struct CLayoutResult *sabre_layout_prepared(const struct CPreparedSabreCircuit *prepared, + const struct CPreparedSabreTarget *prepared_target, + uint8_t objective, + const struct SabreConfigC *config); +``` + +在预备线路与预备目标上执行 SABRE 布局;`LAYOUT_OBJECTIVE_AUTO` 依据预备目标的物理图解析,`config` 传 NULL 使用默认配置。 + +- `prepared` (`const CPreparedSabreCircuit*`):预备线路。 +- `prepared_target` (`const CPreparedSabreTarget*`):预备目标(含物理图与原生代价)。 +- `objective` (`uint8_t`):布局目标标签。 +- `config` (`const SabreConfigC*`):SABRE 配置,见 [SABRE 配置](5_sabre.md)。 + +返回:成功返回新建的 `CLayoutResult*`;任一指针参数为 NULL、`objective` 标签未知或配置非法时返回 NULL。 + +### score_layout(objective, analysis, physical, layout, out) + +```c +int32_t score_layout(uint8_t objective, + const struct CCircuitLayoutAnalysis *analysis, + const struct CPhysicalLayoutGraph *physical, + const struct CLayout *layout, + struct CLayoutScore *out); +``` + +按布局目标给候选布局打分,快照写入 `*out`(字段见上文 `CLayoutScore` 表)。`layout` 必须把 `analysis` 中的逻辑比特映射到 `physical` 中的物理比特。 + +- `objective` (`uint8_t`):布局目标标签。 +- `analysis` (`const CCircuitLayoutAnalysis*`):线路布局分析。 +- `physical` (`const CPhysicalLayoutGraph*`):物理布局图。 +- `layout` (`const CLayout*`):候选布局,见 [Layout](../2_device/3_layout.md)。 +- `out` (`CLayoutScore*`):打分快照输出。 + +返回:`0` 成功;`-1` 任一参数为 NULL;`-8` `objective` 标签未知或目标无法解析(如物理图无校准数据时的 `LAYOUT_OBJECTIVE_FIDELITY_REQUIRED`);`-6` 核心打分失败。 + +--- + +## 布局结果访问 + +`CLayoutResult` 承载选中的映射、可选打分与诊断信息。 + +### layout_result_free(ptr) + +```c +void layout_result_free(struct CLayoutResult *ptr); +``` + +释放布局结果句柄。允许传 NULL。 + +### layout_result_layout(ptr) + +```c +struct CLayout *layout_result_layout(const struct CLayoutResult *ptr); +``` + +取出选中的逻辑→物理映射(深拷贝)。 + +返回:成功返回新建的 `CLayout*`(用 `layout_free` 释放);NULL 输入返回 NULL。 + +### layout_result_has_score(ptr) + +```c +int32_t layout_result_has_score(const struct CLayoutResult *ptr); +``` + +判断结果是否携带观测打分。 + +返回:`1` 有打分;`0` 无打分;`-1` 空指针。 + +### layout_result_score(ptr, out) + +```c +int32_t layout_result_score(const struct CLayoutResult *ptr, struct CLayoutScore *out); +``` + +把打分快照写入 `*out`。 + +返回:`0` 成功;`-1` 任一参数为 NULL;`-8` 结果没有打分。 + +### layout_result_is_perfect(ptr) + +```c +int32_t layout_result_is_perfect(const struct CLayoutResult *ptr); +``` + +判断是否为完美布局:每个正权重交互都落在相邻物理边上。 + +返回:`1` 完美;`0` 不完美;`-1` 空指针。 + +### layout_result_candidates_evaluated(ptr) + +```c +uintptr_t layout_result_candidates_evaluated(const struct CLayoutResult *ptr); +``` + +返回算法考察过的候选布局数。 + +返回:候选数;NULL 输入返回 0。 + +### layout_result_used_fidelity(ptr) + +```c +int32_t layout_result_used_fidelity(const struct CLayoutResult *ptr); +``` + +判断保真度数据是否参与了选中布局的打分。 + +返回:`1` 参与了打分;`0` 未参与;`-1` 空指针。 + +### layout_result_notes_len(ptr) / layout_result_note(ptr, index) + +```c +uintptr_t layout_result_notes_len(const struct CLayoutResult *ptr); +char *layout_result_note(const struct CLayoutResult *ptr, uintptr_t index); +``` + +两步式读取诊断说明:`notes_len` 返回说明条数(NULL 返回 0),`note` 返回第 `index` 条说明的堆上 C 字符串(用 `cqlib_string_free` 释放);越界返回 NULL。 + +--- + +## 示例 + +四个布局入口与结果读取: + +```c +#include +#include "cqlib_c.h" + +int main(void) { + /* Directed line 0 -> 1 -> 2 built from an edge list */ + uint32_t edges[4] = {0, 1, 1, 2}; + struct CDevice *device = device_from_edges("line-3", 3, edges, 2); + struct CCircuit *circuit = circuit_new(3); + circuit_cx(circuit, 0, 2); /* non-adjacent pair on the line */ + if (device == NULL || circuit == NULL) { + return 1; + } + + /* Trivial layout maps logical i to physical i */ + struct CLayoutResult *trivial = + trivial_layout(circuit, device, LAYOUT_OBJECTIVE_TOPOLOGY_ONLY); + if (trivial != NULL) { + struct CLayout *mapping = layout_result_layout(trivial); + uintptr_t n = layout_num_logical(mapping); /* 3 */ + layout_free(mapping); + layout_result_free(trivial); + } + + /* Greedy and SABRE layouts run on the same circuit */ + struct CLayoutResult *greedy = + greedy_layout(circuit, device, LAYOUT_OBJECTIVE_TOPOLOGY_ONLY); + if (greedy != NULL) { + layout_result_free(greedy); + } + + struct CLayoutResult *sabre = + sabre_layout(circuit, device, LAYOUT_OBJECTIVE_TOPOLOGY_ONLY, NULL); + if (sabre != NULL) { + uintptr_t evaluated = layout_result_candidates_evaluated(sabre); /* >= 1 */ + layout_result_free(sabre); + } + + circuit_free(circuit); + device_free(device); + return 0; +} +``` + +预计算管线:线路分析、物理图、SABRE 预备对象与打分(设备先声明原生门集): + +```c +#include +#include +#include "cqlib_c.h" + +int main(void) { + uint32_t edges[4] = {0, 1, 1, 2}; + struct CDevice *device = device_from_edges("line-3", 3, edges, 2); + struct CCircuit *circuit = circuit_new(3); + circuit_cx(circuit, 0, 2); + if (device == NULL || circuit == NULL) { + return 1; + } + + /* Exact device-native SABRE requires declared routing capability */ + if (device_with_native_gates(device, "H,CX,SWAP") != 0) { + circuit_free(circuit); + device_free(device); + return 1; + } + + /* Circuit-side analysis: one cx(0, 2) collapses to one interaction pair */ + struct CCircuitLayoutAnalysis *analysis = analyze_circuit_for_layout(circuit); + uintptr_t num_interactions = circuit_layout_analysis_interactions_len(analysis); + for (uintptr_t i = 0; i < num_interactions; i++) { + struct CInteraction interaction; + if (circuit_layout_analysis_interaction(analysis, i, &interaction) == 0) { + printf("interaction %u-%u weight %f\n", + interaction.left, interaction.right, interaction.weight); + } + } + + /* Physical graph view of the device */ + struct CPhysicalLayoutGraph *graph = physical_layout_graph_from_device(device); + uintptr_t num_physical = physical_layout_graph_num_physical(graph); /* 3 */ + uint32_t *physical_ids = malloc(num_physical * sizeof(uint32_t)); + physical_layout_graph_physical_qubits(graph, physical_ids, num_physical); + uint32_t dist = physical_layout_graph_distance(graph, 0, 2); /* 2 */ + int32_t adjacent = + physical_layout_graph_is_adjacent_undirected(graph, 0, 2); /* 0 */ + free(physical_ids); + + /* Prepared SABRE pipeline */ + struct CPreparedSabreCircuit *prepared = prepare_sabre_circuit(circuit); + struct CPreparedSabreTarget *target = prepare_sabre_device_target(prepared, device); + if (prepared != NULL && target != NULL) { + struct CPhysicalLayoutGraph *target_graph = prepared_sabre_target_physical(target); + physical_layout_graph_free(target_graph); + + struct CLayoutResult *result = sabre_layout_prepared( + prepared, target, LAYOUT_OBJECTIVE_TOPOLOGY_ONLY, NULL); + if (result != NULL) { + struct CLayout *mapping = layout_result_layout(result); + + /* Score the selected mapping against the prepared analysis */ + struct CLayoutScore score; + if (score_layout(LAYOUT_OBJECTIVE_TOPOLOGY_ONLY, + analysis, graph, mapping, &score) == 0) { + printf("total=%f distance=%f\n", score.total, score.distance); + } + layout_free(mapping); + layout_result_free(result); + } + prepared_sabre_target_free(target); + prepared_sabre_circuit_free(prepared); + } + + physical_layout_graph_free(graph); + circuit_layout_analysis_free(analysis); + circuit_free(circuit); + device_free(device); + return 0; +} +``` + +选中布局交给路由阶段插入 SWAP 的流程见 [路由](4_routing.md);设备目标编译一步完成布局与路由的入口见 [编译器](1_compiler.md)。 diff --git a/docs/api/cn/c/4_compile/4_routing.md b/docs/api/cn/c/4_compile/4_routing.md new file mode 100644 index 00000000..4e201eaf --- /dev/null +++ b/docs/api/cn/c/4_compile/4_routing.md @@ -0,0 +1,535 @@ +# 路由(C) + +本页覆盖变换层路由入口 `route_with_layout` / `route_sabre`,以及路由结果句柄 `CRoutedCircuit` / `CSabreRouteResult` 的全部读取接口:路由后线路、初始与最终布局、SWAP 数量、路由诊断与布局元数据。路由在设备拓扑上插入 SWAP,使线路中所有二体交互落在相邻物理比特上;路由后线路使用物理比特编号。错误码、字符串与句柄的释放约定遵循 [Overview](../0_overview.md);`SabreConfigC` 配置结构、`CSabreRoutingDiagnostics` 诊断结构与低层 `sabre_route` 见 [SABRE](5_sabre.md)。 + +--- + +## 常量 + +布局目标标签(`route_sabre` 的 `objective` 参数): + +| 常量 | 值 | 含义 | +| --- | --- | --- | +| `LAYOUT_OBJECTIVE_TOPOLOGY_ONLY` | 0 | 仅按拓扑评分:距离与方向分量。 | +| `LAYOUT_OBJECTIVE_FIDELITY_AWARE` | 1 | 拓扑分数之上叠加保真度项(双比特门误差权重 10.0、读出误差权重 1.0)。 | +| `LAYOUT_OBJECTIVE_AUTO` | 2 | 按设备数据自动选择:设备携带双比特门/读出误差数据时启用对应保真度项,否则退化为仅拓扑。 | +| `LAYOUT_OBJECTIVE_FIDELITY_REQUIRED` | 3 | 强制保真度项:与保真度感知相同,但设备不含可用校准数据时失败。 | + +--- + +## 路由入口 + +### route_with_layout(circuit, device, initial_layout, config) + +在调用方给定的初始布局上做确定性路由:不执行布局选择、细化或评分。适合已持有布局(例如来自布局搜索或上一次 SABRE 运行)的调用方。 + +```c +struct CRoutedCircuit *route_with_layout(const struct CCircuit *circuit, + const struct CDevice *device, + const struct CLayout *initial_layout, + const struct SabreConfigC *config); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `circuit` | `const CCircuit*` | 输入线路。 | +| `device` | `const CDevice*` | 目标设备(构造方式见 [设备与噪声属性](../2_device/2_properties_device.md))。 | +| `initial_layout` | `const CLayout*` | 初始逻辑→物理映射(构造方式见 [Layout](../2_device/3_layout.md))。 | +| `config` | `const SabreConfigC*` | SABRE 配置;NULL 使用默认配置(见 [SABRE](5_sabre.md))。 | + +返回:成功返回新建的 `CRoutedCircuit*`(用 `routed_circuit_free` 释放);任一句柄为 NULL、配置无法转换(例如 `num_lookahead_weights` 超过 8)或路由失败(配置非法、可用物理比特不足、交互不可达等)时返回 NULL。 + +### route_sabre(circuit, device, objective, config) + +带布局选择的 SABRE 路由:先按 `objective` 选出初始布局,再从该布局路由原始正向线路。选中的布局伴随路由结果一起返回。 + +```c +struct CSabreRouteResult *route_sabre(const struct CCircuit *circuit, + const struct CDevice *device, + uint8_t objective, + const struct SabreConfigC *config); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `circuit` | `const CCircuit*` | 输入线路。 | +| `device` | `const CDevice*` | 目标设备。 | +| `objective` | `uint8_t` | 布局目标,取 `LAYOUT_OBJECTIVE_*` 之一。 | +| `config` | `const SabreConfigC*` | SABRE 配置;NULL 使用默认配置。相同种子、相同线路与设备产生相同路由结果。 | + +返回:成功返回新建的 `CSabreRouteResult*`(用 `sabre_route_result_free` 释放);任一句柄为 NULL、`objective` 不是合法标签、配置无法转换或布局选择/路由失败时返回 NULL。 + +--- + +## RoutedCircuit 读取 + +`route_with_layout` 的结果句柄。 + +### routed_circuit_free(ptr) + +释放路由结果句柄,允许传 NULL。 + +```c +void routed_circuit_free(struct CRoutedCircuit *ptr); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `ptr` | `struct CRoutedCircuit*` | 待释放句柄,可为 NULL。 | + +### routed_circuit_circuit(ptr) + +取出路由后的物理线路(深拷贝,线路比特为物理比特编号,所有二体操作在可用物理拓扑上相邻)。 + +```c +struct CCircuit *routed_circuit_circuit(const struct CRoutedCircuit *ptr); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `ptr` | `const CRoutedCircuit*` | 路由结果句柄。 | + +返回:成功返回新建的 `CCircuit*`(用 `circuit_free` 释放);NULL 输入返回 NULL。 + +### routed_circuit_initial_layout(ptr) + +取出路由开始前的初始逻辑→物理布局(深拷贝)。 + +```c +struct CLayout *routed_circuit_initial_layout(const struct CRoutedCircuit *ptr); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `ptr` | `const CRoutedCircuit*` | 路由结果句柄。 | + +返回:成功返回新建的 `CLayout*`(用 `layout_free` 释放);NULL 输入返回 NULL。 + +### routed_circuit_final_layout(ptr) + +取出所有路由操作执行完毕后的最终逻辑→物理布局(深拷贝)。 + +```c +struct CLayout *routed_circuit_final_layout(const struct CRoutedCircuit *ptr); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `ptr` | `const CRoutedCircuit*` | 路由结果句柄。 | + +返回:成功返回新建的 `CLayout*`(用 `layout_free` 释放);NULL 输入返回 NULL。 + +### routed_circuit_swap_count(ptr) + +返回插入的 SWAP 操作数量。 + +```c +uintptr_t routed_circuit_swap_count(const struct CRoutedCircuit *ptr); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `ptr` | `const CRoutedCircuit*` | 路由结果句柄。 | + +返回:SWAP 数量;NULL 输入返回 0。 + +### routed_circuit_diagnostics(ptr, out) + +把路由诊断快照写入 `*out`(字段含义见 [SABRE](5_sabre.md) 的 `CSabreRoutingDiagnostics`)。 + +```c +int32_t routed_circuit_diagnostics(const struct CRoutedCircuit *ptr, + struct CSabreRoutingDiagnostics *out); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `ptr` | `const CRoutedCircuit*` | 路由结果句柄。 | +| `out` | `struct CSabreRoutingDiagnostics*` | 诊断快照输出。 | + +返回:0 成功。 + +| 错误码 | 名称 | 场景 | +| --- | --- | --- | +| `-1` | NullPtr | `ptr` 或 `out` 为 NULL。 | + +### routed_circuit_changed(ptr, original) + +判断路由是否可观察地改动了 `original`:插入了 SWAP、物理比特集合与输入不同、全局相位改变、初始布局非恒等映射,或操作流出现结构差异,任一成立即视为改动。 + +```c +int32_t routed_circuit_changed(const struct CRoutedCircuit *ptr, const struct CCircuit *original); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `ptr` | `const CRoutedCircuit*` | 路由结果句柄。 | +| `original` | `const CCircuit*` | 路由前的原始线路。 | + +返回:1 有改动;0 无改动。 + +| 错误码 | 名称 | 场景 | +| --- | --- | --- | +| `-1` | NullPtr | `ptr` 或 `original` 为 NULL。 | + +--- + +## SabreRouteResult 读取 + +`route_sabre` 的结果句柄,除路由数据外还携带选中布局的元数据。 + +### sabre_route_result_free(ptr) + +释放路由结果句柄,允许传 NULL。 + +```c +void sabre_route_result_free(struct CSabreRouteResult *ptr); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `ptr` | `struct CSabreRouteResult*` | 待释放句柄,可为 NULL。 | + +### sabre_route_result_routed(ptr) + +取出内嵌的路由结果与路由元数据(深拷贝)。 + +```c +struct CRoutedCircuit *sabre_route_result_routed(const struct CSabreRouteResult *ptr); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `ptr` | `const CSabreRouteResult*` | 路由结果句柄。 | + +返回:成功返回新建的 `CRoutedCircuit*`(用 `routed_circuit_free` 释放);NULL 输入返回 NULL。 + +### sabre_route_result_circuit(ptr) + +取出路由后的物理线路(深拷贝),等价于先取 `routed` 再读其线路。 + +```c +struct CCircuit *sabre_route_result_circuit(const struct CSabreRouteResult *ptr); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `ptr` | `const CSabreRouteResult*` | 路由结果句柄。 | + +返回:成功返回新建的 `CCircuit*`(用 `circuit_free` 释放);NULL 输入返回 NULL。 + +### sabre_route_result_initial_layout(ptr) + +取出路由使用的初始逻辑→物理布局(深拷贝)。 + +```c +struct CLayout *sabre_route_result_initial_layout(const struct CSabreRouteResult *ptr); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `ptr` | `const CSabreRouteResult*` | 路由结果句柄。 | + +返回:成功返回新建的 `CLayout*`(用 `layout_free` 释放);NULL 输入返回 NULL。 + +### sabre_route_result_final_layout(ptr) + +取出路由完成后的最终逻辑→物理布局(深拷贝)。 + +```c +struct CLayout *sabre_route_result_final_layout(const struct CSabreRouteResult *ptr); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `ptr` | `const CSabreRouteResult*` | 路由结果句柄。 | + +返回:成功返回新建的 `CLayout*`(用 `layout_free` 释放);NULL 输入返回 NULL。 + +### sabre_route_result_swap_count(ptr) + +返回插入的 SWAP 操作数量。 + +```c +uintptr_t sabre_route_result_swap_count(const struct CSabreRouteResult *ptr); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `ptr` | `const CSabreRouteResult*` | 路由结果句柄。 | + +返回:SWAP 数量;NULL 输入返回 0。 + +### sabre_route_result_diagnostics(ptr, out) + +把路由诊断快照写入 `*out`(字段含义见 [SABRE](5_sabre.md) 的 `CSabreRoutingDiagnostics`)。 + +```c +int32_t sabre_route_result_diagnostics(const struct CSabreRouteResult *ptr, + struct CSabreRoutingDiagnostics *out); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `ptr` | `const CSabreRouteResult*` | 路由结果句柄。 | +| `out` | `struct CSabreRoutingDiagnostics*` | 诊断快照输出。 | + +返回:0 成功。 + +| 错误码 | 名称 | 场景 | +| --- | --- | --- | +| `-1` | NullPtr | `ptr` 或 `out` 为 NULL。 | + +### sabre_route_result_changed(ptr, original) + +判断路由是否可观察地改动了 `original`,判定规则与 `routed_circuit_changed` 一致。 + +```c +int32_t sabre_route_result_changed(const struct CSabreRouteResult *ptr, + const struct CCircuit *original); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `ptr` | `const CSabreRouteResult*` | 路由结果句柄。 | +| `original` | `const CCircuit*` | 路由前的原始线路。 | + +返回:1 有改动;0 无改动。 + +| 错误码 | 名称 | 场景 | +| --- | --- | --- | +| `-1` | NullPtr | `ptr` 或 `original` 为 NULL。 | + +--- + +## 布局元数据 + +`route_sabre` 额外记录的选中布局信息。 + +### sabre_route_result_layout_score(ptr, out) + +把选中初始布局的分数快照写入 `*out`。该分数只作诊断用途,SABRE 按预测原生路由质量选择胜者。 + +```c +int32_t sabre_route_result_layout_score(const struct CSabreRouteResult *ptr, + struct CLayoutScore *out); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `ptr` | `const CSabreRouteResult*` | 路由结果句柄。 | +| `out` | `struct CLayoutScore*` | 布局分数快照输出。 | + +返回:0 成功。 + +| 错误码 | 名称 | 场景 | +| --- | --- | --- | +| `-1` | NullPtr | `ptr` 或 `out` 为 NULL。 | +| `-8` | InvalidParam | 结果未携带布局分数。 | + +`CLayoutScore` 字段: + +| 字段 | 类型 | 含义 | +| --- | --- | --- | +| `total` | `double` | 按布局目标加权后的总分。 | +| `distance` | `double` | 加权距离分量的原始值。 | +| `direction` | `double` | 方向失配分量的原始值。 | +| `two_qubit_error` | `double` | 未加权的有效双比特放置代价。 | +| `readout_error` | `double` | 读出误差分量的原始值。 | +| `used_fidelity` | `uint8_t` | 1 表示布局目标使用了保真度项。 | +| `_reserved[7]` | `uint8_t[7]` | 填充字段,保持结构体布局稳定。 | + +### sabre_route_result_layout_is_perfect(ptr) + +判断选中布局是否把全部正权重交互直接落在相邻硬件边上(无需任何 SWAP)。 + +```c +int32_t sabre_route_result_layout_is_perfect(const struct CSabreRouteResult *ptr); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `ptr` | `const CSabreRouteResult*` | 路由结果句柄。 | + +返回:1 完美;0 非完美。 + +| 错误码 | 名称 | 场景 | +| --- | --- | --- | +| `-1` | NullPtr | `ptr` 为 NULL。 | + +### sabre_route_result_layout_candidates_evaluated(ptr) + +返回选择初始布局时评估过的候选布局数量。 + +```c +uintptr_t sabre_route_result_layout_candidates_evaluated(const struct CSabreRouteResult *ptr); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `ptr` | `const CSabreRouteResult*` | 路由结果句柄。 | + +返回:候选数量;NULL 输入返回 0。 + +### sabre_route_result_layout_used_fidelity(ptr) + +判断保真度数据是否参与了选中布局的评分。 + +```c +int32_t sabre_route_result_layout_used_fidelity(const struct CSabreRouteResult *ptr); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `ptr` | `const CSabreRouteResult*` | 路由结果句柄。 | + +返回:1 参与了评分;0 未参与。 + +| 错误码 | 名称 | 场景 | +| --- | --- | --- | +| `-1` | NullPtr | `ptr` 为 NULL。 | + +### sabre_route_result_layout_notes_len(ptr) / sabre_route_result_layout_note(ptr, index) + +两步式读取布局诊断备注:`*_len` 返回备注条数(NULL 返回 0),`layout_note` 返回第 `index` 条备注。 + +```c +uintptr_t sabre_route_result_layout_notes_len(const struct CSabreRouteResult *ptr); +``` + +```c +char *sabre_route_result_layout_note(const struct CSabreRouteResult *ptr, uintptr_t index); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `ptr` | `const CSabreRouteResult*` | 路由结果句柄。 | +| `index` | `uintptr_t` | 备注下标(`layout_note`)。 | + +返回:`layout_note` 返回堆上 C 字符串,用 `cqlib_string_free` 释放;NULL 输入或下标越界返回 NULL。 + +--- + +## 示例 + +直线拓扑 0–1–2 上路由 `cx(0, 2)`,并读取两类结果的全部元数据: + +```c +#include +#include "cqlib_c.h" + +int main(void) { + /* 1. Line topology 0 - 1 - 2 and a non-adjacent cx(0, 2) */ + uint32_t edges[4] = {0, 1, 1, 2}; + struct CDevice *device = device_from_edges("line-3", 3, edges, 2); + struct CCircuit *circuit = circuit_new(3); + circuit_cx(circuit, 0, 2); + if (device == NULL || circuit == NULL) { + return 1; + } + + /* 2. Route from an explicit identity layout (logical i -> physical i) */ + uint32_t logical[3] = {0, 1, 2}; + uint32_t physical[3] = {0, 1, 2}; + struct CLayout *layout = layout_new(logical, 3, physical, 3); + + struct CRoutedCircuit *routed = route_with_layout(circuit, device, layout, NULL); + if (routed != NULL) { + /* 3. Read the routed circuit, layouts, and diagnostics */ + uintptr_t swaps = routed_circuit_swap_count(routed); /* >= 1 */ + int32_t changed = routed_circuit_changed(routed, circuit); /* 1 */ + struct CCircuit *physical_circuit = routed_circuit_circuit(routed); + struct CLayout *initial = routed_circuit_initial_layout(routed); + struct CLayout *final_layout = routed_circuit_final_layout(routed); + struct CSabreRoutingDiagnostics diagnostics; + if (routed_circuit_diagnostics(routed, &diagnostics) == 0) { + printf("trials=%lu swaps=%lu changed=%d\n", + (unsigned long)diagnostics.trials_evaluated, + (unsigned long)swaps, changed); + } + circuit_free(physical_circuit); + layout_free(initial); + layout_free(final_layout); + routed_circuit_free(routed); + } + layout_free(layout); + + /* 4. route_sabre: select a layout and route in one call */ + struct CSabreRouteResult *result = + route_sabre(circuit, device, LAYOUT_OBJECTIVE_TOPOLOGY_ONLY, NULL); + if (result != NULL) { + struct CLayoutScore score; + if (sabre_route_result_layout_score(result, &score) == 0) { + printf("layout score total=%f perfect=%d\n", + score.total, sabre_route_result_layout_is_perfect(result)); + } + printf("candidates=%lu\n", + (unsigned long)sabre_route_result_layout_candidates_evaluated(result)); + + /* 5. Walk the layout diagnostic notes (two-step pattern) */ + uintptr_t notes = sabre_route_result_layout_notes_len(result); + for (uintptr_t i = 0; i < notes; i++) { + char *note = sabre_route_result_layout_note(result, i); + if (note != NULL) { + cqlib_string_free(note); + } + } + + struct CRoutedCircuit *inner = sabre_route_result_routed(result); + if (inner != NULL) { + routed_circuit_free(inner); + } + sabre_route_result_free(result); + } + + circuit_free(circuit); + device_free(device); + return 0; +} +``` + +把路由结果降级到设备原生门集见 [分解与重综合](6_decompose_resynthesis.md) 的 `decompose_lower_to_device`;完整的编译工作流(含路由与降级)见 [编译器](1_compiler.md)。 diff --git a/docs/api/cn/c/4_compile/5_sabre.md b/docs/api/cn/c/4_compile/5_sabre.md new file mode 100644 index 00000000..4cfb5de6 --- /dev/null +++ b/docs/api/cn/c/4_compile/5_sabre.md @@ -0,0 +1,417 @@ +# SABRE(C) + +`CSabreRoutingResult` 是底层 SABRE 路由 `sabre_route` 的结果句柄。本页覆盖 SABRE 配置结构 `SabreConfigC` 的构造与校验、已知初始布局下的路由入口、路由结果读取,以及布局规范化 `normalize_initial_layout` 与可达性验证 `validate_reachable_interactions`。固定随机种子可复现多轮试探的平局裁决与选择。错误码、字符串与句柄的释放约定遵循 [Overview](../0_overview.md);带布局选择的路由入口 `route_sabre` / `route_with_layout` 见 [路由](4_routing.md)。 + +--- + +## 常量 + +| 常量 | 值 | 含义 | +| --- | --- | --- | +| `SABRE_MAX_LOOKAHEAD_WEIGHTS` | 8 | `SabreHeuristicConfigC.lookahead_weights` 数组长度;`num_lookahead_weights` 超过该值时配置无法转换(-8)。 | + +--- + +## 配置构造与校验 + +### sabre_config_default() + +返回核心默认 SABRE 配置(按值):`layout_trials=10`、`layout_assignment_budget=1000000`、启用 VF2 预检查(`candidate_limit=10`、`call_limit=1000000`)、`refinement_iterations=1`、`routing_trials=1`、无种子;启发式为 `basic_weight=1.0`、`lookahead_weights=[0.5, 0.25, 0.125, 0.0625, 0.03125]`、启用拥塞控制(`decay_increment=0.002`、`decay_reset=10`)、`attempt_limit=1000`、`best_epsilon=1e-10`。 + +```c +struct SabreConfigC sabre_config_default(void); +``` + +返回:默认配置结构体(按值,无需释放)。 + +### sabre_config_deterministic_seeded(seed) + +返回适合可复现运行的紧凑确定性配置:压缩试探规模(`layout_trials=2`、`layout_assignment_budget=100000`、VF2 预检查 `candidate_limit=10`、`call_limit=100000`、`refinement_iterations=1`、`routing_trials=1`),固定随机种子为 `seed`(`has_seed=1`),并把 `attempt_limit` 收紧到 20;其余字段与默认配置一致。 + +```c +struct SabreConfigC sabre_config_deterministic_seeded(uint64_t seed); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `seed` | `uint64_t` | 确定性随机种子。相同种子、相同线路与设备产生相同路由结果。 | + +返回:确定性配置结构体(按值,无需释放)。 + +### sabre_config_validate(config) + +校验配置的路由相关字段:`routing_trials` 必须大于 0;权重(`basic_weight`、各 `lookahead_weights`、`decay_increment`)必须有限且非负;启用拥塞控制时 `decay_reset` 必须大于 0;`best_epsilon` 必须有限且非负。布局细化字段(如 `layout_trials`)不参与校验。 + +```c +int32_t sabre_config_validate(const struct SabreConfigC *config); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `config` | `const SabreConfigC*` | 待校验配置。 | + +返回:0 合法。 + +| 错误码 | 名称 | 场景 | +| --- | --- | --- | +| `-1` | NullPtr | `config` 为 NULL。 | +| `-6` | CompilerError | 核心校验拒绝:`routing_trials` 为 0、权重非法、启用拥塞控制但 `decay_reset` 为 0、`best_epsilon` 非法。 | +| `-8` | InvalidParam | `num_lookahead_weights` 超过 `SABRE_MAX_LOOKAHEAD_WEIGHTS`,配置无法转换。 | + +--- + +## 路由入口 + +### sabre_route(circuit, device, initial_layout, config) + +已知初始布局条件下的底层 SABRE 路由:插入 SWAP 使全部二体交互在设备可用物理拓扑上相邻,控制流块被递归路由并在离开块前恢复入口布局。返回线路使用物理比特编号。 + +```c +struct CSabreRoutingResult *sabre_route(const struct CCircuit *circuit, + const struct CDevice *device, + const struct CLayout *initial_layout, + const struct SabreConfigC *config); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `circuit` | `const CCircuit*` | 输入线路。 | +| `device` | `const CDevice*` | 目标设备(构造方式见 [设备与噪声属性](../2_device/2_properties_device.md))。 | +| `initial_layout` | `const CLayout*` | 初始逻辑→物理映射(构造方式见 [Layout](../2_device/3_layout.md))。 | +| `config` | `const SabreConfigC*` | SABRE 配置;NULL 使用默认配置。 | + +返回:成功返回新建的 `CSabreRoutingResult*`(用 `sabre_routing_result_free` 释放);任一句柄为 NULL、配置无法转换或校验失败(例如 `routing_trials` 为 0)或路由失败时返回 NULL。 + +--- + +## 结果读取 + +### sabre_routing_result_free(ptr) + +释放路由结果句柄,允许传 NULL。 + +```c +void sabre_routing_result_free(struct CSabreRoutingResult *ptr); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `ptr` | `struct CSabreRoutingResult*` | 待释放句柄,可为 NULL。 | + +### sabre_routing_result_circuit(ptr) + +取出插入 SWAP 后的物理线路(深拷贝,线路比特为物理比特编号)。 + +```c +struct CCircuit *sabre_routing_result_circuit(const struct CSabreRoutingResult *ptr); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `ptr` | `const CSabreRoutingResult*` | 路由结果句柄。 | + +返回:成功返回新建的 `CCircuit*`(用 `circuit_free` 释放);NULL 输入返回 NULL。 + +### sabre_routing_result_initial_layout(ptr) + +取出选中试探使用的初始逻辑→物理布局(深拷贝)。 + +```c +struct CLayout *sabre_routing_result_initial_layout(const struct CSabreRoutingResult *ptr); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `ptr` | `const CSabreRoutingResult*` | 路由结果句柄。 | + +返回:成功返回新建的 `CLayout*`(用 `layout_free` 释放);NULL 输入返回 NULL。 + +### sabre_routing_result_final_layout(ptr) + +取出所有路由操作(含控制流尾声)执行完毕后的最终逻辑→物理布局(深拷贝)。 + +```c +struct CLayout *sabre_routing_result_final_layout(const struct CSabreRoutingResult *ptr); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `ptr` | `const CSabreRoutingResult*` | 路由结果句柄。 | + +返回:成功返回新建的 `CLayout*`(用 `layout_free` 释放);NULL 输入返回 NULL。 + +### sabre_routing_result_swap_count(ptr) + +返回插入的 SWAP 操作数量(含控制流尾声)。 + +```c +uintptr_t sabre_routing_result_swap_count(const struct CSabreRoutingResult *ptr); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `ptr` | `const CSabreRoutingResult*` | 路由结果句柄。 | + +返回:SWAP 数量;NULL 输入返回 0。 + +### sabre_routing_result_diagnostics(ptr, out) + +把路由诊断快照写入 `*out`。 + +```c +int32_t sabre_routing_result_diagnostics(const struct CSabreRoutingResult *ptr, + struct CSabreRoutingDiagnostics *out); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `ptr` | `const CSabreRoutingResult*` | 路由结果句柄。 | +| `out` | `struct CSabreRoutingDiagnostics*` | 诊断快照输出。 | + +返回:0 成功。 + +| 错误码 | 名称 | 场景 | +| --- | --- | --- | +| `-1` | NullPtr | `ptr` 或 `out` 为 NULL。 | + +--- + +## 布局工具 + +### normalize_initial_layout(logical_qubits, num_logical, device, initial_layout) + +把完整逻辑→物理布局规范化到设备可用物理比特上:`logical_qubits` 中的每个逻辑比特必须已被 `initial_layout` 映射,且映射目标必须是设备的可用物理比特;返回布局的物理比特集合为设备全部可用物理比特。 + +```c +struct CLayout *normalize_initial_layout(const uint32_t *logical_qubits, + uintptr_t num_logical, + const struct CDevice *device, + const struct CLayout *initial_layout); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `logical_qubits` | `const uint32_t*` | 逻辑比特 ID 数组;`num_logical` 为 0 时可为 NULL。 | +| `num_logical` | `uintptr_t` | 逻辑比特数。 | +| `device` | `const CDevice*` | 目标设备。 | +| `initial_layout` | `const CLayout*` | 待规范化的布局。 | + +返回:成功返回新建的 `CLayout*`(用 `layout_free` 释放);`device` 或 `initial_layout` 为 NULL、`num_logical` 大于 0 但 `logical_qubits` 为 NULL,或规范化失败(逻辑比特未映射、映射到不可用物理比特)时返回 NULL。 + +### validate_reachable_interactions(circuit, device, initial_layout) + +在不执行路由的前提下验证:从 `initial_layout` 出发,线路中的每个交互都可达(交互双方位于设备拓扑的同一连通分量,可通过 SWAP 就近)。验证前会先把布局规范化到设备可用物理比特。 + +```c +int32_t validate_reachable_interactions(const struct CCircuit *circuit, + const struct CDevice *device, + const struct CLayout *initial_layout); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `circuit` | `const CCircuit*` | 输入线路。 | +| `device` | `const CDevice*` | 目标设备。 | +| `initial_layout` | `const CLayout*` | 初始逻辑→物理映射。 | + +返回:0 全部交互可达。 + +| 错误码 | 名称 | 场景 | +| --- | --- | --- | +| `-1` | NullPtr | 任一句柄为 NULL。 | +| `-6` | CompilerError | 存在不可达交互(例如交互一端落在无耦合的物理比特上)或布局无法规范化。 | + +--- + +## SabreConfigC + +SABRE 布局细化与路由共用的配置,按值传递: + +```c +typedef struct SabreConfigC { + uintptr_t layout_trials; /* Randomized starting layouts. */ + uintptr_t layout_assignment_budget; /* Max layout assignment states. */ + uint8_t vf2_prepass_enabled; /* 1 enables the bounded VF2 prepass. */ + struct SabreVf2PrepassConfigC vf2_prepass; + uintptr_t refinement_iterations; /* Refinement rounds per layout trial. */ + uintptr_t routing_trials; /* Complete routing trials per layout. */ + uint64_t seed; /* Used when has_seed != 0. */ + uint8_t has_seed; /* 1 uses seed. */ + struct SabreHeuristicConfigC heuristic; + uint64_t _reserved[4]; /* Reserved padding. */ +} SabreConfigC; +``` + +| 字段 | 类型 | 含义 | +| --- | --- | --- | +| `layout_trials` | `uintptr_t` | 在确定性候选之外增加的随机化初始布局数量。 | +| `layout_assignment_budget` | `uintptr_t` | 布局组件分配状态的最大探索量;耗尽报告预算耗尽,区别于已证明不可行。 | +| `vf2_prepass_enabled` | `uint8_t` | 1 启用有界 VF2 预检查,0 关闭(此时 `vf2_prepass` 被忽略)。 | +| `vf2_prepass` | `struct SabreVf2PrepassConfigC` | VF2 预检查限制(`vf2_prepass_enabled != 0` 时使用)。 | +| `refinement_iterations` | `uintptr_t` | 每个布局试探的正向/反向细化轮数。 | +| `routing_trials` | `uintptr_t` | 每个完整细化布局执行的完整路由试探数,必须大于 0。 | +| `seed` | `uint64_t` | 确定性种子(`has_seed != 0` 时使用)。 | +| `has_seed` | `uint8_t` | 1 使用 `seed`,0 不设置种子。 | +| `heuristic` | `struct SabreHeuristicConfigC` | 交换选择启发式配置。 | +| `_reserved[4]` | `uint64_t[4]` | 填充字段,保持结构体布局稳定,勿写入非零值。 | + +### SabreVf2PrepassConfigC + +```c +typedef struct SabreVf2PrepassConfigC { + uintptr_t candidate_limit; /* Complete perfect mappings scored. */ + uintptr_t call_limit; /* Partial mapping extensions attempted. */ + uint64_t _reserved[2]; /* Reserved padding. */ +} SabreVf2PrepassConfigC; +``` + +| 字段 | 类型 | 含义 | +| --- | --- | --- | +| `candidate_limit` | `uintptr_t` | VF2 打分的完整完美映射数量上限。 | +| `call_limit` | `uintptr_t` | VF2 尝试的部分映射扩展数量上限。 | +| `_reserved[2]` | `uint64_t[2]` | 填充字段,保持结构体布局稳定。 | + +### SabreHeuristicConfigC + +SABRE 交换选择启发式配置:主分数组合当前前沿层距离和、按活跃层缩放的前瞻层与与乘法拥塞;有原生方案时在窄结构窗口内用精确原生双比特代价裁决候选。 + +```c +typedef struct SabreHeuristicConfigC { + double basic_weight; /* Front-layer total distance weight. */ + double lookahead_weights[SABRE_MAX_LOOKAHEAD_WEIGHTS]; + uintptr_t num_lookahead_weights; /* Leading entries in use. */ + double decay_increment; /* Congestion multiplier increment. */ + uint8_t decay_enabled; /* 1 enables congestion control. */ + uintptr_t decay_reset; /* Attempts before decay resets. */ + uintptr_t attempt_limit; /* Heuristic SWAPs before fallback. */ + double best_epsilon; /* Tie tolerance for candidate scores. */ + uint8_t _reserved[8]; /* Reserved padding. */ +} SabreHeuristicConfigC; +``` + +| 字段 | 类型 | 含义 | +| --- | --- | --- | +| `basic_weight` | `double` | 当前前沿层总距离的权重,须有限且非负。 | +| `lookahead_weights` | `double[8]` | 各活跃层缩放前瞻层总和的权重;仅前 `num_lookahead_weights` 个条目生效,其余为填充。 | +| `num_lookahead_weights` | `uintptr_t` | `lookahead_weights` 中前导有效条目数,不得超过 `SABRE_MAX_LOOKAHEAD_WEIGHTS`。 | +| `decay_increment` | `double` | 某物理比特被启发式 SWAP 使用后其拥塞乘数的增量。 | +| `decay_enabled` | `uint8_t` | 1 启用拥塞控制(使用 `decay_increment`),0 关闭。 | +| `decay_reset` | `uintptr_t` | 衰减值重置前的启发式 SWAP 尝试次数;启用拥塞控制时必须大于 0。 | +| `attempt_limit` | `uintptr_t` | 未路由前沿节点时允许的连续启发式 SWAP 次数,超过后回退到最短路径逃逸。 | +| `best_epsilon` | `double` | 候选 SWAP 分数视为平局的浮点容差,须有限且非负。 | +| `_reserved[8]` | `uint8_t[8]` | 填充字段,保持结构体布局稳定。 | + +--- + +## CSabreRoutingDiagnostics + +路由过程诊断快照(`sabre_routing_result_diagnostics` 及 [路由](4_routing.md) 各 diagnostics 接口的输出): + +| 字段 | 类型 | 含义 | +| --- | --- | --- | +| `trials_evaluated` | `uintptr_t` | 评估的路由试探数。 | +| `selected_trial_index` | `uintptr_t` | 选中的路由试探下标(从 0 开始)。 | +| `fallback_count` | `uintptr_t` | 最短路径回退的使用次数。 | +| `control_flow_blocks_routed` | `uintptr_t` | 递归路由的控制流块数。 | +| `two_qubit_depth` | `uintptr_t` | 选中路由操作流的 ASAP 双比特深度。 | +| `operation_count` | `uintptr_t` | 选中路由流中的操作总数。 | +| `native_two_qubit_count` | `uintptr_t` | 设备降级后预测的原生双比特操作数。 | +| `native_two_qubit_depth` | `uintptr_t` | 使用选中精确 qargs 原生方案的 ASAP 双比特深度。 | +| `native_total_depth` | `uintptr_t` | 使用选中精确 qargs 原生方案的 ASAP 总深度。 | +| `native_operation_count` | `uintptr_t` | 设备降级后预测的原生操作总数。 | +| `unknown_loop_count` | `uintptr_t` | 总执行代价未知的动态 `for`/`while` 循环数。 | +| `requirement_signature_count` | `uintptr_t` | 已准备的不同单比特/比特对需求签名数。 | +| `eager_pair_state_count` | `uintptr_t` | 目标急切保留的比特对放置下界状态数。 | +| `lazy_pair_l1_lookup_count` | `uintptr_t` | 选中试探发出的惰性比特对状态下界探测数。 | +| `lazy_pair_l1_hit_count` | `uintptr_t` | 选中试探由其本地 L1 缓存命中的探测数。 | +| `lazy_pair_l1_cached_count` | `uintptr_t` | 选中试探有界 L1 缓存保留的比特对状态条目数。 | + +--- + +## 示例 + +直线拓扑 0–1–2 上,从恒等布局出发做底层 SABRE 路由,随后规范化布局并验证可达性: + +```c +#include +#include "cqlib_c.h" + +int main(void) { + /* 1. Line topology 0 - 1 - 2 with a non-adjacent cx(0, 2) */ + uint32_t edges[4] = {0, 1, 1, 2}; + struct CDevice *device = device_from_edges("line-3", 3, edges, 2); + struct CCircuit *circuit = circuit_new(3); + circuit_cx(circuit, 0, 2); + if (device == NULL || circuit == NULL) { + return 1; + } + + /* 2. Deterministic seeded config: same seed -> same routing result */ + struct SabreConfigC config = sabre_config_deterministic_seeded(42); + if (sabre_config_validate(&config) != 0) { + circuit_free(circuit); + device_free(device); + return 1; + } + + /* 3. Low-level SABRE route from the identity layout */ + uint32_t logical[3] = {0, 1, 2}; + uint32_t physical[3] = {0, 1, 2}; + struct CLayout *layout = layout_new(logical, 3, physical, 3); + + struct CSabreRoutingResult *result = sabre_route(circuit, device, layout, &config); + if (result != NULL) { + /* 4. Routed circuit, swap count, layouts, and diagnostics */ + struct CCircuit *routed = sabre_routing_result_circuit(result); + uintptr_t swaps = sabre_routing_result_swap_count(result); /* >= 1 */ + struct CLayout *final_layout = sabre_routing_result_final_layout(result); + struct CSabreRoutingDiagnostics diagnostics; + if (sabre_routing_result_diagnostics(result, &diagnostics) == 0) { + printf("trials=%lu swaps=%lu\n", + (unsigned long)diagnostics.trials_evaluated, + (unsigned long)swaps); + } + circuit_free(routed); + layout_free(final_layout); + sabre_routing_result_free(result); + } + + /* 5. Normalize the layout onto usable qubits */ + struct CLayout *normalized = normalize_initial_layout(logical, 3, device, layout); + if (normalized != NULL) { + layout_free(normalized); + } + + /* 6. Reachability check: qubits 0 and 2 share one connected component */ + int32_t reachable = validate_reachable_interactions(circuit, device, layout); /* 0 */ + + layout_free(layout); + circuit_free(circuit); + device_free(device); + return 0; +} +``` + +需要自动选择初始布局时使用 `route_sabre`,见 [路由](4_routing.md)。 diff --git a/docs/api/cn/c/4_compile/6_decompose_resynthesis.md b/docs/api/cn/c/4_compile/6_decompose_resynthesis.md new file mode 100644 index 00000000..27c0fa92 --- /dev/null +++ b/docs/api/cn/c/4_compile/6_decompose_resynthesis.md @@ -0,0 +1,1091 @@ +# 分解与重综合(C) + +本页覆盖线路级分解 pass(电路门定义展开 `decompose_expand_definitions`、矩阵酉门合成 `decompose_unitaries`、多控制门改写 `decompose_mc_gates`)、双比特块重综合 `resynthesize_two_qubit_blocks`、数值酉矩阵合成原语(单比特 / 双比特)、酉合成目标 `decompose_unitary_target_*`、KAK(Weyl)分解、目标门集降级 `target_basis_lowerer_*` 与设备降级 `decompose_lower_to_device`。分解按操作可用的表示分层:定义展开先于数值合成执行,使自带实现线路的酉门在进入矩阵合成之前已经展开。矩阵输入使用 `Complex64`(`{double re; double im}` 交错对,行主序),布局说明见 [酉门](../0_circuit/6_gate_unitary.md)。错误码、字符串与句柄的释放约定遵循 [Overview](../0_overview.md)。 + +--- + +## 常量 + +双比特合成基组标签(`synthesize_numeric_2q_unitary` 的 `basis` 参数): + +| 常量 | 值 | 含义 | +| --- | --- | --- | +| `TWO_QUBIT_BASIS_PAULI_ROTATIONS` | 0 | 局域 `U` + `RXX`/`RYY`/`RZZ` 泡利旋转。 | +| `TWO_QUBIT_BASIS_CX` | 1 | 局域 `U` + `CX` 模板。 | +| `TWO_QUBIT_BASIS_CY` | 2 | 局域 `U` + `CY` 模板。 | +| `TWO_QUBIT_BASIS_CZ` | 3 | 局域 `U` + `CZ` 模板。 | +| `TWO_QUBIT_BASIS_RZZ` | 4 | 局域 `U` + `RZZ`。 | + +KAK 局域因子标签(`kak_decomposition_local_factor` 的 `factor` 参数): + +| 常量 | 值 | 含义 | +| --- | --- | --- | +| `KAK_FACTOR_K1L` | 0 | `k1l`(交互之后作用的左侧局域因子)。 | +| `KAK_FACTOR_K1R` | 1 | `k1r`(交互之后作用的右侧局域因子)。 | +| `KAK_FACTOR_K2L` | 2 | `k2l`(交互之前作用的左侧局域因子)。 | +| `KAK_FACTOR_K2R` | 3 | `k2r`(交互之前作用的右侧局域因子)。 | + +--- + +## 配置构造 + +### decompose_unitary_config_default() + +按值返回默认酉门分解配置:`recurse_control_flow=1`(递归进入控制流体)。 + +```c +struct CUnitaryDecomposeConfig decompose_unitary_config_default(void); +``` + +返回:默认配置结构体(按值,无需释放)。 + +### mc_gate_config_default() + +按值返回默认多控制门分解配置:不预布局干净 ancilla(`max_pre_layout_clean_ancillas=0`)、不允许脏借用(`allow_dirty_borrowing=0`)、不设置总比特数硬限制(`has_max_total_qubits=0`)。 + +```c +struct CMcGateDecomposeConfig mc_gate_config_default(void); +``` + +返回:默认配置结构体(按值,无需释放)。 + +### resynthesis_config_normal() + +按值返回常规预算的双比特块重综合配置:`max_block_ops=16`、`max_crossed_ops=4`、`max_scan_span=32`、`skip_labeled_ops=1`、`recurse_control_flow=1`、启用知识规则对易 oracle(`enable_rule_oracle=1`)、关闭矩阵回退(`enable_matrix_fallback=0`)、`max_matrix_qubits=4`。 + +```c +struct CResynthesisConfig resynthesis_config_normal(void); +``` + +返回:常规预算配置结构体(按值,无需释放)。 + +### resynthesis_config_enhanced() + +按值返回增强预算的双比特块重综合配置:在常规预算基础上放宽块收集规模(`max_block_ops=32`、`max_crossed_ops=8`、`max_scan_span=64`),其余字段与常规预算一致。 + +```c +struct CResynthesisConfig resynthesis_config_enhanced(void); +``` + +返回:增强预算配置结构体(按值,无需释放)。 + +--- + +## 线路级分解 pass + +### decompose_expand_definitions(circuit) + +展开线路中电路门(自带实现线路的门)的定义,把门调用替换回其实现线路中的普通操作,返回重建线路。 + +```c +struct CCircuit *decompose_expand_definitions(const struct CCircuit *circuit); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `circuit` | `const CCircuit*` | 输入线路。 | + +返回:成功返回新建的 `CCircuit*`(用 `circuit_free` 释放);`circuit` 为 NULL 或展开失败时返回 NULL。 + +电路门的构造见 [电路门](../0_circuit/8_gate_circuit_gate.md)。 + +### decompose_unitaries(circuit, config) + +合成线路中带固定矩阵的酉门(`circuit_unitary` 添加的门),把矩阵替换为标准门操作序列,返回重建线路。等价于 `stats` 传 NULL 的 `decompose_unitaries_with_rule_stats`。 + +```c +struct CCircuit *decompose_unitaries(const struct CCircuit *circuit, + struct CUnitaryDecomposeConfig config); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `circuit` | `const CCircuit*` | 输入线路。 | +| `config` | `struct CUnitaryDecomposeConfig` | 分解配置(按值)。 | + +返回:成功返回新建的 `CCircuit*`(用 `circuit_free` 释放);`circuit` 为 NULL 或合成失败时返回 NULL。 + +矩阵酉门的构造见 [酉门](../0_circuit/6_gate_unitary.md)。 + +### decompose_unitaries_with_rule_stats(circuit, config, stats) + +`decompose_unitaries` 的诊断形式:当 `stats` 非 NULL 时,把本次运行 pass 局部分解规则缓存的统计快照写入 `*stats`。 + +```c +struct CCircuit *decompose_unitaries_with_rule_stats(const struct CCircuit *circuit, + struct CUnitaryDecomposeConfig config, + struct CDecompositionRuleStats *stats); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `circuit` | `const CCircuit*` | 输入线路。 | +| `config` | `struct CUnitaryDecomposeConfig` | 分解配置(按值)。 | +| `stats` | `struct CDecompositionRuleStats*` | 规则缓存统计输出;NULL 表示不需要统计。 | + +返回:成功返回新建的 `CCircuit*`(用 `circuit_free` 释放);`circuit` 为 NULL 或合成失败时返回 NULL(此时 `*stats` 保持不变)。 + +### decompose_mc_gates(circuit, config) + +改写线路中的多控制门(`circuit_multi_control` 添加的门),将其替换为单控制 / 无控制操作序列,返回重建线路。等价于 `stats` 传 NULL 的 `decompose_mc_gates_with_rule_stats`。 + +```c +struct CCircuit *decompose_mc_gates(const struct CCircuit *circuit, + struct CMcGateDecomposeConfig config); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `circuit` | `const CCircuit*` | 输入线路。 | +| `config` | `struct CMcGateDecomposeConfig` | 资源与限制配置(按值)。 | + +返回:成功返回新建的 `CCircuit*`(用 `circuit_free` 释放);`circuit` 为 NULL 或改写失败时返回 NULL。 + +多控制门的构造见 [多控制门](../0_circuit/7_gate_mc_gate.md)。 + +### decompose_mc_gates_with_rule_stats(circuit, config, stats) + +`decompose_mc_gates` 的诊断形式:当 `stats` 非 NULL 时,把本次运行 pass 局部分解规则缓存的统计快照写入 `*stats`。 + +```c +struct CCircuit *decompose_mc_gates_with_rule_stats(const struct CCircuit *circuit, + struct CMcGateDecomposeConfig config, + struct CDecompositionRuleStats *stats); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `circuit` | `const CCircuit*` | 输入线路。 | +| `config` | `struct CMcGateDecomposeConfig` | 资源与限制配置(按值)。 | +| `stats` | `struct CDecompositionRuleStats*` | 规则缓存统计输出;NULL 表示不需要统计。 | + +返回:成功返回新建的 `CCircuit*`(用 `circuit_free` 释放);`circuit` 为 NULL 或改写失败时返回 NULL(此时 `*stats` 保持不变)。 + +### decompose_mc_gates_for_device(circuit, device, policy) + +在设备可用比特容量内改写线路中的多控制门(预布局逻辑变换):改写方案受设备的可用物理比特数约束。`policy` 可为 NULL,此时选用默认资源策略。 + +```c +struct CCircuit *decompose_mc_gates_for_device(const struct CCircuit *circuit, + const struct CDevice *device, + const struct CResourcePolicy *policy); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `circuit` | `const CCircuit*` | 输入线路。 | +| `device` | `const CDevice*` | 目标设备(构造方式见 [设备与噪声属性](../2_device/2_properties_device.md))。 | +| `policy` | `const CResourcePolicy*` | 资源策略;NULL 选用默认值(见 [资源策略](8_resource.md))。 | + +返回:成功返回新建的 `CCircuit*`(用 `circuit_free` 释放);任一句柄为 NULL 或改写失败时返回 NULL。 + +--- + +## 双比特块重综合 + +### resynthesize_two_qubit_blocks(circuit, config) + +重综合线路中的双比特块:围绕双比特锚点在有限预算内收集候选块,利用对易引擎重排并重新合成,返回重建线路。 + +```c +struct CCircuit *resynthesize_two_qubit_blocks(const struct CCircuit *circuit, + struct CResynthesisConfig config); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `circuit` | `const CCircuit*` | 输入线路。 | +| `config` | `struct CResynthesisConfig` | 重综合预算配置(按值)。 | + +返回:成功返回新建的 `CCircuit*`(用 `circuit_free` 释放);`circuit` 为 NULL 或重综合失败时返回 NULL。 + +--- + +## 数值酉合成原语 + +### synthesize_numeric_1q_unitary(matrix, out) + +把 2×2 酉矩阵合成为 Cqlib 的 `U` 门约定:表示的矩阵为 `exp(i * global_phase) * U(theta, phi, lambda)`。`matrix` 指向 4 个行主序 `Complex64` 元素。 + +```c +int32_t synthesize_numeric_1q_unitary(const Complex64 *matrix, + struct COneQubitUnitaryDecomposition *out); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `matrix` | `const Complex64*` | 4 个行主序复数元素(2×2 矩阵)。 | +| `out` | `struct COneQubitUnitaryDecomposition*` | 合成结果输出。 | + +返回:0 成功。 + +| 错误码 | 名称 | 场景 | +| --- | --- | --- | +| `-1` | NullPtr | `matrix` 或 `out` 为 NULL。 | +| `-6` | CompilerError | 矩阵含非有限值、非酉,或被合成器拒绝。 | + +### synthesize_numeric_2q_unitary(matrix, first, second, basis) + +把 4×4 酉矩阵合成为标准门操作序列,输出到 `CTwoQubitUnitarySynthesis` 句柄。`matrix` 指向 16 个行主序 `Complex64` 元素;`first` 与 `second` 为目标比特;`basis` 为 `TWO_QUBIT_BASIS_*` 之一。 + +```c +struct CTwoQubitUnitarySynthesis *synthesize_numeric_2q_unitary(const Complex64 *matrix, + uint32_t first, + uint32_t second, + uint8_t basis); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `matrix` | `const Complex64*` | 16 个行主序复数元素(4×4 矩阵)。 | +| `first` | `uint32_t` | 第一个目标比特 ID。 | +| `second` | `uint32_t` | 第二个目标比特 ID。 | +| `basis` | `uint8_t` | 合成基组,`TWO_QUBIT_BASIS_*` 之一。 | + +返回:成功返回新建的 `CTwoQubitUnitarySynthesis*`(用 `two_qubit_synthesis_free` 释放);`matrix` 为 NULL、`basis` 不是有效标签,或合成失败(例如矩阵含非有限值或非酉)时返回 NULL。 + +--- + +## 酉合成目标 + +`CDecomposeUnitaryTarget` 描述酉合成步骤可以发出的原生门集:原生单比特基组、原生双比特基组与 Pauli 旋转回退开关。它还能在原生基组下评估固定操作序列的精确目标基组代价。 + +### decompose_unitary_target_unconstrained() + +```c +struct CDecomposeUnitaryTarget *decompose_unitary_target_unconstrained(void); +``` + +创建无约束合成目标:不带任何原生基组限制,启用中性的精确 Pauli 旋转回退。 + +返回:新建的 `CDecomposeUnitaryTarget*`(用 `decompose_unitary_target_free` 释放)。 + +### decompose_unitary_target_from_instructions(gate_names, len) + +从工作流风格的目标基组列表(标准门名,如 `"H"`、`"RZ"`、`"CX"`)构造合成目标:单比特门名成为原生单比特基组,双比特门名成为原生双比特基组,精确 Pauli 旋转回退保持启用。 + +```c +struct CDecomposeUnitaryTarget *decompose_unitary_target_from_instructions( + const char *const *gate_names, + uintptr_t len); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `gate_names` | `const char* const*` | 标准门名数组;`len` 为 0 时可为 NULL。 | +| `len` | `uintptr_t` | 门名数量。 | + +返回:成功返回新建的 `CDecomposeUnitaryTarget*`(用 `decompose_unitary_target_free` 释放);`len` 大于 0 但 `gate_names` 为 NULL、某条目非法 UTF-8 或为未知门名,或核心拒绝该基组(例如空基组)时返回 NULL。 + +### decompose_unitary_target_from_standard_gates(native_1q_names, num_1q, native_2q_names, num_2q, fallback_pauli) + +从显式的原生单比特与双比特标准门名列表以及 Pauli 旋转回退开关构造合成目标。`native_1q_names` 中的每个名字必须是单比特标准门,`native_2q_names` 中的每个名字必须是双比特标准门。 + +```c +struct CDecomposeUnitaryTarget *decompose_unitary_target_from_standard_gates( + const char *const *native_1q_names, + uintptr_t num_1q, + const char *const *native_2q_names, + uintptr_t num_2q, + uint8_t fallback_pauli); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `native_1q_names` | `const char* const*` | 原生单比特标准门名;`num_1q` 为 0 时可为 NULL。 | +| `num_1q` | `uintptr_t` | 单比特门名数量。 | +| `native_2q_names` | `const char* const*` | 原生双比特标准门名;`num_2q` 为 0 时可为 NULL。 | +| `num_2q` | `uintptr_t` | 双比特门名数量。 | +| `fallback_pauli` | `uint8_t` | 非 0 启用精确 Pauli 旋转回退。 | + +返回:成功返回新建的 `CDecomposeUnitaryTarget*`(用 `decompose_unitary_target_free` 释放);NULL 输入、非法 UTF-8、未知门名、门元数不符或合并基组为空时返回 NULL。 + +### decompose_unitary_target_free(ptr) + +释放酉分解合成目标句柄,允许传 NULL。 + +```c +void decompose_unitary_target_free(struct CDecomposeUnitaryTarget *ptr); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `ptr` | `struct CDecomposeUnitaryTarget*` | 待释放句柄,可为 NULL。 | + +### decompose_unitary_target_fallback_pauli(ptr) + +判断目标是否允许精确 Pauli 旋转回退。 + +```c +int32_t decompose_unitary_target_fallback_pauli(const struct CDecomposeUnitaryTarget *ptr); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `ptr` | `const CDecomposeUnitaryTarget*` | 合成目标句柄。 | + +返回:`1` 允许回退;`0` 不允许;`-1` NULL 输入。 + +### decompose_unitary_target_set_fallback_pauli(ptr, value) + +设置目标是否允许 Pauli 旋转回退。目标会围绕其原生门列表重建,这要求基组非空;调用被拒绝时目标保持不变。 + +```c +int32_t decompose_unitary_target_set_fallback_pauli(struct CDecomposeUnitaryTarget *ptr, + uint8_t value); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `ptr` | `struct CDecomposeUnitaryTarget*` | 合成目标句柄。 | +| `value` | `uint8_t` | 非 0 允许回退。 | + +返回:`0` 成功。 + +| 错误码 | 名称 | 场景 | +| --- | --- | --- | +| `-1` | NullPtr | `ptr` 为 NULL。 | +| `-8` | InvalidParam | 目标无约束(无原生门)。 | + +### decompose_unitary_target_native_1q_len(ptr) / decompose_unitary_target_native_1q(ptr, out, len) + +```c +uintptr_t decompose_unitary_target_native_1q_len(const struct CDecomposeUnitaryTarget *ptr); +uintptr_t decompose_unitary_target_native_1q(const struct CDecomposeUnitaryTarget *ptr, + char **out, + uintptr_t len); +``` + +两步式读取原生单比特门名(第一步:`*_len` 返回总数;第二步:填充接口把门名拷入 `out`,实际拷贝 `min(总数, len)` 个)。每个写入条目都是新建的 C 字符串,用 `cqlib_string_free` 释放;`out` 为 NULL 时填充接口只返回总数、不拷贝。 + +返回(两者一致):原生单比特门总数;NULL 输入返回 0。 + +### decompose_unitary_target_native_2q_len(ptr) / decompose_unitary_target_native_2q(ptr, out, len) + +```c +uintptr_t decompose_unitary_target_native_2q_len(const struct CDecomposeUnitaryTarget *ptr); +uintptr_t decompose_unitary_target_native_2q(const struct CDecomposeUnitaryTarget *ptr, + char **out, + uintptr_t len); +``` + +两步式读取原生双比特门名(缓冲区拷贝模式与单比特一对相同;每个写入条目用 `cqlib_string_free` 释放)。 + +返回(两者一致):原生双比特门总数;NULL 输入返回 0。 + +### decompose_unitary_target_basis_len(ptr) / decompose_unitary_target_basis(ptr, out, len) + +```c +uintptr_t decompose_unitary_target_basis_len(const struct CDecomposeUnitaryTarget *ptr); +uintptr_t decompose_unitary_target_basis(const struct CDecomposeUnitaryTarget *ptr, + char **out, + uintptr_t len); +``` + +两步式读取合并的原生基组(先单比特门、后双比特门;缓冲区拷贝模式相同;每个写入条目用 `cqlib_string_free` 释放)。 + +返回(两者一致):基组门总数;NULL 输入返回 0。 + +### decompose_unitary_target_cost_of_fixed_operations(ptr, gate_names, qubit_ids, qubit_counts, num_ops, out) + +在目标的原生基组下计算固定标准门操作序列的精确目标基组代价。序列由 `num_ops` 个门名给出,各操作作用的比特 ID 按顺序平铺在 `qubit_ids` 中,每个操作的比特数记录在 `qubit_counts` 中。参数按固定 0 值处理。要求目标受约束(无约束目标没有可用于计价的基组)。代价快照写入 `*out`(`CTargetBasisCost` 布局见 [配置与结果结构体](#配置与结果结构体))。 + +```c +int32_t decompose_unitary_target_cost_of_fixed_operations( + const struct CDecomposeUnitaryTarget *ptr, + const char *const *gate_names, + const uint32_t *qubit_ids, + const uint32_t *qubit_counts, + uintptr_t num_ops, + struct CTargetBasisCost *out); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `ptr` | `const CDecomposeUnitaryTarget*` | 合成目标句柄。 | +| `gate_names` | `const char* const*` | 序列中每个操作的门名。 | +| `qubit_ids` | `const uint32_t*` | 全部操作的比特 ID,按序列顺序平铺。 | +| `qubit_counts` | `const uint32_t*` | 每个操作作用的比特数。 | +| `num_ops` | `uintptr_t` | 序列中的操作数。 | +| `out` | `struct CTargetBasisCost*` | 代价快照输出。 | + +返回:`0` 成功。 + +| 错误码 | 名称 | 场景 | +| --- | --- | --- | +| `-1` | NullPtr | `ptr` 或 `out` 为 NULL。 | +| `-4` | ParseError | 某门名非法 UTF-8 或为未知门名。 | +| `-6` | CompilerError | 核心拒绝该序列。 | +| `-8` | InvalidParam | 目标无约束(无原生基组)。 | + +--- + +## CTwoQubitUnitarySynthesis 读取 + +`synthesize_numeric_2q_unitary` 的结果句柄:发出的操作序列与全局相位。 + +### two_qubit_synthesis_free(ptr) + +释放双比特合成结果句柄,允许传 NULL。 + +```c +void two_qubit_synthesis_free(struct CTwoQubitUnitarySynthesis *ptr); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `ptr` | `struct CTwoQubitUnitarySynthesis*` | 待释放句柄,可为 NULL。 | + +### two_qubit_synthesis_num_operations(ptr) + +返回发出的操作数量。 + +```c +uintptr_t two_qubit_synthesis_num_operations(const struct CTwoQubitUnitarySynthesis *ptr); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `ptr` | `const CTwoQubitUnitarySynthesis*` | 合成结果句柄。 | + +返回:操作数量;NULL 输入返回 0。 + +### two_qubit_synthesis_global_phase(ptr) + +返回乘在整个操作序列上的全局相位。 + +```c +double two_qubit_synthesis_global_phase(const struct CTwoQubitUnitarySynthesis *ptr); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `ptr` | `const CTwoQubitUnitarySynthesis*` | 合成结果句柄。 | + +返回:全局相位(弧度);NULL 输入返回 0.0。 + +### two_qubit_synthesis_operation_name(ptr, index) + +返回第 `index` 个操作的指令名。 + +```c +char *two_qubit_synthesis_operation_name(const struct CTwoQubitUnitarySynthesis *ptr, + uintptr_t index); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `ptr` | `const CTwoQubitUnitarySynthesis*` | 合成结果句柄。 | +| `index` | `uintptr_t` | 操作下标(从 0 开始)。 | + +返回:成功返回新建的 C 字符串(用 `cqlib_string_free` 释放);NULL 输入或下标越界时返回 NULL。 + +### two_qubit_synthesis_operation_qubits_len(ptr, index) + +返回第 `index` 个操作作用的比特数量(两步式读取的第一步)。 + +```c +uintptr_t two_qubit_synthesis_operation_qubits_len(const struct CTwoQubitUnitarySynthesis *ptr, + uintptr_t index); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `ptr` | `const CTwoQubitUnitarySynthesis*` | 合成结果句柄。 | +| `index` | `uintptr_t` | 操作下标(从 0 开始)。 | + +返回:比特数量;NULL 输入或下标越界时返回 0。 + +### two_qubit_synthesis_operation_qubits(ptr, index, out, len) + +把第 `index` 个操作作用的比特 ID 拷贝到 `out`(两步式读取的第二步;先调用 `two_qubit_synthesis_operation_qubits_len` 查询长度)。当 `len` 小于总数时只拷贝前 `len` 个。 + +```c +uintptr_t two_qubit_synthesis_operation_qubits(const struct CTwoQubitUnitarySynthesis *ptr, + uintptr_t index, + uint32_t *out, + uintptr_t len); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `ptr` | `const CTwoQubitUnitarySynthesis*` | 合成结果句柄。 | +| `index` | `uintptr_t` | 操作下标(从 0 开始)。 | +| `out` | `uint32_t*` | 比特 ID 输出数组。 | +| `len` | `uintptr_t` | `out` 的容量。 | + +返回:该操作作用的总比特数;NULL 输入或下标越界时返回 0。 + +--- + +## KAK 分解 + +### kak_decompose(matrix) + +把 4×4 酉矩阵分解为 KAK(Weyl)坐标与局域因子。`matrix` 指向 16 个行主序 `Complex64` 元素。 + +```c +struct CKakDecomposition *kak_decompose(const Complex64 *matrix); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `matrix` | `const Complex64*` | 16 个行主序复数元素(4×4 矩阵)。 | + +返回:成功返回新建的 `CKakDecomposition*`(用 `kak_decomposition_free` 释放);`matrix` 为 NULL 或分解失败(例如矩阵非有限或非酉)时返回 NULL。 + +### kak_decomposition_free(ptr) + +释放 KAK 分解句柄,允许传 NULL。 + +```c +void kak_decomposition_free(struct CKakDecomposition *ptr); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `ptr` | `struct CKakDecomposition*` | 待释放句柄,可为 NULL。 | + +### kak_decomposition_global_phase(ptr) + +返回乘在整个分解上的标量相位。 + +```c +double kak_decomposition_global_phase(const struct CKakDecomposition *ptr); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `ptr` | `const CKakDecomposition*` | KAK 分解句柄。 | + +返回:全局相位(弧度);NULL 输入返回 0.0。 + +### kak_decomposition_coordinates(ptr, a, b, c) + +把规范 Pauli XX/YY/ZZ 交互坐标 `a`、`b`、`c` 写入给定输出指针。 + +```c +int32_t kak_decomposition_coordinates(const struct CKakDecomposition *ptr, + double *a, + double *b, + double *c); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `ptr` | `const CKakDecomposition*` | KAK 分解句柄。 | +| `a` | `double*` | XX 交互坐标输出。 | +| `b` | `double*` | YY 交互坐标输出。 | +| `c` | `double*` | ZZ 交互坐标输出。 | + +返回:0 成功。 + +| 错误码 | 名称 | 场景 | +| --- | --- | --- | +| `-1` | NullPtr | 任一指针为 NULL。 | + +### kak_decomposition_local_factor(ptr, factor, out) + +按 `KAK_FACTOR_*` 标签选取一个 2×2 局域因子,作为 4 个行主序 `Complex64` 元素拷贝到 `out`。 + +```c +int32_t kak_decomposition_local_factor(const struct CKakDecomposition *ptr, + uint8_t factor, + Complex64 *out); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `ptr` | `const CKakDecomposition*` | KAK 分解句柄。 | +| `factor` | `uint8_t` | 局域因子标签,`KAK_FACTOR_*` 之一。 | +| `out` | `Complex64*` | 4 个行主序复数元素输出。 | + +返回:0 成功。 + +| 错误码 | 名称 | 场景 | +| --- | --- | --- | +| `-1` | NullPtr | `ptr` 或 `out` 为 NULL。 | +| `-8` | InvalidParam | `factor` 不是有效的 `KAK_FACTOR_*` 标签。 | + +--- + +## 目标门集降级 + +### target_basis_lowerer_new(gate_names, len) + +从标准门名数组构造目标门集降级器:`gate_names` 的每个条目必须是 C ABI 各处接受的标准门名(例如 `"H"`、`"RZ"`、`"CX"`),这些门构成目标基组。 + +```c +struct CTargetBasisLowerer *target_basis_lowerer_new(const char *const *gate_names, uintptr_t len); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `gate_names` | `const char* const*` | 标准门名数组;`len` 为 0 时可为 NULL。 | +| `len` | `uintptr_t` | 门名数量。 | + +返回:成功返回新建的 `CTargetBasisLowerer*`(用 `target_basis_lowerer_free` 释放);`len` 大于 0 但 `gate_names` 为 NULL、某条目为 NULL、含无效 UTF-8、含未知门名,或基组为空(`len` 为 0)时返回 NULL。 + +### target_basis_lowerer_free(ptr) + +释放目标门集降级器句柄,允许传 NULL。 + +```c +void target_basis_lowerer_free(struct CTargetBasisLowerer *ptr); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `ptr` | `struct CTargetBasisLowerer*` | 待释放句柄,可为 NULL。 | + +### target_basis_lowerer_num_gates(ptr) + +返回目标基组中的门数量。 + +```c +uintptr_t target_basis_lowerer_num_gates(const struct CTargetBasisLowerer *ptr); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `ptr` | `const CTargetBasisLowerer*` | 降级器句柄。 | + +返回:门数量;NULL 输入返回 0。 + +### target_basis_lowerer_requires_lowering(ptr, circuit) + +判断把线路转换到目标基组是否可能改变或拒绝其中的类门操作。 + +```c +int32_t target_basis_lowerer_requires_lowering(const struct CTargetBasisLowerer *ptr, + const struct CCircuit *circuit); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `ptr` | `const CTargetBasisLowerer*` | 降级器句柄。 | +| `circuit` | `const CCircuit*` | 输入线路。 | + +返回:1 表示需要降级(存在基组外的类门操作);0 表示不需要。 + +| 错误码 | 名称 | 场景 | +| --- | --- | --- | +| `-1` | NullPtr | `ptr` 或 `circuit` 为 NULL。 | + +### target_basis_lowerer_apply(ptr, circuit) + +把线路降到目标基组,返回重建线路。 + +```c +struct CCircuit *target_basis_lowerer_apply(const struct CTargetBasisLowerer *ptr, + const struct CCircuit *circuit); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `ptr` | `const CTargetBasisLowerer*` | 降级器句柄。 | +| `circuit` | `const CCircuit*` | 输入线路。 | + +返回:成功返回新建的 `CCircuit*`(用 `circuit_free` 释放);任一句柄为 NULL 或降级失败时返回 NULL。 + +--- + +## 设备降级 + +### decompose_lower_to_device(circuit, device) + +把线路降到设备的原生指令集,返回重建线路。 + +```c +struct CCircuit *decompose_lower_to_device(const struct CCircuit *circuit, + const struct CDevice *device); +``` + +参数: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `circuit` | `const CCircuit*` | 输入线路。 | +| `device` | `const CDevice*` | 目标设备;其原生门集(`device_with_native_gates` 设置,见 [设备与噪声属性](../2_device/2_properties_device.md))决定降级目标。 | + +返回:成功返回新建的 `CCircuit*`(用 `circuit_free` 释放);任一句柄为 NULL 或降级失败时返回 NULL。 + +--- + +## 配置与结果结构体 + +### CUnitaryDecomposeConfig + +```c +typedef struct CUnitaryDecomposeConfig { + uint8_t recurse_control_flow; + uint8_t _reserved[7]; +} CUnitaryDecomposeConfig; +``` + +| 字段 | 类型 | 含义 | +| --- | --- | --- | +| `recurse_control_flow` | `uint8_t` | 1 递归进入控制流体,0 不递归。 | +| `_reserved[7]` | `uint8_t[7]` | 填充字段,保持结构体布局稳定。 | + +### CMcGateDecomposeConfig + +```c +typedef struct CMcGateDecomposeConfig { + uintptr_t max_pre_layout_clean_ancillas; + uint8_t allow_dirty_borrowing; + uint8_t has_max_total_qubits; + uint8_t _pad[6]; + uintptr_t max_total_qubits; + uint64_t _reserved[2]; +} CMcGateDecomposeConfig; +``` + +| 字段 | 类型 | 含义 | +| --- | --- | --- | +| `max_pre_layout_clean_ancillas` | `uintptr_t` | 布局前允许创建的干净逻辑 ancilla 总数上限。 | +| `allow_dirty_borrowing` | `uint8_t` | 1 允许按脏契约借用输入比特。 | +| `has_max_total_qubits` | `uint8_t` | 1 启用 `max_total_qubits` 硬限制,0 不限制。 | +| `_pad[6]` | `uint8_t[6]` | 填充字段,保持结构体布局稳定。 | +| `max_total_qubits` | `uintptr_t` | 总逻辑比特数硬限制(`has_max_total_qubits != 0` 时生效)。 | +| `_reserved[2]` | `uint64_t[2]` | 填充字段,保持结构体布局稳定。 | + +### CResynthesisConfig + +```c +typedef struct CResynthesisConfig { + uintptr_t max_block_ops; + uintptr_t max_crossed_ops; + uintptr_t max_scan_span; + uint8_t skip_labeled_ops; + uint8_t recurse_control_flow; + uint8_t enable_rule_oracle; + uint8_t enable_matrix_fallback; + uint8_t _pad[4]; + uintptr_t max_matrix_qubits; + uint64_t _reserved[2]; +} CResynthesisConfig; +``` + +| 字段 | 类型 | 含义 | +| --- | --- | --- | +| `max_block_ops` | `uintptr_t` | 单个有界候选块内源操作数上限。 | +| `max_crossed_ops` | `uintptr_t` | 收集块期间允许跨越的非块操作数上限。 | +| `max_scan_span` | `uintptr_t` | 双比特锚点每一侧的收集预算。 | +| `skip_labeled_ops` | `uint8_t` | 1 把带标签操作视为硬边界。 | +| `recurse_control_flow` | `uint8_t` | 1 递归进入结构化经典控制体。 | +| `enable_rule_oracle` | `uint8_t` | 1 启用知识规则对易 oracle。 | +| `enable_matrix_fallback` | `uint8_t` | 1 启用对易引擎的局部矩阵回退。 | +| `_pad[4]` | `uint8_t[4]` | 填充字段,保持结构体布局稳定。 | +| `max_matrix_qubits` | `uintptr_t` | 矩阵回退的最大联合支持规模。 | +| `_reserved[2]` | `uint64_t[2]` | 填充字段,保持结构体布局稳定。 | + +### CDecompositionRuleStats + +分解 pass 局部规则缓存统计快照(`decompose_unitaries_with_rule_stats` / `decompose_mc_gates_with_rule_stats` 的输出): + +| 字段 | 类型 | 含义 | +| --- | --- | --- | +| `hits` | `uintptr_t` | 本次运行的缓存命中数。 | +| `misses` | `uintptr_t` | 本次运行的缓存未命中数。 | +| `inserts` | `uintptr_t` | 本次运行的缓存写入数。 | + +### CTargetBasisCost + +目标基组降级代价快照(`decompose_unitary_target_cost_of_fixed_operations` 的输出): + +| 字段 | 类型 | 含义 | +| --- | --- | --- | +| `two_qubit_ops` | `uintptr_t` | 降级序列中的双比特操作数。 | +| `depth` | `uintptr_t` | 降级序列的关键路径深度。 | +| `total_ops` | `uintptr_t` | 降级序列的操作总数(不含全局相位)。 | +| `parameterized_ops` | `uintptr_t` | 降级序列中带参数的操作数。 | + +### COneQubitUnitaryDecomposition + +单比特数值合成结果快照(`synthesize_numeric_1q_unitary` 的输出);表示的矩阵为 `exp(i * global_phase) * U(theta, phi, lambda)`: + +| 字段 | 类型 | 含义 | +| --- | --- | --- | +| `theta` | `double` | 极向旋转角。 | +| `phi` | `double` | 第一方位角。 | +| `lambda` | `double` | 第二方位角。 | +| `global_phase` | `double` | 乘在合成门上的标量相位。 | + +--- + +## 示例 + +先跑一遍线路级分解 pass(定义展开 → 酉门合成 → 多控制门改写 → 双比特块重综合),调用顺序对照测试套件: + +```c +#include +#include +#include "cqlib_c.h" + +int main(void) { + /* 1. A frozen circuit-gate is expanded back into plain operations */ + struct CCircuit *inner = circuit_new(1); + circuit_h(inner, 0); + struct CCircuitGate *gate = circuit_to_gate(inner, "my_h"); + struct CCircuit *outer = circuit_new(2); + uint32_t gate_qubits[1] = {0}; + circuit_circuit_gate(outer, gate, gate_qubits, 1, NULL, 0); + struct CCircuit *expanded = decompose_expand_definitions(outer); /* 1 op */ + + /* 2. A matrix-backed H is synthesized into standard operations */ + struct CCircuit *unitary_circuit = circuit_new(2); + Complex64 h_matrix[4] = { + {1.0 / sqrt(2.0), 0.0}, {1.0 / sqrt(2.0), 0.0}, + {1.0 / sqrt(2.0), 0.0}, {-1.0 / sqrt(2.0), 0.0}, + }; + uint32_t unitary_targets[1] = {0}; + circuit_unitary(unitary_circuit, "my_u", 1, h_matrix, unitary_targets, 1); + struct CDecompositionRuleStats stats; + struct CCircuit *synthesized = decompose_unitaries_with_rule_stats( + unitary_circuit, decompose_unitary_config_default(), &stats); + /* stats.hits / stats.misses / stats.inserts reflect the rule cache */ + + /* 3. A doubly-controlled X is rewritten within device capacity */ + struct CCircuit *mc_circuit = circuit_new(3); + uint32_t controls[2] = {0, 1}; + uint32_t targets[1] = {2}; + circuit_multi_control(mc_circuit, "X", controls, 2, targets, 1, NULL, 0); + uint32_t edges[4] = {0, 1, 1, 2}; + struct CDevice *device = device_from_edges("line-3", 3, edges, 2); + struct CCircuit *rewritten = decompose_mc_gates_for_device(mc_circuit, device, NULL); + + /* 4. Two-qubit blocks are resynthesized under the enhanced budget */ + struct CCircuit *block_circuit = circuit_new(2); + circuit_h(block_circuit, 0); + circuit_cx(block_circuit, 0, 1); + struct CCircuit *resynthesized = + resynthesize_two_qubit_blocks(block_circuit, resynthesis_config_enhanced()); + + circuit_free(resynthesized); + circuit_free(block_circuit); + circuit_free(rewritten); + device_free(device); + circuit_free(mc_circuit); + circuit_free(synthesized); + circuit_free(unitary_circuit); + circuit_free(expanded); + circuit_gate_free(gate); + circuit_free(outer); + circuit_free(inner); + return 0; +} +``` + +数值合成、KAK 分解与降级(单位阵的 2q 合成与 KAK 分解的交互坐标均为零): + +```c +#include +#include +#include +#include "cqlib_c.h" + +int main(void) { + /* 1. 1q synthesis of the identity yields U(0, 0, 0), zero phase */ + Complex64 identity2[4] = {{1.0, 0.0}, {0.0, 0.0}, {0.0, 0.0}, {1.0, 0.0}}; + struct COneQubitUnitaryDecomposition one_q; + memset(&one_q, 0, sizeof(one_q)); + if (synthesize_numeric_1q_unitary(identity2, &one_q) != 0) { + return 1; + } + + /* 2. 2q synthesis of the identity in the CX basis */ + Complex64 identity4[16]; + memset(identity4, 0, sizeof(identity4)); + for (int i = 0; i < 16; i += 5) { + identity4[i].re = 1.0; + } + struct CTwoQubitUnitarySynthesis *synthesis = + synthesize_numeric_2q_unitary(identity4, 0, 1, TWO_QUBIT_BASIS_CX); + if (synthesis != NULL) { + for (uintptr_t i = 0; i < two_qubit_synthesis_num_operations(synthesis); i++) { + char *name = two_qubit_synthesis_operation_name(synthesis, i); + uint32_t qubits[8]; + uintptr_t len = two_qubit_synthesis_operation_qubits_len(synthesis, i); + uintptr_t copied = two_qubit_synthesis_operation_qubits(synthesis, i, qubits, len); + printf("op=%s qubits=%lu\n", name, (unsigned long)copied); + cqlib_string_free(name); + } + two_qubit_synthesis_free(synthesis); + } + + /* 3. KAK decomposition of the identity: zero interaction coordinates */ + struct CKakDecomposition *kak = kak_decompose(identity4); + if (kak != NULL) { + double a = 0.0, b = 0.0, c = 0.0; + Complex64 k1l[4]; + if (kak_decomposition_coordinates(kak, &a, &b, &c) == 0) { + printf("a=%f b=%f c=%f\n", a, b, c); /* ~0 */ + } + kak_decomposition_local_factor(kak, KAK_FACTOR_K1L, k1l); /* 0 */ + kak_decomposition_free(kak); + } + + /* 4. Target-basis lowering: T is outside {H, RZ, CX} */ + const char *basis_names[3] = {"H", "RZ", "CX"}; + struct CTargetBasisLowerer *lowerer = target_basis_lowerer_new(basis_names, 3); + if (lowerer != NULL) { + struct CCircuit *t_circuit = circuit_new(1); + circuit_t(t_circuit, 0); + if (target_basis_lowerer_requires_lowering(lowerer, t_circuit) == 1) { + struct CCircuit *lowered = target_basis_lowerer_apply(lowerer, t_circuit); + circuit_free(lowered); + } + circuit_free(t_circuit); + target_basis_lowerer_free(lowerer); + } + + /* 5. Device lowering: a native circuit passes through unchanged */ + uint32_t edges[2] = {0, 1}; + struct CDevice *device = device_from_edges("line-2", 2, edges, 1); + device_with_native_gates(device, "H,RZ,CX"); + struct CCircuit *native_circuit = circuit_new(2); + circuit_h(native_circuit, 0); + circuit_cx(native_circuit, 0, 1); + struct CCircuit *device_lowered = decompose_lower_to_device(native_circuit, device); /* 2 ops */ + + circuit_free(device_lowered); + circuit_free(native_circuit); + device_free(device); + return 0; +} +``` + +酉合成目标:原生基组读回与代价评估: + +```c +#include +#include +#include "cqlib_c.h" + +static void sort_names(char **names, uintptr_t len) { + for (uintptr_t i = 0; i + 1 < len; i++) { + for (uintptr_t j = i + 1; j < len; j++) { + if (strcmp(names[i], names[j]) > 0) { + char *tmp = names[i]; + names[i] = names[j]; + names[j] = tmp; + } + } + } +} + +int main(void) { + /* 1. 由工作流风格的基组列表构建目标 */ + const char *const basis_gates[3] = {"H", "RZ", "CX"}; + struct CDecomposeUnitaryTarget *target = + decompose_unitary_target_from_instructions(basis_gates, 3); + if (target == NULL) { + return 1; + } + /* decompose_unitary_target_fallback_pauli(target) == 1, + native_1q_len == 2(H、RZ),native_2q_len == 1(CX),basis_len == 3 */ + + /* 2. 两步式读出合并后的基组(单比特门在前) */ + char *basis[3] = {NULL, NULL, NULL}; + uintptr_t total = decompose_unitary_target_basis(target, basis, 3); + sort_names(basis, total); + for (uintptr_t i = 0; i < total && i < 3; i++) { + printf("basis[%llu]=%s\n", (unsigned long long)i, basis[i]); + cqlib_string_free(basis[i]); + } + + /* 3. 固定门操作序列在原生基组下的代价 */ + const char *const ops[3] = {"H", "CX", "H"}; + const uint32_t qubit_ids[4] = {0, 0, 1, 1}; + const uint32_t op_counts[3] = {1, 2, 1}; + struct CTargetBasisCost cost; + memset(&cost, 0, sizeof(cost)); + if (decompose_unitary_target_cost_of_fixed_operations(target, ops, qubit_ids, + op_counts, 3, &cost) == 0) { + /* cost.total_ops == 3, cost.two_qubit_ops == 1, + cost.parameterized_ops == 0, cost.depth == 3 */ + printf("ops=%llu two_qubit=%llu depth=%llu\n", + (unsigned long long)cost.total_ops, + (unsigned long long)cost.two_qubit_ops, + (unsigned long long)cost.depth); + } + + /* 4. 带参数的门按带参数操作计数 */ + const char *const rz_ops[1] = {"RZ"}; + const uint32_t rz_counts[1] = {1}; + decompose_unitary_target_cost_of_fixed_operations(target, rz_ops, qubit_ids, + rz_counts, 1, &cost); + /* cost.total_ops == 1, cost.parameterized_ops == 1 */ + + /* 5. 切换 fallback 会围绕原生列表重建目标 */ + decompose_unitary_target_set_fallback_pauli(target, 0); + /* decompose_unitary_target_fallback_pauli(target) == 0 */ + decompose_unitary_target_set_fallback_pauli(target, 1); + + decompose_unitary_target_free(target); + return 0; +} +``` diff --git a/docs/api/cn/c/4_compile/7_knowledge.md b/docs/api/cn/c/4_compile/7_knowledge.md new file mode 100644 index 00000000..fdb658af --- /dev/null +++ b/docs/api/cn/c/4_compile/7_knowledge.md @@ -0,0 +1,1272 @@ +# 知识库(C) + +`CKnowledgeLibrary` 是验证后的编译规则库句柄:一组按类别登记、按名称索引的知识规则;`CKnowledgeRule` 是单条规则的独立句柄,可解析、校验后插入任意库。知识规则描述一个相邻操作模式(match)、可选符号参数约束与替换序列(rewrite),是知识重写与门分解的规则来源。一次匹配产生记录绑定比特与参数的 `CKnowledgeBindings*`;在这些绑定下实例化的替换目标以 `CKnowledgeReplacements*` 返回。错误码、字符串与句柄的释放约定遵循 [Overview](../0_overview.md)。 + +--- + +## 常量 + +规则类别标签(`kind` 参数与 `knowledge_library_rule_kind` 的返回值): + +| 常量 | 值 | 类别 | +| --- | --- | --- | +| `RULE_KIND_SIMPLIFY` | 0 | 化简规则。 | +| `RULE_KIND_CANCEL` | 1 | 抵消规则(相邻互逆操作删除)。 | +| `RULE_KIND_MERGE` | 2 | 合并规则。 | +| `RULE_KIND_COMMUTE` | 3 | 显式对易规则(`A; B -> B; A`)。 | +| `RULE_KIND_DECOMPOSE` | 4 | 分解或降级规则。 | +| `RULE_KIND_CANONICALIZE` | 5 | 规范化规则。 | +| `RULE_KIND_HARDWARE_NATIVE` | 6 | 硬件原生规则。 | +| `RULE_KIND_OTHER` | 7 | 其他类别。 | + +--- + +## DSL 规则 + +`knowledge_library_from_dsl_str` / `knowledge_library_from_dsl_file` / `knowledge_rule_from_dsl` 接受 `.rule` DSL 文本: + +```text +rule merge_rz { + match { + RZ(a) 0 + RZ(b) 0 + } + rewrite { + RZ(a + b) 0 + } +} +``` + +- `rule `:规则名,库内唯一。 +- `match { ... }`:被匹配的相邻操作模式,至少一条。 +- `rewrite { ... }`:替换序列,可为空(空替换表示抵消删除)。 +- 每条操作为 `门名 (参数) 比特标签...`;无参数时省略括号(如 `H 0`)。 +- 比特标签是非负整数占位符,必须从 0 开始连续排列,匹配时绑定到具体比特。 +- 参数可为数值、符号(`a`、`b`)或表达式(`a + b`)。 + +--- + +## 库构造与释放 + +### knowledge_library_new() + +```c +struct CKnowledgeLibrary *knowledge_library_new(void); +``` + +创建空规则库。 + +返回值: + +| 返回 | 场景 | +| --- | --- | +| 非 NULL | 新建的空库句柄,用 `knowledge_library_free` 释放。 | + +### knowledge_library_builtin() + +```c +struct CKnowledgeLibrary *knowledge_library_builtin(void); +``` + +创建包含内置编译规则的库。规则按各自的编译类别登记,可用 `knowledge_library_rules_by_kind_len` 按类别查询。 + +返回值: + +| 返回 | 场景 | +| --- | --- | +| 非 NULL | 新建的内置库句柄,用 `knowledge_library_free` 释放。 | +| NULL | 内置规则源验证失败。 | + +### knowledge_library_from_dsl_str(source, kind, out) + +```c +int32_t knowledge_library_from_dsl_str(const char *source, + uint8_t kind, + struct CKnowledgeLibrary **out); +``` + +解析并验证 DSL 源文本构造规则库;`kind`(`RULE_KIND_*` 之一)作为全部解析规则的类别。 + +参数: + +- `source` (`const char*`):DSL 源文本。 +- `kind` (`uint8_t`):规则类别标签,取 `RULE_KIND_*` 之一。 +- `out` (`struct CKnowledgeLibrary**`):输出指针,成功时接收新建库句柄。 + +返回:0 成功并把新建的 `CKnowledgeLibrary*`(用 `knowledge_library_free` 释放)写入 `*out`。 + +错误码: + +| 值 | 场景 | +| --- | --- | +| 0 | 成功。 | +| -1 | `source` 或 `out` 为 NULL。 | +| -4 | `source` 非法 UTF-8,或 DSL 解析、下降、验证失败。 | +| -8 | `kind` 不是合法的 `RULE_KIND_*` 标签。 | + +### knowledge_library_from_dsl_file(path, kind, out) + +```c +int32_t knowledge_library_from_dsl_file(const char *path, + uint8_t kind, + struct CKnowledgeLibrary **out); +``` + +从 DSL 文件加载、解析并验证规则构造规则库;`kind` 作为全部规则的类别。 + +参数: + +- `path` (`const char*`):DSL 文件路径。 +- `kind` (`uint8_t`):规则类别标签,取 `RULE_KIND_*` 之一。 +- `out` (`struct CKnowledgeLibrary**`):输出指针,成功时接收新建库句柄。 + +返回:0 成功并把新建的 `CKnowledgeLibrary*` 写入 `*out`。 + +错误码: + +| 值 | 场景 | +| --- | --- | +| 0 | 成功。 | +| -1 | `path` 或 `out` 为 NULL。 | +| -4 | `path` 非法 UTF-8,或 DSL 解析、下降、验证失败。 | +| -5 | 文件读取失败。 | +| -8 | `kind` 不是合法的 `RULE_KIND_*` 标签。 | + +### knowledge_library_free(ptr) + +```c +void knowledge_library_free(struct CKnowledgeLibrary *ptr); +``` + +释放规则库句柄,允许传 NULL。 + +--- + +## 库查询 + +### knowledge_library_len(ptr) + +```c +uintptr_t knowledge_library_len(const struct CKnowledgeLibrary *ptr); +``` + +返回库中规则数。 + +返回值: + +| 返回 | 场景 | +| --- | --- | +| 非负整数 | 规则数。 | +| 0 | `ptr` 为 NULL 或库为空。 | + +### knowledge_library_is_empty(ptr) + +```c +int32_t knowledge_library_is_empty(const struct CKnowledgeLibrary *ptr); +``` + +判断库是否不含任何规则。 + +返回值: + +| 值 | 场景 | +| --- | --- | +| 1 | 库为空。 | +| 0 | 库非空。 | +| -1 | `ptr` 为 NULL。 | + +### knowledge_library_contains(ptr, name) + +```c +int32_t knowledge_library_contains(const struct CKnowledgeLibrary *ptr, const char *name); +``` + +判断库中是否存在名为 `name` 的规则。 + +参数: + +- `ptr` (`const CKnowledgeLibrary*`):规则库句柄。 +- `name` (`const char*`):规则名。 + +返回值: + +| 值 | 场景 | +| --- | --- | +| 1 | 存在同名规则。 | +| 0 | 不存在同名规则。 | +| -1 | `ptr` 或 `name` 为 NULL。 | +| -4 | `name` 非法 UTF-8。 | + +### knowledge_library_id_by_name(ptr, name) + +```c +int64_t knowledge_library_id_by_name(const struct CKnowledgeLibrary *ptr, const char *name); +``` + +查询规则名对应的规则 id(即插入顺序下标,规则 id 等于插入时的库长度)。 + +参数: + +- `ptr` (`const CKnowledgeLibrary*`):规则库句柄。 +- `name` (`const char*`):规则名。 + +返回值: + +| 返回 | 场景 | +| --- | --- | +| 非负整数 | 规则 id。 | +| -1 | `ptr` 或 `name` 为 NULL,或不存在同名规则。 | +| -4 | `name` 非法 UTF-8。 | + +### knowledge_library_rules_by_kind_len(ptr, kind) / knowledge_library_rules_by_kind(ptr, kind, out, len) + +```c +uintptr_t knowledge_library_rules_by_kind_len(const struct CKnowledgeLibrary *ptr, uint8_t kind); +uintptr_t knowledge_library_rules_by_kind(const struct CKnowledgeLibrary *ptr, + uint8_t kind, + uint32_t *out, + uintptr_t len); +``` + +两步式读取指定类别的规则 id 列表:`*_len` 返回该类别的规则总数;填充接口把 id 拷入 `out`(实际拷贝 `min(总数, len)` 个),返回总数。`out` 为 NULL 时只返回总数、不拷贝。 + +参数: + +- `ptr` (`const CKnowledgeLibrary*`):规则库句柄。 +- `kind` (`uint8_t`):规则类别标签,取 `RULE_KIND_*` 之一。 +- `out` (`uint32_t*`):接收 id 的缓冲区。 +- `len` (`uintptr_t`):缓冲区长度。 + +返回值(两者一致): + +| 返回 | 场景 | +| --- | --- | +| 非负整数 | 该类别的规则总数。 | +| 0 | `ptr` 为 NULL,或 `kind` 不是合法的 `RULE_KIND_*` 标签。 | + +### knowledge_rules_len(ptr) / knowledge_rules(ptr, out, len) + +```c +uintptr_t knowledge_rules_len(const struct CKnowledgeLibrary *ptr); +uintptr_t knowledge_rules(const struct CKnowledgeLibrary *ptr, uint32_t *out, uintptr_t len); +``` + +两步式读取库中的规则 id(插入顺序下标)列表:`*_len` 返回规则总数;填充接口把 id 拷入 `out`(实际拷贝 `min(总数, len)` 个),返回总数。`out` 为 NULL 时只返回总数、不拷贝。 + +参数: + +- `ptr` (`const CKnowledgeLibrary*`):规则库句柄。 +- `out` (`uint32_t*`):接收 id 的缓冲区。 +- `len` (`uintptr_t`):缓冲区长度。 + +返回值(两者一致): + +| 返回 | 场景 | +| --- | --- | +| 非负整数 | 规则总数。 | +| 0 | `ptr` 为 NULL。 | + +### knowledge_get_by_name(ptr, name) + +```c +struct CKnowledgeRule *knowledge_get_by_name(const struct CKnowledgeLibrary *ptr, const char *name); +``` + +按名称查询规则并返回克隆句柄。 + +参数: + +- `ptr` (`const CKnowledgeLibrary*`):规则库句柄。 +- `name` (`const char*`):规则名。 + +返回值: + +| 返回 | 场景 | +| --- | --- | +| 非 NULL | 新建的 `CKnowledgeRule*` 克隆,用 `knowledge_rule_free` 释放。 | +| NULL | `ptr` 或 `name` 为 NULL,或不存在同名规则。 | + +--- + +## 规则元数据 + +以下接口按插入下标 `index` 读取库中规则的预计算元数据;`index` 的合法范围是 `[0, knowledge_library_len(ptr))`。 + +### knowledge_library_rule_name(ptr, index) + +```c +char *knowledge_library_rule_name(const struct CKnowledgeLibrary *ptr, uintptr_t index); +``` + +返回第 `index` 条规则的名称。 + +返回值: + +| 返回 | 场景 | +| --- | --- | +| 非 NULL | 堆上 C 字符串,用 `cqlib_string_free` 释放。 | +| NULL | `ptr` 为 NULL 或 `index` 越界。 | + +### knowledge_library_rule_kind(ptr, index) + +```c +int32_t knowledge_library_rule_kind(const struct CKnowledgeLibrary *ptr, uintptr_t index); +``` + +返回第 `index` 条规则的类别标签。 + +返回值: + +| 值 | 场景 | +| --- | --- | +| 0–7 | `RULE_KIND_*` 类别标签。 | +| -1 | `ptr` 为 NULL。 | +| -8 | `index` 越界。 | + +### knowledge_library_rule_first_instruction(ptr, index) + +```c +char *knowledge_library_rule_first_instruction(const struct CKnowledgeLibrary *ptr, + uintptr_t index); +``` + +返回第 `index` 条规则匹配模式的首条指令名(按首指令可筛选候选规则)。 + +返回值: + +| 返回 | 场景 | +| --- | --- | +| 非 NULL | 堆上 C 字符串,用 `cqlib_string_free` 释放。 | +| NULL | `ptr` 为 NULL 或 `index` 越界。 | + +### knowledge_library_rule_pattern_len(ptr, index) + +```c +uintptr_t knowledge_library_rule_pattern_len(const struct CKnowledgeLibrary *ptr, uintptr_t index); +``` + +返回规则匹配模式的操作数。 + +返回值: + +| 返回 | 场景 | +| --- | --- | +| 非负整数 | 模式操作数。 | +| 0 | `ptr` 为 NULL 或 `index` 越界。 | + +### knowledge_library_rule_rewrite_len(ptr, index) + +```c +uintptr_t knowledge_library_rule_rewrite_len(const struct CKnowledgeLibrary *ptr, uintptr_t index); +``` + +返回规则替换目标发出的操作数。 + +返回值: + +| 返回 | 场景 | +| --- | --- | +| 非负整数 | 替换目标操作数。 | +| 0 | `ptr` 为 NULL 或 `index` 越界。 | + +### knowledge_library_rule_qubit_count(ptr, index) + +```c +uintptr_t knowledge_library_rule_qubit_count(const struct CKnowledgeLibrary *ptr, uintptr_t index); +``` + +返回规则使用的不同规则局部比特标签数。 + +返回值: + +| 返回 | 场景 | +| --- | --- | +| 非负整数 | 比特标签数。 | +| 0 | `ptr` 为 NULL 或 `index` 越界。 | + +### knowledge_library_rule_cost_delta(ptr, index) + +```c +int64_t knowledge_library_rule_cost_delta(const struct CKnowledgeLibrary *ptr, uintptr_t index); +``` + +返回规则的静态操作数差 `rewrite_len - pattern_len`(可为负,表示规则减少操作数)。 + +返回值: + +| 返回 | 场景 | +| --- | --- | +| 整数 | `rewrite_len - pattern_len`。 | +| 0 | `ptr` 为 NULL 或 `index` 越界。 | + +### knowledge_library_rule_has_conditions(ptr, index) + +```c +int32_t knowledge_library_rule_has_conditions(const struct CKnowledgeLibrary *ptr, uintptr_t index); +``` + +判断规则是否带非空符号参数条件。 + +返回值: + +| 值 | 场景 | +| --- | --- | +| 1 | 带非空参数条件。 | +| 0 | 不带参数条件。 | +| -1 | `ptr` 为 NULL。 | +| -8 | `index` 越界。 | + +--- + +## 单条规则解析与插入 + +### knowledge_rule_from_dsl(source, out) + +```c +int32_t knowledge_rule_from_dsl(const char *source, struct CKnowledgeRule **out); +``` + +解析必须恰好定义一条规则的 DSL 源文本,构造单条规则句柄(由 `knowledge_library_add_rule` 插入库)。 + +参数: + +- `source` (`const char*`):DSL 源文本(恰好一条 `rule` 定义)。 +- `out` (`struct CKnowledgeRule**`):输出指针,成功时接收新句柄。 + +返回:0 成功并把新建的 `CKnowledgeRule*`(用 `knowledge_rule_free` 释放)写入 `*out`。 + +错误码: + +| 值 | 场景 | +| --- | --- | +| 0 | 成功。 | +| -1 | `source` 或 `out` 为 NULL。 | +| -4 | `source` 非法 UTF-8,或 DSL 解析、下降、验证失败。 | +| -8 | 源文本定义的规则数不等于 1。 | + +### knowledge_rule_free(ptr) + +```c +void knowledge_rule_free(struct CKnowledgeRule *ptr); +``` + +释放单条规则句柄,允许传 NULL。 + +### knowledge_library_add_rule(ptr, rule, kind) + +```c +int64_t knowledge_library_add_rule(struct CKnowledgeLibrary *ptr, + const struct CKnowledgeRule *rule, + uint8_t kind); +``` + +把解析好的规则插入库并登记到类别 `kind`,返回分配的规则 id。插入前执行结构校验;规则名在库内必须唯一。 + +参数: + +- `ptr` (`CKnowledgeLibrary*`):目标规则库。 +- `rule` (`const CKnowledgeRule*`):待插入规则(克隆使用,句柄不被消耗,调用方继续负责释放)。 +- `kind` (`uint8_t`):规则类别标签,取 `RULE_KIND_*` 之一。 + +返回值: + +| 返回 | 场景 | +| --- | --- | +| 非负整数 | 分配的规则 id。 | +| -1 | `ptr` 或 `rule` 为 NULL。 | +| -4 | 结构验证失败或规则名重复。 | +| -6 | 规则首指令无法被索引。 | +| -8 | `kind` 不是合法的 `RULE_KIND_*` 标签。 | + +--- + +## 单条规则访问器 + +### knowledge_rule_name(ptr) + +```c +char *knowledge_rule_name(const struct CKnowledgeRule *ptr); +``` + +返回规则名。 + +返回值: + +| 返回 | 场景 | +| --- | --- | +| 非 NULL | 堆上 C 字符串,用 `cqlib_string_free` 释放。 | +| NULL | `ptr` 为 NULL。 | + +### knowledge_rule_num_qubits(ptr) + +```c +uintptr_t knowledge_rule_num_qubits(const struct CKnowledgeRule *ptr); +``` + +返回规则涉及的比特标签数。 + +返回值: + +| 返回 | 场景 | +| --- | --- | +| 非负整数 | 比特标签数。 | +| 0 | `ptr` 为 NULL。 | + +### knowledge_rule_pattern_len(ptr) + +```c +uintptr_t knowledge_rule_pattern_len(const struct CKnowledgeRule *ptr); +``` + +返回规则匹配模式的操作数。 + +返回值: + +| 返回 | 场景 | +| --- | --- | +| 非负整数 | 模式操作数。 | +| 0 | `ptr` 为 NULL。 | + +### knowledge_rule_rewrite_len(ptr) + +```c +uintptr_t knowledge_rule_rewrite_len(const struct CKnowledgeRule *ptr); +``` + +返回规则替换目标发出的操作数。 + +返回值: + +| 返回 | 场景 | +| --- | --- | +| 非负整数 | 替换目标操作数。 | +| 0 | `ptr` 为 NULL。 | + +### knowledge_rule_operation_symbols_len(ptr) / knowledge_rule_operation_symbols(ptr, out, len) + +```c +uintptr_t knowledge_rule_operation_symbols_len(const struct CKnowledgeRule *ptr); +int32_t knowledge_rule_operation_symbols(const struct CKnowledgeRule *ptr, + char **out, + uintptr_t len); +``` + +两步式读取规则匹配块绑定的不同符号:`*_len` 返回符号总数;填充接口把符号(已排序、去重)以新建 C 字符串形式拷入 `out`,返回 0。每个字符串须用 `cqlib_string_free` 释放。 + +参数: + +- `ptr` (`const CKnowledgeRule*`):规则句柄。 +- `out` (`char**`):接收符号字符串的缓冲区。 +- `len` (`uintptr_t`):缓冲区长度,必须等于 `knowledge_rule_operation_symbols_len` 返回的数量。 + +返回值(`*_len`): + +| 返回 | 场景 | +| --- | --- | +| 非负整数 | 符号总数。 | +| 0 | `ptr` 为 NULL。 | + +错误码(填充接口): + +| 值 | 场景 | +| --- | --- | +| 0 | 成功;`out` 持有 `len` 个新建字符串。 | +| -1 | `ptr` 为 NULL,或 `len > 0` 时 `out` 为 NULL。 | +| -8 | `len` 与符号数量不符。 | + +### knowledge_rule_target_symbols_len(ptr) / knowledge_rule_target_symbols(ptr, out, len) + +```c +uintptr_t knowledge_rule_target_symbols_len(const struct CKnowledgeRule *ptr); +int32_t knowledge_rule_target_symbols(const struct CKnowledgeRule *ptr, + char **out, + uintptr_t len); +``` + +两步式读取规则替换目标引用的不同符号:`*_len` 返回符号总数;填充接口把符号(已排序、去重)以新建 C 字符串形式拷入 `out`,返回 0。每个字符串须用 `cqlib_string_free` 释放。 + +参数: + +- `ptr` (`const CKnowledgeRule*`):规则句柄。 +- `out` (`char**`):接收符号字符串的缓冲区。 +- `len` (`uintptr_t`):缓冲区长度,必须等于 `knowledge_rule_target_symbols_len` 返回的数量。 + +返回值(`*_len`): + +| 返回 | 场景 | +| --- | --- | +| 非负整数 | 符号总数。 | +| 0 | `ptr` 为 NULL。 | + +错误码(填充接口): + +| 值 | 场景 | +| --- | --- | +| 0 | 成功;`out` 持有 `len` 个新建字符串。 | +| -1 | `ptr` 为 NULL,或 `len > 0` 时 `out` 为 NULL。 | +| -8 | `len` 与符号数量不符。 | + +### knowledge_rule_condition_symbols_len(ptr) / knowledge_rule_condition_symbols(ptr, out, len) + +```c +uintptr_t knowledge_rule_condition_symbols_len(const struct CKnowledgeRule *ptr); +int32_t knowledge_rule_condition_symbols(const struct CKnowledgeRule *ptr, + char **out, + uintptr_t len); +``` + +两步式读取规则参数条件引用的不同符号:`*_len` 返回符号总数;填充接口把符号(已排序、去重)以新建 C 字符串形式拷入 `out`,返回 0。每个字符串须用 `cqlib_string_free` 释放。 + +参数: + +- `ptr` (`const CKnowledgeRule*`):规则句柄。 +- `out` (`char**`):接收符号字符串的缓冲区。 +- `len` (`uintptr_t`):缓冲区长度,必须等于 `knowledge_rule_condition_symbols_len` 返回的数量。 + +返回值(`*_len`): + +| 返回 | 场景 | +| --- | --- | +| 非负整数 | 符号总数。 | +| 0 | `ptr` 为 NULL。 | + +错误码(填充接口): + +| 值 | 场景 | +| --- | --- | +| 0 | 成功;`out` 持有 `len` 个新建字符串。 | +| -1 | `ptr` 为 NULL,或 `len > 0` 时 `out` 为 NULL。 | +| -8 | `len` 与符号数量不符。 | + +### knowledge_rule_has_conditions(ptr) + +```c +int32_t knowledge_rule_has_conditions(const struct CKnowledgeRule *ptr); +``` + +判断规则是否带非空符号参数条件。 + +返回值: + +| 值 | 场景 | +| --- | --- | +| 1 | 带非空参数条件。 | +| 0 | 不带参数条件。 | +| -1 | `ptr` 为 NULL。 | + +### knowledge_rule_validate(ptr) + +```c +int32_t knowledge_rule_validate(const struct CKnowledgeRule *ptr); +``` + +运行规则的结构校验。 + +错误码: + +| 值 | 场景 | +| --- | --- | +| 0 | 规则结构合法。 | +| -1 | `ptr` 为 NULL。 | +| -4 | 校验失败。 | + +--- + +## 匹配引擎 + +匹配引擎用于手动驱动一条规则:把模式匹配到具体操作得到 `CKnowledgeBindings*`,在绑定上评估参数条件,再把替换目标实例化为 `CKnowledgeReplacements*`。操作以 `operation_new` 创建的 `COperation*` 句柄传入(见 [Operation / Instruction(操作与指令)](../0_circuit/4_operation_instruction.md))。 + +### knowledge_bindings_new() + +```c +struct CKnowledgeBindings *knowledge_bindings_new(void); +``` + +创建空的匹配绑定记录,供 `knowledge_match_rule_item` 使用。 + +返回值: + +| 返回 | 场景 | +| --- | --- | +| 非 NULL | 新建的绑定句柄,用 `knowledge_bindings_free` 释放。 | + +### knowledge_bindings_free(ptr) + +```c +void knowledge_bindings_free(struct CKnowledgeBindings *ptr); +``` + +释放匹配绑定,允许传 NULL。 + +### knowledge_match_rule_item(ptr, item_index, op, bindings) + +```c +int32_t knowledge_match_rule_item(const struct CKnowledgeRule *ptr, + uintptr_t item_index, + const struct COperation *op, + struct CKnowledgeBindings *bindings); +``` + +把规则匹配模式下标 `item_index` 处的条目匹配到具体操作,并把绑定的比特与参数记录进 `bindings`。匹配是事务性的:条目不匹配时 `bindings` 保持不变,失败的尝试不会破坏先前已绑定的内容。 + +参数: + +- `ptr` (`const CKnowledgeRule*`):规则句柄。 +- `item_index` (`uintptr_t`):匹配模式中的条目下标。 +- `op` (`const COperation*`):待匹配操作。 +- `bindings` (`CKnowledgeBindings*`):成功时被更新的绑定记录。 + +错误码: + +| 值 | 场景 | +| --- | --- | +| 1 | 条目匹配;`bindings` 已记录绑定的比特与参数。 | +| 0 | 条目不匹配;`bindings` 不变。 | +| -1 | `ptr`、`op` 或 `bindings` 为 NULL。 | +| -2 | `item_index` 越界。 | +| -8 | 操作携带索引式(表解析)参数。 | +| -6 | 规则条目使用了匹配器无法表示的指令。 | + +### knowledge_rule_matches_operations(ptr, operations, operations_len, out) + +```c +int32_t knowledge_rule_matches_operations(const struct CKnowledgeRule *ptr, + const struct COperation *const *operations, + uintptr_t operations_len, + struct CKnowledgeBindings **out); +``` + +把规则的完整匹配模式匹配到 `operations_len` 个相邻操作。成功时把新建的 `CKnowledgeBindings*` 写入 `*out` 并返回 1。参数条件不在此处评估;需用 `knowledge_conditions_hold` 在返回的绑定上单独检查。 + +参数: + +- `ptr` (`const CKnowledgeRule*`):规则句柄。 +- `operations` (`const COperation* const*`):相邻操作数组。 +- `operations_len` (`uintptr_t`):操作数量。 +- `out` (`struct CKnowledgeBindings**`):输出指针,成功时接收绑定句柄。 + +错误码: + +| 值 | 场景 | +| --- | --- | +| 1 | 模式匹配;新建的 `CKnowledgeBindings*`(用 `knowledge_bindings_free` 释放)已写入 `*out`。 | +| 0 | 模式不匹配;`out` 保持不变。 | +| -1 | `ptr`、`operations` 或 `out` 为 NULL。 | +| -8 | `operations_len` 与模式长度不一致,或某操作携带索引式参数。 | + +### knowledge_conditions_hold(ptr, bindings) + +```c +int32_t knowledge_conditions_hold(const struct CKnowledgeRule *ptr, + const struct CKnowledgeBindings *bindings); +``` + +在绑定下评估规则的符号参数条件。 + +返回值: + +| 值 | 场景 | +| --- | --- | +| 1 | 全部参数条件成立。 | +| 0 | 至少一个条件不成立。 | +| -1 | `ptr` 或 `bindings` 为 NULL。 | + +### knowledge_equivalent_to(lhs, lhs_item, rhs, rhs_item) + +```c +int32_t knowledge_equivalent_to(const struct CKnowledgeRule *lhs, + uintptr_t lhs_item, + const struct CKnowledgeRule *rhs, + uintptr_t rhs_item); +``` + +判断 `lhs` 匹配模式下标 `lhs_item` 处的条目与 `rhs` 下标 `rhs_item` 处的条目是否等价。 + +参数: + +- `lhs` (`const CKnowledgeRule*`):第一条规则句柄。 +- `lhs_item` (`uintptr_t`):第一条规则模式中的条目下标。 +- `rhs` (`const CKnowledgeRule*`):第二条规则句柄。 +- `rhs_item` (`uintptr_t`):第二条规则模式中的条目下标。 + +返回值: + +| 值 | 场景 | +| --- | --- | +| 1 | 两个条目等价。 | +| 0 | 条目不等价。 | +| -1 | `lhs` 或 `rhs` 为 NULL。 | +| -2 | 条目下标越界。 | + +### knowledge_candidates_for_first_instruction_len(ptr, name) / knowledge_candidates_for_first_instruction(ptr, name, out, len) + +```c +uintptr_t knowledge_candidates_for_first_instruction_len(const struct CKnowledgeLibrary *ptr, + const char *name); +uintptr_t knowledge_candidates_for_first_instruction(const struct CKnowledgeLibrary *ptr, + const char *name, + uint32_t *out, + uintptr_t len); +``` + +两步式读取首条匹配指令与门 `name` 具有相同匹配器键的规则 id 列表:`*_len` 返回候选规则总数;填充接口把 id 拷入 `out`(实际拷贝 `min(总数, len)` 个),返回总数。`out` 为 NULL 时只返回总数、不拷贝。 + +参数: + +- `ptr` (`const CKnowledgeLibrary*`):规则库句柄。 +- `name` (`const char*`):门名。 +- `out` (`uint32_t*`):接收 id 的缓冲区。 +- `len` (`uintptr_t`):缓冲区长度。 + +返回值(两者一致): + +| 返回 | 场景 | +| --- | --- | +| 非负整数 | 候选规则总数。 | +| 0 | `ptr` 或 `name` 为 NULL,或 `name` 为未知门名。 | + +### knowledge_collect_free_symbols_len(ptr) / knowledge_collect_free_symbols(ptr, out, len) + +```c +uintptr_t knowledge_collect_free_symbols_len(const struct CKnowledgeRule *ptr); +uintptr_t knowledge_collect_free_symbols(const struct CKnowledgeRule *ptr, + char **out, + uintptr_t len); +``` + +两步式读取规则的自由符号(已排序):`*_len` 返回符号总数;填充接口把符号以新建 C 字符串形式拷入 `out`(实际拷贝 `min(总数, len)` 个),返回总数。每个字符串须用 `cqlib_string_free` 释放。 + +参数: + +- `ptr` (`const CKnowledgeRule*`):规则句柄。 +- `out` (`char**`):接收符号字符串的缓冲区。 +- `len` (`uintptr_t`):缓冲区长度。 + +返回值(两者一致): + +| 返回 | 场景 | +| --- | --- | +| 非负整数 | 自由符号总数。 | +| 0 | `ptr` 为 NULL。 | + +### knowledge_filter_rule_ids_by_instruction_keys_len(ptr, ops, ops_len, targets, targets_len) / knowledge_filter_rule_ids_by_instruction_keys(ptr, ops, ops_len, targets, targets_len, out, len) + +```c +uintptr_t knowledge_filter_rule_ids_by_instruction_keys_len(const struct CKnowledgeLibrary *ptr, + const char *const *ops, + uintptr_t ops_len, + const char *const *targets, + uintptr_t targets_len); +uintptr_t knowledge_filter_rule_ids_by_instruction_keys(const struct CKnowledgeLibrary *ptr, + const char *const *ops, + uintptr_t ops_len, + const char *const *targets, + uintptr_t targets_len, + uint32_t *out, + uintptr_t len); +``` + +两步式读取匹配侧覆盖 `ops` 中每个门、且替换目标侧覆盖 `targets` 中每个门(均为门名数组)的规则 id 列表:`*_len` 返回符合条件且覆盖的规则总数;填充接口把 id 拷入 `out`(实际拷贝 `min(总数, len)` 个),返回总数。`out` 为 NULL 时只返回总数、不拷贝。 + +参数: + +- `ptr` (`const CKnowledgeLibrary*`):规则库句柄。 +- `ops` (`const char* const*`):匹配侧门名数组。 +- `ops_len` (`uintptr_t`):匹配侧门名数量。 +- `targets` (`const char* const*`):替换目标侧门名数组。 +- `targets_len` (`uintptr_t`):替换目标侧门名数量。 +- `out` (`uint32_t*`):接收 id 的缓冲区。 +- `len` (`uintptr_t`):缓冲区长度。 + +返回值(两者一致): + +| 返回 | 场景 | +| --- | --- | +| 非负整数 | 符合条件且覆盖的规则总数。 | +| 0 | `ptr` 为 NULL,或某门名为 NULL 或未知门名。 | + +### knowledge_extend_rules(ptr, rules, rules_len, kind, out, out_len) + +```c +int32_t knowledge_extend_rules(struct CKnowledgeLibrary *ptr, + const struct CKnowledgeRule *const *rules, + uintptr_t rules_len, + uint8_t kind, + uint32_t *out, + uintptr_t out_len); +``` + +把若干解析好的规则原子地插入库并登记到类别 `kind`,把分配的规则 id 写入 `out`(`out` 必须能容纳 `rules_len` 个条目)。任一规则失败时,库保持不变。 + +参数: + +- `ptr` (`CKnowledgeLibrary*`):目标规则库。 +- `rules` (`const CKnowledgeRule* const*`):解析好的规则句柄数组(克隆使用,句柄不被消耗)。 +- `rules_len` (`uintptr_t`):规则数量。 +- `kind` (`uint8_t`):规则类别标签,取 `RULE_KIND_*` 之一。 +- `out` (`uint32_t*`):接收分配规则 id 的缓冲区。 +- `out_len` (`uintptr_t`):缓冲区长度,至少为 `rules_len`。 + +错误码: + +| 值 | 场景 | +| --- | --- | +| 0 | 成功;`out` 持有分配的 id。 | +| -1 | `ptr`、`rules` 或 `out` 为 NULL。 | +| -8 | `kind` 不是合法的 `RULE_KIND_*` 标签,或 `out_len` 小于 `rules_len`。 | +| -4 | 结构验证失败或规则名重复;库保持不变。 | + +### knowledge_target_qubits_len(ptr) / knowledge_target_qubits(ptr, out, len) + +```c +uintptr_t knowledge_target_qubits_len(const struct CKnowledgeRule *ptr); +uintptr_t knowledge_target_qubits(const struct CKnowledgeRule *ptr, uint32_t *out, uintptr_t len); +``` + +两步式读取规则替换目标使用的规则局部比特标签(已排序):`*_len` 返回标签总数;填充接口把标签拷入 `out`(实际拷贝 `min(总数, len)` 个),返回总数。`out` 为 NULL 时只返回总数、不拷贝。 + +参数: + +- `ptr` (`const CKnowledgeRule*`):规则句柄。 +- `out` (`uint32_t*`):接收标签的缓冲区。 +- `len` (`uintptr_t`):缓冲区长度。 + +返回值(两者一致): + +| 返回 | 场景 | +| --- | --- | +| 非负整数 | 标签总数。 | +| 0 | `ptr` 为 NULL。 | + +### knowledge_instantiate_target(ptr, bindings, out) + +```c +int32_t knowledge_instantiate_target(const struct CKnowledgeRule *ptr, + const struct CKnowledgeBindings *bindings, + struct CKnowledgeReplacements **out); +``` + +在匹配绑定下实例化规则的替换目标,用绑定的值评估目标中的每个参数表达式。 + +参数: + +- `ptr` (`const CKnowledgeRule*`):规则句柄。 +- `bindings` (`const CKnowledgeBindings*`):匹配产生的绑定。 +- `out` (`struct CKnowledgeReplacements**`):输出指针,成功时接收实例化结果。 + +错误码: + +| 值 | 场景 | +| --- | --- | +| 0 | 成功;新建的 `CKnowledgeReplacements*`(用 `knowledge_replacements_free` 释放)已写入 `*out`。 | +| -1 | `ptr`、`bindings` 或 `out` 为 NULL。 | +| -2 | 替换目标引用了匹配未绑定的比特。 | +| -8 | 替换目标引用了匹配未绑定的符号。 | +| -6 | 替换目标使用了匹配器无法表示的指令。 | + +### knowledge_replacements_len(ptr) + +```c +uintptr_t knowledge_replacements_len(const struct CKnowledgeReplacements *ptr); +``` + +返回实例化替换条目的数量。 + +返回值: + +| 返回 | 场景 | +| --- | --- | +| 非负整数 | 替换条目数。 | +| 0 | `ptr` 为 NULL。 | + +### knowledge_replacement_instruction(ptr, index) + +```c +char *knowledge_replacement_instruction(const struct CKnowledgeReplacements *ptr, uintptr_t index); +``` + +返回下标 `index` 处替换条目的指令名(如 `"RZ"`)。 + +参数: + +- `ptr` (`const CKnowledgeReplacements*`):实例化结果句柄。 +- `index` (`uintptr_t`):替换条目下标。 + +返回值: + +| 返回 | 场景 | +| --- | --- | +| 非 NULL | 堆上 C 字符串,用 `cqlib_string_free` 释放。 | +| NULL | `ptr` 为 NULL 或 `index` 越界。 | + +### knowledge_replacement_qubits_len(ptr, index) / knowledge_replacement_qubits(ptr, index, out, len) + +```c +uintptr_t knowledge_replacement_qubits_len(const struct CKnowledgeReplacements *ptr, + uintptr_t index); +uintptr_t knowledge_replacement_qubits(const struct CKnowledgeReplacements *ptr, + uintptr_t index, + uint32_t *out, + uintptr_t len); +``` + +两步式读取下标 `index` 处替换条目的具体比特:`*_len` 返回比特总数;填充接口把比特拷入 `out`(实际拷贝 `min(总数, len)` 个),返回总数。`out` 为 NULL 时只返回总数、不拷贝。 + +参数: + +- `ptr` (`const CKnowledgeReplacements*`):实例化结果句柄。 +- `index` (`uintptr_t`):替换条目下标。 +- `out` (`uint32_t*`):接收比特的缓冲区。 +- `len` (`uintptr_t`):缓冲区长度。 + +返回值(两者一致): + +| 返回 | 场景 | +| --- | --- | +| 非负整数 | 比特总数。 | +| 0 | `ptr` 为 NULL 或 `index` 越界。 | + +### knowledge_replacement_params_len(ptr, index) / knowledge_replacement_params(ptr, index, out, len) + +```c +uintptr_t knowledge_replacement_params_len(const struct CKnowledgeReplacements *ptr, + uintptr_t index); +uintptr_t knowledge_replacement_params(const struct CKnowledgeReplacements *ptr, + uintptr_t index, + double *out, + uintptr_t len); +``` + +两步式读取下标 `index` 处替换条目的已求值 double 参数:`*_len` 返回参数总数;填充接口把参数拷入 `out`(实际拷贝 `min(总数, len)` 个),返回总数。`out` 为 NULL 时只返回总数、不拷贝。 + +参数: + +- `ptr` (`const CKnowledgeReplacements*`):实例化结果句柄。 +- `index` (`uintptr_t`):替换条目下标。 +- `out` (`double*`):接收已求值参数的缓冲区。 +- `len` (`uintptr_t`):缓冲区长度。 + +返回值(两者一致): + +| 返回 | 场景 | +| --- | --- | +| 非负整数 | 参数总数。 | +| 0 | `ptr` 为 NULL 或 `index` 越界。 | + +### knowledge_replacements_free(ptr) + +```c +void knowledge_replacements_free(struct CKnowledgeReplacements *ptr); +``` + +释放替换目标的实例化结果,允许传 NULL。 + +--- + +## 示例 + +内置库遍历与按类别查询: + +```c +#include +#include +#include "cqlib_c.h" + +int main(void) { + /* 1. Load the builtin rule library. */ + struct CKnowledgeLibrary *lib = knowledge_library_builtin(); + if (lib == NULL) { + return 1; + } + + /* 2. Walk the rule metadata by insertion index. */ + for (uintptr_t i = 0; i < knowledge_library_len(lib); i++) { + char *name = knowledge_library_rule_name(lib, i); + int32_t kind = knowledge_library_rule_kind(lib, i); + uintptr_t pattern = knowledge_library_rule_pattern_len(lib, i); + if (name != NULL) { + printf("rule %s: kind=%d pattern_len=%llu\n", + name, kind, (unsigned long long)pattern); + cqlib_string_free(name); + } + } + + /* 3. Two-step read of the decompose-rule ids. */ + uintptr_t n = knowledge_library_rules_by_kind_len(lib, RULE_KIND_DECOMPOSE); + uint32_t *ids = malloc(n * sizeof(uint32_t)); + if (ids != NULL && n > 0) { + uintptr_t written = knowledge_library_rules_by_kind(lib, RULE_KIND_DECOMPOSE, ids, n); + printf("%llu decompose rules (first id %u)\n", + (unsigned long long)written, ids[0]); + free(ids); + } + + knowledge_library_free(lib); + return 0; +} +``` + +DSL 构造与查询: + +```c +#include +#include "cqlib_c.h" + +int main(void) { + /* 1. Build a library from DSL text (merge two adjacent RZ rotations). */ + const char *source = + "rule merge_rz {\n" + " match {\n" + " RZ(a) 0\n" + " RZ(b) 0\n" + " }\n" + " rewrite {\n" + " RZ(a + b) 0\n" + " }\n" + "}"; + struct CKnowledgeLibrary *lib = NULL; + if (knowledge_library_from_dsl_str(source, RULE_KIND_MERGE, &lib) != 0) { + return 1; + } + + /* 2. Query by name. */ + if (knowledge_library_contains(lib, "merge_rz") == 1) { + printf("id=%lld\n", (long long)knowledge_library_id_by_name(lib, "merge_rz")); + } + + /* 3. Read the metadata of rule 0. */ + char *first = knowledge_library_rule_first_instruction(lib, 0); /* "RZ" */ + printf("first=%s pattern=%llu rewrite=%llu cost_delta=%lld\n", + first, + (unsigned long long)knowledge_library_rule_pattern_len(lib, 0), + (unsigned long long)knowledge_library_rule_rewrite_len(lib, 0), + (long long)knowledge_library_rule_cost_delta(lib, 0)); + cqlib_string_free(first); + + knowledge_library_free(lib); + return 0; +} +``` + +单条规则解析与插入: + +```c +#include +#include "cqlib_c.h" + +int main(void) { + /* 1. Parse exactly one rule: cancel two adjacent H gates. */ + const char *source = + "rule cancel_h {\n" + " match {\n" + " H 0\n" + " H 0\n" + " }\n" + " rewrite {}\n" + "}"; + struct CKnowledgeRule *rule = NULL; + if (knowledge_rule_from_dsl(source, &rule) != 0) { + return 1; + } + + /* 2. Inspect and validate the standalone rule. */ + if (knowledge_rule_validate(rule) != 0) { + knowledge_rule_free(rule); + return 1; + } + char *name = knowledge_rule_name(rule); /* "cancel_h" */ + printf("rule %s: qubits=%llu pattern=%llu rewrite=%llu conditions=%d\n", + name, + (unsigned long long)knowledge_rule_num_qubits(rule), + (unsigned long long)knowledge_rule_pattern_len(rule), + (unsigned long long)knowledge_rule_rewrite_len(rule), + knowledge_rule_has_conditions(rule)); + cqlib_string_free(name); + + /* 3. Insert into a fresh library (assigns id 0), then reject duplicates. */ + struct CKnowledgeLibrary *lib = knowledge_library_new(); + if (knowledge_library_add_rule(lib, rule, RULE_KIND_CANCEL) != 0) { + knowledge_rule_free(rule); + knowledge_library_free(lib); + return 1; + } + if (knowledge_library_add_rule(lib, rule, RULE_KIND_CANCEL) == -4) { + printf("duplicate name rejected\n"); + } + + knowledge_rule_free(rule); + knowledge_library_free(lib); + return 0; +} +``` + +把规则匹配到具体操作并实例化其替换目标: + +```c +#include +#include "cqlib_c.h" + +int main(void) { + /* 1. 构造 merge_rz 规则。 */ + const char *source = + "rule merge_rz {\n" + " match {\n" + " RZ(a) 0\n" + " RZ(b) 0\n" + " }\n" + " rewrite {\n" + " RZ(a + b) 0\n" + " }\n" + "}"; + struct CKnowledgeRule *rule = NULL; + if (knowledge_rule_from_dsl(source, &rule) != 0) { + return 1; + } + + /* 2. 构造两个相邻 RZ 操作。 */ + uint32_t q0[1] = {7}; + double p1[1] = {0.25}; + double p2[1] = {0.5}; + struct COperation *a = operation_new("RZ", q0, 1, p1, 1); + struct COperation *b = operation_new("RZ", q0, 1, p2, 1); + const struct COperation *ops[2] = {a, b}; + + /* 3. 匹配完整模式(参数条件另行检查)。 */ + struct CKnowledgeBindings *bindings = NULL; + if (knowledge_rule_matches_operations(rule, ops, 2, &bindings) == 1) { + /* 4. 实例化替换目标:RZ(a + b),其中 a + b == 0.75。 */ + struct CKnowledgeReplacements *repl = NULL; + if (knowledge_instantiate_target(rule, bindings, &repl) == 0) { + for (uintptr_t i = 0; i < knowledge_replacements_len(repl); i++) { + char *name = knowledge_replacement_instruction(repl, i); /* "RZ" */ + uint32_t qubits[1]; + double params[1]; + knowledge_replacement_qubits(repl, i, qubits, 1); /* {7} */ + knowledge_replacement_params(repl, i, params, 1); /* {0.75} */ + printf("repl %s q=%u p=%f\n", name, qubits[0], params[0]); + cqlib_string_free(name); + } + knowledge_replacements_free(repl); + } + knowledge_bindings_free(bindings); + } + + /* 5. 查看规则内部:自由符号与替换目标比特标签。 */ + uintptr_t nsym = knowledge_collect_free_symbols_len(rule); + char **symbols = malloc(nsym * sizeof(char *)); + if (symbols != NULL && knowledge_collect_free_symbols(rule, symbols, nsym) == nsym) { + for (uintptr_t i = 0; i < nsym; i++) { + printf("symbol %s\n", symbols[i]); + cqlib_string_free(symbols[i]); + } + free(symbols); + } + + operation_free(a); + operation_free(b); + knowledge_rule_free(rule); + return 0; +} +``` + +由规则库驱动的对易检查器见 [对易检查](9_commutation.md)。 diff --git a/docs/api/cn/c/4_compile/8_resource.md b/docs/api/cn/c/4_compile/8_resource.md new file mode 100644 index 00000000..dcbb7bbf --- /dev/null +++ b/docs/api/cn/c/4_compile/8_resource.md @@ -0,0 +1,415 @@ +# 资源管理(C) + +`CResourceManager` 管理 ancilla 辅助比特的申请、规划、租借与回收:预览(preview)产生无副作用的规划快照,提交(commit)才真正占用比特并扩展管理器的工作线路,租约(lease)持有已占用的比特,释放(release)把它们归还资源池。干净资源与脏资源是恢复契约:干净资源进出变换时必须处于 `|0>`;脏资源可处于未知或纠缠态,但消耗资源的变换必须在释放租约前完整还原其输入状态。错误码、字符串与句柄的释放约定遵循 [Overview](../0_overview.md);线路句柄的构造见 [Circuit](../0_circuit/1_circuit.md)。 + +--- + +## 常量 + +ancilla 需求标签(`resource_manager_preview` 的 `requirement` 参数与 `resource_plan_requirement` / `resource_lease_requirement` 的返回值): + +| 常量 | 值 | 需求 | +| --- | --- | --- | +| `RESOURCE_REQUIREMENT_CLEAN_ZERO` | 0 | 干净资源:进出变换时处于 `\|0>`。 | +| `RESOURCE_REQUIREMENT_DIRTY` | 1 | 脏资源:可进入未知态,必须精确还原。 | + +--- + +## 配置结构体 + +### CResourcePolicy / resource_policy_default() + +```c +typedef struct CResourcePolicy { + uintptr_t max_pre_layout_clean_ancillas; + uint8_t allow_dirty_borrowing; + uint8_t _reserved[7]; +} CResourcePolicy; + +struct CResourcePolicy resource_policy_default(void); +``` + +`resource_policy_default` 按值返回默认策略:不创建干净 ancilla、不允许脏借用。 + +| 字段 | 类型 | 含义 | +| --- | --- | --- | +| `max_pre_layout_clean_ancillas` | `uintptr_t` | 布局前编译器可创建的逻辑干净 ancilla 总数;已释放比特仍计入总数、可复用。 | +| `allow_dirty_borrowing` | `uint8_t` | 1 允许在脏资源契约下借用输入比特;0 禁止。 | +| `_reserved[7]` | `uint8_t[7]` | 填充字段,保持结构体布局稳定,勿写入非零值。 | + +### CResourceLimits / resource_limits_default() + +```c +typedef struct CResourceLimits { + uint8_t has_max_total_qubits; + uint8_t _pad[7]; + uintptr_t max_total_qubits; + uint64_t _reserved[2]; +} CResourceLimits; + +struct CResourceLimits resource_limits_default(void); +``` + +`resource_limits_default` 按值返回默认限制:不强制总比特上限。 + +| 字段 | 类型 | 含义 | +| --- | --- | --- | +| `has_max_total_qubits` | `uint8_t` | 1 启用 `max_total_qubits` 上限;0 不设上限。 | +| `_pad[7]` | `uint8_t[7]` | 填充字段,保持结构体布局稳定,勿写入非零值。 | +| `max_total_qubits` | `uintptr_t` | 完整逻辑线路的最大总比特数(含输入比特与编译器分配的干净 ancilla),仅在 `has_max_total_qubits != 0` 时生效。 | +| `_reserved[2]` | `uint64_t[2]` | 填充字段,勿写入非零值。 | + +--- + +## 管理器生命周期 + +### resource_manager_from_circuit(circuit, policy, limits) + +```c +struct CResourceManager *resource_manager_from_circuit(const struct CCircuit *circuit, + const struct CResourcePolicy *policy, + const struct CResourceLimits *limits); +``` + +创建与线路同步的资源管理器。管理器持有线路的私有工作副本(提交规划时被修改),不接管入参线路。`policy` 与 `limits` 可传 NULL,选用默认值。 + +参数: + +- `circuit` (`const CCircuit*`):源线路。 +- `policy` (`const CResourcePolicy*`):资源策略;NULL 选用默认值。 +- `limits` (`const CResourceLimits*`):资源上限;NULL 选用默认值。 + +返回值: + +| 返回 | 场景 | +| --- | --- | +| 非 NULL | 新建管理器句柄,用 `resource_manager_free` 释放。 | +| NULL | `circuit` 为 NULL,或线路已超出配置上限。 | + +### resource_manager_free(ptr) + +```c +void resource_manager_free(struct CResourceManager *ptr); +``` + +释放管理器句柄,允许传 NULL。管理器内部的工作线路随之释放;`resource_manager_circuit` 取出的副本不受影响。 + +### resource_manager_circuit(ptr) + +```c +struct CCircuit *resource_manager_circuit(const struct CResourceManager *ptr); +``` + +取出管理器当前工作线路的独立副本(提交规划会扩展该线路)。 + +返回值: + +| 返回 | 场景 | +| --- | --- | +| 非 NULL | 新建 `CCircuit*`,用 `circuit_free` 释放;与 `ptr` 互相独立。 | +| NULL | `ptr` 为 NULL。 | + +### resource_manager_enter_post_layout(ptr) + +```c +int32_t resource_manager_enter_post_layout(struct CResourceManager *ptr); +``` + +把管理器切换到布局后阶段:此后不再分配新的逻辑干净 ancilla。 + +错误码: + +| 值 | 场景 | +| --- | --- | +| 0 | 成功。 | +| -1 | `ptr` 为 NULL。 | +| -6 | 管理器与工作线路一致性校验失败。 | + +--- + +## 规划(预览) + +### resource_manager_preview(ptr, requirement, count, excluded, excluded_len) + +```c +struct CResourcePlan *resource_manager_preview(const struct CResourceManager *ptr, + uint8_t requirement, + uintptr_t count, + const uint32_t *excluded, + uintptr_t excluded_len); +``` + +对照管理器预演一次 ancilla 申请:不占用资源、不修改工作线路。`excluded` 列出不得作为辅助资源消耗的比特(通常包含数据、控制与目标比特);`excluded_len` 为 0 时 `excluded` 可为 NULL。规划是管理器专属快照:任何成功的资源池变更(如提交另一个规划)都会使旧规划失效。 + +参数: + +- `ptr` (`const CResourceManager*`):资源管理器。 +- `requirement` (`uint8_t`):ancilla 需求标签,取 `RESOURCE_REQUIREMENT_*` 之一。 +- `count` (`uintptr_t`):申请的辅助比特数。 +- `excluded` (`const uint32_t*`):排除比特 id 数组。 +- `excluded_len` (`uintptr_t`):排除数组长度。 + +返回值: + +| 返回 | 场景 | +| --- | --- | +| 非 NULL | 新建规划句柄,用 `resource_plan_free` 释放。 | +| NULL | `ptr` 为 NULL、`requirement` 非法、`excluded_len` 大于 0 而 `excluded` 为 NULL,或申请无法满足。 | + +### resource_plan_free(ptr) + +```c +void resource_plan_free(struct CResourcePlan *ptr); +``` + +释放规划句柄,允许传 NULL。规划被 `resource_manager_commit` 提交时句柄由该调用接管,无需再释放。 + +### resource_plan_qubits_len(ptr) / resource_plan_qubits(ptr, out, len) + +```c +uintptr_t resource_plan_qubits_len(const struct CResourcePlan *ptr); +uintptr_t resource_plan_qubits(const struct CResourcePlan *ptr, uint32_t *out, uintptr_t len); +``` + +两步式读取规划比特:`*_len` 返回规划比特数;填充接口把比特 id 拷入 `out`(实际拷贝 `min(总数, len)` 个),返回总数。`out` 为 NULL 时只返回总数、不拷贝。 + +参数: + +- `ptr` (`const CResourcePlan*`):规划句柄。 +- `out` (`uint32_t*`):接收比特 id 的缓冲区。 +- `len` (`uintptr_t`):缓冲区长度。 + +返回值(两者一致): + +| 返回 | 场景 | +| --- | --- | +| 非负整数 | 规划比特总数。 | +| 0 | `ptr` 为 NULL。 | + +### resource_plan_num_new_qubits(ptr) + +```c +uintptr_t resource_plan_num_new_qubits(const struct CResourcePlan *ptr); +``` + +返回规划提交时引入的新逻辑比特数(超出输入线路宽度的部分)。 + +返回值: + +| 返回 | 场景 | +| --- | --- | +| 非负整数 | 新比特数。 | +| 0 | `ptr` 为 NULL。 | + +### resource_plan_requirement(ptr) + +```c +int32_t resource_plan_requirement(const struct CResourcePlan *ptr); +``` + +返回规划的 ancilla 需求标签。 + +返回值: + +| 值 | 场景 | +| --- | --- | +| 0 / 1 | `RESOURCE_REQUIREMENT_*` 标签。 | +| -1 | `ptr` 为 NULL。 | + +--- + +## 提交与租约 + +### resource_manager_commit(ptr, plan) + +```c +struct CResourceLease *resource_manager_commit(struct CResourceManager *ptr, + struct CResourcePlan *plan); +``` + +提交规划:占用其比特、扩展管理器的工作线路并返回生效的租约。 + +**所有权约定**:`plan` 句柄被此调用接管(无论提交成功与否),调用之后不得再使用或释放原句柄。 + +参数: + +- `ptr` (`CResourceManager*`):资源管理器。 +- `plan` (`CResourcePlan*`):待提交规划,所有权被接管。 + +返回值: + +| 返回 | 场景 | +| --- | --- | +| 非 NULL | 新建租约句柄;先用 `resource_manager_release` 归还比特,再用 `resource_lease_free` 释放。 | +| NULL | `ptr` 或 `plan` 为 NULL,或规划已失效。 | + +### resource_lease_free(ptr) + +```c +void resource_lease_free(struct CResourceLease *ptr); +``` + +释放租约句柄,允许传 NULL。释放前应已调用 `resource_manager_release` 归还比特。 + +### resource_lease_qubits_len(ptr) / resource_lease_qubits(ptr, out, len) + +```c +uintptr_t resource_lease_qubits_len(const struct CResourceLease *ptr); +uintptr_t resource_lease_qubits(const struct CResourceLease *ptr, uint32_t *out, uintptr_t len); +``` + +两步式读取租约比特:`*_len` 返回租借比特数;填充接口把比特 id 拷入 `out`(实际拷贝 `min(总数, len)` 个),返回总数。`out` 为 NULL 时只返回总数、不拷贝。 + +参数: + +- `ptr` (`const CResourceLease*`):租约句柄。 +- `out` (`uint32_t*`):接收比特 id 的缓冲区。 +- `len` (`uintptr_t`):缓冲区长度。 + +返回值(两者一致): + +| 返回 | 场景 | +| --- | --- | +| 非负整数 | 租借比特总数。 | +| 0 | `ptr` 为 NULL。 | + +### resource_lease_requirement(ptr) + +```c +int32_t resource_lease_requirement(const struct CResourceLease *ptr); +``` + +返回租约的 ancilla 需求标签。 + +返回值: + +| 值 | 场景 | +| --- | --- | +| 0 / 1 | `RESOURCE_REQUIREMENT_*` 标签。 | +| -1 | `ptr` 为 NULL。 | + +### resource_manager_release(ptr, lease) + +```c +int32_t resource_manager_release(struct CResourceManager *ptr, const struct CResourceLease *lease); +``` + +释放生效的租约,把其比特归还资源池。消耗资源的变换必须在调用前完成状态恢复:干净 ancilla 回到 `|0>`,脏 ancilla 还原输入状态。租约句柄在释放后仍可读取,直到用 `resource_lease_free` 释放。 + +错误码: + +| 值 | 场景 | +| --- | --- | +| 0 | 成功。 | +| -1 | `ptr` 或 `lease` 为 NULL。 | +| -6 | 租约未知或已释放。 | + +--- + +## 一致性检查 + +### resource_manager_verify_consistency(ptr) + +```c +int32_t resource_manager_verify_consistency(const struct CResourceManager *ptr); +``` + +校验管理器与其工作线路仍然一致。 + +错误码: + +| 值 | 场景 | +| --- | --- | +| 0 | 一致。 | +| -1 | `ptr` 为 NULL。 | +| -6 | 不一致。 | + +### resource_manager_verify_idle(ptr) + +```c +int32_t resource_manager_verify_idle(const struct CResourceManager *ptr); +``` + +校验全部租约已释放、资源池空闲。 + +错误码: + +| 值 | 场景 | +| --- | --- | +| 0 | 空闲。 | +| -1 | `ptr` 为 NULL。 | +| -6 | 仍有活跃租约。 | + +--- + +## 示例 + +申请、提交与释放一个干净 ancilla 的完整生命周期: + +```c +#include +#include "cqlib_c.h" + +int main(void) { + /* 1. Input circuit and a policy allowing two clean ancillas. */ + struct CCircuit *qc = circuit_new(2); + circuit_h(qc, 0); + + struct CResourcePolicy policy = resource_policy_default(); + policy.max_pre_layout_clean_ancillas = 2; + + struct CResourceManager *manager = resource_manager_from_circuit(qc, &policy, NULL); + if (manager == NULL) { + circuit_free(qc); + return 1; + } + + /* 2. Preview one clean ancilla without touching the circuit. */ + struct CResourcePlan *plan = + resource_manager_preview(manager, RESOURCE_REQUIREMENT_CLEAN_ZERO, 1, NULL, 0); + if (plan == NULL) { + resource_manager_free(manager); + circuit_free(qc); + return 1; + } + uint32_t planned[1]; + resource_plan_qubits(plan, planned, 1); /* planned[0] == 2 */ + uintptr_t new_qubits = resource_plan_num_new_qubits(plan); /* 1 */ + + /* 3. Commit: the plan handle is consumed and the lease becomes active. */ + struct CResourceLease *lease = resource_manager_commit(manager, plan); + if (lease == NULL) { + resource_manager_free(manager); + circuit_free(qc); + return 1; + } + uint32_t leased[1]; + resource_lease_qubits(lease, leased, 1); /* leased[0] == planned[0] */ + printf("leased qubit %u, %llu new qubits\n", + leased[0], (unsigned long long)new_qubits); + + /* 4. The working circuit now spans the borrowed qubit. */ + struct CCircuit *working = resource_manager_circuit(manager); + printf("working qubits: %llu\n", + (unsigned long long)circuit_num_qubits(working)); /* 3 */ + circuit_free(working); + + /* 5. Restore the ancilla to |0>, then release the lease. */ + if (resource_manager_release(manager, lease) != 0) { + resource_lease_free(lease); + resource_manager_free(manager); + circuit_free(qc); + return 1; + } + if (resource_manager_verify_idle(manager) == 0) { + printf("all leases released\n"); + } + + resource_lease_free(lease); + resource_manager_free(manager); + circuit_free(qc); + return 0; +} +``` + +预览失败返回 NULL 的常见原因:需求标签非法、申请数超出策略允许的干净 ancilla 数,或排除列表后无法满足。 diff --git a/docs/api/cn/c/4_compile/9_commutation.md b/docs/api/cn/c/4_compile/9_commutation.md new file mode 100644 index 00000000..13588c68 --- /dev/null +++ b/docs/api/cn/c/4_compile/9_commutation.md @@ -0,0 +1,268 @@ +# 对易检查(C) + +判断两个具体门应用是否对易。每个门应用由标准门名(如 `"H"`、`"RZ"`、`"CX"`)、比特 id 数组与数值参数数组描述;证明为相差全局相位时把相位写入 `phase` 出参。证明结果是保守的:`COMMUTATION_RESULT_UNPROVEN` 只表示配置的证明源无法建立对易性,不表示两个操作不对易。错误码、字符串与句柄的释放约定遵循 [Overview](../0_overview.md)。 + +--- + +## 常量 + +对易证明标签(`commutation_check` 系列的返回值): + +| 常量 | 值 | 证明 | +| --- | --- | --- | +| `COMMUTATION_RESULT_UNPROVEN` | 0 | 配置的证明源无法建立对易性。 | +| `COMMUTATION_RESULT_EXACT` | 1 | 精确对易。 | +| `COMMUTATION_RESULT_UP_TO_GLOBAL_PHASE` | 2 | 相差全局相位对易(`lhs·rhs = exp(i·phase)·rhs·lhs`),相位写入 `phase`。 | + +--- + +## 配置结构体 + +### CCommutationConfig / commutation_config_default() + +```c +typedef struct CCommutationConfig { + uint8_t enable_rule_oracle; + uint8_t enable_matrix_fallback; + uint8_t _pad[6]; + uintptr_t max_matrix_qubits; + uint64_t _reserved[2]; +} CCommutationConfig; + +struct CCommutationConfig commutation_config_default(void); +``` + +`commutation_config_default` 按值返回默认配置:启用规则预言机与矩阵回退,`max_matrix_qubits` 为 4。 + +| 字段 | 类型 | 含义 | +| --- | --- | --- | +| `enable_rule_oracle` | `uint8_t` | 1 启用知识库显式对易规则(`A; B -> B; A`)匹配。 | +| `enable_matrix_fallback` | `uint8_t` | 1 启用小规模局部矩阵比较回退。 | +| `_pad[6]` | `uint8_t[6]` | 填充字段,保持结构体布局稳定,勿写入非零值。 | +| `max_matrix_qubits` | `uintptr_t` | 矩阵回退允许的最大支撑并集比特数(作用于两操作支撑的排序并集)。 | +| `_reserved[2]` | `uint64_t[2]` | 填充字段,勿写入非零值。 | + +--- + +## 一次性检查 + +### commutation_check(lhs_gate, lhs_qubits, lhs_qubits_len, lhs_params, lhs_params_len, rhs_gate, rhs_qubits, rhs_qubits_len, rhs_params, rhs_params_len, phase) + +```c +int32_t commutation_check(const char *lhs_gate, + const uint32_t *lhs_qubits, + uintptr_t lhs_qubits_len, + const double *lhs_params, + uintptr_t lhs_params_len, + const char *rhs_gate, + const uint32_t *rhs_qubits, + uintptr_t rhs_qubits_len, + const double *rhs_params, + uintptr_t rhs_params_len, + double *phase); +``` + +用共享内置检查器(知识规则与矩阵回退启用)判断两个门应用是否对易。证明为相差全局相位时把相位写入 `phase`(无法数值评估的符号相位以 NaN 写出);精确对易写入 0.0;无法证明时不写入。`phase` 可为 NULL。 + +参数: + +- `lhs_gate` (`const char*`):左侧门名。 +- `lhs_qubits` (`const uint32_t*`):左侧比特 id 数组。 +- `lhs_qubits_len` (`uintptr_t`):左侧比特数。 +- `lhs_params` (`const double*`):左侧数值参数数组;长度为 0 时可为 NULL。 +- `lhs_params_len` (`uintptr_t`):左侧参数数。 +- `rhs_gate` (`const char*`):右侧门名。 +- `rhs_qubits` (`const uint32_t*`):右侧比特 id 数组。 +- `rhs_qubits_len` (`uintptr_t`):右侧比特数。 +- `rhs_params` (`const double*`):右侧数值参数数组;长度为 0 时可为 NULL。 +- `rhs_params_len` (`uintptr_t`):右侧参数数。 +- `phase` (`double*`):全局相位出参,可为 NULL。 + +返回值: + +| 值 | 场景 | +| --- | --- | +| 0 / 1 / 2 | `COMMUTATION_RESULT_*` 证明标签。 | +| -1 | 门名为 NULL,或长度大于 0 的比特/参数数组为 NULL。 | +| -4 | 门名非法 UTF-8。 | +| -8 | 门名不是已知的标准门。 | + +### commutation_check_algebraic(lhs_gate, lhs_qubits, lhs_qubits_len, lhs_params, lhs_params_len, rhs_gate, rhs_qubits, rhs_qubits_len, rhs_params, rhs_params_len, phase) + +```c +int32_t commutation_check_algebraic(const char *lhs_gate, + const uint32_t *lhs_qubits, + uintptr_t lhs_qubits_len, + const double *lhs_params, + uintptr_t lhs_params_len, + const char *rhs_gate, + const uint32_t *rhs_qubits, + uintptr_t rhs_qubits_len, + const double *rhs_params, + uintptr_t rhs_params_len, + double *phase); +``` + +只用符号代数证明源(结构事实、对角/轴代数、受控轴与对称双比特门族)判断对易。参数与返回值同 `commutation_check`;无法证明时返回 `COMMUTATION_RESULT_UNPROVEN`(0),保持结果保守。 + +--- + +## 可复用检查器 + +### commutation_checker_new(config) + +```c +struct CCommutationChecker *commutation_checker_new(struct CCommutationConfig config); +``` + +按显式配置创建可复用检查器(使用内置对易规则),用 `commutation_checker_free` 释放。 + +参数: + +- `config` (`struct CCommutationConfig`):检查器配置,按值传递。 + +返回值: + +| 返回 | 场景 | +| --- | --- | +| 非 NULL | 新建检查器句柄,用 `commutation_checker_free` 释放。 | + +### commutation_checker_from_library(library, config) + +```c +struct CCommutationChecker *commutation_checker_from_library(const struct CKnowledgeLibrary *library, + struct CCommutationConfig config); +``` + +从已加载的规则库创建检查器:库中对易类别的规则参与证明。规则库句柄(构造方式见 [知识库](7_knowledge.md))不被接管,调用方继续负责释放。 + +参数: + +- `library` (`const CKnowledgeLibrary*`):规则库句柄。 +- `config` (`struct CCommutationConfig`):检查器配置,按值传递。 + +返回值: + +| 返回 | 场景 | +| --- | --- | +| 非 NULL | 新建检查器句柄,用 `commutation_checker_free` 释放。 | +| NULL | `library` 为 NULL。 | + +### commutation_checker_free(ptr) + +```c +void commutation_checker_free(struct CCommutationChecker *ptr); +``` + +释放检查器句柄,允许传 NULL。 + +### commutation_checker_check(ptr, lhs_gate, lhs_qubits, lhs_qubits_len, lhs_params, lhs_params_len, rhs_gate, rhs_qubits, rhs_qubits_len, rhs_params, rhs_params_len, phase) + +```c +int32_t commutation_checker_check(const struct CCommutationChecker *ptr, + const char *lhs_gate, + const uint32_t *lhs_qubits, + uintptr_t lhs_qubits_len, + const double *lhs_params, + uintptr_t lhs_params_len, + const char *rhs_gate, + const uint32_t *rhs_qubits, + uintptr_t rhs_qubits_len, + const double *rhs_params, + uintptr_t rhs_params_len, + double *phase); +``` + +用该检查器判断两个门应用是否对易;检查器配置决定参与证明的证明源。除首参数外,参数与返回值同 `commutation_check`,另在 -1 场景中增加 `ptr` 为 NULL。 + +参数: + +- `ptr` (`const CCommutationChecker*`):检查器句柄。 +- 其余参数同 `commutation_check`。 + +返回值: + +| 值 | 场景 | +| --- | --- | +| 0 / 1 / 2 | `COMMUTATION_RESULT_*` 证明标签。 | +| -1 | `ptr` 为 NULL,或 `commutation_check` 的 -1 场景。 | +| -4 | 门名非法 UTF-8。 | +| -8 | 门名不是已知的标准门。 | + +--- + +## 示例 + +一次性检查: + +```c +#include +#include +#include "cqlib_c.h" + +int main(void) { + uint32_t q0[1] = {0}; + uint32_t q1[1] = {1}; + double angle_a[1] = {0.3}; + double angle_b[1] = {0.5}; + double phase = 0.0; + + /* 1. Disjoint supports commute exactly. */ + int32_t r = commutation_check("X", q0, 1, NULL, 0, "X", q1, 1, NULL, 0, &phase); + printf("disjoint: %d (phase %f)\n", r, phase); /* 1, 0.0 */ + + /* 2. X and Z on the same qubit commute up to a global phase of pi. */ + phase = 0.0; + r = commutation_check("X", q0, 1, NULL, 0, "Z", q0, 1, NULL, 0, &phase); + printf("x/z: %d (phase %f)\n", r, phase); /* 2, pi */ + + /* 3. Diagonal rotations with numeric parameters commute exactly. */ + r = commutation_check("RZ", q0, 1, angle_a, 1, "RZ", q0, 1, angle_b, 1, &phase); + printf("rz/rz: %d\n", r); /* 1 */ + + /* 4. The algebraic-only variant proves the same structural facts. */ + r = commutation_check_algebraic("X", q0, 1, NULL, 0, "X", q1, 1, NULL, 0, &phase); + printf("algebraic disjoint: %d\n", r); /* 1 */ + return 0; +} +``` + +可复用检查器: + +```c +#include +#include "cqlib_c.h" + +int main(void) { + uint32_t q0[1] = {0}; + uint32_t q1[1] = {1}; + double phase = 0.0; + + /* 1. A reusable checker with the default configuration. */ + struct CCommutationConfig config = commutation_config_default(); + struct CCommutationChecker *checker = commutation_checker_new(config); + if (checker == NULL) { + return 1; + } + int32_t r = commutation_checker_check(checker, "X", q0, 1, NULL, 0, + "X", q1, 1, NULL, 0, &phase); + printf("checker disjoint: %d\n", r); /* 1 */ + commutation_checker_free(checker); + + /* 2. A checker drawing commutation rules from a knowledge library. */ + struct CKnowledgeLibrary *lib = knowledge_library_builtin(); + if (lib != NULL) { + struct CCommutationChecker *from_lib = commutation_checker_from_library(lib, config); + if (from_lib != NULL) { + r = commutation_checker_check(from_lib, "X", q0, 1, NULL, 0, + "X", q1, 1, NULL, 0, &phase); + printf("library checker disjoint: %d\n", r); /* 1 */ + commutation_checker_free(from_lib); + } + knowledge_library_free(lib); + } + return 0; +} +``` + +检查器证明顺序为:结构事实 → 支持门族的符号代数 → 显式知识库对易规则(启用时)→ 具体局部矩阵比较(启用且规模足够小时);参数比较是符号且带容差的,可证明相等的表达式被视为同一应用。 diff --git a/docs/api/cn/c/5_visualization/0_overview.md b/docs/api/cn/c/5_visualization/0_overview.md new file mode 100644 index 00000000..0227ed1d --- /dev/null +++ b/docs/api/cn/c/5_visualization/0_overview.md @@ -0,0 +1,25 @@ +# 可视化(C) + +可视化模块提供四组输出:`circuit_to_text` 输出框线字符的 UTF-8 文本线路图,`circuit_to_figure` 输出 SVG 标记字符串,`render_figure_to_file` 直接把线路渲染到 `.svg` / `.png` 文件;状态图(Bloch、多 Bloch、city、paulivec)与结果图(直方图、分布图)从模拟器态和执行结果生成 SVG,Visual IR 则把线路编译为后端无关的绘图中间表示。错误码、字符串与句柄的释放约定遵循 [Overview](../0_overview.md)。 + +--- + +## 页面导航 + +| 页面 | 内容 | +| --- | --- | +| [文本线路图](1_draw_text.md) | `circuit_to_text` 与 `TextDrawerOptionsC` 选项表。 | +| [SVG 线路图](2_draw_figure.md) | `circuit_to_figure` 与 `FigureDrawerOptionsC` 选项表。 | +| [状态图](3_state_plots.md) | `plot_bloch_vector`、`plot_bloch_multivector`、`plot_state_city`、`plot_state_paulivec` 及 `CStatePlotOptionsC` 选项表。 | +| [结果图](4_result_plots.md) | `plot_histogram`、`plot_distribution` 及 `CResultPlotOptionsC` 选项表。 | +| [落盘与输出](5_render_to_file.md) | `render_figure_to_file`、`render_state_plot_to_file`、`render_result_plot_to_file`:按扩展名渲染为 `.svg` 或 `.png` 文件。 | +| [Visual IR](6_visual_ir.md) | `build_visual_circuit`、`CVisualCircuit` 句柄族与 `draw_text_from_visual`、`draw_figure_from_visual`。 | + +--- + +## 共同约定 + +- 各绘制接口的 `options` 均可传 NULL,此时使用库默认选项;显式配置时按值填充对应的选项结构体(`TextDrawerOptionsC` / `FigureDrawerOptionsC` / `CStatePlotOptionsC` / `CResultPlotOptionsC` / `CVisualBuildOptionsC`),结构体内存归调用方管理。 +- 返回 `char*` 的接口(`circuit_to_text`、`circuit_to_figure`、`plot_*`、`draw_*_from_visual`)用 `cqlib_string_free` 释放。 +- `CVisualCircuit*` 句柄用 `visual_circuit_free` 释放(允许 NULL);其访问器族见 [Visual IR](6_visual_ir.md)。 +- `render_figure_to_file` 与两个 plot 落盘接口返回 `int32_t` 错误码:0 成功;-5 文件读写或渲染失败;-1 空指针(plot 落盘接口另有 -4:字符串不是有效的 UTF-8)。 diff --git a/docs/api/cn/c/5_visualization/1_draw_text.md b/docs/api/cn/c/5_visualization/1_draw_text.md new file mode 100644 index 00000000..52c626bc --- /dev/null +++ b/docs/api/cn/c/5_visualization/1_draw_text.md @@ -0,0 +1,68 @@ +# 文本线路图(C) + +`circuit_to_text` 把线路绘制为 UTF-8 文本线路图(框线字符)。错误码、字符串与句柄的释放约定遵循 [Overview](../0_overview.md)。 + +--- + +## circuit_to_text(circuit, options) + +绘制线路的 UTF-8 文本图。 + +参数: + +- `circuit` (`const CCircuit*`):待绘制的线路。 +- `options` (`const TextDrawerOptionsC*`):绘制选项;NULL 时使用库默认值。 + +返回:堆上 C 字符串,用 `cqlib_string_free` 释放;失败返回 NULL。 + +--- + +## TextDrawerOptionsC + +文本绘制选项,按值填充;未设置的字段使用库默认值: + +| 字段 | 类型 | 含义 | 取值行为 | +| --- | --- | --- | --- | +| `show_params` | `uint8_t` | 是否在门标签后附加参数文本。 | `0` 关闭,`1` 开启。 | +| `decompose_circuit_gates` | `uint8_t` | 绘制前是否分解复合门。 | `0` 不分解,`1` 分解。 | +| `line_width` | `intptr_t` | 折行后每段的最大宽度。 | `<= 0` 禁用折行。 | +| `initial_state` | `uint8_t` | 是否在每个量子比特线起始处显示 `\|0>`。 | `0` 不显示,`1` 显示。 | +| `reverse_bits` | `uint8_t` | 是否反转显示的比特顺序。 | `0` 原序,`1` 反序。 | + +--- + +## 示例 + +```c +#include +#include "cqlib_c.h" + +int main(void) { + struct CCircuit *qc = circuit_new(2); + circuit_h(qc, 0); + circuit_cx(qc, 0, 1); + + /* 1. 默认选项 */ + char *text = circuit_to_text(qc, NULL); + if (text != NULL) { + printf("%s\n", text); + cqlib_string_free(text); + } + + /* 2. 显式选项:显示初态、禁用折行 */ + struct TextDrawerOptionsC options = {0}; + options.show_params = 1; + options.initial_state = 1; + options.line_width = 0; /* <= 0 disables wrapping */ + char *text2 = circuit_to_text(qc, &options); + if (text2 != NULL) { + printf("%s\n", text2); + cqlib_string_free(text2); + } + + circuit_free(qc); + return 0; +} +``` + +SVG 图形绘制与文件输出见 [SVG 线路图](2_draw_figure.md)、[落盘与输出](5_render_to_file.md)。 diff --git a/docs/api/cn/c/5_visualization/2_draw_figure.md b/docs/api/cn/c/5_visualization/2_draw_figure.md new file mode 100644 index 00000000..5b177dee --- /dev/null +++ b/docs/api/cn/c/5_visualization/2_draw_figure.md @@ -0,0 +1,72 @@ +# SVG 线路图(C) + +`circuit_to_figure` 把线路绘制为 SVG 图形,返回 SVG 标记字符串。错误码、字符串与句柄的释放约定遵循 [Overview](../0_overview.md)。 + +--- + +## circuit_to_figure(circuit, options) + +绘制线路的 SVG 图形。 + +参数: + +- `circuit` (`const CCircuit*`):待绘制的线路。 +- `options` (`const FigureDrawerOptionsC*`):绘制选项;NULL 时使用库默认值。 + +返回:堆上 C 字符串(SVG 标记),用 `cqlib_string_free` 释放;失败返回 NULL。 + +--- + +## FigureDrawerOptionsC + +图形绘制选项,按值填充;未设置的字段使用库默认值: + +| 字段 | 类型 | 含义 | 取值行为 | +| --- | --- | --- | --- | +| `show_params` | `uint8_t` | 是否在门标签后附加参数文本。 | `0` 关闭,`1` 开启。 | +| `decompose_circuit_gates` | `uint8_t` | 绘制前是否分解复合门。 | `0` 不分解,`1` 分解。 | +| `width_per_column` | `double` | 每个逻辑列的图形宽度缩放。 | 正值生效。 | +| `height_per_qubit` | `double` | 每个量子比特的图形高度缩放。 | 正值生效。 | +| `dpi` | `uint32_t` | 导出 PNG 时的栅格化 DPI。 | 必须 `> 0`。 | +| `fold` | `int32_t` | 每行最大列数(超过则折叠换行)。 | `<= 0` 禁用折叠。 | +| `initial_state` | `uint8_t` | 是否在比特标签中显示 `\|0>`。 | `0` 不显示,`1` 显示。 | +| `reverse_bits` | `uint8_t` | 是否反转显示的比特顺序。 | `0` 原序,`1` 反序。 | + +`dpi` 只在 PNG 栅格化时参与计算(见 [落盘与输出](5_render_to_file.md));`fold` 控制宽线路的折叠换行,`<= 0` 时整行输出不折叠。 + +--- + +## 示例 + +```c +#include +#include "cqlib_c.h" + +int main(void) { + struct CCircuit *qc = circuit_new(2); + circuit_h(qc, 0); + circuit_cx(qc, 0, 1); + + /* 1. 默认选项:返回 SVG 标记字符串 */ + char *svg = circuit_to_figure(qc, NULL); + if (svg != NULL) { + printf("%s\n", svg); /* "..." */ + cqlib_string_free(svg); + } + + /* 2. 显式选项:显示初态、每行最多 8 列、PNG 导出 300 DPI */ + struct FigureDrawerOptionsC options = {0}; + options.initial_state = 1; + options.fold = 8; /* <= 0 disables folding */ + options.dpi = 300; + char *svg2 = circuit_to_figure(qc, &options); + if (svg2 != NULL) { + cqlib_string_free(svg2); + } + + circuit_free(qc); + return 0; +} +``` + +文本绘制与文件输出见 [文本线路图](1_draw_text.md)、[落盘与输出](5_render_to_file.md)。 diff --git a/docs/api/cn/c/5_visualization/3_state_plots.md b/docs/api/cn/c/5_visualization/3_state_plots.md new file mode 100644 index 00000000..2cef5286 --- /dev/null +++ b/docs/api/cn/c/5_visualization/3_state_plots.md @@ -0,0 +1,248 @@ +# 量子态可视化(C) + +本页覆盖量子态的可视化:布洛赫向量图 `plot_bloch_vector`、约化布洛赫向量图 `plot_bloch_multivector`、密度矩阵图 `plot_state_city`、泡利基期望值图 `plot_state_paulivec`,绘图选项 `CStatePlotOptions`,以及数值化的状态检查接口 `local_bloch_vectors` 与 `state_to_density_matrix`。绘图入口返回堆上 SVG 标记字符串,用 `cqlib_string_free` 释放;数值接口按两步式数组约定写入调用方分配的缓冲区。错误码、字符串与句柄的释放约定遵循 [Overview](../0_overview.md)。 + +态类入口各有两个变体:基础名接受 `CStatevector`(纯态按 `ρ = |ψ⟩⟨ψ|` 展开为密度矩阵),`_density_matrix` 后缀接受 `CDensityMatrix`(直接使用其矩阵数据),两者共用同一套 `CStatePlotOptions`。态句柄的构造见 [Statevector](../3_qis/1_statevector.md) 与 [DensityMatrix](../3_qis/2_density_matrix.md)。 + +--- + +## plot_bloch_vector(x, y, z, options) + +绘制单个布洛赫向量。 + +```c +char *plot_bloch_vector(double x, double y, double z, + const struct CStatePlotOptions *options); +``` + +参数: + +- `x`、`y`、`z` (`double`):布洛赫坐标 `(x, y, z)`,分量通常落在 `[-1, 1]`。模长大于 `1` 的向量会被投影回单位球面,落在球内的向量保持不变。 +- `options` (`const CStatePlotOptions*`):绘图选项;NULL 时使用库默认值。 + +返回:堆上 C 字符串(SVG 标记),用 `cqlib_string_free` 释放;失败返回 NULL。向量标签固定为 `q0`。 + +--- + +## plot_bloch_multivector(state, options) / plot_bloch_multivector_density_matrix(state, options) + +为每个量子比特绘制一个约化布洛赫向量。每个向量由该比特的约化密度矩阵计算得到,标签按量子比特编号写成 `q0`、`q1` 等;多个球排成网格,每行最多 4 个。 + +```c +char *plot_bloch_multivector(const struct CStatevector *state, + const struct CStatePlotOptions *options); +char *plot_bloch_multivector_density_matrix(const struct CDensityMatrix *state, + const struct CStatePlotOptions *options); +``` + +参数: + +- `state`:输入态句柄(前者 `const CStatevector*`,后者 `const CDensityMatrix*`)。 +- `options` (`const CStatePlotOptions*`):绘图选项;NULL 时使用库默认值。 + +返回:堆上 C 字符串(SVG 标记),用 `cqlib_string_free` 释放;失败返回 NULL。 + +失败场景: + +- `state` 为 NULL。 +- `options` 中出现非法 UTF-8 字符串或 `color` 数组内含 NULL 元素。 +- 状态缓冲长度与量子比特数不符,或量子比特数过大导致矩阵维度溢出。 + +--- + +## plot_state_city(state, options) / plot_state_city_density_matrix(state, options) + +绘制密度矩阵的实部与虚部,每部分一个面板,面板标题为 `Re[rho]` 与 `Im[rho]`。当所有元素的虚部均为零时,SVG 中不生成虚部面板。每个单元的面积随 `sqrt(|value| / max_abs)` 缩放,使小幅度元素仍可见;`max_abs` 取矩阵元素模长的最大值,下限为 `1e-12`。柱体填充不透明度由 `CStatePlotOptions.alpha` 控制。 + +```c +char *plot_state_city(const struct CStatevector *state, + const struct CStatePlotOptions *options); +char *plot_state_city_density_matrix(const struct CDensityMatrix *state, + const struct CStatePlotOptions *options); +``` + +参数: + +- `state`:输入态句柄(前者 `const CStatevector*`,后者 `const CDensityMatrix*`)。 +- `options` (`const CStatePlotOptions*`):绘图选项;NULL 时使用库默认值。 + +返回:堆上 C 字符串(SVG 标记),用 `cqlib_string_free` 释放;失败返回 NULL。 + +失败场景:与 [plot_bloch_multivector](#plot_bloch_multivectorstate-options--plot_bloch_multivector_density_matrixstate-options) 相同。 + +--- + +## plot_state_paulivec(state, options) / plot_state_paulivec_density_matrix(state, options) + +把泡利基期望值绘制为柱状图。标签为 `I`、`X`、`Y`、`Z` 的张量积,共 `4^n` 条,其中 `n` 为量子比特数;非负柱与负值柱使用不同颜色。柱数不超过 `64` 时才绘制旋转的横轴标签。 + +```c +char *plot_state_paulivec(const struct CStatevector *state, + const struct CStatePlotOptions *options); +char *plot_state_paulivec_density_matrix(const struct CDensityMatrix *state, + const struct CStatePlotOptions *options); +``` + +参数: + +- `state`:输入态句柄(前者 `const CStatevector*`,后者 `const CDensityMatrix*`)。 +- `options` (`const CStatePlotOptions*`):绘图选项;NULL 时使用库默认值。 + +返回:堆上 C 字符串(SVG 标记),用 `cqlib_string_free` 释放;失败返回 NULL。 + +失败场景:与 [plot_bloch_multivector](#plot_bloch_multivectorstate-options--plot_bloch_multivector_density_matrixstate-options) 相同。 + +--- + +## local_bloch_vectors_len(state) / local_bloch_vectors(state, out, len) + +计算态矢量每个量子比特的约化布洛赫向量,是 [plot_bloch_multivector](#plot_bloch_multivectorstate-options--plot_bloch_multivector_density_matrixstate-options) 图的数值版本,结果以数据形式返回而非绘图。 + +```c +uintptr_t local_bloch_vectors_len(const struct CStatevector *state); +int32_t local_bloch_vectors(const struct CStatevector *state, double *out, uintptr_t len); +``` + +两步式数组输出:`local_bloch_vectors_len` 返回所需缓冲区长度 `3 * num_qubits`(NULL 返回 0);分配相应数量的 `double` 后,把缓冲区连同相同的 `len` 传给 `local_bloch_vectors`。输出按 `[x0, y0, z0, x1, y1, z1, ...]` 扁平排列:第 `q * 3 .. q * 3 + 3` 项为量子比特 `q` 约化态的 ``、``、`` 期望值。 + +参数: + +- `state` (`const CStatevector*`):输入态矢量。 +- `out` (`double*`):长度为 `len` 的输出缓冲区。 +- `len` (`uintptr_t`):缓冲区长度,必须等于 `local_bloch_vectors_len(state)`。 + +错误码(填充接口): + +| 值 | 场景 | +| --- | --- | +| `0` | 成功。 | +| `-1` | `state` 或 `out` 为 NULL。 | +| `-8` | `len` 与 `local_bloch_vectors_len(state)` 不符,或状态数据非法。 | + +贝尔态的每个约化布洛赫向量都为零,六个值全为 `0`;比特 0 处于 `|+⟩`、比特 1 处于 `|0⟩` 时,缓冲区为 `[1, 0, 0, 0, 0, 1]`。 + +--- + +## state_to_density_matrix_len(state) / state_to_density_matrix(state, out, len) + +把态矢量转换为密度矩阵 `ρ = |ψ⟩⟨ψ|`,是 [plot_state_city](#plot_state_citystate-options--plot_state_city_density_matrixstate-options) 图的数值版本,结果以数据形式返回。 + +```c +uintptr_t state_to_density_matrix_len(const struct CStatevector *state); +int32_t state_to_density_matrix(const struct CStatevector *state, Complex64 *out, uintptr_t len); +``` + +两步式数组输出:`state_to_density_matrix_len` 返回所需缓冲区长度 `4^num_qubits`(NULL 返回 0);`state_to_density_matrix` 把相应数量的 `Complex64` 值拷入 `out`,按行主序排列为 `N × N` 矩阵(`N = 2^num_qubits`):元素 `(row, col)` 存储在下标 `row * N + col` 处。 + +参数: + +- `state` (`const CStatevector*`):输入态矢量。 +- `out` (`Complex64*`):长度为 `len` 的输出缓冲区。 +- `len` (`uintptr_t`):缓冲区长度,必须等于 `state_to_density_matrix_len(state)`。 + +错误码(填充接口): + +| 值 | 场景 | +| --- | --- | +| `0` | 成功。 | +| `-1` | `state` 或 `out` 为 NULL。 | +| `-8` | `len` 与 `state_to_density_matrix_len(state)` 不符,或状态数据非法。 | + +两比特贝尔态的 `4 × 4` 矩阵在对角角落与 `(0,3)`、`(3,0)` 相干项处为 `0.5`,其余元素均为 `0`。 + +--- + +## CStatePlotOptions + +量子态绘图选项,按值填充;`options` 传 NULL 时使用库默认值。选项中的字符串须为有效 UTF-8,否则绘图入口返回 NULL。 + +| 字段 | 类型 | 含义 | 取值行为 | +| --- | --- | --- | --- | +| `title` | `const char*` | 图标题。 | NULL 不绘制标题。 | +| `color` | `const char* const*` | 配色数组,长度由 `color_len` 给出。 | NULL 或 `color_len == 0` 使用各绘图族的默认配色;数组内含 NULL 元素时返回 NULL。 | +| `color_len` | `uintptr_t` | `color` 的条目数。 | `0` 视为未提供配色。 | +| `alpha` | `double` | `plot_state_city` 柱体的填充不透明度。 | 按给定值生效;库默认值为 `1.0`(仅当 `options` 为 NULL 时应用,显式传入结构体时零值即完全透明)。 | +| `reverse_bits` | `uint8_t` | 是否反转显示的计算基位序。 | `0` 原序,`1` 反序。置为 `1` 时,密度矩阵图的行列标签按相反顺序排列,矩阵元素按位序映射回存储位置;布洛赫图与泡利图的标签顺序随之反转。 | +| `has_figsize` | `uint8_t` | `fig_width` / `fig_height` 是否携带自定义图尺寸。 | `0` 忽略宽高(各绘图族按自身布局计算画布尺寸),非 `0` 生效。 | +| `fig_width` | `double` | 图宽,单位为类英寸单位,按每单位 `100` 像素换算为 SVG 像素。 | `has_figsize == 0` 时忽略。给定尺寸时,密度矩阵图与泡利图的宽、高下限分别为 `4` 与 `3`,布洛赫图的宽、高下限均为 `3`。 | +| `fig_height` | `double` | 图高,单位与换算同 `fig_width`。 | `has_figsize == 0` 时忽略;下限规则同 `fig_width`。 | + +`color` 的用法因绘图族而异:`plot_state_paulivec` 取第一个元素作为非负柱颜色(缺省 `#4569d4`)、第二个元素作为负值柱颜色(缺省 `#d64b5f`);其余绘图族使用固定配色。 + +--- + +## 示例 + +```c +#include +#include +#include +#include "cqlib_c.h" + +int main(void) { + /* 1. Build a Bell state */ + double inv = 0.7071067811865476; + Complex64 amplitudes[4] = { + {inv, 0.0}, {0.0, 0.0}, {0.0, 0.0}, {inv, 0.0}, + }; + struct CStatevector *state = statevector_from_state(2, amplitudes, 4); + if (state == NULL) { + return 1; + } + + /* 2. Plot with default options; every entry returns SVG markup */ + char *svg = plot_bloch_multivector(state, NULL); + if (svg != NULL) { + printf("%s\n", svg); + cqlib_string_free(svg); + } + + /* 3. Plot with explicit options */ + struct CStatePlotOptions options; + memset(&options, 0, sizeof(options)); + options.title = "Bell state"; + options.alpha = 0.75; + options.reverse_bits = 1; + options.has_figsize = 1; + options.fig_width = 4.0; + options.fig_height = 3.0; + svg = plot_state_city(state, &options); + if (svg != NULL) { + printf("%s\n", svg); + cqlib_string_free(svg); + } + + /* 4. Density-matrix input works through the _density_matrix variants */ + struct CDensityMatrix *rho = density_matrix_from_state(2, amplitudes, 4); + if (rho != NULL) { + svg = plot_state_paulivec_density_matrix(rho, NULL); + if (svg != NULL) { + cqlib_string_free(svg); + } + density_matrix_free(rho); + } + + /* 5. Single Bloch vector: vectors longer than the unit + sphere are clamped, not rejected */ + svg = plot_bloch_vector(0.0, 0.0, 1.0, NULL); + if (svg != NULL) { + cqlib_string_free(svg); + } + + /* 6. Numeric state inspection via the two-step array pattern */ + uintptr_t bloch_len = local_bloch_vectors_len(state); /* 6 = 3 * 2 */ + double *bloch = malloc(bloch_len * sizeof(double)); + local_bloch_vectors(state, bloch, bloch_len); /* all zeros for the Bell state */ + free(bloch); + + uintptr_t rho_len = state_to_density_matrix_len(state); /* 16 = 4^2 */ + Complex64 *rho_buf = malloc(rho_len * sizeof(Complex64)); + state_to_density_matrix(state, rho_buf, rho_len); /* 4x4 matrix, corners 0.5 */ + free(rho_buf); + + statevector_free(state); + return 0; +} +``` + +同一纯态分别经 `plot_state_paulivec`(态矢径)与 `plot_state_paulivec_density_matrix`(密度矩阵径)绘制时,两者输出的 SVG 标记一致。把绘制结果写出为文件见 [落盘与输出](5_render_to_file.md)。 diff --git a/docs/api/cn/c/5_visualization/4_result_plots.md b/docs/api/cn/c/5_visualization/4_result_plots.md new file mode 100644 index 00000000..f6f85ad7 --- /dev/null +++ b/docs/api/cn/c/5_visualization/4_result_plots.md @@ -0,0 +1,133 @@ +# 测量结果可视化(C) + +本页覆盖测量结果的可视化:直方图 `plot_histogram` 与概率分布图 `plot_distribution`,以及绘图选项 `CResultPlotOptions`。两个入口都接受 `CExecutionResult` 句柄,返回堆上 SVG 标记字符串,用 `cqlib_string_free` 释放。错误码、字符串与句柄的释放约定遵循 [Overview](../0_overview.md)。 + +`CExecutionResult` 句柄由 `execution_result_new` 创建、`execution_result_finish` 填入测量计数,构造方式见 [Result](../2_device/5_result.md)。 + +--- + +## plot_histogram(result, options) + +把执行结果的测量计数绘制为直方图。计数按结果的量子比特数格式化为比特串标签,作为横轴刻度;纵轴从零起算,不绘制负值区间。 + +```c +char *plot_histogram(const struct CExecutionResult *result, + const struct CResultPlotOptions *options); +``` + +参数: + +- `result` (`const CExecutionResult*`):包含测量计数的执行结果。 +- `options` (`const CResultPlotOptions*`):绘图选项;NULL 时使用库默认值。 + +返回:堆上 C 字符串(SVG 标记),用 `cqlib_string_free` 释放;失败返回 NULL。 + +失败场景: + +- `result` 为 NULL,或执行结果不含计数。 +- `options` 中出现非法 UTF-8 字符串、`color` / `legend` 数组内含 NULL 元素,或绘图选项不一致。 + +--- + +## plot_distribution(result, options) + +把执行结果的测量计数归一化为概率后绘制。每一条计数除以计数总和得到概率,柱高与纵轴刻度按概率标注。 + +```c +char *plot_distribution(const struct CExecutionResult *result, + const struct CResultPlotOptions *options); +``` + +参数: + +- `result` (`const CExecutionResult*`):包含测量计数的执行结果。 +- `options` (`const CResultPlotOptions*`):绘图选项;NULL 时使用库默认值。 + +返回:堆上 C 字符串(SVG 标记),用 `cqlib_string_free` 释放;失败返回 NULL。 + +失败场景:在 [plot_histogram](#plothistogramresult-options) 的基础上,计数求和为零时同样返回 NULL。 + +--- + +## CResultPlotOptions + +结果图选项,按值填充;`options` 传 NULL 时使用库默认值。选项中的字符串须为有效 UTF-8,否则绘图入口返回 NULL。 + +| 字段 | 类型 | 含义 | 取值行为 | +| --- | --- | --- | --- | +| `has_figsize` | `uint8_t` | `fig_width` / `fig_height` 是否携带自定义图尺寸。 | `0` 忽略宽高,非 `0` 生效。 | +| `fig_width` | `double` | 图宽,单位为类英寸单位,按每单位 `100` 像素换算为 SVG 像素。 | `has_figsize == 0` 时忽略。给定尺寸时宽、高的下限均为 `2`。 | +| `fig_height` | `double` | 图高,单位与换算同 `fig_width`。 | `has_figsize == 0` 时忽略;下限规则同 `fig_width`。 | +| `color` | `const char* const*` | 每个数据集的颜色,长度由 `color_len` 给出。 | NULL 或 `color_len == 0` 使用内置的定性调色板;数组内含 NULL 元素时返回 NULL。 | +| `color_len` | `uintptr_t` | `color` 的条目数。 | `0` 视为未提供配色。 | +| `number_to_keep` | `intptr_t` | 保留取值最大的 `k` 条,其余聚合为标签 `rest` 的条目。 | 负值禁用聚合;`k` 为 `0` 或不小于条目数时不聚合。取值并列时按键的标签排序,保证输出跨平台一致。 | +| `sort` | `const char*` | 排序策略。 | NULL 默认 `asc`;取值见下表。非法 UTF-8 时返回 NULL。 | +| `target_string` | `const char*` | `sort = "hamming"` 使用的目标比特串。 | NULL 表示未设置。 | +| `legend` | `const char* const*` | 图例条目,每个数据集一条,长度由 `legend_len` 给出。 | NULL 不绘制图例。结果绘制入口只产生一个数据集,因此给出图例时其长度必须为 `1`。 | +| `legend_len` | `uintptr_t` | `legend` 的条目数。 | 与 `legend` 配对使用。 | +| `bar_labels` | `uint8_t` | 是否在柱顶绘制数值标签。 | `0` 不绘制,`1` 绘制;库默认绘制(仅当 `options` 为 NULL 时应用)。 | +| `title` | `const char*` | 图标题。 | NULL 不绘制标题。 | + +### 排序策略 + +| `sort` 取值 | 说明 | +| --- | --- | +| `asc` | 按比特串标签的字典序升序。 | +| `desc` | 按比特串标签的字典序降序。 | +| `value` | 按标签在各数据集中的峰值升序;峰值并列时按标签升序。 | +| `value_desc` | 按标签在各数据集中的峰值降序;峰值并列时按标签升序。 | +| `hamming` | 按与 `target_string` 的汉明距离升序;距离并列时按标签升序。标签 `rest` 始终排在末尾。 | + +--- + +## 示例 + +```c +#include +#include +#include "cqlib_c.h" + +int main(void) { + /* 1. Build an execution result with measurement counts */ + const char *task_id = "task-visualization"; + uint32_t qubits[2] = {0, 1}; + struct CExecutionResult *result = + execution_result_new(task_id, qubits, 2, 7, NULL); + if (result == NULL) { + return 1; + } + const char *bitstrings[2] = {"00", "11"}; + uint64_t counts[2] = {2, 5}; + if (execution_result_finish(result, bitstrings, counts, 2) != 0) { + execution_result_free(result); + return 1; + } + + /* 2. Histogram with default options */ + char *svg = plot_histogram(result, NULL); + if (svg != NULL) { + printf("%s\n", svg); + cqlib_string_free(svg); + } + + /* 3. Distribution with explicit options: custom canvas and title */ + struct CResultPlotOptions options; + memset(&options, 0, sizeof(options)); + options.has_figsize = 1; + options.fig_width = 3.2; + options.fig_height = 2.4; + options.number_to_keep = -1; /* negative disables aggregation */ + options.bar_labels = 1; + options.title = "Measurement counts"; + svg = plot_distribution(result, &options); + if (svg != NULL) { + printf("%s\n", svg); + cqlib_string_free(svg); + } + + execution_result_free(result); + return 0; +} +``` + +按与目标比特串的汉明距离排序并聚合其余条目:把 `sort` 设为 `"hamming"`、`target_string` 设为目标比特串、`number_to_keep` 设为保留条数即可,其余条目聚合为 `rest`。把绘制结果写出为文件见 [落盘与输出](5_render_to_file.md)。 diff --git a/docs/api/cn/c/5_visualization/5_render_to_file.md b/docs/api/cn/c/5_visualization/5_render_to_file.md new file mode 100644 index 00000000..675608e5 --- /dev/null +++ b/docs/api/cn/c/5_visualization/5_render_to_file.md @@ -0,0 +1,86 @@ +# 落盘与输出(C) + +`render_figure_to_file` 把线路直接渲染到输出文件,按扩展名选择格式:`.svg` 输出矢量图,`.png` 按 `FigureDrawerOptionsC.dpi` 栅格化输出。`render_state_plot_to_file` 与 `render_result_plot_to_file` 把状态图与结果图的 SVG 标记字符串写入文件,扩展名选择同样的两种格式。错误码、字符串与句柄的释放约定遵循 [Overview](../0_overview.md)。 + +--- + +## render_figure_to_file(circuit, output_path, options) + +把线路渲染到输出文件。 + +参数: + +- `circuit` (`const CCircuit*`):待绘制的线路。 +- `output_path` (`const char*`):输出文件路径,扩展名 `.svg` 或 `.png` 决定输出格式。 +- `options` (`const FigureDrawerOptionsC*`):绘制选项(字段表见 [SVG 线路图](2_draw_figure.md));NULL 时使用库默认值。`.png` 输出时 `dpi` 必须大于 0。 + +返回:0 成功;-5 文件读写或渲染失败;-1 空指针。 + +--- + +## render_state_plot_to_file(svg, output_path) + +把状态图的 SVG 标记字符串写入输出文件,`.svg` 输出矢量图,`.png` 输出栅格图。SVG 标记串来自 [状态图](3_state_plots.md) 的 `plot_*` 接口。 + +参数: + +- `svg` (`const char*`):`plot_bloch_vector`、`plot_state_city` 等接口返回的 SVG 标记字符串(该字符串本身仍由调用方用 `cqlib_string_free` 释放)。 +- `output_path` (`const char*`):输出文件路径,扩展名 `.svg` 或 `.png` 决定输出格式。 + +返回:0 成功;-1 空指针;-4 字符串不是有效的 UTF-8;-5 文件读写或渲染失败。 + +--- + +## render_result_plot_to_file(svg, output_path) + +把结果图的 SVG 标记字符串写入输出文件,`.svg` 输出矢量图,`.png` 输出栅格图。SVG 标记串来自 [结果图](4_result_plots.md) 的 `plot_histogram`、`plot_distribution`。 + +参数: + +- `svg` (`const char*`):结果图接口返回的 SVG 标记字符串(该字符串本身仍由调用方用 `cqlib_string_free` 释放)。 +- `output_path` (`const char*`):输出文件路径,扩展名 `.svg` 或 `.png` 决定输出格式。 + +返回:0 成功;-1 空指针;-4 字符串不是有效的 UTF-8;-5 文件读写或渲染失败。 + +--- + +## 示例 + +```c +#include +#include "cqlib_c.h" + +int main(void) { + struct CCircuit *qc = circuit_new(2); + circuit_h(qc, 0); + circuit_cx(qc, 0, 1); + + /* 1. 默认选项输出 SVG */ + int32_t rc = render_figure_to_file(qc, "bell.svg", NULL); + if (rc != 0) { + printf("render failed: %d\n", rc); /* -5: I/O or rendering failure */ + } + + /* 2. 指定 DPI 输出 PNG */ + struct FigureDrawerOptionsC options = {0}; + options.dpi = 300; + options.fold = 8; + rc = render_figure_to_file(qc, "bell.png", &options); + + /* 3. 把状态图的 SVG 标记串写入文件 */ + struct CStatevector *sv = statevector_new(2); + statevector_apply_h(sv, 0); + statevector_apply_cx(sv, 0, 1); + char *svg = plot_bloch_vector(sv, NULL); + if (svg != NULL) { + render_state_plot_to_file(svg, "bell_bloch.svg"); + cqlib_string_free(svg); + } + + statevector_free(sv); + circuit_free(qc); + return 0; +} +``` + +在内存中取得 SVG 标记字符串见 [SVG 线路图](2_draw_figure.md)。 diff --git a/docs/api/cn/c/5_visualization/6_visual_ir.md b/docs/api/cn/c/5_visualization/6_visual_ir.md new file mode 100644 index 00000000..6c1e5095 --- /dev/null +++ b/docs/api/cn/c/5_visualization/6_visual_ir.md @@ -0,0 +1,379 @@ +# 可视化 IR(C) + +本页覆盖线路绘制的中间表示层:构建入口 `build_visual_circuit`、构建选项 `CVisualBuildOptions` / `CParameterFormatOptions`,以及 IR 句柄 `CVisualCircuit` 的查询与绘制函数。错误码、字符串与句柄的释放约定遵循 [Overview](../0_overview.md)。 + +线路绘制分两步:先把线路构建为与绘制后端无关的可视化 IR,再由具体后端绘制: + +```text +CCircuit ──build_visual_circuit──▶ CVisualCircuit ──┬── draw_text_from_visual ──▶ 文本图 + └── draw_figure_from_visual ──▶ SVG 图 +``` + +构建阶段承担与线路语义相关的工作:把量子比特映射为下标(lane)、把参数格式化为标签文本、把指令归类为绘制样式,并把操作排入互不重叠的列。绘制阶段只消费这些结果,不再读回线路。因此同一条线路在两种后端下的门顺序、列划分与标签一致;构建选项也可以独立于绘制选项调整。 + +--- + +## 常量 + +### 参数显示模式(`CParameterFormatOptions.mode`) + +| 常量 | 值 | 模式 | +| --- | --- | --- | +| `PARAM_MODE_NUMERIC` | 0 | 可求值时优先数值显示。 | +| `PARAM_MODE_SYMBOLIC` | 1 | 优先符号表达式显示。 | +| `PARAM_MODE_SYMBOLIC_WITH_VALUE` | 2 | 符号表达式附数值。 | +| `PARAM_MODE_PI_FRACTION_PREFERRED` | 3 | 优先 `k*pi/n` 分数显示。 | + +未知取值回退为数值模式(`PARAM_MODE_NUMERIC`)。 + +### 操作绘制样式(`visual_circuit_operation_style` 返回值) + +| 常量 | 值 | 样式 | +| --- | --- | --- | +| `VISUAL_OP_STYLE_GATE` | 0 | 普通门块。 | +| `VISUAL_OP_STYLE_CONTROLLED` | 1 | 受控操作;控制位数用 `visual_circuit_operation_num_controls` 查询。 | +| `VISUAL_OP_STYLE_CZ` | 2 | 受控 Z 标记,按两个以竖线相连的圆点绘制。 | +| `VISUAL_OP_STYLE_SWAP` | 3 | 交换标记。 | +| `VISUAL_OP_STYLE_BARRIER` | 4 | 屏障标记。 | +| `VISUAL_OP_STYLE_MEASURE` | 5 | 测量标记。 | +| `VISUAL_OP_STYLE_RESET` | 6 | 复位标记。 | +| `VISUAL_OP_STYLE_DELAY` | 7 | 延时标记。 | +| `VISUAL_OP_STYLE_CONTROL_FLOW` | 8 | 控制流标记;具体类别用 `visual_circuit_operation_control_flow_kind` 查询。 | + +### 控制流类别(`visual_circuit_operation_control_flow_kind` 返回值) + +| 常量 | 值 | 类别 | +| --- | --- | --- | +| `VISUAL_CF_KIND_IF_ELSE_BLOCK` | 0 | 条件分支块。 | +| `VISUAL_CF_KIND_WHILE_BLOCK` | 1 | `while` 循环块。 | +| `VISUAL_CF_KIND_FOR_BLOCK` | 2 | `for` 循环块。 | +| `VISUAL_CF_KIND_SWITCH_BLOCK` | 3 | 多分支选择块。 | +| `VISUAL_CF_KIND_BREAK` | 4 | 结构化 `break` 标记。 | +| `VISUAL_CF_KIND_CONTINUE` | 5 | 结构化 `continue` 标记。 | +| `VISUAL_CF_KIND_IF_START` | 6 | 展开后的条件分支起始标记。 | +| `VISUAL_CF_KIND_ELSE_START` | 7 | 展开后的假分支起始标记。 | +| `VISUAL_CF_KIND_WHILE_START` | 8 | 展开后的循环起始标记。 | +| `VISUAL_CF_KIND_FOR_START` | 9 | 展开后的 `for` 起始标记。 | +| `VISUAL_CF_KIND_SWITCH_START` | 10 | 展开后的选择起始标记。 | +| `VISUAL_CF_KIND_CASE_START` | 11 | 展开后的分支起始标记。 | +| `VISUAL_CF_KIND_DEFAULT_START` | 12 | 展开后的默认分支起始标记。 | +| `VISUAL_CF_KIND_END` | 13 | 展开后的块结束标记。 | + +块类别(`*_BLOCK`)由构建阶段产生;绘制后端在绘制前把块展开为逐条标记(`*_START`、`VISUAL_CF_KIND_END`),标记标签形如 `If-0 true`、`End-0`,序号在同一张图内递增。 + +--- + +## 构建选项 + +### CParameterFormatOptions + +参数标签文本的格式化选项: + +| 字段 | 类型 | 含义 | 取值行为 | +| --- | --- | --- | --- | +| `mode` | `uint32_t` | 参数显示模式。 | 取 `PARAM_MODE_*` 常量之一;未知值回退为数值模式。 | +| `decimal_precision` | `uintptr_t` | 定点与科学计数法的小数位数。 | 按给定值生效。 | +| `scientific_lower_bound` | `double` | 科学计数法下界。 | 落在 `(0, scientific_lower_bound)` 的值使用科学计数法。 | +| `scientific_upper_bound` | `double` | 科学计数法上界。 | `>= scientific_upper_bound` 的值使用科学计数法。 | +| `pi_tolerance` | `double` | π 分数匹配容差。 | `value / pi` 与有理分数的距离在该容差内时按分数显示。 | +| `pi_max_denominator` | `int64_t` | π 分数匹配的最大分母。 | 分母超过该值的分数不参与匹配。 | + +### CVisualBuildOptions + +可视化 IR 的构建选项,只影响构建阶段,由文本与图形后端共享;`options` 传 NULL 时使用库默认值: + +| 字段 | 类型 | 含义 | 取值行为 | +| --- | --- | --- | --- | +| `decompose_circuit_gates` | `uint8_t` | 构建前是否展开线路门定义。 | `0` 不展开(默认),`1` 展开。展开失败时构建返回 NULL。 | +| `reserve_full_span_for_multi_qubit` | `uint8_t` | 是否为多比特操作保留整段下标(`min..=max`)。 | `0` 只保留操作自身的下标,`1` 保留整段(默认)。 | +| `parameter_format` | `struct CParameterFormatOptions` | 参数标签格式化选项,按值嵌入。 | 字段表见上节。 | + +--- + +## 构建与释放 + +### build_visual_circuit(circuit, options) + +把线路构建为可视化 IR。 + +```c +struct CVisualCircuit *build_visual_circuit(const struct CCircuit *circuit, + const struct CVisualBuildOptions *options); +``` + +参数: + +- `circuit` (`const CCircuit*`):待构建的线路。 +- `options` (`const CVisualBuildOptions*`):构建选项;NULL 时使用库默认值。 + +返回:成功返回新建的 `CVisualCircuit*`(用 `visual_circuit_free` 释放);NULL 输入或构建失败(展开线路门失败、操作引用未知量子比特、符号参数索引越界)时返回 NULL。IR 的量子比特数等于线路的量子比特数。 + +### visual_circuit_free(ptr) + +释放 `CVisualCircuit` 句柄,允许传 NULL。 + +--- + +## 线路级查询 + +### visual_circuit_num_qubits(ptr) / visual_circuit_num_operations(ptr) / visual_circuit_num_columns(ptr) + +分别返回量子比特数、操作数与被占用的列数;NULL 句柄返回 0。 + +### visual_circuit_qubits_len(ptr) / visual_circuit_qubits(ptr, buffer, len) + +两步式读取按显示顺序排列的量子比特编号:`*_len` 返回条目数(NULL 返回 0),填充接口把编号拷入 `buffer`。 + +```c +uintptr_t visual_circuit_qubits_len(const struct CVisualCircuit *ptr); +int32_t visual_circuit_qubits(const struct CVisualCircuit *ptr, + uint32_t *buffer, + uintptr_t len); +``` + +返回(填充接口):0 成功;-1 句柄为 NULL(或 `buffer` 为 NULL 而条目数非零);-8 `len` 小于条目数。 + +--- + +## 操作级查询 + +以下接口均按操作下标 `index` 访问 `VisualCircuit` 的操作列表(构建时的线路顺序)。`index` 越界或句柄为 NULL 时的返回值见各接口说明。 + +### visual_circuit_operation_label(ptr, index) + +返回操作的主显示标签,堆上 C 字符串,用 `cqlib_string_free` 释放;NULL 输入或越界返回 NULL。 + +```c +char *visual_circuit_operation_label(const struct CVisualCircuit *ptr, + uintptr_t index); +``` + +### visual_circuit_operation_column(ptr, index) + +返回操作排入的列序号;NULL 输入或越界返回 `UINTPTR_MAX`。 + +### visual_circuit_operation_span_cols(ptr, index) + +返回操作保留的逻辑列数;NULL 输入或越界返回 `UINTPTR_MAX`。普通操作为 `1`,控制流块按子线路的列数加上分隔与边距计算。 + +### visual_circuit_operation_is_span_box(ptr, index) + +返回操作是否按跨多条量子比特线的整体块绘制:`1` 是、`0` 否;NULL 输入或越界返回 `-1`。自定义酉门、线路门与存储指令为 `1`。 + +### visual_circuit_operation_lanes_len(ptr, index) / visual_circuit_operation_lanes(ptr, index, buffer, len) + +两步式读取操作的操作数下标(`lanes`):按操作的操作数顺序给出所涉量子比特在显示序列中的下标。共享同一量子比特的操作必须排在相邻列,落在不同量子比特上的操作可以共用一列。 + +```c +uintptr_t visual_circuit_operation_lanes_len(const struct CVisualCircuit *ptr, + uintptr_t index); +int32_t visual_circuit_operation_lanes(const struct CVisualCircuit *ptr, + uintptr_t index, + uintptr_t *buffer, + uintptr_t len); +``` + +返回(填充接口):0 成功;-1 句柄为 NULL(或 `buffer` 为 NULL 而条目数非零);-8 `len` 小于条目数。 + +### visual_circuit_operation_covered_lanes_len(ptr, index) / visual_circuit_operation_covered_lanes(ptr, index, buffer, len) + +两步式读取操作在本列保留的下标(`covered_lanes`):为避免重叠而保留的量子比特下标。控制流块保留全部下标;屏障在 `lanes` 为空时保留全部下标;其余操作在 `lanes` 为空时保留 `[0]`。涉及多于一个下标时,若 `reserve_full_span_for_multi_qubit` 为 `1` 则保留 `lanes` 的 `min..=max` 整段,否则保留 `lanes` 本身。 + +```c +uintptr_t visual_circuit_operation_covered_lanes_len(const struct CVisualCircuit *ptr, + uintptr_t index); +int32_t visual_circuit_operation_covered_lanes(const struct CVisualCircuit *ptr, + uintptr_t index, + uintptr_t *buffer, + uintptr_t len); +``` + +返回(填充接口):0 成功;-1 句柄为 NULL(或 `buffer` 为 NULL 而条目数非零);-8 `len` 小于条目数。 + +### visual_circuit_operation_params_len(ptr, index) / visual_circuit_operation_params(ptr, index, out, len) + +两步式读取操作已格式化的参数标签:`*_len` 返回条目数(NULL 输入或越界返回 0),填充接口把每个标签写为新建的堆上 C 字符串,逐个用 `cqlib_string_free` 释放。 + +```c +uintptr_t visual_circuit_operation_params_len(const struct CVisualCircuit *ptr, + uintptr_t index); +int32_t visual_circuit_operation_params(const struct CVisualCircuit *ptr, + uintptr_t index, + char **out, + uintptr_t len); +``` + +返回(填充接口):0 成功;-1 句柄为 NULL(或 `out` 为 NULL 而条目数非零);-8 `len` 小于条目数;-3 标签含内嵌 NUL、无法构造 C 字符串。 + +### visual_circuit_operation_style(ptr, index) + +返回操作的绘制样式,取 `VISUAL_OP_STYLE_*` 常量之一;NULL 输入或越界返回 `UINT32_MAX`。 + +```c +uint32_t visual_circuit_operation_style(const struct CVisualCircuit *ptr, + uintptr_t index); +``` + +### visual_circuit_operation_num_controls(ptr, index) + +返回操作的控制量子比特数;仅当样式为 `VISUAL_OP_STYLE_CONTROLLED` 时有意义。NULL 输入、越界或非受控样式返回 0。 + +### visual_circuit_operation_control_flow_kind(ptr, index) + +返回控制流操作的类别,取 `VISUAL_CF_KIND_*` 常量之一;仅当样式为 `VISUAL_OP_STYLE_CONTROL_FLOW` 时有意义。NULL 输入、越界或非控制流样式返回 `UINT32_MAX`。 + +```c +uint32_t visual_circuit_operation_control_flow_kind(const struct CVisualCircuit *ptr, + uintptr_t index); +``` + +### 样式与标签的归类规则 + +构建阶段按下表把指令归类为样式与标签: + +| 指令 | 样式 | 标签 | +| --- | --- | --- | +| 标准门 `SWAP` | `VISUAL_OP_STYLE_SWAP` | `SWAP` | +| 标准门 `CZ` | `VISUAL_OP_STYLE_CZ` | `CZ` | +| 标准门且控制位数大于 0 | `VISUAL_OP_STYLE_CONTROLLED` | 目标门名 | +| 其余标准门 | `VISUAL_OP_STYLE_GATE` | 门名 | +| 多控门,控制位为 `1` 且基门为 `Z` | `VISUAL_OP_STYLE_CZ` | `CZ` | +| 其余多控门 | `VISUAL_OP_STYLE_CONTROLLED` | 基门门名 | +| 自定义酉门 | `VISUAL_OP_STYLE_GATE`,span box | 酉门标签,为空时 `Unitary` | +| 线路门 | `VISUAL_OP_STYLE_GATE`,span box | 线路门名称,为空时 `Gate` | +| 屏障 | `VISUAL_OP_STYLE_BARRIER` | `B` | +| 测量 | `VISUAL_OP_STYLE_MEASURE` | `M` | +| 复位 | `VISUAL_OP_STYLE_RESET` | `R` | +| 延时 | `VISUAL_OP_STYLE_DELAY` | `D` | +| 经典控制流 | `VISUAL_OP_STYLE_CONTROL_FLOW` | `IF`、`WH`、`FOR`、`SW`、`Break` 或 `Continue` | +| 测量到位指令 | `VISUAL_OP_STYLE_MEASURE` | `M` | +| 存储指令 | `VISUAL_OP_STYLE_GATE`,span box | `STORE` | + +门名按标准门口径缩写:`SDG` 显示为 `SD`,`TDG` 显示为 `TD`,`Phase` 与 `GPhase` 显示为 `P`。受控门的标签取其目标门名:`CX` 与 `CCX` 为 `X`,`CY` 为 `Y`,`CRX`、`CRY`、`CRZ` 分别为 `RX`、`RY`、`RZ`,其余情形去掉门名开头的控制前缀。 + +--- + +## 绘制 + +### draw_text_from_visual(visual, options) + +把已构建的可视化 IR 绘制为 UTF-8 文本线路图(框线字符)。 + +```c +char *draw_text_from_visual(const struct CVisualCircuit *visual, + const struct TextDrawerOptionsC *options); +``` + +参数: + +- `visual` (`const CVisualCircuit*`):可视化 IR 句柄。 +- `options` (`const TextDrawerOptionsC*`):绘制选项(字段表见 [文本线路图](1_draw_text.md));NULL 时使用库默认值。 + +返回:堆上 C 字符串,用 `cqlib_string_free` 释放;失败返回 NULL。 + +### draw_figure_from_visual(visual, options) + +把已构建的可视化 IR 绘制为 SVG 图形。 + +```c +char *draw_figure_from_visual(const struct CVisualCircuit *visual, + const struct FigureDrawerOptionsC *options); +``` + +参数: + +- `visual` (`const CVisualCircuit*`):可视化 IR 句柄。 +- `options` (`const FigureDrawerOptionsC*`):绘制选项(字段表见 [SVG 线路图](2_draw_figure.md));NULL 时使用库默认值。 + +返回:堆上 C 字符串(SVG 标记),用 `cqlib_string_free` 释放;失败返回 NULL。 + +--- + +## 示例 + +```c +#include +#include +#include +#include "cqlib_c.h" + +int main(void) { + struct CCircuit *circuit = circuit_new(2); + circuit_h(circuit, 0); + circuit_cx(circuit, 0, 1); + + /* 1. Build the IR with default options */ + struct CVisualCircuit *visual = build_visual_circuit(circuit, NULL); + if (visual == NULL) { + circuit_free(circuit); + return 1; + } + + /* 2. Circuit-level queries */ + uintptr_t num_qubits = visual_circuit_num_qubits(visual); /* 2 */ + uintptr_t num_ops = visual_circuit_num_operations(visual); /* 2 */ + uintptr_t num_cols = visual_circuit_num_columns(visual); /* 2 */ + + /* 3. Two-step read of the qubit ids in display order */ + uintptr_t qubits_len = visual_circuit_qubits_len(visual); + uint32_t *qubits = malloc(qubits_len * sizeof(uint32_t)); + if (visual_circuit_qubits(visual, qubits, qubits_len) == 0) { + /* qubits[0] == 0, qubits[1] == 1 */ + } + free(qubits); + + /* 4. Operation-level queries */ + char *label = visual_circuit_operation_label(visual, 0); + if (label != NULL) { + printf("label: %s\n", label); /* "H" */ + cqlib_string_free(label); + } + uintptr_t column = visual_circuit_operation_column(visual, 1); /* 1 */ + uint32_t style = visual_circuit_operation_style(visual, 1); /* CONTROLLED */ + uintptr_t num_controls = visual_circuit_operation_num_controls(visual, 1); /* 1 */ + + uintptr_t lanes_len = visual_circuit_operation_lanes_len(visual, 1); + uintptr_t *lanes = malloc(lanes_len * sizeof(uintptr_t)); + if (visual_circuit_operation_lanes(visual, 1, lanes, lanes_len) == 0) { + /* lanes[0] == 0, lanes[1] == 1 */ + } + free(lanes); + + /* 5. Out-of-bounds access fails gracefully */ + if (visual_circuit_operation_label(visual, 2) == NULL) { + printf("index 2 out of range\n"); + } + + /* 6. Draw the pre-built IR with both backends */ + char *text = draw_text_from_visual(visual, NULL); + if (text != NULL) { + printf("%s\n", text); + cqlib_string_free(text); + } + char *svg = draw_figure_from_visual(visual, NULL); + if (svg != NULL) { + cqlib_string_free(svg); + } + + /* 7. Explicit build options: pi-fraction parameter labels */ + struct CVisualBuildOptions options; + memset(&options, 0, sizeof(options)); + options.reserve_full_span_for_multi_qubit = 1; + options.parameter_format.mode = PARAM_MODE_PI_FRACTION_PREFERRED; + options.parameter_format.decimal_precision = 2; + options.parameter_format.scientific_lower_bound = 1e-3; + options.parameter_format.scientific_upper_bound = 1e4; + options.parameter_format.pi_tolerance = 1e-3; + options.parameter_format.pi_max_denominator = 16; + struct CVisualCircuit *visual2 = build_visual_circuit(circuit, &options); + if (visual2 != NULL) { + visual_circuit_free(visual2); + } + + visual_circuit_free(visual); + circuit_free(circuit); + return 0; +} +``` + +直接从线路绘制(不经 IR)见 [文本线路图](1_draw_text.md) 与 [SVG 线路图](2_draw_figure.md);把 SVG 标记字符串写出到文件见 [落盘与输出](5_render_to_file.md)。 diff --git a/docs/api/cn/c/6_error_mitigation/0_overview.md b/docs/api/cn/c/6_error_mitigation/0_overview.md new file mode 100644 index 00000000..4d7e0c1c --- /dev/null +++ b/docs/api/cn/c/6_error_mitigation/0_overview.md @@ -0,0 +1,112 @@ +# 错误缓解(C) + +错误缓解模块通过三种互补的流水线抑制噪声对期望值估计的影响:零噪声外推(ZNE)在多个噪声因子下折叠线路并外推回零噪声极限,虚拟蒸馏(VD)利用多份拷贝与 copy-swap 线路估计比值 `Tr(O ρ^M) / Tr(ρ^M)`,统一流水线 `CErrorMitigation` 把两者封装为「采样 → 后处理」两个阶段。错误码、字符串与句柄的释放约定遵循 [Overview](../0_overview.md)。 + +三个子模块共享同一个估计器回调类型 `CEstimatorFn`:缓解流程把待估计的线路交给回调,由回调给出期望值与方差。 + +--- + +## 页面导航 + +| 页面 | 内容 | +| --- | --- | +| [零噪声外推](1_zne.md) | `CZneMitigation`:折叠线路、噪声因子、外推与缓解序列;`CCircuitList` 遍历。 | +| [虚拟蒸馏](2_virtual_distillation.md) | `CVirtualDistillation`:copy-swap 线路、分子/分母电路与完整协议。 | +| [统一流水线](3_unified.md) | `CErrorMitigation`:`new → run → get_mitigated` 两阶段流程。 | + +--- + +## 常量 + +缓解方法标签(`error_mitigation_new` 的 `method` 参数): + +| 常量 | 值 | 方法 | 附加参数 | +| --- | --- | --- | --- | +| `MITIGATION_ZNE` | 0 | 零噪声外推。 | `fold_levels` 折叠层数组。 | +| `MITIGATION_VIRTUAL_DISTILLATION` | 1 | 虚拟蒸馏。 | `copies` 副本数。 | + +后处理方法标签(`error_mitigation_get_mitigated` 的 `process_method` 参数): + +| 常量 | 值 | 流程 | +| --- | --- | --- | +| `PROCESS_ZNE_POLYNOMIAL` | 0 | ZNE + 多项式外推。 | +| `PROCESS_ZNE_EXPONENTIAL` | 1 | ZNE + 指数外推。 | +| `PROCESS_VIRTUAL_DISTILLATION` | 2 | 虚拟蒸馏。 | + +ZNE 外推方法标签(`zne_extrapolate` 的 `method` 参数): + +| 常量 | 值 | 方法 | +| --- | --- | --- | +| `ZNE_EXTRAPOLATE_POLYNOMIAL` | 0 | 多项式拟合。 | +| `ZNE_EXTRAPOLATE_EXPONENTIAL` | 1 | 指数衰减拟合。 | + +--- + +## CEstimatorFn 回调契约 + +```c +typedef void (*CEstimatorFn)(const struct CCircuit *circuit, + const struct CHamiltonian *hamiltonian, + uintptr_t shots, + double *expectation, + double *variance); +``` + +缓解流程通过该回调取得期望值估计,契约如下: + +- **指针有效期**:`circuit`、`hamiltonian` 及全部出参指针仅在回调执行期间有效;回调返回后不得继续使用,也不得保存到回调之外。 +- **hamiltonian 所有权**:`hamiltonian` 句柄归库所有,回调不得释放它。回调收到的哈密顿量可能已按流程需要扩展到折叠/拷贝线路的宽度。 +- **出参写入**:回调必须通过 `expectation` 写入期望值;方差已知时写入 `variance`,未知时写入 NaN。虚拟蒸馏的分母电路估计中 `hamiltonian` 为 NULL,回调按无可观测量处理。 +- **shots 语义**:`shots` 为 0 表示「未指定」,回调自行决定采样规模;非 0 时为调用方建议的采样数。 + +回调内部可自由调用模拟器接口(如 [态矢量](../3_qis/1_statevector.md) 的 `statevector_from_circuit` + `statevector_expectation`)完成估计。 + +--- + +## 示例 + +```c +#include +#include "cqlib_c.h" + +/* 估计器回调:精确模拟折叠线路并计算 */ +static void estimate(const struct CCircuit *circuit, + const struct CHamiltonian *hamiltonian, + uintptr_t shots, + double *expectation, + double *variance) { + struct CStatevector *sv = statevector_from_circuit(circuit); + double value = 0.0; + statevector_expectation(sv, hamiltonian, &value); + statevector_free(sv); + *expectation = value; + *variance = 0.0; /* unknown variance: NaN is also valid */ + (void)shots; +} + +int main(void) { + struct CCircuit *qc = circuit_new(1); + circuit_x(qc, 0); + + /* 统一流水线:ZNE,折叠层 {0, 1, 2} */ + int32_t levels[3] = {0, 1, 2}; + struct CErrorMitigation *em = + error_mitigation_new(qc, MITIGATION_ZNE, levels, 3, 0); + + struct CPauliString *z = pauli_string_parse("Z"); + struct CHamiltonian *obs = hamiltonian_from_pauli(z); + + /* 采样阶段 → 后处理阶段 */ + error_mitigation_run(em, MITIGATION_ZNE, obs, 0, 0, estimate); + double value = 0.0, variance = 0.0; + error_mitigation_get_mitigated(em, PROCESS_ZNE_POLYNOMIAL, 0, + &value, &variance); + + error_mitigation_free(em); + hamiltonian_free(obs); + circuit_free(qc); + return 0; +} +``` + +低层入口(ZNE 折叠/外推的细粒度控制、虚拟蒸馏分子/分母分离执行)见 [零噪声外推](1_zne.md) 与 [虚拟蒸馏](2_virtual_distillation.md)。 diff --git a/docs/api/cn/c/6_error_mitigation/1_zne.md b/docs/api/cn/c/6_error_mitigation/1_zne.md new file mode 100644 index 00000000..04787e5b --- /dev/null +++ b/docs/api/cn/c/6_error_mitigation/1_zne.md @@ -0,0 +1,297 @@ +# 零噪声外推(C) + +零噪声外推(ZNE)把线路折叠到多个噪声因子(`2 × 折叠层 + 1`)下,逐个估计期望值,再外推回零噪声极限。本页覆盖 `CZneMitigation` 句柄的创建、折叠线路生成、配置读取、外推与缓解序列,以及折叠结果列表 `CCircuitList` 的遍历。回调契约与模块常量见 [模块概览](0_overview.md),错误码与句柄释放约定见 [Overview](../0_overview.md)。 + +--- + +## 句柄生命周期 + +### zne_mitigation_new(circuit, fold_levels, num_levels) + +基于基底线路与折叠层数组创建 ZNE 缓解句柄。噪声因子由折叠层推导:`2 × level + 1`。 + +- `circuit` (`const struct CCircuit *`):基底线路,后续折叠均作用于它的副本。 +- `fold_levels` (`const int32_t *`):折叠层数组,如 `{0, 1, 2}`。 +- `num_levels` (`uintptr_t`):折叠层数组的元素个数。 + +返回 `struct CZneMitigation *`:新建句柄,用 `zne_mitigation_free` 释放;`circuit` 为 NULL、`num_levels` 为 0 或 `fold_levels` 为 NULL 时返回 NULL。 + +### zne_mitigation_free(ptr) + +释放 ZNE 缓解句柄。 + +- `ptr` (`struct CZneMitigation *`):待释放句柄,允许传 NULL。 + +--- + +## 折叠与线路列表 + +### zne_mitigation_fold_circuit(ptr, gate_names) + +在每个配置的折叠层上折叠线路,返回折叠线路列表(每个折叠层一条线路)。 + +- `ptr` (`const struct CZneMitigation *`):ZNE 缓解句柄。 +- `gate_names` (`const char *`):可选的逗号分隔门名列表(如 `"H,CX"`),只折叠指定门;传 NULL 时全局折叠整条线路。 + +返回 `struct CCircuitList *`:折叠线路列表,用 `circuit_list_free` 释放;`ptr` 为 NULL、`gate_names` 含未知门名或非法 UTF-8、或折叠失败时返回 NULL。 + +### circuit_list_len(ptr) + +返回折叠线路列表中的线路数量。 + +- `ptr` (`const struct CCircuitList *`):折叠线路列表。 + +返回 `uintptr_t`:线路数量;`ptr` 为 NULL 时返回 0。 + +### circuit_list_get(ptr, index) + +读取列表中指定下标的线路,返回独立副本。 + +- `ptr` (`const struct CCircuitList *`):折叠线路列表。 +- `index` (`uintptr_t`):线路下标。 + +返回 `struct CCircuit *`:线路的 owned 副本,用 `circuit_free` 释放;`ptr` 为 NULL 或 `index` 越界时返回 NULL。 + +### circuit_list_free(ptr) + +释放折叠线路列表。`circuit_list_get` 取出的线路副本不随之释放,需逐个 `circuit_free`。 + +- `ptr` (`struct CCircuitList *`):待释放列表,允许传 NULL。 + +--- + +## 配置读取 + +### zne_circuit(ptr) + +返回原始(未折叠)线路的独立副本。 + +- `ptr` (`const struct CZneMitigation *`):ZNE 缓解句柄。 + +返回 `struct CCircuit *`:线路副本,用 `circuit_free` 释放;`ptr` 为 NULL 时返回 NULL。 + +### zne_mitigation_noise_factor(ptr, index) + +返回指定折叠层对应的噪声因子(`2 × level + 1`)。 + +- `ptr` (`const struct CZneMitigation *`):ZNE 缓解句柄。 +- `index` (`uintptr_t`):折叠层下标。 + +返回 `int32_t`:噪声因子;`ptr` 为 NULL 或 `index` 越界时返回 0。 + +### zne_fold_levels_len(ptr) / zne_fold_levels(ptr, buffer, len) + +两步式读取配置的折叠层数组:`zne_fold_levels_len` 返回元素个数,`zne_fold_levels` 把数组拷入调用方缓冲区。 + +- `ptr` (`const struct CZneMitigation *`):ZNE 缓解句柄。 +- `buffer` (`int32_t *`):接收折叠层的缓冲区,长度不小于 `zne_fold_levels_len(ptr)`。 +- `len` (`uintptr_t`):缓冲区长度,必须等于 `zne_fold_levels_len(ptr)`。 + +`zne_fold_levels_len` 返回 `uintptr_t`:元素个数,`ptr` 为 NULL 时返回 0。`zne_fold_levels` 返回 `int32_t`:0 成功;-1(`ptr` 或 `buffer` 为 NULL);-8(`len` 与元素个数不符)。 + +### zne_noise_factors_len(ptr) / zne_noise_factors(ptr, buffer, len) + +两步式读取噪声因子数组(每个折叠层 `2 × level + 1`):`zne_noise_factors_len` 返回元素个数,`zne_noise_factors` 把数组拷入调用方缓冲区。 + +- `ptr` (`const struct CZneMitigation *`):ZNE 缓解句柄。 +- `buffer` (`int32_t *`):接收噪声因子的缓冲区,长度不小于 `zne_noise_factors_len(ptr)`。 +- `len` (`uintptr_t`):缓冲区长度,必须等于 `zne_noise_factors_len(ptr)`。 + +`zne_noise_factors_len` 返回 `uintptr_t`:元素个数,`ptr` 为 NULL 时返回 0。`zne_noise_factors` 返回 `int32_t`:0 成功;-1(`ptr` 或 `buffer` 为 NULL);-8(`len` 与元素个数不符)。 + +--- + +## 两步式折叠输出 + +### zne_fold_circuits_len(ptr, gate_names) + +返回两步式折叠输出的线路数量(每个折叠层一条)。 + +- `ptr` (`const struct CZneMitigation *`):ZNE 缓解句柄。 +- `gate_names` (`const char *`):可选的逗号分隔门名列表,语义同 `zne_mitigation_fold_circuit`。 + +返回 `uintptr_t`:线路数量;`ptr` 为 NULL 或 `gate_names` 含未知门名/非法 UTF-8 时返回 0。 + +### zne_fold_circuits(ptr, gate_names, buffer, len) + +在每个配置的折叠层上折叠线路,把每条折叠线路作为 owned 句柄写入调用方缓冲区。 + +- `ptr` (`const struct CZneMitigation *`):ZNE 缓解句柄。 +- `gate_names` (`const char *`):可选的逗号分隔门名列表,传 NULL 时全局折叠。 +- `buffer` (`struct CCircuit **`):接收线路句柄的缓冲区,长度为 `zne_fold_circuits_len(ptr, gate_names)`;每个元素都是 owned 的 `CCircuit *`,须逐个 `circuit_free`。 +- `len` (`uintptr_t`):缓冲区长度,必须等于 `zne_fold_circuits_len(ptr, gate_names)`。 + +返回 `int32_t`:0 成功;-1(`ptr` 或 `buffer` 为 NULL);-4(`gate_names` 含未知门名或非法 UTF-8);-8(`len` 与折叠层数不符);-3(折叠失败)。 + +--- + +## 外推 + +`zne_extrapolate` 的 `method` 参数取以下标签: + +| 常量 | 值 | 方法 | +| --- | --- | --- | +| `ZNE_EXTRAPOLATE_POLYNOMIAL` | 0 | 多项式拟合。 | +| `ZNE_EXTRAPOLATE_EXPONENTIAL` | 1 | 指数衰减拟合。 | + +### zne_extrapolate(ptr, noisy_results, len, method, degree, out_value) + +从各噪声因子下的含噪期望值外推零噪声期望值。 + +- `ptr` (`const struct CZneMitigation *`):ZNE 缓解句柄。 +- `noisy_results` (`const double *`):每个噪声因子一个期望值,按折叠层顺序排列。 +- `len` (`uintptr_t`):期望值数量。 +- `method` (`uint8_t`):外推方法标签,见上表。多项式方法使用 `degree`;指数方法要求全部值为正。 +- `degree` (`uintptr_t`):多项式次数,必须小于数据点数。 +- `out_value` (`double *`):写出外推结果。 + +返回 `int32_t`:0 成功;-1(`ptr`、`noisy_results` 或 `out_value` 为 NULL);-8(`method` 非法、结果为空、`len` 与噪声因子数不符、多项式次数不合法或指数拟合遇到非正值);-7(其他拟合错误)。 + +### zne_poly_extrapolate(ptr, noisy_results, len, degree, out_value) + +用给定次数的多项式拟合外推零噪声期望值,等价于 `zne_extrapolate` 的多项式分支。 + +- `ptr` (`const struct CZneMitigation *`):ZNE 缓解句柄。 +- `noisy_results` (`const double *`):每个噪声因子一个期望值,按折叠层顺序排列。 +- `len` (`uintptr_t`):期望值数量。 +- `degree` (`uintptr_t`):多项式次数,必须小于噪声因子数。 +- `out_value` (`double *`):写出外推结果。 + +返回 `int32_t`:0 成功;-1(`ptr`、`noisy_results` 或 `out_value` 为 NULL);-8(结果为空、`len` 与噪声因子数不符或次数不合法);-7(其他拟合错误)。 + +### zne_exp_extrapolate(ptr, noisy_results, len, out_value) + +用指数衰减模型 `y(x) = A · exp(-x / τ)` 拟合外推零噪声期望值,写出 `x = 0` 处的拟合值 `A`。要求全部输入值为正。 + +- `ptr` (`const struct CZneMitigation *`):ZNE 缓解句柄。 +- `noisy_results` (`const double *`):每个噪声因子一个期望值,按折叠层顺序排列。 +- `len` (`uintptr_t`):期望值数量。 +- `out_value` (`double *`):写出拟合值 `A`。 + +返回 `int32_t`:0 成功;-1(`ptr`、`noisy_results` 或 `out_value` 为 NULL);-8(结果为空、`len` 与噪声因子数不符或存在非正值);-7(其他拟合错误)。 + +--- + +## 缓解序列 + +### zne_run_em_sequence_len(ptr) + +返回缓解序列产出的期望值数量(每个折叠层一个)。 + +- `ptr` (`const struct CZneMitigation *`):ZNE 缓解句柄。 + +返回 `uintptr_t`:期望值数量;`ptr` 为 NULL 时返回 0。 + +### zne_run_em_sequence_with_shots_len(ptr) + +返回带显式 shot 数的缓解序列产出的期望值数量(每个折叠层一个),与 `zne_run_em_sequence_len` 相同。 + +- `ptr` (`const struct CZneMitigation *`):ZNE 缓解句柄。 + +返回 `uintptr_t`:期望值数量;`ptr` 为 NULL 时返回 0。 + +### zne_run_em_sequence(ptr, gate_names, hamiltonian, estimator, buffer, len) + +执行缓解序列:在每个配置的折叠层上折叠线路,对每条折叠线路经 `estimator` 回调估计一个期望值。回调收到的 shot 数为 0(「未指定」)。 + +- `ptr` (`const struct CZneMitigation *`):ZNE 缓解句柄。 +- `gate_names` (`const char *`):可选的逗号分隔门名列表,传 NULL 时全局折叠。 +- `hamiltonian` (`const struct CHamiltonian *`):待估计的可观测量,构造方式见 [哈密顿量](../3_qis/7_hamiltonian.md)。 +- `estimator` (`CEstimatorFn`):估计器回调,契约见 [模块概览](0_overview.md)。 +- `buffer` (`double *`):接收期望值的缓冲区,长度为 `zne_run_em_sequence_len(ptr)`。 +- `len` (`uintptr_t`):缓冲区长度,必须等于 `zne_run_em_sequence_len(ptr)`。 + +返回 `int32_t`:0 成功;-1(`ptr`、`hamiltonian` 或 `buffer` 为 NULL);-4(`gate_names` 含未知门名或非法 UTF-8);-8(`len` 与噪声因子数不符);-3(折叠失败);-7(其他执行错误)。 + +### zne_run_em_sequence_with_shots(ptr, gate_names, hamiltonian, shots, estimator, buffer, len) + +与 `zne_run_em_sequence` 相同,但把 `shots` 转发给估计器回调作为建议采样数。 + +- `ptr` (`const struct CZneMitigation *`):ZNE 缓解句柄。 +- `gate_names` (`const char *`):可选的逗号分隔门名列表,传 NULL 时全局折叠。 +- `hamiltonian` (`const struct CHamiltonian *`):待估计的可观测量。 +- `shots` (`uintptr_t`):转发给估计器的 shot 数。 +- `estimator` (`CEstimatorFn`):估计器回调。 +- `buffer` (`double *`):接收期望值的缓冲区,长度为 `zne_run_em_sequence_with_shots_len(ptr)`。 +- `len` (`uintptr_t`):缓冲区长度,必须等于 `zne_run_em_sequence_with_shots_len(ptr)`。 + +返回 `int32_t`:错误码同 `zne_run_em_sequence`。 + +--- + +## 示例 + +折叠 → 逐线路估计 → 多项式外推的完整流程(估计器用态矢量精确模拟,见 [态矢量](../3_qis/1_statevector.md)): + +```c +#include +#include +#include "cqlib_c.h" + +/* 估计器回调:精确模拟折叠线路并计算 */ +static void estimate(const struct CCircuit *circuit, + const struct CHamiltonian *hamiltonian, + uintptr_t shots, + double *expectation, + double *variance) { + struct CStatevector *sv = statevector_from_circuit(circuit); + double value = 0.0; + statevector_expectation(sv, hamiltonian, &value); + statevector_free(sv); + *expectation = value; + *variance = 0.0; + (void)shots; +} + +int main(void) { + /* Bell 线路:H(0) + CX(0, 1) */ + struct CCircuit *qc = circuit_new(2); + circuit_h(qc, 0); + circuit_cx(qc, 0, 1); + + /* 折叠层 {0, 1, 2} → 噪声因子 {1, 3, 5} */ + int32_t levels[3] = {0, 1, 2}; + struct CZneMitigation *zne = zne_mitigation_new(qc, levels, 3); + + struct CPauliString *zz = pauli_string_parse("ZZ"); + struct CHamiltonian *obs = hamiltonian_from_pauli(zz); /* takes ownership of zz */ + + /* 折叠 + 逐线路估计 → 每个噪声因子一个期望值 */ + uintptr_t len = zne_run_em_sequence_len(zne); + double noisy_results[3]; + zne_run_em_sequence(zne, NULL, obs, estimate, noisy_results, len); + + /* 多项式外推回零噪声极限 */ + double value = 0.0; + zne_extrapolate(zne, noisy_results, len, ZNE_EXTRAPOLATE_POLYNOMIAL, 2, &value); + printf("ZNE estimate: %f\n", value); + + zne_mitigation_free(zne); + hamiltonian_free(obs); + circuit_free(qc); + return 0; +} +``` + +细粒度路径:先折叠成 `CCircuitList` 遍历,或用两步式接口直接取得 owned 线路数组: + +```c +/* 折叠成列表后逐个读取(每次 get 返回 owned 副本) */ +struct CCircuitList *folded = zne_mitigation_fold_circuit(zne, "H,CX"); +for (uintptr_t i = 0; i < circuit_list_len(folded); i++) { + struct CCircuit *c = circuit_list_get(folded, i); + int32_t factor = zne_mitigation_noise_factor(zne, i); /* noise factor of level i */ + /* 在此估计线路 c 上的期望值 */ + circuit_free(c); +} +circuit_list_free(folded); + +/* 两步式:缓冲区接收 owned 线路句柄,逐个释放 */ +uintptr_t m = zne_fold_circuits_len(zne, NULL); +struct CCircuit **buf = malloc(m * sizeof(struct CCircuit *)); +zne_fold_circuits(zne, NULL, buf, m); +for (uintptr_t i = 0; i < m; i++) { + circuit_free(buf[i]); +} +free(buf); +``` diff --git a/docs/api/cn/c/6_error_mitigation/2_virtual_distillation.md b/docs/api/cn/c/6_error_mitigation/2_virtual_distillation.md new file mode 100644 index 00000000..b3c6f069 --- /dev/null +++ b/docs/api/cn/c/6_error_mitigation/2_virtual_distillation.md @@ -0,0 +1,173 @@ +# 虚拟蒸馏(C) + +虚拟蒸馏通过多份密度矩阵拷贝与 copy-swap 线路估计比值 `Tr(O ρ^M) / Tr(ρ^M)`,其中 `M` 为副本数,`O` 为哈密顿量表示的可观测量。本页覆盖 `CVirtualDistillation` 句柄的创建与配置、copy-swap 线路构造、分子/分母线路的分离执行与完整协议。回调契约见 [模块概览](0_overview.md),错误码与句柄释放约定见 [Overview](../0_overview.md)。 + +--- + +## 句柄与配置 + +### virtual_distillation_new(circuit, copies) + +基于基底线路与副本数创建虚拟蒸馏句柄。后续所有派生线路都由基底线路构造。 + +- `circuit` (`const struct CCircuit *`):基底线路。 +- `copies` (`uintptr_t`):副本数,最少为 2。 + +返回 `struct CVirtualDistillation *`:新建句柄,用 `virtual_distillation_free` 释放;`circuit` 为 NULL 或 `copies` 小于 2 时返回 NULL。 + +### virtual_distillation_free(ptr) + +释放虚拟蒸馏句柄。 + +- `ptr` (`struct CVirtualDistillation *`):待释放句柄,允许传 NULL。 + +### virtual_distillation_copies(ptr) + +返回当前配置的副本数。 + +- `ptr` (`const struct CVirtualDistillation *`):虚拟蒸馏句柄。 + +返回 `uintptr_t`:副本数;`ptr` 为 NULL 时返回 0。 + +### virtual_distillation_set_copies(ptr, copies) + +更新副本数。更新失败时保持原值不变。 + +- `ptr` (`struct CVirtualDistillation *`):虚拟蒸馏句柄。 +- `copies` (`uintptr_t`):新副本数,最少为 2。 + +返回 `int32_t`:0 成功;-1(`ptr` 为 NULL);-8(`copies` 小于 2)。 + +--- + +## copy-swap 线路 + +### virtual_distillation_build_circuit(ptr) + +构造虚拟蒸馏协议使用的 copy-swap 线路:先对基底线路做门分解,再把基底线路并排准备 `copies` 份——第 `i` 份整体平移到从 `i × 基底宽度` 开始的比特区间,各份之间互不重叠——最后在首份与其他每一份之间逐比特插入 `SWAP`。 + +- `ptr` (`const struct CVirtualDistillation *`):虚拟蒸馏句柄。 + +线路宽度为 `copies × 基底宽度`(基底宽度取分解后线路的宽度);操作数为 `copies` 份基底操作加上 `(copies - 1) × 基底宽度` 个 `SWAP`。 + +返回 `struct CCircuit *`:copy-swap 线路的 owned 句柄,用 `circuit_free` 释放;`ptr` 为 NULL 或构造失败时返回 NULL。 + +--- + +## 分子与分母 + +估计器回调通过 `hamiltonian` 参数区分两种线路:分子线路带可观测量,分母线路无可观测量。 + +### virtual_distillation_run_numerator_circuit(ptr, hamiltonian, shots, estimator, out_mean, out_variance) + +通过估计器回调执行分子线路,估计 `Tr(O ρ^M)`。估计器收到 copy-swap 线路、扩展到 copy-swap 全宽的哈密顿量与 `shots`。扩展规则:原 Pauli 项保持比特索引、相位与系数不变,新增的高索引比特上补 `Z`。 + +- `ptr` (`const struct CVirtualDistillation *`):虚拟蒸馏句柄。 +- `hamiltonian` (`const struct CHamiltonian *`):可观测量,比特数须与基底线路宽度一致,构造方式见 [哈密顿量](../3_qis/7_hamiltonian.md)。 +- `shots` (`uintptr_t`):转发给估计器的 shot 数。 +- `estimator` (`CEstimatorFn`):估计器回调,契约见 [模块概览](0_overview.md)。 +- `out_mean` (`double *`):写出估计均值。 +- `out_variance` (`double *`):写出估计方差。 + +返回 `int32_t`:0 成功;-1(`ptr`、`hamiltonian`、`out_mean` 或 `out_variance` 为 NULL);-3(哈密顿量比特数与基底线路宽度不符,或 copy-swap 线路构造失败);-7(其他执行错误)。 + +### virtual_distillation_run_denominator_circuit(ptr, shots, estimator, out_mean, out_variance) + +通过估计器回调执行分母线路,估计 `Tr(ρ^M)`。估计器收到 copy-swap 线路、NULL 哈密顿量与 `shots`。 + +- `ptr` (`const struct CVirtualDistillation *`):虚拟蒸馏句柄。 +- `shots` (`uintptr_t`):转发给估计器的 shot 数。 +- `estimator` (`CEstimatorFn`):估计器回调。 +- `out_mean` (`double *`):写出估计均值。 +- `out_variance` (`double *`):写出估计方差。 + +返回 `int32_t`:0 成功;-1(`ptr`、`out_mean` 或 `out_variance` 为 NULL);-3(执行失败)。 + +### virtual_distillation_build_copy_swap_circuit(ptr) + +从基底线路构造 copy-swap 线路:先对基底线路做门分解,再把基底线路并排准备 `copies` 份——第 `i` 份整体平移到从 `i × 基底宽度` 开始的比特区间,各份之间互不重叠——最后在首份与其他每一份之间逐比特插入 `SWAP`。线路宽度为 `copies × 基底宽度`(基底宽度取分解后线路的宽度);操作数为 `copies` 份基底操作加上 `(copies - 1) × 基底宽度` 个 `SWAP`。 + +- `ptr` (`const struct CVirtualDistillation *`):虚拟蒸馏句柄。 + +返回 `struct CCircuit *`:copy-swap 线路的 owned 句柄,用 `circuit_free` 释放;`ptr` 为 NULL 或构造失败时返回 NULL。 + +--- + +## 完整协议 + +### virtual_distillation_run_vd(ptr, hamiltonian, shots_numerator, shots_denominator, estimator, out_mean, out_variance) + +执行完整虚拟蒸馏协议:先校验哈密顿量的比特数等于基底线路宽度,再依次执行分子线路与分母线路,最后组合为缓解结果。分子与分母使用各自的 shot 数。 + +- `ptr` (`const struct CVirtualDistillation *`):虚拟蒸馏句柄。 +- `hamiltonian` (`const struct CHamiltonian *`):可观测量,比特数须与基底线路宽度一致。 +- `shots_numerator` (`uintptr_t`):分子线路的 shot 数。 +- `shots_denominator` (`uintptr_t`):分母线路的 shot 数。 +- `estimator` (`CEstimatorFn`):估计器回调。 +- `out_mean` (`double *`):写出缓解后的期望值 `mu_num / mu_den`。 +- `out_variance` (`double *`):写出缓解后的方差。 + +返回 `int32_t`:0 成功;-1(任一指针为 NULL);-8(哈密顿量比特数与基底线路宽度不符);-7(分母均值为零或其他执行错误)。 + +方差按一阶泰勒近似、假设分子与分母相互独立组合:`var_num / mu_den^2 + mu_num^2 * var_den / mu_den^4`。采样随机性由估计器决定(核心层不接收随机种子)。 + +--- + +## 示例 + +```c +#include +#include "cqlib_c.h" + +/* 分子线路带可观测量,分母线路不带 */ +static void estimate(const struct CCircuit *circuit, + const struct CHamiltonian *hamiltonian, + uintptr_t shots, + double *expectation, + double *variance) { + if (hamiltonian != NULL) { + *expectation = 1.5; + *variance = 0.25; + } else { + *expectation = 2.0; + *variance = 1.0; + } + (void)circuit; + (void)shots; +} + +int main(void) { + /* 两份单比特副本 */ + struct CCircuit *qc = circuit_new(1); + circuit_x(qc, 0); + struct CVirtualDistillation *vd = virtual_distillation_new(qc, 2); + + /* copy-swap 线路:宽度 = 2 × 1 = 2 */ + struct CCircuit *copy_swap = virtual_distillation_build_circuit(vd); + + struct CPauliString *z = pauli_string_parse("Z"); + struct CHamiltonian *obs = hamiltonian_from_pauli(z); /* takes ownership of z */ + + /* mu_vd = 1.5 / 2.0 = 0.75 */ + double mean = 0.0, variance = 0.0; + virtual_distillation_run_vd(vd, obs, 3, 2, estimate, &mean, &variance); + printf("VD estimate: %f\n", mean); + + circuit_free(copy_swap); + virtual_distillation_free(vd); + hamiltonian_free(obs); + circuit_free(qc); + return 0; +} +``` + +分子与分母也可以分离执行,各自取得均值与方差: + +```c +double num_mean = 0.0, num_var = 0.0; +virtual_distillation_run_numerator_circuit(vd, obs, 512, estimate, + &num_mean, &num_var); +double den_mean = 0.0, den_var = 0.0; +virtual_distillation_run_denominator_circuit(vd, 512, estimate, + &den_mean, &den_var); +``` diff --git a/docs/api/cn/c/6_error_mitigation/3_unified.md b/docs/api/cn/c/6_error_mitigation/3_unified.md new file mode 100644 index 00000000..858ad020 --- /dev/null +++ b/docs/api/cn/c/6_error_mitigation/3_unified.md @@ -0,0 +1,128 @@ +# 统一流水线(C) + +统一流水线 `CErrorMitigation` 把零噪声外推与虚拟蒸馏封装为「采样 → 后处理」两个阶段:`error_mitigation_run` 折叠/派生线路并经估计器回调采样,`error_mitigation_get_mitigated` 在采样完成后产出缓解结果。回调契约与常量见 [模块概览](0_overview.md),错误码与句柄释放约定见 [Overview](../0_overview.md)。 + +`method` 参数的取值: + +| 常量 | 值 | 方法 | 附加参数 | +| --- | --- | --- | --- | +| `MITIGATION_ZNE` | 0 | 零噪声外推。 | `fold_levels` 折叠层数组。 | +| `MITIGATION_VIRTUAL_DISTILLATION` | 1 | 虚拟蒸馏。 | `copies` 副本数。 | + +`process_method` 参数的取值: + +| 常量 | 值 | 流程 | +| --- | --- | --- | +| `PROCESS_ZNE_POLYNOMIAL` | 0 | ZNE + 多项式外推。 | +| `PROCESS_ZNE_EXPONENTIAL` | 1 | ZNE + 指数外推。 | +| `PROCESS_VIRTUAL_DISTILLATION` | 2 | 虚拟蒸馏。 | + +--- + +## error_mitigation_new(circuit, method, fold_levels, num_levels, copies) + +基于基底线路与缓解方法创建统一流水线句柄。 + +- `circuit` (`const struct CCircuit *`):基底线路。 +- `method` (`uint8_t`):缓解方法标签,见上表。 +- `fold_levels` (`const int32_t *`):折叠层数组,`method == MITIGATION_ZNE` 时使用(如 `{0, 1, 2}`)。 +- `num_levels` (`uintptr_t`):折叠层数组的元素个数,`method == MITIGATION_ZNE` 时必须大于 0。 +- `copies` (`uintptr_t`):副本数,`method == MITIGATION_VIRTUAL_DISTILLATION` 时使用,最少为 2。 + +返回 `struct CErrorMitigation *`:新建句柄,用 `error_mitigation_free` 释放;`circuit` 为 NULL、`method` 不是合法标签、参数与方法不匹配(ZNE 缺折叠层、VD 副本数小于 2)时返回 NULL。 + +--- + +## error_mitigation_free(ptr) + +释放统一流水线句柄。 + +- `ptr` (`struct CErrorMitigation *`):待释放句柄,允许传 NULL。 + +--- + +## error_mitigation_run(ptr, method, hamiltonian, shots_a, shots_b, estimator) + +执行采样阶段:按配置的方法折叠/派生线路,对每条待执行线路调用一次估计器回调。`method` 必须与 `error_mitigation_new` 配置的方法一致。 + +- `ptr` (`struct CErrorMitigation *`):统一流水线句柄。 +- `method` (`uint8_t`):缓解方法标签,必须与创建时一致。 +- `hamiltonian` (`const struct CHamiltonian *`):待估计的可观测量,构造方式见 [哈密顿量](../3_qis/7_hamiltonian.md)。 +- `shots_a` (`uintptr_t`):ZNE 方法下的 shot 数(0 = 未指定);虚拟蒸馏方法下为分子线路的 shot 数。 +- `shots_b` (`uintptr_t`):ZNE 方法下被忽略;虚拟蒸馏方法下为分母线路的 shot 数。 +- `estimator` (`CEstimatorFn`):估计器回调,契约见 [模块概览](0_overview.md)。 + +返回 `int32_t`:0 成功;-1(`ptr` 或 `hamiltonian` 为 NULL);-8(`method` 不是合法标签);-7(`method` 与配置的方法不一致或其他执行错误)。 + +--- + +## error_mitigation_get_mitigated(ptr, process_method, degree, out_expectation, out_variance) + +执行后处理阶段:在 `error_mitigation_run` 之后产出缓解结果。 + +- `ptr` (`struct CErrorMitigation *`):统一流水线句柄。 +- `process_method` (`uint8_t`):后处理方法标签,见上表。 +- `degree` (`uintptr_t`):ZNE 多项式外推的次数,0 表示自动选择。 +- `out_expectation` (`double *`):写出缓解后的期望值。 +- `out_variance` (`double *`):写出缓解后的方差,不可用时写 NaN。 + +返回 `int32_t`:0 成功;-1(`ptr`、`out_expectation` 或 `out_variance` 为 NULL);-8(`process_method` 不是合法标签);-7(`process_method` 与配置的方法不一致或其他后处理错误)。 + +--- + +## 示例 + +虚拟蒸馏方法的完整流程(分子线路带可观测量、分母线路无可观测量的回调分支,见 [模块概览](0_overview.md)): + +```c +#include +#include "cqlib_c.h" + +/* 估计器回调:分子线路带可观测量(精确模拟),分母线路无可观测量 */ +static void estimate(const struct CCircuit *circuit, + const struct CHamiltonian *hamiltonian, + uintptr_t shots, + double *expectation, + double *variance) { + if (hamiltonian != NULL) { + struct CStatevector *sv = statevector_from_circuit(circuit); + double value = 0.0; + statevector_expectation(sv, hamiltonian, &value); + statevector_free(sv); + *expectation = value; + *variance = 0.0; + } else { + /* 分母 Tr(rho^M):由采样型估计器从测量结果估计,此处返回示例值 */ + *expectation = 1.0; + *variance = 0.0; + } + (void)shots; +} + +int main(void) { + struct CCircuit *qc = circuit_new(1); + circuit_x(qc, 0); + + /* 统一流水线:虚拟蒸馏,3 份拷贝 */ + struct CErrorMitigation *em = + error_mitigation_new(qc, MITIGATION_VIRTUAL_DISTILLATION, NULL, 0, 3); + + struct CPauliString *z = pauli_string_parse("Z"); + struct CHamiltonian *obs = hamiltonian_from_pauli(z); /* takes ownership of z */ + + /* 采样阶段:分子 shots=512、分母 shots=256 */ + error_mitigation_run(em, MITIGATION_VIRTUAL_DISTILLATION, obs, 512, 256, estimate); + + /* 后处理阶段:直接得到缓解后的期望值与方差 */ + double value = 0.0, variance = 0.0; + error_mitigation_get_mitigated(em, PROCESS_VIRTUAL_DISTILLATION, 0, + &value, &variance); + + error_mitigation_free(em); + hamiltonian_free(obs); + circuit_free(qc); + return 0; +} +``` + +ZNE 方法的 `new → run → get_mitigated` 流程示例见 [模块概览](0_overview.md);对折叠与外推的细粒度控制见 [零噪声外推](1_zne.md),虚拟蒸馏分子/分母的分离执行见 [虚拟蒸馏](2_virtual_distillation.md)。 diff --git a/docs/api/python/0_circuit/0_overview.md b/docs/api/cn/python/0_circuit/0_overview.md similarity index 96% rename from docs/api/python/0_circuit/0_overview.md rename to docs/api/cn/python/0_circuit/0_overview.md index 34c55345..b7caf528 100644 --- a/docs/api/python/0_circuit/0_overview.md +++ b/docs/api/cn/python/0_circuit/0_overview.md @@ -4,6 +4,8 @@ `cqlib.circuit` 是 Cqlib 中用于构建、表示和分析量子线路的核心模块。它提供了从基础量子门、参数化线路、复合门、测量与经典数据,到结构化控制流和矩阵转换的一套统一接口,是使用 Cqlib 进行量子程序开发的主要入口。 +## Overview + `cqlib.circuit` 主要覆盖以下能力: - **基础线路构造**:创建逻辑量子比特集合,并按顺序追加量子门、测量、重置、屏障等操作。 @@ -16,6 +18,25 @@ --- +## 常用入口 + +```python +from cqlib import Circuit, Parameter + +theta = Parameter("theta") + +circuit = Circuit(2) +circuit.h(0) +circuit.cx(0, 1) +circuit.rz(1, theta) + +print(circuit.used_symbols) # ['theta'] +bound = circuit.assign_parameters({"theta": 0.5}) +print(bound.used_symbols) # [] +``` + +--- + ## 核心概念与术语 本文档给出 `cqlib.circuit` 文档中常见术语的简要说明。更详细的接口签名、参数约束和示例请参考后续 API 文档。 @@ -77,7 +98,7 @@ | --- | --- | --- | | `barrier` | `Circuit.barrier(qubits)` 或 `Directive.barrier()` | 插入编译边界,限制相关量子比特上的操作跨越该边界重排。 | | `measure` | `Circuit.measure(qubit)`、`Circuit.measure_bits(qubits)` | 执行计算基测量,产生 `Measurement` 回执和经典结果。 | -| `reset` | `Circuit.reset(qubit)` 或 `Directive.reset()` | 将量子比特复位到 `|0>`,通常会破坏原有相干性。 | +| `reset` | `Circuit.reset(qubit)` 或 `Directive.reset()` | 将量子比特复位到 `\|0>`,通常会破坏原有相干性。 | | `delay` | `Circuit.delay(qubit, duration)` | 在指定量子比特上插入空闲时间或调度延迟。 | | `store` | `Circuit.store(target, value)` | 将经典表达式写入经典变量。 | @@ -215,7 +236,7 @@ circuit.cx(0, 1) matrix = circuit.to_matrix() ``` -该示例创建两比特 Bell 态线路,并计算其小规模矩阵表示。 +该示例创建双比特 Bell 态线路,并计算其小规模矩阵表示。 ### 2. 参数化旋转层 diff --git a/docs/api/python/0_circuit/10_symbolic_matrix.md b/docs/api/cn/python/0_circuit/10_symbolic_matrix.md similarity index 96% rename from docs/api/python/0_circuit/10_symbolic_matrix.md rename to docs/api/cn/python/0_circuit/10_symbolic_matrix.md index c1146139..d0080329 100644 --- a/docs/api/python/0_circuit/10_symbolic_matrix.md +++ b/docs/api/cn/python/0_circuit/10_symbolic_matrix.md @@ -210,3 +210,11 @@ print(matrix.symbols) ### 5. 复杂度与适用边界 对于 `n` 个量子比特的线路,完整矩阵大小为 `2^n × 2^n`,矩阵元素数量为 `4^n`。当矩阵元素中还包含符号表达式时,表达式复杂度也可能随门数量和参数数量增长。 + +--- + +## 其他行为 + +- `SymbolicComplex` 与 `SymbolicMatrix` 都支持 `copy`、`deepcopy`。 +- `SymbolicComplex` 支持 `==` 比较;`SymbolicMatrix` 不提供相等比较,如需比较两个矩阵,可以比较 `rows()` 的结果。 +- 两者都带有便于调试的 `repr`。 diff --git a/docs/api/python/0_circuit/11_ansatz.md b/docs/api/cn/python/0_circuit/11_ansatz.md similarity index 94% rename from docs/api/python/0_circuit/11_ansatz.md rename to docs/api/cn/python/0_circuit/11_ansatz.md index b9037d0a..9e9a3ca6 100644 --- a/docs/api/python/0_circuit/11_ansatz.md +++ b/docs/api/cn/python/0_circuit/11_ansatz.md @@ -16,10 +16,17 @@ from cqlib.circuit.ansatz import ( PauliFeatureMap, QAOAAnsatz, EvolutionStrategy, + EvolutionInfo, PauliEvolutionAnsatz, + real_amplitudes, + efficient_su2, + zz_feature_map, + pauli_feature_map, ) ``` +模块导出的全部名称如上,包括 `real_amplitudes`、`efficient_su2`、`zz_feature_map` 和 `pauli_feature_map` 四个便捷函数,以及 `EvolutionInfo`。 + `cqlib.circuit.ansatz` 提供了一组可复用的参数化线路模板,主要用于变分量子算法、量子机器学习、特征映射、QAOA 以及 Hamiltonian 演化等场景。用户可以通过这些模板快速生成结构化量子线路,而不必手动逐层添加旋转门和纠缠门。 --- @@ -47,6 +54,8 @@ assert len(circuit.symbols) == ansatz.num_parameters() assert circuit.num_qubits == ansatz.num_qubits() ``` +`validate()` 会在配置不合法时抛出 `ValueError`,例如量子比特数量、门类型、拓扑或参数设置相互不匹配;`build_circuit()` 在同样的情形下同样抛出 `ValueError`。各模板对象都支持 `copy`、`deepcopy`,并带有便于调试的 `repr` 与 `str`。 + --- ## 不可变 builder 模式 @@ -267,11 +276,13 @@ zz_feature_map(num_qubits, reps, entanglement) -> ZZFeatureMap ## `PauliFeatureMap` ```python -PauliFeatureMap(num_qubits) +ansatz = ( + PauliFeatureMap(num_qubits) .reps(n) .paulis(paulis) .entanglement(topology) .parameter_prefix(prefix) +) ``` `PauliFeatureMap` 支持通过任意 Pauli string 模板构造特征映射。用户可以指定单体项、多体项以及对应的纠缠拓扑,从而灵活表达不同阶数的特征交互。 @@ -307,10 +318,12 @@ Layer 模板用于构造常见的可训练参数化层。与 feature map 不同 ### 1. `BasicEntanglerLayers` ```python -BasicEntanglerLayers(num_qubits) +ansatz = ( + BasicEntanglerLayers(num_qubits) .reps(n) .rotation_gate(gate) .entanglement_gate(gate) +) ``` `BasicEntanglerLayers` 表示基础的“单量子比特旋转 + 环形纠缠”层结构。每一层通常先在各量子比特上施加参数化旋转门,再按固定环形结构施加纠缠门。 @@ -320,10 +333,12 @@ BasicEntanglerLayers(num_qubits) ### 2. `StronglyEntanglingLayers` ```python -StronglyEntanglingLayers(num_qubits) +ansatz = ( + StronglyEntanglingLayers(num_qubits) .reps(n) .entanglement_gate(gate) .ranges(ranges) +) ``` `StronglyEntanglingLayers` 表示表达能力更强的纠缠层模板。它通常使用通用单量子比特旋转门和范围化环形纠缠结构。`ranges` 用于指定每一层的连接跨度,使不同层可以采用不同的纠缠范围。 @@ -419,9 +434,11 @@ second = EvolutionStrategy.trotter(TrotterMode.second_order(), steps=8) ### 3. `PauliEvolutionAnsatz` ```python -PauliEvolutionAnsatz(hamiltonian) +ansatz = ( + PauliEvolutionAnsatz(hamiltonian) .with_strategy(strategy) .with_time_param_name(name) +) ``` `PauliEvolutionAnsatz` 用于构造 `exp(-i H t)` 的参数化演化线路。生成的线路通常只包含一个时间参数,默认参数名为 `{prefix}_t`,也可以通过 `with_time_param_name()` 自定义。 diff --git a/docs/api/python/0_circuit/12_circuit_to_matrix.md b/docs/api/cn/python/0_circuit/12_circuit_to_matrix.md similarity index 95% rename from docs/api/python/0_circuit/12_circuit_to_matrix.md rename to docs/api/cn/python/0_circuit/12_circuit_to_matrix.md index ef843881..e77fe639 100644 --- a/docs/api/python/0_circuit/12_circuit_to_matrix.md +++ b/docs/api/cn/python/0_circuit/12_circuit_to_matrix.md @@ -37,6 +37,8 @@ circuit_to_matrix( ) -> np.ndarray[np.complex128] ``` +该转换只适用于纯量子门线路。如果线路中还存在未绑定的符号参数,或者线路中包含测量、经典数据操作或控制流,`Circuit.to_matrix()` 与 `circuit_to_matrix()` 都会抛出 `CircuitError`。 + 下面的示例构造一条 Bell 线路,并分别通过对象方法和函数式入口计算矩阵: ```python diff --git a/docs/api/cn/python/0_circuit/13_circuit_dag.md b/docs/api/cn/python/0_circuit/13_circuit_dag.md new file mode 100644 index 00000000..9fe3cd5d --- /dev/null +++ b/docs/api/cn/python/0_circuit/13_circuit_dag.md @@ -0,0 +1,479 @@ +# 线路 DAG + +`cqlib.circuit` + +`CircuitDag` 是线路在操作依赖层面的算法视图:节点表示一条操作,边表示两条操作因共享同一份资源而存在的先后顺序。可共享的资源包括量子比特、可变经典变量、不可变经典值,以及用于承载没有具体数据资源之操作的全局序。该视图不改变线路本身的存储结构,主要用于依赖分析、分层调度和子图替换。 + +本页覆盖 `CircuitDag`、`DagWire`、`DagControlFlow` 与 `DagSwitchCase`。 + +--- + +## 导入 + +```python +from cqlib.circuit import CircuitDag, DagControlFlow, DagSwitchCase, DagWire +``` + +--- + +## CircuitDag + +`CircuitDag` 是线路的操作级依赖图。图中的每一条操作对应一个操作节点,此外每条线还带有输入哨兵节点和输出哨兵节点。 + +### 构造线路 DAG + +```python +CircuitDag.from_circuit(circuit: Circuit) -> CircuitDag +circuit.dag() -> CircuitDag +``` + +`CircuitDag.from_circuit()` 是静态方法,用于从一条已有线路构造依赖图。`Circuit.dag()` 是等价入口,直接在当前线路上构造依赖图。 + +构造过程结束后会自动校验 DAG 不变量。如果线路本身不满足约束,或者操作引用了线路之外的资源,构造会抛出 `CircuitError`。 + +示例: + +```python +from cqlib import Circuit, CircuitDag + +circuit = Circuit(2) +circuit.h(0) +circuit.x(1) +circuit.cx(0, 1) + +dag = circuit.dag() +same = CircuitDag.from_circuit(circuit) +``` + +### 导出回线路 + +```python +dag.to_circuit() -> Circuit +``` + +`to_circuit()` 按 DAG 的确定性拓扑序重建线路。返回的线路会带上 DAG 中记录的量子比特、参数表、经典变量、经典值和全局相位。如果 DAG 中含环、引用了非法参数,或重建出的操作序列无法通过线路构造校验,则抛出 `CircuitError`。 + +```python +recovered = dag.to_circuit() +print([operation.name for operation in recovered.operations]) +``` + +### 校验 + +```python +dag.validate() -> None +``` + +`validate()` 检查图与元数据的一致性,包括:图是否含环、每条线的输入输出哨兵是否合法、操作节点序号是否唯一、操作引用的量子比特与参数索引是否有效、经典变量与经典值是否属于当前线路、控制流载荷是否与所属操作匹配。 + +从外部 IR 导入或手动构造 DAG 之后,建议显式调用一次: + +```python +dag.validate() +``` + +### 属性 + +| 属性 | 类型 | 说明 | +| --- | --- | --- | +| `num_qubits` | `int` | 量子比特数量。 | +| `num_ops` | `int` | 操作节点数量。 | +| `is_empty` | `bool` | 是否没有操作节点。 | +| `qubits` | `list[Qubit]` | 按插入顺序返回量子比特。 | +| `parameters` | `list[Parameter]` | 参数表中的参数表达式。 | +| `symbols` | `list[str]` | 线路中出现的自由符号名。 | +| `classical_vars` | `list[ClassicalType]` | 已分配的可变经典变量类型。 | +| `classical_values` | `list[ClassicalType]` | 不可变经典值类型,通常由测量产生。 | + +```python +assert dag.num_qubits == 2 +assert dag.num_ops == 3 +assert len(dag) == 3 +assert dag.is_empty is False +assert dag.qubits == circuit.qubits +``` + +### 其他行为 + +- `len(dag)` 返回操作节点数量,与 `num_ops` 一致。 +- `repr` 形如 `CircuitDag(num_qubits=2, num_ops=3)`。 +- 支持 `copy`、`deepcopy`,复制后的 DAG 与原 DAG 拓扑序一致。 + +--- + +## 节点标识 + +Python 侧用 `int` 表示一个节点。节点标识由以下入口取得: + +| 取得方式 | 说明 | +| --- | --- | +| `op_nodes()` | 按线路源顺序返回全部操作节点。 | +| `topological_op_nodes()` | 按确定性拓扑序返回全部操作节点。 | +| `front_layer()`、`layers()`、`node_layers()` | 分层结果中的节点。 | +| `wire_in(wire)` / `wire_out(wire)` | 某条线的输入与输出哨兵节点。 | +| `apply_operation_front()` / `apply_operation_back()` | 追加操作后返回新节点标识。 | + +节点不一定是操作节点。可以用 `is_operation()` 与 `node_kind()` 判断节点种类: + +```python +node = dag.topological_op_nodes()[0] + +assert dag.is_operation(node) +assert dag.node_kind(node) == "operation" +assert dag.node_kind(dag.wire_in(DagWire.qubit(0))) == "wire_in" +``` + +`node_kind()` 的取值为 `"wire_in"`、`"wire_out"` 或 `"operation"`;节点不存在时返回 `None`。 + +`operation(node)` 返回该节点对应的 `ValueOperation`,其中包含指令、作用量子比特、参数和标签;节点不是操作节点时返回 `None`。节点标识不属于当前 DAG 时,遍历类方法会抛出 `CircuitError`。 + +```python +from cqlib import Circuit, Parameter + +theta = Parameter("theta") + +circuit = Circuit(1) +circuit.rx(0, theta) + +dag = circuit.dag() +operation = dag.operation(dag.topological_op_nodes()[0]) + +assert operation.name == "RX" +assert operation.params[0] == theta +assert dag.parameters == [theta] +assert dag.symbols == ["theta"] +``` + +--- + +## DagWire + +`DagWire` 表示 DAG 边所承载的资源,共有四种形态: + +| 构造 | 说明 | +| --- | --- | +| `DagWire.qubit(qubit)` | 量子比特线,参数为逻辑量子比特编号 `int` 或 `Qubit` 对象。 | +| `DagWire.classical_var(var)` | 可变经典变量线。 | +| `DagWire.classical_value(value)` | 不可变经典值线,通常为测量结果。 | +| `DagWire.global_order()` | 全局序,用于承载没有具体数据资源的操作之间的稳定顺序。 | + +以上均为静态方法。 + +### 属性 + +| 属性 | 类型 | 说明 | +| --- | --- | --- | +| `kind` | `str` | 线的种类,取值为 `"qubit"`、`"classical_var"`、`"classical_value"` 或 `"global_order"`。 | +| `qubit_value` | `Qubit \| None` | 量子比特线携带的量子比特,其余形态为 `None`。 | +| `classical_var_value` | `ClassicalVar \| None` | 经典变量线携带的变量,其余形态为 `None`。 | +| `classical_value_value` | `ClassicalValue \| None` | 经典值线携带的值,其余形态为 `None`。 | + +### 其他行为 + +- 支持 `==` 与 `hash`,可作为字典键或集合元素。 +- 支持 `copy`、`deepcopy`。 +- `repr` 形如 `DagWire.qubit(0)`。 + +```python +from cqlib import Circuit +from cqlib.circuit import ClassicalType, DagWire + +circuit = Circuit(1) +measured = circuit.measure(0) +flag = circuit.var(ClassicalType.bool()) +circuit.store(flag, measured.expr().bit_to_bool()) + +dag = circuit.dag() + +qubit_wire = DagWire.qubit(0) +value_wire = DagWire.classical_value(measured.value) +var_wire = DagWire.classical_var(flag) + +assert qubit_wire.kind == "qubit" +assert qubit_wire.qubit_value == circuit.qubits[0] +assert value_wire.classical_value_value == measured.value +assert var_wire.classical_var_value == flag +assert DagWire.global_order().kind == "global_order" +``` + +--- + +## 线查询 + +| 方法 | 说明 | +| --- | --- | +| `wires()` | 返回当前 DAG 中已物化的全部线。 | +| `has_wire(wire)` | 判断某条线是否已在当前 DAG 中物化。 | +| `is_wire_idle(wire)` | 判断某条线上是否没有任何操作。 | +| `wire_in(wire)` | 返回该线的输入哨兵节点标识,不存在时为 `None`。 | +| `wire_out(wire)` | 返回该线的输出哨兵节点标识,不存在时为 `None`。 | +| `nodes_on_wire(wire)` | 按线上顺序返回该线上的操作节点。 | + +线不是合法资源、或线上时间线断裂或分叉时,`is_wire_idle()` 与 `nodes_on_wire()` 抛出 `CircuitError`。 + +```python +nodes = dag.topological_op_nodes() +measure_node, store_node = nodes + +assert dag.has_wire(value_wire) +assert dag.nodes_on_wire(value_wire) == [measure_node, store_node] +assert dag.wire_in(qubit_wire) in dag.predecessors(measure_node) +``` + +--- + +## 图遍历 + +前驱与后继的查询分三个层次:不限资源的全部邻居、限定在某一条线上的邻居、以及按量子或经典资源分类的邻居。 + +| 方法 | 说明 | +| --- | --- | +| `op_nodes()` | 按源顺序返回全部操作节点。 | +| `topological_op_nodes()` | 按拓扑序返回全部操作节点。 | +| `predecessors(node)` | 返回全部前驱节点,含哨兵节点。 | +| `successors(node)` | 返回全部后继节点,含哨兵节点。 | +| `predecessors_on_wire(node, wire)` | 返回仅通过指定线相连的操作前驱。 | +| `successors_on_wire(node, wire)` | 返回仅通过指定线相连的操作后继。 | +| `quantum_predecessors(node)` | 返回通过量子比特线相连的操作前驱。 | +| `quantum_successors(node)` | 返回通过量子比特线相连的操作后继。 | +| `classical_predecessors(node)` | 返回通过经典变量线或经典值线相连的操作前驱。 | +| `classical_successors(node)` | 返回通过经典变量线或经典值线相连的操作后继。 | + +`predecessors()` 与 `successors()` 返回的邻居可能包含线的哨兵节点;其余方法只返回操作节点。带 `wire` 参数的方法在该线不属于当前 DAG 时抛出 `CircuitError`;节点标识不存在时同样抛出 `CircuitError`。 + +```python +assert dag.quantum_successors(measure_node) == [] +assert dag.classical_predecessors(store_node) == [measure_node] +assert dag.predecessors_on_wire(store_node, value_wire) == [measure_node] +``` + +--- + +## 分层与深度 + +| 方法 | 说明 | +| --- | --- | +| `front_layer()` | 返回没有操作前驱的操作节点,即最早可执行的一层。 | +| `layers()` | 按尽早调度方式返回分层结果,每层是一个操作节点列表。 | +| `node_layers()` | 返回每个操作节点到所在层号的映射。 | +| `depth()` | 返回依赖深度,即层数。 | + +`layers()` 与 `node_layers()` 使用同一套层号,`depth()` 等于最大层号加一。图中含环,或操作前驱的层号与拓扑序不一致时,抛出 `CircuitError`。 + +```python +circuit = Circuit(2) +circuit.h(0) +circuit.x(1) +circuit.cx(0, 1) +dag = circuit.dag() + +assert len(dag.front_layer()) == 2 +assert [len(layer) for layer in dag.layers()] == [2, 1] +assert dag.depth() == 2 +``` + +`node_layers()` 返回的字典按拓扑序排列,键为节点标识,值为层号。 + +```python +assert list(dag.node_layers().keys()) == dag.topological_op_nodes() +``` + +--- + +## 控制流 + +当线路中包含结构化控制流时,控制流操作节点会额外携带一份递归的 DAG 载荷。控制流本身的构造接口与低层 IR 见 [Classical Data / Control Flow](9_classical_control_flow.md)。 + +| 方法 | 说明 | +| --- | --- | +| `control_flow(node)` | 返回该节点携带的 `DagControlFlow`,没有时为 `None`。 | +| `has_control_flow()` | 判断顶层操作中是否存在结构化控制流。 | +| `has_nested_control_flow()` | 判断控制流体内部是否还包含结构化控制流。 | +| `has_measurement()` | 判断顶层操作中是否直接或递归包含测量。 | + +### DagControlFlow + +`kind` 属性给出控制流形态,取值为 `"if"`、`"while"`、`"for"`、`"switch"`、`"break"` 或 `"continue"`。各形态暴露的属性如下,不适用于该形态的属性返回 `None` 或空列表。 + +| 属性 | 适用形态 | 类型 | 说明 | +| --- | --- | --- | --- | +| `kind` | 全部 | `str` | 控制流形态。 | +| `then_body` | `if` | `CircuitDag \| None` | 条件成立时执行的分支体。 | +| `else_body` | `if` | `CircuitDag \| None` | `else` 分支体;没有该分支时为 `None`。 | +| `body` | `while`、`for` | `CircuitDag \| None` | 循环体。 | +| `cases` | `switch` | `list[DagSwitchCase]` | 各精确整数匹配分支。 | +| `default_body` | `switch` | `CircuitDag \| None` | `default` 分支体;没有该分支时为 `None`。 | + +```python +from cqlib import Circuit +from cqlib.circuit import ClassicalType + +circuit = Circuit(1) +flag = circuit.var(ClassicalType.bool()) +circuit.if_else( + flag.expr(), + lambda body: body.x(0), + lambda body: body.z(0), +) + +dag = circuit.dag() +control = dag.control_flow(dag.topological_op_nodes()[0]) + +assert control.kind == "if" +assert [op.name for op in control.then_body.to_circuit().operations] == ["X"] +assert [op.name for op in control.else_body.to_circuit().operations] == ["Z"] +``` + +### DagSwitchCase + +`DagSwitchCase` 表示 `switch` 中的一个精确整数匹配分支。 + +| 属性 | 类型 | 说明 | +| --- | --- | --- | +| `value` | `int` | 该分支匹配的整数值。 | +| `body` | `CircuitDag` | 命中该分支时执行的控制流体。 | + +```python +circuit = Circuit(1) +target = circuit.var(ClassicalType.uint(8)) + +circuit.switch( + target.expr(), + lambda case: ( + case.value(1, lambda body: body.x(0)), + case.value(2, lambda body: body.y(0)), + case.default(lambda body: body.z(0)), + ), +) + +dag = circuit.dag() +control = dag.control_flow(dag.topological_op_nodes()[0]) + +assert control.kind == "switch" +assert [case.value for case in control.cases] == [1, 2] +assert [op.name for op in control.cases[0].body.to_circuit().operations] == ["X"] +assert [op.name for op in control.default_body.to_circuit().operations] == ["Z"] +``` + +控制流体本身也是 `CircuitDag`,可以继续调用 `to_circuit()`、`layers()`、`control_flow()` 等接口,用于递归分析嵌套结构。 + +--- + +## 操作计数与连续段 + +| 方法 | 说明 | +| --- | --- | +| `operation_count_by_name()` | 按指令名统计顶层操作数量。 | +| `operation_count_by_name_recursive()` | 按指令名统计顶层与各控制流体内部的操作数量。 | +| `collect_1q_runs()` | 在每条量子比特线上收集连续的单量子比特门段。 | +| `collect_2q_runs()` | 在每条量子比特线上收集连续的双量子比特门段。 | + +计数结果返回字典,键为指令名,值为数量。连续段以一个列表的列表返回,其中每个元素是一段连续的门节点标识序列,顺序与线上顺序一致。非门操作、测量、复位、屏障和多量子比特操作会中断单量子比特门段;同一个双量子比特操作可能同时出现在其两个量子比特线的段中。 + +```python +from cqlib import Circuit +from cqlib.circuit import ClassicalExpr, ClassicalType + +circuit = Circuit(2) +circuit.h(0) +circuit.x(0) +circuit.cx(0, 1) +circuit.cz(0, 1) +counter = circuit.var(ClassicalType.uint(8)) +circuit.for_uint( + counter, + ClassicalExpr.uint_literal(8, 0), + ClassicalExpr.uint_literal(8, 2), + ClassicalExpr.uint_literal(8, 1), + lambda body, _: body.z(1), +) + +dag = circuit.dag() +top_counts = dag.operation_count_by_name() +recursive_counts = dag.operation_count_by_name_recursive() +oneq_runs = dag.collect_1q_runs() +twoq_runs = dag.collect_2q_runs() + +assert dag.has_control_flow() +assert dag.has_nested_control_flow() is False +assert top_counts["H"] == 1 +assert top_counts["for"] == 1 +assert "Z" not in top_counts +assert recursive_counts["Z"] == 1 +assert any(len(run) == 2 for run in oneq_runs) +assert any(len(run) == 2 for run in twoq_runs) +``` + +--- + +## 修改图 + +| 方法 | 说明 | +| --- | --- | +| `add_parameter(parameter)` | 将参数插入 DAG 参数表,返回 `(index, is_new)`。 | +| `apply_operation_front(operation)` | 在 DAG 前端追加一条操作,返回新节点标识。 | +| `apply_operation_back(operation)` | 在 DAG 后端追加一条操作,返回新节点标识。 | +| `remove_op_node(node)` | 删除一个操作节点,返回被删除的 `ValueOperation`。 | +| `substitute_node(node, operation)` | 用一条 `ValueOperation` 替换指定操作节点。 | +| `substitute_node_with_dag(node, replacement)` | 用另一个 `CircuitDag` 替换指定操作节点。 | + +追加操作时传入的是自包含的 `ValueOperation`。替换控制流节点时,替换内容必须只包含一条同类型的控制流操作;替换 DAG 读取或写入的资源必须落在被替换节点的资源范围内,否则抛出 `CircuitError`。 + +```python +from cqlib import Circuit +from cqlib.circuit import StandardGate, ValueOperation + +circuit = Circuit(2) +circuit.h(0) +circuit.cx(0, 1) +dag = circuit.dag() +q0, q1 = dag.qubits + +front = ValueOperation.from_standard_gate(StandardGate.X, [q1]) +back = ValueOperation.from_standard_gate(StandardGate.Z, [q0]) +front_node = dag.apply_operation_front(front) +back_node = dag.apply_operation_back(back) + +assert dag.operation(front_node).name == "X" +assert dag.operation(back_node).name == "Z" +assert [op.name for op in dag.to_circuit().operations] == ["X", "H", "CX", "Z"] + +nodes = dag.topological_op_nodes() +removed = dag.remove_op_node(nodes[1]) +assert removed.name == "H" +assert [op.name for op in dag.to_circuit().operations] == ["X", "CX", "Z"] + +nodes = dag.topological_op_nodes() +replacement = ValueOperation.from_standard_gate(StandardGate.Y, [q1]) +dag.substitute_node(nodes[0], replacement) +assert [op.name for op in dag.to_circuit().operations] == ["Y", "CX", "Z"] +``` + +用子图整体替换一个节点: + +```python +circuit = Circuit(1) +circuit.h(0) +dag = circuit.dag() + +replacement_circuit = Circuit(1) +replacement_circuit.x(0) +replacement_circuit.z(0) +replacement = replacement_circuit.dag() + +dag.substitute_node_with_dag(dag.topological_op_nodes()[0], replacement) + +assert [op.name for op in dag.to_circuit().operations] == ["X", "Z"] +``` + +--- + +## 典型用途 + +编译 pass 与路由算法通常在 DAG 上完成依赖分析:用 `front_layer()` 与 `node_layers()` 决定哪些操作已经就绪,用 `predecessors()` 与 `successors()` 判断两条操作之间是否存在顺序约束,用 `collect_1q_runs()` 之类的连续段收集定位可以整体处理的门序列,再用 `substitute_node()` 系列接口把变换结果写回图中。 + +--- + +## 异常情况 + +- `CircuitError`:节点标识不属于当前 DAG、线不属于当前 DAG、图含环、线的时间线断裂或分叉、控制流载荷与所属操作不匹配、替换 DAG 的资源范围或控制流形态非法、参数索引越界、经典变量或经典值属于其他线路等。 diff --git a/docs/api/python/0_circuit/1_circuit.md b/docs/api/cn/python/0_circuit/1_circuit.md similarity index 82% rename from docs/api/python/0_circuit/1_circuit.md rename to docs/api/cn/python/0_circuit/1_circuit.md index e51b8a32..c264e262 100644 --- a/docs/api/python/0_circuit/1_circuit.md +++ b/docs/api/cn/python/0_circuit/1_circuit.md @@ -68,7 +68,8 @@ circuit = Circuit.from_operations([Qubit(0)], [op]) | `width` | `int` | `num_qubits` 的别名。 | | `qubits` | `list[Qubit]` | 按线路内部顺序保存的量子比特列表。 | | `parameters` | `list[Parameter]` | 线路中记录的参数表达式列表。 | -| `symbols` | `list[str]` | 线路中所有自由符号名称。 | +| `symbols` | `list[str]` | 线路中所有自由符号名称。该列表是一个稳定的符号登记表,可能包含已不再被可执行 IR 引用的符号。 | +| `used_symbols` | `list[str]` | 当前可执行 IR 实际引用的自由符号名称。 | | `global_phase` | `Parameter` | 线路整体全局相位。 | | `classical_vars` | `list[ClassicalType]` | 已分配的可变经典变量类型列表。 | | `classical_values` | `list[ClassicalType]` | 测量等操作产生的不可变经典值类型列表。 | @@ -95,15 +96,21 @@ print(circuit.operations) | --- | --- | | `add_qubits(qubits)` | 向当前线路追加新的量子比特。 | | `append(operation)` | 向线路追加一个自包含的 `ValueOperation`。 | -| `operation(index)` | 返回指定位置的 `ValueOperation`。 | +| `operation(index)` | 返回指定位置的 `ValueOperation`;索引越界时抛 `IndexError`。 | +| `remove_operation(index)` | 移除指定位置的操作,并返回被移除的 `ValueOperation`;索引越界时抛 `IndexError`。 | +| `remove_operations(indices)` | 批量移除多个位置的操作,并返回被移除的 `ValueOperation` 列表;索引越界时抛 `IndexError`。若待移除的测量结果仍被 `store` 引用,则抛 `CircuitError`。 | | `__len__()` | 返回线路中的操作数量。 | | `__getitem__(index)` | 支持使用正向或负向索引读取操作。 | +| `uses_symbol(symbol)` | 判断当前可执行 IR 是否引用了指定符号,返回 `bool`。 | +| `var(ty)` | 分配一个由当前线路持有的可变经典变量,`ty` 为 `ClassicalType`。 | +| `store(target, value)` | 将经典表达式 `value` 写入经典变量 `target`。 | +| `dag()` | 返回当前线路对应的有向无环图表示 `CircuitDag`。 | | `validate()` | 校验线路结构、经典句柄、作用域和控制流不变量。 | | `depth(recurse=False)` | 计算线路深度。 | | `set_global_phase(phase)` | 设置线路全局相位。 | -`depth()` 用于估算线路深度。在线性量子线路中,深度通常表示按尽早调度(ASAP)方式排列后,最长量子比特路径上的操作层数。普通门和普通指令通常贡献一层;`barrier` 会约束其覆盖量子比特上的重排;`CircuitGate` 默认可视为一个不透明操作。当线路中包含 `if`、`while`、`for`、`switch`、`break` 或 `continue` 等控制流结构时,深度计算需要额外处理分支和循环语义。 +`depth()` 用于估算线路深度。在线性量子线路中,深度通常表示按尽早调度(ASAP)方式排列后,最长量子比特路径上的操作层数。普通门和普通指令通常贡献一层;`barrier` 会约束其覆盖量子比特上的重排;`CircuitGate` 默认可视为一个不透明操作。当线路中包含 `if`、`while`、`for`、`switch`、`break` 或 `continue` 等控制流结构时,深度计算需要额外处理分支和循环语义:默认的 `recurse=False` 不会展开这些结构,此时若线路中存在控制流,`depth()` 会抛出 `CircuitError`;传入 `recurse=True` 则递归展开分支与循环体,返回估算深度。 ```python @@ -147,15 +154,15 @@ depth = circuit.depth() | 方法 | 参数 | 说明 | | --- | --- | --- | -| `rx(qubit, theta)` | `float | Parameter` | 绕 X 轴旋转。 | -| `ry(qubit, theta)` | `float | Parameter` | 绕 Y 轴旋转。 | -| `rz(qubit, theta)` | `float | Parameter` | 绕 Z 轴旋转。 | -| `phase(qubit, lambda_)` | `float | Parameter` | 相位门 `P(lambda)`。 | -| `u(qubit, theta, phi, lambda_)` | `float | Parameter` | 通用单量子比特门。 | -| `xy(qubit, theta)` | `float | Parameter` | XY 交互族。 | -| `xy2p(qubit, theta)` | `float | Parameter` | 正半角 XY 门。 | -| `xy2m(qubit, theta)` | `float | Parameter` | 负半角 XY 门。 | -| `rxy(qubit, theta, phi)` | `float | Parameter` | XY 平面任意轴旋转。 | +| `rx(qubit, theta)` | `float \| Parameter` | 绕 X 轴旋转。 | +| `ry(qubit, theta)` | `float \| Parameter` | 绕 Y 轴旋转。 | +| `rz(qubit, theta)` | `float \| Parameter` | 绕 Z 轴旋转。 | +| `phase(qubit, lambda)` | `float \| Parameter` | 相位门 `P(lambda)`。 | +| `u(qubit, theta, phi, lambda)` | `float \| Parameter` | 通用单量子比特门。 | +| `xy(qubit, theta)` | `float \| Parameter` | XY 交互族。 | +| `xy2p(qubit, theta)` | `float \| Parameter` | 正半角 XY 门。 | +| `xy2m(qubit, theta)` | `float \| Parameter` | 负半角 XY 门。 | +| `rxy(qubit, theta, phi)` | `float \| Parameter` | XY 平面任意轴旋转。 | ### 3. 多量子比特门 @@ -283,6 +290,8 @@ larger.append_circuit_gate(bell_gate, [2, 3]) - `qubits=None`:按 `other` 的逻辑量子比特编号将其合并到当前线路中,必要时向当前线路加入新的量子比特; - `qubits=[...]`:将 `other.qubits` 按顺序映射到当前线路中的目标量子比特上。 +与 `inverse()`、`decompose()` 等返回新线路的方法不同,`compose()` 就地修改当前线路,并返回 `None`。 + 需要区分的是,`compose()` 会把另一条线路的操作直接追加到当前线路中,而`to_gate()` 会保留子线路的模块边界。 --- @@ -313,6 +322,8 @@ matrix_reversed = circuit.to_matrix([1, 0]) - `if_()`、`if_else()`、`while_()`、`for_uint()`、`switch()`; - 手动构造 `ClassicalControlOp` 后调用 `append_control()`。 +在 `while_()` 与 `for_uint()` 的回调体中,可以调用 `break_loop()` 提前跳出当前循环或 `switch` 分支,或调用 `continue_loop()` 跳过本轮剩余部分。这两者只允许出现在对应的控制流体内部,否则会抛 `CircuitError`。 + ```python from cqlib import Circuit from cqlib.circuit import ClassicalExpr diff --git a/docs/api/python/0_circuit/2_qubit.md b/docs/api/cn/python/0_circuit/2_qubit.md similarity index 88% rename from docs/api/python/0_circuit/2_qubit.md rename to docs/api/cn/python/0_circuit/2_qubit.md index 153eab0e..15c262f4 100644 --- a/docs/api/python/0_circuit/2_qubit.md +++ b/docs/api/cn/python/0_circuit/2_qubit.md @@ -17,11 +17,11 @@ from cqlib import Qubit Qubit(index: int) ``` -`Qubit(index)` 用于创建一个逻辑量子比特句柄。`index` 必须是非负整数,内部会以无符号整数形式保存。 +`Qubit(index)` 用于创建一个逻辑量子比特句柄。`index` 必须是非负整数,内部会以无符号整数形式保存。可接受的取值范围为 `0` 到 `4294967295`(即 `2**32 - 1`):传入负整数或大于该上界的整数会触发 `QubitError`,传入非整数(例如浮点数或字符串)则触发 `TypeError`。 | 参数 | 类型 | 说明 | | --- | --- | --- | -| `index` | `int` | 逻辑量子比特编号,需为非负整数。 | +| `index` | `int` | 逻辑量子比特编号,取值需在 `0` 到 `4294967295` 之间。 | 示例: @@ -111,7 +111,7 @@ circuit.cx(Qubit(10), Qubit(20)) ## 稀疏逻辑索引 -`Qubit` 的编号是逻辑标识,不要求从 `0` 开始连续排列。例如,`Qubit(10)` 和 `Qubit(20)` 可以共同构成一条两比特线路。 +`Qubit` 的编号是逻辑标识,不要求从 `0` 开始连续排列。例如,`Qubit(10)` 和 `Qubit(20)` 可以共同构成一条双比特线路。 ```python from cqlib import Circuit, Qubit @@ -122,7 +122,7 @@ assert circuit.num_qubits == 2 assert [q.index for q in circuit.qubits] == [10, 20] ``` -需要注意的是,逻辑编号不一定等同于矩阵或状态向量中的轴顺序。对于 `Circuit([Qubit(10), Qubit(20)])`,线路内部只包含两个量子比特,矩阵维度为 `4 × 4`。默认情况下,矩阵转换会按照线路中保存的 `qubits` 顺序解释量子比特。 +需要注意的是,逻辑编号不一定等同于矩阵或态矢量中的轴顺序。对于 `Circuit([Qubit(10), Qubit(20)])`,线路内部只包含两个量子比特,矩阵维度为 `4 × 4`。默认情况下,矩阵转换会按照线路中保存的 `qubits` 顺序解释量子比特。 如果需要明确控制矩阵比特顺序,可以在矩阵转换时传入 `qubits_order`: diff --git a/docs/api/python/0_circuit/3_parameter.md b/docs/api/cn/python/0_circuit/3_parameter.md similarity index 100% rename from docs/api/python/0_circuit/3_parameter.md rename to docs/api/cn/python/0_circuit/3_parameter.md diff --git a/docs/api/python/0_circuit/4_operation_instruction.md b/docs/api/cn/python/0_circuit/4_operation_instruction.md similarity index 79% rename from docs/api/python/0_circuit/4_operation_instruction.md rename to docs/api/cn/python/0_circuit/4_operation_instruction.md index eb59c6fe..868dd572 100644 --- a/docs/api/python/0_circuit/4_operation_instruction.md +++ b/docs/api/cn/python/0_circuit/4_operation_instruction.md @@ -38,8 +38,9 @@ Cqlib 将线路操作拆分为多个层次,以便同时支持普通量子门 | 静态方法 | 说明 | | --- | --- | -| `Instruction.from_standard_gate(gate)` | 从 `StandardGate` 创建标准门指令。 | -| `Instruction.from_mc_gate(gate)` | 从 `MCGate` 创建多控制门指令。 | +| `Instruction.from_name(name)` | 按标准门名称创建标准门指令,名称匹配不区分大小写;名称无法匹配任何标准门时抛 `ValueError`。 | +| `Instruction.from_standard_gate(gate)` | 从 `StandardGate` 创建标准门指令;`gate` 必须是不带参数的未绑定定义,否则抛 `CircuitError`。 | +| `Instruction.from_mc_gate(gate)` | 从 `MCGate` 创建多控制门指令;其基础门同样必须是不带参数的未绑定定义,否则抛 `CircuitError`。 | | `Instruction.from_unitary_gate(gate)` | 从 `UnitaryGate` 创建自定义酉门指令。 | | `Instruction.from_circuit_gate(gate)` | 从 `CircuitGate` 创建子线路复合门指令。 | | `Instruction.from_directive(directive)` | 从 `Directive` 创建非酉指令。 | @@ -59,7 +60,7 @@ delay_inst = Instruction.delay() | 属性 | 类型 | 说明 | | --- | --- | --- | -| `name` | `str` | 指令的可读名称,例如 `"h"`、`"cx"`、`"measure"`。 | +| `name` | `str` | 指令的可读名称。标准门指令使用规范化的大写名称,例如 `"H"`、`"CX"`;`Directive` 与 `delay` 指令使用小写名称,例如 `"barrier"`、`"measure"`、`"delay"`。 | | `instruction_type` | `str` | 指令类别,例如 `"standard"`、`"mcgate"`、`"unitary"`、`"circuit"`、`"directive"`、`"classical_data"`、`"classical_control"`、`"delay"`。 | | `is_standard` | `bool` | 是否为标准门指令。 | | `is_mcgate` | `bool` | 是否为多控制门指令。 | @@ -93,6 +94,11 @@ delay_inst = Instruction.delay() | `is_classical_control` | `bool` | 是否包裹结构化控制流。 | | `instruction` | `Instruction / None` | 普通指令内容。 | | `classical_control` | `ClassicalControlOp / None` | 控制流内容。 | +| `name` | `str` | 指令名称;包裹控制流时返回结构名,例如 `"if"`、`"while"`。 | +| `instruction_type` | `str` | 指令类别,取值与 `Instruction.instruction_type` 一致;包裹控制流时为 `"classical_control"`。 | +| `is_standard` / `is_mcgate` / `is_unitary` / `is_circuit_gate` / `is_directive` / `is_classical_data` / `is_delay` | `bool` | 与 `Instruction` 同名的类别判定属性;包裹控制流时均为 `False`。 | +| `standard_gate` | `StandardGate / None` | 当包裹的是标准门指令时,返回内部标准门对象。 | +| `directive` | `Directive / None` | 当包裹的是 `Directive` 指令时,返回内部 directive 对象。 | --- @@ -141,9 +147,14 @@ op = ValueOperation.from_standard_gate( | --- | --- | --- | | `instruction` | `ValueInstruction` | 操作对应的构造层指令。 | | `qubits` | `list[Qubit]` | 操作作用的量子比特列表。 | -| `params` | `list[float | Parameter]` | 本次操作携带的参数列表。 | -| `label` | `str | None` | 可选标签。 | -| `matrix()` | `np.ndarray` | 返回该操作对应的酉矩阵。 | +| `params` | `list[float \| Parameter]` | 本次操作携带的参数列表。 | +| `label` | `str \| None` | 可选标签。 | +| `name` | `str` | 操作名称;控制流操作返回结构名,例如 `"if"`。 | +| `num_qubits` | `int` | 本次操作作用的量子比特数量。 | +| `num_params` | `int` | 本次操作携带的参数数量。 | +| `instruction_type` | `str` | 指令类别,取值与 `Instruction.instruction_type` 一致。 | +| `is_standard` / `is_mcgate` / `is_unitary` / `is_circuit_gate` / `is_directive` / `is_classical_data` / `is_classical_control` / `is_delay` | `bool` | 与 `Instruction` 同名的类别判定属性。 | +| `matrix()` | `np.ndarray` | 返回该操作对应的酉矩阵;携带符号参数或为控制流操作时抛 `ValueError`。 | --- diff --git a/docs/api/python/0_circuit/5_gates_standard.md b/docs/api/cn/python/0_circuit/5_gates_standard.md similarity index 87% rename from docs/api/python/0_circuit/5_gates_standard.md rename to docs/api/cn/python/0_circuit/5_gates_standard.md index 25640292..fb0ede8d 100644 --- a/docs/api/python/0_circuit/5_gates_standard.md +++ b/docs/api/cn/python/0_circuit/5_gates_standard.md @@ -127,6 +127,28 @@ assert len(gate.params) == 1 --- +## 查找与枚举 + +```python +StandardGate.all() -> list[StandardGate] +StandardGate.from_name(name: str) -> StandardGate +``` + +`all()` 按标准门枚举顺序返回全部内置标准门定义,列表中的每一项都是未绑定参数的门的定义。 + +`from_name(name)` 按名称查找标准门定义,匹配不区分大小写;名称无法匹配任何标准门时抛 `ValueError`。 + +```python +from cqlib.circuit.gates import StandardGate + +assert len(StandardGate.all()) == 36 + +assert StandardGate.from_name("H") == StandardGate.H +assert StandardGate.from_name("cx") == StandardGate.CX +``` + +--- + ## 参数绑定 参数化标准门可以通过调用门工厂并传入参数来生成带参数的门实例。 @@ -160,6 +182,8 @@ matrix(params: list[float] | None = None) -> np.ndarray[np.complex128] - 如果门实例已经绑定了可求值的常量参数,可以直接调用 `matrix()`; - 如果门实例或门定义中包含符号参数,需要在 `matrix(params)` 中传入具体数值参数,或先在线路中完成参数绑定后再进行矩阵转换。 +传入的 `params` 长度必须与门的 `num_params` 一致,否则抛 `CircuitError`;已绑定的参数若无法求值为常量,则抛 `ParameterError`。参数化门在未绑定任何参数时直接调用 `matrix()` 同样抛 `CircuitError`。 + ```python import numpy as np from cqlib.circuit.gates import StandardGate @@ -189,7 +213,9 @@ assert StandardGate.S.inverse() == StandardGate.SDG assert StandardGate.T.inverse() == StandardGate.TDG ``` -当门实例包含参数时,`inverse()` 会按照该门的逆变换规则处理参数。该能力常用于构造逆线路、uncompute 结构、门级测试和编译优化验证。 +当门实例包含参数时,`inverse()` 会按照该门的逆变换规则处理参数:例如 `StandardGate.RZ(0.5).inverse()` 返回 `StandardGate.RZ(-0.5)`。该能力常用于构造逆线路、uncompute 结构、门级测试和编译优化验证。 + +参数化门在未绑定全部参数时调用 `inverse()` 会抛 `CircuitError`;如果该门本身不可逆,则抛 `RuntimeError`。 --- @@ -213,6 +239,9 @@ assert mcx.base_gate == StandardGate.X 应用多控制门时,量子比特通常按照“控制比特在前、目标比特在后”的顺序传入。例如,三控制 `X` 门共需要四个量子比特,前三个为控制比特,最后一个为目标比特。 ```python +from cqlib import Circuit + +circuit = Circuit(4) circuit.append_mc_gate(mcx, [0, 1, 2, 3]) # controls: 0, 1, 2; target: 3 ``` diff --git a/docs/api/python/0_circuit/6_gates_unitary.md b/docs/api/cn/python/0_circuit/6_gates_unitary.md similarity index 86% rename from docs/api/python/0_circuit/6_gates_unitary.md rename to docs/api/cn/python/0_circuit/6_gates_unitary.md index 220d955f..c9a382d8 100644 --- a/docs/api/python/0_circuit/6_gates_unitary.md +++ b/docs/api/cn/python/0_circuit/6_gates_unitary.md @@ -134,6 +134,32 @@ bell_gate = sub.to_gate("BellPrep") --- +## 矩阵求值 + +```python +matrix() -> np.ndarray +matrix_for_params(params: list[float]) -> np.ndarray +``` + +`matrix()` 返回由 `with_matrix()` 附加的数值矩阵。如果该门尚未定义,或改用 `with_symbolic_matrix()`、`with_circuit()` 定义,则不存在可直接返回的数值矩阵,此时抛 `CircuitError`。 + +`matrix_for_params(params)` 用于求出该门在给定参数下的数值矩阵,支持数值矩阵、符号矩阵和冻结线路三种定义方式;`params` 按 `matrix_params` 给出的顺序提供。 + +```python +import numpy as np +from cqlib.circuit.gates import UnitaryGate + +z_like = np.array([[1, 0], [0, -1]], dtype=np.complex128) +gate = UnitaryGate("ZLike", 1).with_matrix(z_like) + +assert gate.matrix().shape == (2, 2) +assert np.array(gate).shape == (2, 2) +``` + +`UnitaryGate` 同时实现数组协议,因此也可以通过 `np.array(gate)` 或 `np.asarray(gate)` 直接取得矩阵表示。 + +--- + ## 追加到 `Circuit` ```python diff --git a/docs/api/python/0_circuit/7_gates_mc_gate.md b/docs/api/cn/python/0_circuit/7_gates_mc_gate.md similarity index 87% rename from docs/api/python/0_circuit/7_gates_mc_gate.md rename to docs/api/cn/python/0_circuit/7_gates_mc_gate.md index 7e87423a..5030c31a 100644 --- a/docs/api/python/0_circuit/7_gates_mc_gate.md +++ b/docs/api/cn/python/0_circuit/7_gates_mc_gate.md @@ -114,6 +114,8 @@ assert matrix.shape == (4, 4) 需要注意的是,`matrix()` 返回的是该门自身的局部矩阵,不包含所在 `Circuit` 的全局相位,也不考虑线路中其他操作。 +调用时需要保证参数数量与 `num_params` 一致:显式传入的 `params` 长度不等于 `num_params`,或者未传入 `params` 而门上的参数尚未全部绑定为具体数值时,都会抛出 `CircuitError`。传入的数值参数必须为有限值,否则抛出 `ParameterError`。 + --- ## 反门 @@ -135,6 +137,8 @@ assert inverse.base_gate == StandardGate.SDG 在上述示例中,`S` 门的逆为 `SDG`,因此单控制 `S` 门的逆为单控制 `SDG` 门。对于自反门,例如 `X`、`Z`、`H` 等,其多控制形式的逆通常与自身相同。 +调用 `inverse()` 时要求门上的参数已经完整绑定:已绑定参数数量不等于 `num_params` 时抛出 `CircuitError`;基础门本身不可取逆时抛出 `RuntimeError`。 + --- ## 与标准受控门的关系 @@ -183,3 +187,11 @@ circuit = Circuit(4) mcx = MCGate(3, StandardGate.X) circuit.append_mc_gate(mcx, [0, 1, 2, 3]) ``` + +--- + +## 其他行为 + +- 支持 `==` 与 `hash`,比较与哈希同时考虑控制结构、基础门与已绑定参数,因此 `MCGate` 可以作为字典键或集合元素。 +- 支持 `copy`、`deepcopy`。 +- `repr` 可直接求值重建原门:无参数时形如 `MCGate(2, StandardGate.X)`,带参数时形如 `MCGate(1, StandardGate.RX(1.5))`。 diff --git a/docs/api/python/0_circuit/8_gates_circuit_gate.md b/docs/api/cn/python/0_circuit/8_gates_circuit_gate.md similarity index 62% rename from docs/api/python/0_circuit/8_gates_circuit_gate.md rename to docs/api/cn/python/0_circuit/8_gates_circuit_gate.md index 7db7514f..9a669f65 100644 --- a/docs/api/python/0_circuit/8_gates_circuit_gate.md +++ b/docs/api/cn/python/0_circuit/8_gates_circuit_gate.md @@ -55,7 +55,7 @@ circuit.append_circuit_gate(bell_gate, [0, 1]) circuit.append_circuit_gate(bell_gate, [2, 3]) ``` -在上述示例中,`bell.to_gate("Bell")` 会将两比特 Bell 子线路封装为一个名为 `Bell` 的 `CircuitGate`。随后,该复合门可以像普通门一样追加到其他线路中,并作用在指定的量子比特上。 +在上述示例中,`bell.to_gate("Bell")` 会将双比特 Bell 子线路封装为一个名为 `Bell` 的 `CircuitGate`。随后,该复合门可以像普通门一样追加到其他线路中,并作用在指定的量子比特上。 --- @@ -97,9 +97,10 @@ frozen = FrozenCircuit(sub.qubits, sub.operations) | `qubits` | `list[Qubit]` | 子线路定义中保存的量子比特顺序。 | | `num_operations` | `int` | 子线路包含的操作数量。 | | `operations` | `list[ValueOperation]` | 子线路中的自包含操作列表。 | -| `symbols` | `list[str]` | 子线路中出现的自由符号参数名。 | +| `symbols` | `list[str]` | 子线路内部按插入顺序登记的符号名,可能包含已不再被引用的符号。 | +| `used_symbols` | `list[str]` | 冻结后实际被引用的符号名。 | -`FrozenCircuit.symbols` 可用于了解该子线路是否为参数化线路。如果冻结线路中包含符号参数,那么由它生成的 `CircuitGate` 在应用时也需要按照相应符号顺序传入位置参数。 +`FrozenCircuit.used_symbols` 可用于了解该子线路是否为参数化线路。需要注意 `symbols` 与 `used_symbols` 的区别:前者是一份稳定的符号登记表,删除或替换操作之后其中可能残留不再被引用的符号;后者只包含当前实际被引用的符号。省略参数签名构造的 `CircuitGate` 会以 `used_symbols` 作为位置参数签名,因此由它生成的复合门在应用时也需要按照 `used_symbols` 的顺序传入位置参数。 --- @@ -108,13 +109,18 @@ frozen = FrozenCircuit(sub.qubits, sub.operations) `CircuitGate` 是由 `FrozenCircuit` 定义的复合门。它将一段子线路包装为一个门对象,使其可以通过 `Circuit.append_circuit_gate()` 添加到其他线路中。 ```python -CircuitGate(name: str, circuit: FrozenCircuit) +CircuitGate( + name: str, + circuit: FrozenCircuit, + signature_params: list[str] | None = None, +) -> CircuitGate ``` | 参数 | 说明 | |---|---| | `name` | 复合门名称,主要用于显示、调试和 IR 表达。 | | `circuit` | 用于定义该复合门的不可变线路快照。 | +| `signature_params` | 位置参数签名。省略时取 `circuit.used_symbols`;显式给出时按该列表的顺序绑定参数。显式签名中允许出现子线路未引用的参数名,这些参数仍然计入门的参数数量。 | 示例: @@ -128,15 +134,47 @@ gate = CircuitGate("Bell", frozen) |---|---|---| | `name` | `str` | 复合门名称。 | | `num_qubits` | `int` | 应用该门时需要提供的量子比特数量。 | -| `num_params` | `int` | 应用该门时需要提供的位置参数数量。 | -| `symbols` | `list[str]` | 子线路中的符号参数名,决定位置参数绑定顺序。 | +| `num_params` | `int` | 应用该门时需要提供的位置参数数量,等于 `signature_params` 的长度。 | +| `signature_params` | `list[str]` | 位置参数签名,决定应用该门时位置参数的绑定顺序与数量。 | +| `used_symbols` | `list[str]` | 该门所依据的子线路中实际被引用的符号名。 | +| `symbols` | `list[str]` | `used_symbols` 的向后兼容别名,不代表位置参数的绑定顺序。 | | `circuit` | `FrozenCircuit` | 该复合门内部持有的不可变线路定义。 | --- +## 显式参数签名 + +当门的位置参数需要与子线路中实际出现的符号解耦时,可以使用静态方法 `CircuitGate.with_signature()` 显式声明调用签名: + +```python +CircuitGate.with_signature( + name: str, + circuit: FrozenCircuit, + signature_params: list[str], +) -> CircuitGate +``` + +```python +from cqlib import Circuit, Parameter +from cqlib.circuit.gates import CircuitGate, FrozenCircuit + +body = Circuit(1) +body.rx(0, Parameter("used")) + +frozen_body = FrozenCircuit(body.qubits, body.operations) + +gate = CircuitGate.with_signature("Declared", frozen_body, ["used", "declared_unused"]) +``` + +显式声明签名时,`signature_params` 中不允许出现重复的参数名;同时子线路中实际引用的每一个符号都必须已在 `signature_params` 中声明。上述两种情形都会抛出 `CircuitError`。 + +被声明但子线路并未引用的参数仍然计入门的参数数量,因此 `gate.num_params` 等于 `signature_params` 的长度,而不是 `gate.used_symbols` 的长度。 + +--- + ## 参数化子线路门 -如果子线路中包含符号参数,`CircuitGate` 会记录这些符号参数,并在应用时按照 `gate.symbols` 的顺序进行位置绑定。 +如果子线路中包含符号参数,`CircuitGate` 会记录这些符号参数,并在应用时按照 `gate.signature_params` 的顺序进行位置绑定。 ```python from cqlib import Circuit, Parameter @@ -155,10 +193,10 @@ circuit.append_circuit_gate(gate, [0], params=[Parameter("alpha")]) 在上述示例中,子线路 `block` 中包含符号参数 `theta`。当它被封装为 `ParamBlock` 后,外部应用该复合门时传入的 `alpha` 会按照位置参数规则替换内部的 `theta`。 -需要注意的是,`CircuitGate` 的参数绑定是按位置绑定。也就是说,应用时传入的第 `i` 个参数会替换 `gate.symbols` 中第 `i` 个符号。 +需要注意的是,`CircuitGate` 的参数绑定是按位置绑定。也就是说,应用时传入的第 `i` 个参数会替换 `gate.signature_params` 中第 `i` 个符号。 ```python -print(gate.symbols) +print(gate.signature_params) print(gate.num_params) ``` @@ -206,13 +244,13 @@ main.append_circuit_gate(rx_block, [0], params=[alpha]) CircuitGate.inverse() -> CircuitGate ``` -`inverse()` 会返回一个新的 `CircuitGate`。新门的底层冻结线路由原子线路逐操作取逆并反向排列得到。 +`inverse()` 会返回一个新的 `CircuitGate`。新门的底层冻结线路由原子线路逐操作取逆并反向排列得到,同时新门的名称在原名称后追加 `_dg` 后缀,位置参数签名保持不变。 ```python inverse_gate = gate.inverse() ``` -该接口适用于可逆子线路,如果子线路定义中包含测量、`reset` 或其他不可逆操作,反演过程会抛出 `CircuitError`。并且,`inverse()` 不会修改原始 `CircuitGate`,而是返回一个新的复合门定义。 +该接口适用于可逆子线路。如果子线路定义中包含测量、`reset` 或其他不可逆操作,例如经典数据操作、控制流,以及无法取逆的指令,反演过程会抛出 `CircuitError`。并且,`inverse()` 不会修改原始 `CircuitGate`,而是返回一个新的复合门定义。 --- @@ -229,3 +267,11 @@ expanded = outer.decompose() 分解后,复合门会被替换为其内部的原始操作。对于参数化 `CircuitGate`,应用时传入的位置参数会在分解过程中替换到内部操作中。 +--- + +## 其他行为 + +- `FrozenCircuit` 与 `CircuitGate` 都支持结构相等比较 `==`。`FrozenCircuit` 的比较忽略线路标识与符号矩阵缓存,只比较定义本身;`CircuitGate` 的比较包含名称、有序签名与底层定义线路。 +- `repr` 形如 `FrozenCircuit(qubits=2, operations=2)` 与 `CircuitGate("Bell", qubits=2, params=0)`。 +- 两者都支持 `copy`、`deepcopy`。由于二者都是不可变对象,复制结果与原对象相等。 + diff --git a/docs/api/python/0_circuit/9_classical_control_flow.md b/docs/api/cn/python/0_circuit/9_classical_control_flow.md similarity index 84% rename from docs/api/python/0_circuit/9_classical_control_flow.md rename to docs/api/cn/python/0_circuit/9_classical_control_flow.md index d1b4a4d5..41926948 100644 --- a/docs/api/python/0_circuit/9_classical_control_flow.md +++ b/docs/api/cn/python/0_circuit/9_classical_control_flow.md @@ -1,5 +1,6 @@ # Classical Data / Control Flow +- `cqlib.circuit.CircuitId` - `cqlib.circuit.ClassicalType` - `cqlib.circuit.ClassicalVar` - `cqlib.circuit.ClassicalValue` @@ -11,6 +12,7 @@ ```python from cqlib.circuit import ( + CircuitId, ClassicalType, ClassicalExpr, ClassicalControlOp, @@ -60,6 +62,8 @@ ClassicalType.bit_vec(width: int) -> ClassicalType | `ClassicalType.uint(width)` | 指定位宽的无符号整数。 | 计数器、循环变量、`switch` 目标。 | | `ClassicalType.bit_vec(width)` | 指定位宽的 bit 向量。 | 多比特测量结果、位拼接、位打包结果。 | +`uint(width)` 与 `bit_vec(width)` 的位宽必须大于零,否则抛出 `CircuitError`。 + ```python from cqlib.circuit import ClassicalType @@ -87,11 +91,22 @@ one = u4.one_literal() `CircuitId` 是线路本地身份标识,用于区分不同 `Circuit` 创建的经典变量和经典值。 +```python +CircuitId() -> CircuitId +Circuit.id -> CircuitId +``` + +`Circuit.id` 给出所属线路的身份。低层 IR 构造需要脱离线路直接创建句柄时,可以用 `CircuitId()` 显式构造一个身份,同一组相关句柄必须使用同一个 `CircuitId`。 + ```python from cqlib import Circuit +from cqlib.circuit import CircuitId circuit = Circuit(1) print(circuit.id) + +explicit = CircuitId() +print(explicit) ``` --- @@ -110,11 +125,17 @@ flag = circuit.var(ClassicalType.bool()) counter = circuit.var(ClassicalType.uint(4)) ``` +低层 IR 构造需要脱离线路直接创建变量句柄时,可以使用: + +```python +ClassicalVar(circuit_id: CircuitId, index: int, ty: ClassicalType) -> ClassicalVar +``` + 常用属性和方法如下: | 接口 | 说明 | |---|---| -| `id` | 由 `circuit_id` 和 `index` 组成的稳定标识。 | +| `id` | 变量在所属线路中的局部标识,取值与 `index` 相同。 | | `index` | 变量在所属线路中的位置。 | | `circuit_id` | 创建该变量的线路身份。 | | `ty` | 变量的静态经典类型。 | @@ -131,7 +152,7 @@ print(flag.ty) print(flag.circuit_id == circuit.id) ``` -`store(target, value)` 用于向经典变量写入表达式。写入时,`value` 的类型必须与 `target.ty` 兼容。若类型不匹配,后续校验或转换通常会失败。 +`store(target, value)` 用于向经典变量写入表达式。写入时,`value.ty` 必须与 `target.ty` 完全相同,否则 `store()` 直接抛出 `CircuitError`;如果 `target` 属于其他线路,同样会抛出 `CircuitError`。 需要注意的是,`ClassicalVar` 具有线路归属关系,不应在不同 `Circuit` 之间直接混用。如果需要在另一条线路中表达相同逻辑,应在该线路中重新创建对应变量。 @@ -150,6 +171,12 @@ measurement = circuit.measure(0) value = measurement.value ``` +低层 IR 构造需要脱离线路直接创建经典值句柄时,可以使用: + +```python +ClassicalValue(circuit_id: CircuitId, index: int, ty: ClassicalType) -> ClassicalValue +``` + 常用属性和方法如下: | 接口 | 说明 | @@ -169,6 +196,12 @@ expr = value.expr() `Measurement` 是测量操作返回的回执对象,用于记录测量结果句柄和被测量的量子比特顺序。 +```python +Measurement(value: ClassicalValue, qubits: list[Qubit]) -> Measurement +``` + +`Measurement` 通常由 `Circuit.measure()` 或 `Circuit.measure_bits()` 返回;低层 IR 构造也可以用上述形式直接创建。 + 常用属性和方法如下: | 属性/方法 | 说明 | @@ -196,7 +229,7 @@ print(m.qubits) expr = m.expr() ``` -如果需要将测量结果作为条件使用,可以将 `Bit` 或 `BitVec` 转换为适合控制流条件的表达式。例如,单比特测量结果可以转换为 `Bool`: +如果需要将测量结果作为条件使用,单比特测量结果可以直接转换为 `Bool`: ```python single = circuit.measure(1) @@ -227,8 +260,13 @@ true_expr = ClassicalExpr.bool_literal(True) bit_one = ClassicalExpr.bit_literal(True) u4_three = ClassicalExpr.uint_literal(4, 3) bits = ClassicalExpr.bit_vec_literal(4, 0b1010) + +print(true_expr.ty) # ClassicalType.bool() +print(bits.ty) # ClassicalType.bit_vec(4) ``` +表达式对象带有 `ty` 属性,返回其静态经典类型,可用于在构造控制流之前核对条件与操作数的类型。 + ### 2. 逻辑与位操作 `ClassicalExpr` 支持常见逻辑运算。运算符 `~`、`&`、`|`、`^` 分别对应非、与、或和异或。 @@ -255,9 +293,9 @@ print(expr.simplified()) | 方法 | 说明 | |---|---| | `bit_to_bool()` | 将 `Bit` 转换为 `Bool`。 | -| `to_bool()` | 将 `Bit` 或 `UInt` 转换为 `Bool`。 | +| `to_bool()` | 将 `Bit` 转换为 `Bool`,等价于 `bit_to_bool()`。 | | `bit_vec_to_uint()` | 将 `BitVec` 转换为 `UInt`。 | -| `to_uint()` | 将 `Bit` 或 `BitVec` 转换为 `UInt`。 | +| `to_uint()` | 将 `BitVec` 转换为 `UInt`,等价于 `bit_vec_to_uint()`。 | ```python from cqlib.circuit import ClassicalExpr @@ -266,7 +304,7 @@ bit_as_bool = ClassicalExpr.bit_literal(True).bit_to_bool() bits_as_uint = ClassicalExpr.bit_vec_literal(4, 0b1010).to_uint() ``` -当表达式需要作为 `if_()` 或 `while_()` 条件时,应确保其类型为 `Bool`。如果原始表达式是 `Bit` 或 `UInt`,可以通过 `to_bool()` 进行显式转换。 +当表达式需要作为 `if_()` 或 `while_()` 条件时,应确保其类型为 `Bool`。如果原始表达式是 `Bit`,可以通过 `to_bool()` 进行显式转换;`UInt` 或 `BitVec` 表达式则需要先用 `extract_bit()` 取出单个 `Bit`,再转换为 `Bool`。 ### 4. 比较与选择 @@ -299,9 +337,9 @@ selected = ClassicalExpr.select(cond1, x, y) | 方法 | 说明 | |---|---| -| `extract_bit(index)` | 从 `UInt` 或 `BitVec` 中提取一个 bit。 | -| `extract_bits(offset, width)` | 提取连续 `Bit` 区间。 | -| `concat(parts)` | 拼接多个 `BitVec` 表达式。 | +| `extract_bit(index)` | 从 `UInt` 或 `BitVec` 中提取一个 `Bit`。 | +| `extract_bits(offset, width)` | 从 `UInt` 或 `BitVec` 中提取连续位区间,结果为指定位宽的 `BitVec`。 | +| `concat(parts)` | 拼接多个 `Bit` 或 `BitVec` 表达式,结果为 `BitVec`。 | | `pack_bits(bits)` | 将多个 `Bit` 表达式打包为 `BitVec`。 | ```python @@ -328,6 +366,13 @@ packed = ClassicalExpr.pack_bits( | `is_bool_true()` / `is_bool_false()` | 判断是否为 `Bool` 常量。 | | `is_bit_true()` / `is_bit_false()` | 判断是否为 `Bit` 常量。 | +### 7. 对象行为 + +- 支持 `==` 与 `hash`,可作为字典键或集合元素。 +- `expr1 == expr2` 是表达式的结构相等比较,判断两个表达式对象是否表示同一段结构;要构造运行时比较的表达式,应使用 `ClassicalExpr.equal()` 等静态方法。 +- 支持 `copy`、`deepcopy`。 +- `repr` 给出表达式的结构表示,便于调试。 + --- ## 高层控制流 API @@ -513,6 +558,8 @@ ValueSwitchCase(value: int, body: ValueControlBody) | `has_measurement()` | 判断控制流内部是否包含测量。 | | `reads_value(value)` | 判断控制流内部是否读取指定经典值。 | +`ValueControlBody`、`ValueSwitchCase` 与 `ClassicalControlOp` 都支持 `copy`、`deepcopy`,并带有便于调试的 `repr`。 + --- ## 验证规则 @@ -545,7 +592,7 @@ circuit.validate() ## 与矩阵转换的关系 -包含测量、经典数据写入或控制流的线路通常不能直接表示为单个固定的幺正矩阵。原因在于,这类线路描述的是带有运行时经典信息、条件分支或循环结构的程序,而不是固定顺序的纯量子门序列。 +包含测量、经典数据写入或控制流的线路通常不能直接表示为单个固定的酉矩阵。原因在于,这类线路描述的是带有运行时经典信息、条件分支或循环结构的程序,而不是固定顺序的纯量子门序列。 ```python from cqlib import Circuit diff --git a/docs/api/cn/python/0_overview.md b/docs/api/cn/python/0_overview.md new file mode 100644 index 00000000..e73ddf13 --- /dev/null +++ b/docs/api/cn/python/0_overview.md @@ -0,0 +1,81 @@ +# Python API 概览 + +## 欢迎查阅 Cqlib Python API 参考手册! + +## 文档导航 + +### 量子电路(`cqlib.circuit`) + +- [Overview](0_circuit/0_overview.md) +- [Circuit](0_circuit/1_circuit.md) +- [Qubit](0_circuit/2_qubit.md) +- [Parameter](0_circuit/3_parameter.md) +- [Operation / Instruction](0_circuit/4_operation_instruction.md) +- [StandardGate](0_circuit/5_gates_standard.md) +- [UnitaryGate](0_circuit/6_gates_unitary.md) +- [MCGate](0_circuit/7_gates_mc_gate.md) +- [CircuitGate / FrozenCircuit](0_circuit/8_gates_circuit_gate.md) +- [Classical / Control Flow](0_circuit/9_classical_control_flow.md) +- [SymbolicMatrix](0_circuit/10_symbolic_matrix.md) +- [Ansatz](0_circuit/11_ansatz.md) +- [Circuit To Matrix](0_circuit/12_circuit_to_matrix.md) +- [CircuitDag](0_circuit/13_circuit_dag.md) + +### 中间表达(`cqlib.ir`) + +- [Overview](1_ir/0_overview.md) +- [QCIS](1_ir/1_qcis.md) +- [OpenQASM 2.0](1_ir/2_qasm2.md) +- [OpenQASM 3.0](1_ir/3_qasm3.md) + +### 设备模块(`cqlib.device`) + +- [Overview](2_device/0_overview.md) +- [Topology](2_device/1_topology.md) +- [Device 与噪声属性](2_device/2_properties_device.md) +- [Layout](2_device/3_layout.md) +- [Noise](2_device/4_noise.md) +- [Result](2_device/5_result.md) +- [量子比特标识](2_device/6_qubits.md) + +### 量子信息(`cqlib.qis`) + +- [Overview](3_qis/0_overview.md) +- [Statevector](3_qis/1_statevector.md) +- [DensityMatrix](3_qis/2_density_matrix.md) +- [DensityMatrixNoise](3_qis/3_density_matrix_noise.md) +- [Stabilizer](3_qis/4_stabilizer.md) +- [ClassicalState](3_qis/5_classical_state.md) +- [Pauli](3_qis/6_pauli.md) +- [Hamiltonian](3_qis/7_hamiltonian.md) +- [Evolution](3_qis/8_evolution.md) +- [Metrics / Entropy](3_qis/9_metrics_entropy.md) + +### 编译优化(`cqlib.compile`) + +- [Overview](4_compile/0_overview.md) +- [Compiler](4_compile/1_compiler.md) +- [Transform](4_compile/2_transform.md) +- [Layout](4_compile/3_layout.md) +- [Routing](4_compile/4_routing.md) +- [SABRE](4_compile/5_sabre.md) +- [Decompose / Resynthesis](4_compile/6_decompose_resynthesis.md) +- [Knowledge](4_compile/7_knowledge.md) +- [Resource](4_compile/8_resource.md) +- [Commutation](4_compile/9_commutation.md) + +### 可视化(`cqlib.visualization`) + +- [Overview](5_visualization/0_overview.md) +- [文本线路图](5_visualization/1_draw_text.md) +- [SVG 线路图](5_visualization/2_draw_figure.md) +- [量子态可视化](5_visualization/3_state_plots.md) +- [测量结果可视化](5_visualization/4_result_plots.md) +- [落盘与内联显示](5_visualization/5_render_to_file.md) + +### 错误缓解(`cqlib.error_mitigation`) + +- [Overview](6_error_mitigation/0_overview.md) +- [零噪声外推](6_error_mitigation/1_zne.md) +- [虚拟蒸馏](6_error_mitigation/2_virtual_distillation.md) +- [统一流水线](6_error_mitigation/3_unified.md) diff --git a/docs/api/cn/python/1_ir/0_overview.md b/docs/api/cn/python/1_ir/0_overview.md new file mode 100644 index 00000000..43df56a3 --- /dev/null +++ b/docs/api/cn/python/1_ir/0_overview.md @@ -0,0 +1,193 @@ +# 中间表达 + +`cqlib.ir` + +`cqlib.ir` 是 Cqlib 的中间表达模块,提供 `Circuit` 与三种线路文本格式之间的双向转换:QCIS、OpenQASM 2.0 与 OpenQASM 3.0。每种格式是一个子模块,接口形态完全一致。 + +## Overview + +中间表达解决的是线路的文本化问题:线路在文件、工具与服务之间传递时是文本,而 Cqlib 内部统一用 `Circuit` 表示。`cqlib.ir` 的每个子模块都提供「文本进、`Circuit` 出」与「`Circuit` 进、文本出」两个方向,转换过程不修改传入的 `Circuit`。 + +### 统一接口:解析与导出 + +三个子模块使用同一套四个函数: + +- 解析方向:`loads(text)` 从字符串解析,`load(path)` 从文件读取后解析,返回 `Circuit`。 +- 导出方向:`dumps(circuit)` 返回文本字符串,`dump(circuit, path)` 把文本写入文件并返回 `None`。 + +`loads` / `dumps` 不接触文件系统,`load` / `dump` 则要读写文件,因此「文件读写失败」与「内容解析失败」是两类不同的异常。 + +### 三个格式 + +| 格式 | 子模块 | 文本特点 | +| --- | --- | --- | +| QCIS | `cqlib.ir.qcis` | 由操作行组成,导出只接受 QCIS 原生门集。 | +| OpenQASM 2.0 | `cqlib.ir.qasm2` | 带 `OPENQASM 2.0;` 与 `include "qelib1.inc";` 头部及 `qreg` 声明,支持自定义门。 | +| OpenQASM 3.0 | `cqlib.ir.qasm3` | 见 [qasm3](3_qasm3.md)。 | + +### 双向转换与边界 + +`loads(dumps(circuit))` 得到的线路与输入线路在操作序列上等价,这就是双向转换。但文本格式的表达能力有限: + +- QCIS 只接受原生门集内的门,线路中若含其他门,需先分解到该门集再导出。 +- QCIS 文本不记录比特数,只有比特而没有操作的线路导出为空串。 + +### 与编译的关系 + +导出是编译产物的可选落盘形式:线路降到目标门集之后即可用对应格式写出。格式差异只体现在门集与文本头上,`Circuit` 一侧的接口保持不变,因此也可以由一种格式读入、另一种格式写出,完成格式转换。 + +--- + +## 常用入口 + +```python +from cqlib import Circuit +from cqlib.ir import qcis + +c1 = Circuit(2) +c1.h(0) +c1.cz(0, 1) + +qcis_text = qcis.dumps(c1) +c2 = qcis.loads(qcis_text) + +assert c2.num_qubits == 2 +assert len(c2) == 2 +``` + +--- + +## 核心概念与术语 + +| 术语 | 说明 | +| --- | --- | +| **中间表达** | `Circuit` 与线路文本格式之间的表示层,由 `cqlib.ir` 下的格式子模块提供。 | +| **解析** | 文本到 `Circuit` 的方向,对应 `loads`(字符串)与 `load`(文件)。 | +| **导出** | `Circuit` 到文本的方向,对应 `dumps`(字符串)与 `dump`(文件)。 | +| **双向转换** | 先导出再解析回到等价线路,即 `loads(dumps(circuit))`。 | +| **格式转换** | 一种格式读入、另一种格式写出,例如把 OpenQASM 2.0 文本写成 QCIS 文本。 | +| **原生门集** | 文本格式自身定义的门集合;导出时线路中的门必须都能落在该集合内。 | + +--- + +## `cqlib.ir` API 概览 + +### 解析入口 + +| 名字 | 简介 | +| --- | --- | +| [`qcis.loads`](1_qcis.md) / [`qcis.load`](1_qcis.md) | 从 QCIS 字符串或文件解析 `Circuit`。 | +| [`qasm2.loads`](2_qasm2.md) / [`qasm2.load`](2_qasm2.md) | 从 OpenQASM 2.0 字符串或文件解析 `Circuit`。 | +| [`qasm3.loads`](3_qasm3.md) / [`qasm3.load`](3_qasm3.md) | 从 OpenQASM 3.0 字符串或文件解析 `Circuit`。 | + +### 导出入口 + +| 名字 | 简介 | +| --- | --- | +| [`qcis.dumps`](1_qcis.md) / [`qcis.dump`](1_qcis.md) | 把 `Circuit` 导出为 QCIS 字符串或文件。 | +| [`qasm2.dumps`](2_qasm2.md) / [`qasm2.dump`](2_qasm2.md) | 把 `Circuit` 导出为 OpenQASM 2.0 字符串或文件。 | +| [`qasm3.dumps`](3_qasm3.md) / [`qasm3.dump`](3_qasm3.md) | 把 `Circuit` 导出为 OpenQASM 3.0 字符串或文件。 | + +### 格式子模块 + +| 名字 | 简介 | +| --- | --- | +| [`cqlib.ir.qcis`](1_qcis.md) | QCIS 文本格式,导出受 QCIS 原生门集约束。 | +| [`cqlib.ir.qasm2`](2_qasm2.md) | OpenQASM 2.0 文本格式,支持标准门、参数表达式、测量与自定义门。 | +| [`cqlib.ir.qasm3`](3_qasm3.md) | OpenQASM 3.0 文本格式。 | + +--- + +## 快速示例 + +### 1. QCIS 文本的双向转换 + +```python +from cqlib.circuit import Circuit +from cqlib.ir.qcis import dumps, loads + +c1 = Circuit(2) +c1.h(0) +c1.cz(0, 1) + +qcis = dumps(c1) +c2 = loads(qcis) + +assert c2.num_qubits == 2 +assert len(c2) == 2 +``` + +导出的文本是逐行的操作,可以对内容做精确断言: + +```python +from cqlib.circuit import Circuit +from cqlib.ir.qcis import dumps + +c = Circuit(2) +c.cx(0, 1) + +assert dumps(c) == "CX Q0 Q1\n" +``` + +### 2. OpenQASM 2.0 文本的解析与导出 + +```python +from cqlib.ir import qasm2 + +code = """OPENQASM 2.0; +include "qelib1.inc"; +qreg q[1]; +h q[0]; +""" + +circuit = qasm2.loads(code) + +text = qasm2.dumps(circuit) +lines = text.strip().split("\n") +assert lines[1] == "OPENQASM 2.0;" +assert lines[2] == 'include "qelib1.inc";' +assert "h q[0];" in text +``` + +### 3. QCIS 文件读写 + +```python +import os +import tempfile + +from cqlib.circuit import Circuit +from cqlib.ir import qcis + +c = Circuit(2) +c.h(0) +c.cz(0, 1) + +with tempfile.NamedTemporaryFile(mode="w", suffix=".qcis", delete=False) as f: + path = f.name + +try: + qcis.dump(c, path) + + with open(path) as f: + assert "H Q0" in f.read() + + c2 = qcis.load(path) + assert c2.num_qubits == 2 +finally: + os.unlink(path) +``` + +--- + +## 校验与错误处理 + +内容问题抛 `ValueError`,文件问题抛 `OSError`(`IOError` 是它的别名),两者的消息前缀区分了格式与动作。 + +| 异常 | 触发场景 | +| --- | --- | +| `ValueError` | 文本语法不合法或解析失败,消息前缀为 `QCIS parse error:`、`QASM parse error:`、`QASM3 parse error:`。 | +| `ValueError` | 导出失败,例如线路中含目标格式无法表达的门,消息前缀为 `QCIS dump error:`、`QASM dump error:`、`QASM3 dump error:`。 | +| `OSError` | `load` 读取文件失败,消息前缀为 `<格式> load error:`。 | +| `OSError` | `dump` 写入文件失败,消息前缀为 `<格式> dump error:`。 | + +使用 `load` / `dump` 时,同一个前缀既可能来自文件读写,也可能来自内容处理;需要区分时按异常类型判断即可。导出前线路必须已经落在目标格式的门集内,否则先做门分解再导出。 diff --git a/docs/api/python/1_ir/1_qcis.md b/docs/api/cn/python/1_ir/1_qcis.md similarity index 65% rename from docs/api/python/1_ir/1_qcis.md rename to docs/api/cn/python/1_ir/1_qcis.md index dae85f22..831b1c79 100644 --- a/docs/api/python/1_ir/1_qcis.md +++ b/docs/api/cn/python/1_ir/1_qcis.md @@ -12,13 +12,13 @@ from cqlib.ir import qcis ## 函数 -### qcis.loads(qcis_text) +### qcis.loads(qcis) 从 QCIS 字符串解析电路。 参数: -- `qcis_text` (`str`):QCIS 文本。 +- `qcis` (`str`):QCIS 文本。 返回: @@ -92,18 +92,22 @@ circuit = qcis.loads(qcis_code) 异常情况: -- `IOError`:文件写入失败。 -- `IOError`:导出失败(当前绑定将导出错误也映射为 IO 异常,错误前缀 `QCIS dump error: ...`)。 +- `OSError`:文件写入失败。 +- `ValueError`:导出失败(线路中含 QCIS 无法表达的门等,错误前缀 `QCIS dump error: ...`)。 ## 注意事项 `qcis.dumps` / `qcis.dump` 只支持 QCIS 原生门集。遇到非原生门会报错,需先分解/编译到 QCIS 门集再导出。 -当前QCIS原生门集为: +当前 QCIS 原生门集为: -- `X2P`, `X2M`, `Y2P`, `Y2M`, `XY2P`, `XY2M` -- `CZ`, `RZ`, `I`, `X`, `Y`, `Z`, `H`, `S`, `SD`, `T`, `TD` -- `RX`, `RY`, `RXY` +- 单比特门:`H`, `S`, `SD`, `T`, `TD`, `X`, `X2P`, `X2M`, `Y`, `Y2P`, `Y2M`, `Z` +- 含参单比特门:`RX`, `RXY`, `RY`, `RZ`, `U`, `XY`, `XY2P`, `XY2M`, `PHASE` +- 多比特门:`RXX`, `RYY`, `RZX`, `RZZ`, `SWAP`, `CX`, `CCX`, `CY`, `CZ`, `CRX`, `CRY`, `CRZ`, `FSIM` + +除门之外,QCIS 文本还表示测量(`M`)、屏障(`B`)与延迟(`I Qn t`,`t` 为延迟刻度数,单位 0.5 ns,必须是非负整数)。 + +`SDG` 与 `TDG` 在解析时作为 `SD` 与 `TD` 的等价写法接受,导出时统一写成 `SD` 与 `TD`。标准恒等门与全局相位不在 QCIS 的原生门集内,导出时会报错;QCIS 的 `I` 一律表示延迟,不是恒等门。 ## 示例 diff --git a/docs/api/python/1_ir/2_qasm2.md b/docs/api/cn/python/1_ir/2_qasm2.md similarity index 53% rename from docs/api/python/1_ir/2_qasm2.md rename to docs/api/cn/python/1_ir/2_qasm2.md index 883602dc..326dfc46 100644 --- a/docs/api/python/1_ir/2_qasm2.md +++ b/docs/api/cn/python/1_ir/2_qasm2.md @@ -12,13 +12,13 @@ from cqlib.ir import qasm2 ## 函数 -### qasm2.loads(qasm_text) +### qasm2.loads(qasm) 从 OpenQASM 2.0 字符串解析电路。 参数: -- `qasm_text` (`str`):QASM 文本。 +- `qasm` (`str`):QASM 文本。 返回: @@ -57,7 +57,8 @@ circuit = qasm2.loads(code) 异常情况: -- `ValueError`:加载或解析失败(`QASM load error: ...`)。 +- `OSError`:文件读取失败(`QASM load error: ...`)。 +- `ValueError`:文件内容解析失败(`QASM load error: ...`)。 ### qasm2.dumps(circuit) @@ -95,16 +96,32 @@ circuit = qasm2.loads(code) 异常情况: -- `OSError`:写文件失败。 +- `OSError`:写文件失败(`QASM dump error: ...`)。 +- `ValueError`:导出失败(`QASM dump error: ...`)。 ## 支持能力 当前`qasm2` 接口支持: -- 常见标准门:`h/x/y/z/s/sdg/t/tdg/cx/cy/cz/swap/ccx` 等 -- 参数门:`rx/ry/rz/u1/u2/u3` 及参数表达式(如 `pi`, `pi/2`, `3*pi/4`) +- 常见标准门:`h/x/y/z/s/sdg/t/tdg/id`、`cx/cy/cz/swap/ccx` 等 +- 参数门:`rx/ry/rz/u1/u2/u3/p`,以及 `sx/sxdg/crx/cry/crz/rxx/ryy/rzz/fsim` 与 `ch/cu1/cu3` +- 参数表达式:如 `pi`, `pi/2`, `3*pi/4`,加载还接受 `1e-5` 与 `asin`、`acos`、`atan` - 指令:`measure`, `barrier`, `reset` -- 自定义门(`CircuitGate`)导出与加载 +- 声明与定义:`qreg`、`creg`、`opaque`,以及 `gate` 自定义门定义与 `if (creg == 整数) qop;` 条件语句 +- 自定义门(`CircuitGate`)导出与加载;只带矩阵的酉门导出为 `opaque` 声明 + +`include "qelib1.inc";` 按内建引入处理,不要求本地存在该文件;qelib1 的门名在未写 `include` 时同样可用,且不能被重新定义。 + +## 限制 + +导出会按 OpenQASM 2.0 的表达能力取舍,下列结构会报错: + +- 经典控制中的 `else` 分支、循环、`switch` 与嵌套控制流 +- 无法表示为 `creg` 的经典类型,例如 `bool` 与无符号整数 +- `gate` 定义体内的测量与复位 +- 逐位条件(如 `if (c[0] == 1)`)与条件屏障 + +`rzx` 只出现在导出侧的扩展门定义中,加载不接受;全局相位不写成语句,只作为注释输出,重新加载时不会恢复。 ## 常见流程 diff --git a/docs/api/cn/python/1_ir/3_qasm3.md b/docs/api/cn/python/1_ir/3_qasm3.md new file mode 100644 index 00000000..fd53bcb8 --- /dev/null +++ b/docs/api/cn/python/1_ir/3_qasm3.md @@ -0,0 +1,226 @@ +# qasm3 + +`cqlib.ir` 下的 `qasm3` 子模块提供 OpenQASM 3.0 与 `Circuit` 的双向转换接口。 + +## 导入 + +```python +from cqlib.ir import qasm3 +``` + +--- + +## 函数 + +### qasm3.loads(qasm_text) + +从 OpenQASM 3.0 字符串解析电路。 + +参数: + +- `qasm_text` (`str`):QASM 文本。 + +返回: + +- `Circuit` + +异常情况: + +- `ValueError`:语法错误、语义错误,或使用了当前实现不支持的特性(`QASM3 parse error: ...`)。 + +示例: + +```python +from cqlib.ir import qasm3 + +code = """OPENQASM 3; +include "stdgates.inc"; +qubit[2] q; +h q[0]; +cx q[0], q[1]; +""" + +circuit = qasm3.loads(code) +``` + +### qasm3.load(path) + +从 QASM 文件读取并解析电路。 + +参数: + +- `path` (`str`):文件路径。 + +返回: + +- `Circuit` + +异常情况: + +- `OSError`:文件读取失败(不存在、权限不足等)。 +- `ValueError`:文件内容解析失败(`QASM3 load error: ...`)。 + +说明: + +- `load` 读取“文件路径”,`loads` 读取“字符串内容”。 +- 相对路径的 `include` 从输入文件所在目录解析。 + +### qasm3.dumps(circuit) + +将电路导出为 OpenQASM 3.0 字符串。 + +参数: + +- `circuit` (`Circuit`) + +返回: + +- `str` + +异常情况: + +- `ValueError`:导出失败(`QASM3 dump error: ...`)。 + +说明: + +- 输出为规范化文本,不保留原始输入的空格、注释与变量名。 +- 内容依次为头部、标准库引入、扩展门定义、自定义门定义、量子与经典声明、主线路操作。 + +### qasm3.dump(circuit, path) + +将电路导出为 QASM 文件。 + +参数: + +- `circuit` (`Circuit`) +- `path` (`str`):输出路径。 + +返回: + +- `None` + +异常情况: + +- `OSError`:写文件失败。 +- `ValueError`:导出失败(`QASM3 dump error: ...`)。 + +## 支持能力 + +当前 `qasm3` 接口支持: + +- 头部 `OPENQASM 3;` 与 `OPENQASM 3.0;` +- 标准库引入 `include "stdgates.inc";`,解析时使用内置标准库,不要求所在目录存在该文件 +- 量子声明:标量 `qubit q;` 与一维寄存器 `qubit[2] q;` +- 经典声明:`bit`、`bit[n]`、`bool`、`uint[n]` +- 角度输入声明,如 `input angle[64] theta;`,作为线路的符号参数 +- 映射到 `StandardGate` 的标准门:`id`、`i`、`x`、`y`、`z`、`h`、`s`、`sdg`、`t`、`tdg`、`rx`、`ry`、`rz`、`p`、`phase`、`u1`、`u`、`U`、`u3`、`u2`、`cx`、`cy`、`cz`、`swap`、`ccx`、`crx`、`cry`、`crz`,以及 `sx`、`sxdg`(分别对应 X2P、X2M) +- Cqlib 扩展门:`x2p`、`x2m`、`y2p`、`y2m`、`xy2p`、`xy2m`、`rxx`、`ryy`、`rzz`、`rzx`、`fsim`、`gphase` +- 自定义门 `gate` 的定义与调用,含带参数的自定义门 +- 测量:`c = measure q;`、`v[0] = measure q[0];`,以及独立的 `measure q;` +- 指令 `reset`、`barrier` +- 全局相位 `gphase(theta);` +- 控制流:`if` / `else`、可静态展开的 `for`、常量分支的 `switch` + +## 与 OpenQASM 2.0 接口的差异 + +在同一 `Circuit` 表示之上,本接口与 OpenQASM 2.0 接口的主要差异: + +| 方面 | OpenQASM 2.0 接口 | OpenQASM 3.0 接口 | +|---|---|---| +| 头部与标准库 | `OPENQASM 2.0;`、`include "qelib1.inc";` | `OPENQASM 3;` 或 `OPENQASM 3.0;`、`include "stdgates.inc";` | +| 量子声明 | `qreg q[2];` | `qubit[2] q;`,并支持标量 `qubit q;` | +| 经典声明 | `creg c[2];` | `bit`、`bit[n]`、`bool`、`uint[n]` | +| 测量 | `measure q[0] -> c[0];` | `c[0] = measure q[0];`、`c = measure q;` | +| 控制流 | `if (c == 1) ...` 后接单条语句 | `if` / `else` 块、可静态展开的 `for`、常量分支的 `switch` | +| 全局相位 | 导出时仅以注释形式给出,不可加载 | `gphase(theta);`,可加载、可导出 | +| 输入声明 | 无 | 角度与浮点 `input` 声明 | + +## 限制 + +以下 OpenQASM 3.0 特性在当前实现中不支持,遇到时给出明确错误,不做部分降级: + +- 定时与脉冲:`delay`、`box`、`cal`、`defcal` +- 子程序与外部调用:`def` 子程序、`extern` +- 硬件相关:硬件量子比特、`output` 声明,以及除角度与浮点以外的 `input` 声明 +- 其它语句:`alias`、`pragma`、注解、旧式声明 +- 门修饰符:`negctrl`、`pow`;`ctrl` 与 `inv` 仅在目标门存在对应实现时可用 +- 部分标准门:`ch`、`cp`、`cu`、`cswap` +- 控制流:`while` 在实现中存在下降路径,但运行时条件的循环当前会被语义检查拒绝;`for` 只支持范围为常量的静态展开形式 +- 复杂经典算术、复杂左值切片、多维索引 +- 作用域内的量子比特声明 + +导出侧另有约束: + +- 不导出 `delay`、仅矩阵形式的 `UnitaryGate`、写入标量 `bit` 变量的普通赋值,以及门定义体内的测量 +- 不导出 `while`、`for`、`break`、`continue` 等控制流 +- 不导出 `XY`、`RXY` 两种标准门 + +## 往返行为 + +导出结果可被本接口重新加载。规范化规则: + +- 头部固定为 `OPENQASM 3.0;` 与 `include "stdgates.inc";`,与输入中的头部写法无关。 +- 顶层量子比特统一导出为 `qubit[n] q`,单比特情形同样写作 `qubit[1] q`。 +- 用户经典变量按 `c0`、`c1`… 命名,不可变的测量值按 `v0`、`v1`… 命名。 +- 测量紧接兼容的存储时折叠为赋值形式,如 `c0 = measure q;`。 +- 独立测量写入自动生成的 `bit[k] meas` 寄存器,如 `meas[0] = measure q[0];`。 +- 非连续或重排的寄存器测量拆分为逐位赋值,如 `c0[0] = measure q[2];`。 +- `stdgates.inc` 已提供的门直接调用;扩展门在主线路前生成一次 `gate` 定义,调用处仍使用其名称。 +- 全局相位以 `gphase(...);` 语句导出,重新加载后进入电路的全局相位。 +- 自动生成的名称会避开电路中已有的门名与寄存器名。 + +示例: + +```python +from cqlib.ir import qasm3 + +code = """OPENQASM 3; +include "stdgates.inc"; +qubit[2] q; +h q[0]; +cx q[0], q[1]; +""" + +circuit = qasm3.loads(code) +print(qasm3.dumps(circuit)) +``` + +输出: + +```text +OPENQASM 3.0; +include "stdgates.inc"; + +qubit[2] q; + +h q[0]; +cx q[0],q[1]; +``` + +## 常见流程 + +```python +from cqlib.ir import qasm3 + +code = """OPENQASM 3; +include "stdgates.inc"; +qubit[2] q; +h q[0]; +cx q[0], q[1]; +""" + +circuit = qasm3.loads(code) +print(qasm3.dumps(circuit)) + +qasm3.dump(circuit, "input.qasm") + +# 1) 读取 QASM 文件 +c = qasm3.load("input.qasm") + +# 2) 处理电路(示例) +c2 = c.decompose() + +# 3) 导出为字符串或文件 +text = qasm3.dumps(c2) +qasm3.dump(c2, "output.qasm") +``` diff --git a/docs/api/cn/python/2_device/0_overview.md b/docs/api/cn/python/2_device/0_overview.md new file mode 100644 index 00000000..29d8863c --- /dev/null +++ b/docs/api/cn/python/2_device/0_overview.md @@ -0,0 +1,257 @@ +# 设备 + +`cqlib.device` + +`cqlib.device` 描述量子设备的静态信息与任务执行状态:比特与耦合构成的拓扑、比特与边的校准属性、逻辑比特到物理比特的布局、噪声模型,以及执行结果与状态。 + +## Overview + +设备模块把硬件相关的数据拆成几组互相独立的对象:拓扑只描述连接关系,属性只描述标定数据,布局只描述映射,噪声模型只描述信道。编译与执行各自按需取用,互不耦合。 + +### 拓扑 + +`Topology` 由物理比特集合与耦合列表构成。耦合是有向的:`(control, target)` 上有耦合,不代表反方向也有,因此连接性查询分成 `successors` / `predecessors`(有向)与 `neighbors_undirected`(无向)两类,度数查询也分成 `out_degree` / `in_degree`。耦合可以带一个名字标签,仅作标识之用。 + +### 设备与属性 + +`Device` 在拓扑之上叠加设备级默认值与局部覆盖,并记录设备名、已注册比特、不可用比特、原生门集与校准时间。比特级属性由 `QubitProp` 描述(`t1`、`t2`、`frequency`、读出误差与读出混淆概率),边级属性由 `EdgeProp` 描述,两者都持有一组 `InstructionProp`(指令、误差率与可选的时长)。 + +查询某个比特的属性时,局部值优先,未设置则回退到设备级默认值:`device.get_t1(0)` 取局部值,`device.get_t1(2)` 取 `default_t1`。设备还提供 `line`、`ring`、`star`、`grid` 等静态构造方法,按常见形态直接生成拓扑与比特集合。 + +### 原生指令能力 + +设备级 `native_gates` 是全局默认能力;某个比特或某条有向边若通过 `add_native_instruction` 配置了自己的指令列表,该列表就是此处能力的完整覆盖,不会回写默认值。校准数据只让受支持的指令带上误差率与时长,不会让不受支持的指令变为受支持。`validate_operation` / `validate_circuit` 用同一套规则校验线路是否能在该设备上执行。 + +### 布局 + +`Layout` 维护逻辑比特到物理比特的映射,未承载逻辑比特的物理比特是空闲物理比特。映射可以按 `l2p_map` / `p2l_map` 整体读取,也可以按单个比特查询;`swap_physical` 交换两个物理位置上承载的逻辑比特,这是路由过程中移动逻辑比特的基本操作。 + +### 噪声模型 + +`NoiseModel` 按比特与操作登记噪声信道:读出误差用 `ReadoutError` 表示,单比特信道由 `SingleQubitNoise` 的静态方法生成(比特翻转、相位翻转、Pauli、去极化、振幅阻尼、相位阻尼),双比特信道由 `TwoQubitNoise` 生成(去极化、独立信道组合、关联 Pauli)。信道可以用 `to_kraus` 转成 Kraus 算符,供密度矩阵模拟使用。查询时用 `OperationKey` 指定门与比特组合。 + +```python +from cqlib.circuit import StandardGate +from cqlib.device import NoiseModel, ReadoutError, TwoQubitNoise + +model = NoiseModel() +model.add_readout_error(0, ReadoutError(0.1, 0.2)) +model.add_two_qubit_error(StandardGate.CX, 0, 1, TwoQubitNoise.depolarizing(0.02)) +``` + +### 执行结果 + +`ExecutionResult` 记录一次任务的生命周期:创建后处于 `Status.queued()`,`start` 后进入运行态,`finish` / `fail` / `cancel` 结束任务。测量结果以 `Outcome` 表示,它把比特串压缩成 64 位分块,同时支持按位置取位与还原比特串;`counts` 与 `probabilities` 分别是各结果的测量次数与归一化概率。 + +--- + +## 常用入口 + +```python +from cqlib import Qubit +from cqlib.circuit import Instruction, StandardGate +from cqlib.device import Device, QubitProp, Topology + +topo = Topology([0, 1, 2], [(0, 1, "G1"), (1, 2, "G2")]) +device = Device("mock_backend", [Qubit(0), Qubit(1), Qubit(2)], topo) +device.default_t1 = 50.0 +device.native_gates = [Instruction.from_standard_gate(StandardGate.X)] + +qp0 = QubitProp(0.02) +qp0.t1 = 80.0 +device.add_qubit_properties(0, qp0) + +assert device.get_t1(0) == 80.0 # 局部属性 +assert device.get_t1(2) == 50.0 # 回退到设备级默认值 +``` + +--- + +## 核心概念与术语 + +| 术语 | 说明 | +| --- | --- | +| **物理比特** | 设备上的一个物理位置,用 `PhysicalQubit` 表示。 | +| **逻辑比特** | 线路中的一根线,用 `LogicalQubit` 表示;即使编号相同,也与物理比特互相区分。 | +| **拓扑** | 物理比特与它们之间耦合构成的图,由 `Topology` 表示。 | +| **耦合** | 两个物理比特之间可以执行双比特门的连接关系。 | +| **有向耦合** | 带方向的耦合:`(control, target)` 上存在耦合不代表反方向也存在。 | +| **原生指令** | 设备上可以直接执行的指令;设备级集合是默认能力,比特与边可以各自覆盖。 | +| **局部覆盖** | 比特或边的原生指令列表,一旦非空就完整取代设备级默认能力,不回写默认值。 | +| **默认值回退** | 查询比特属性时,该比特没有局部值就返回设备级默认值。 | +| **初始布局** | 逻辑比特到物理比特的初始映射,由 `Layout` 表示。 | +| **空闲物理比特** | 当前没有承载逻辑比特的物理比特。 | +| **噪声信道** | 描述一种噪声作用的算符集合,可用 `to_kraus` 转为 Kraus 算符。 | +| **读出误差** | 测量时把 `\|0>` 读成 1 或把 `\|1>` 读成 0 的概率。 | +| **测量结果** | 一次采样得到的比特串,用 `Outcome` 表示。 | + +--- + +## `cqlib.device` API 概览 + +### 拓扑 + +| 名字 | 简介 | +| --- | --- | +| [`Topology`](1_topology.md) | 物理比特与耦合列表构成的有向图,含增删与连接性查询。 | +| [`Topology.line`](1_topology.md) | 按直线形态构造拓扑的静态方法。 | + +### 设备与属性 + +| 名字 | 简介 | +| --- | --- | +| [`Device`](2_properties_device.md) | 设备对象:拓扑、比特集合、原生门集、校准时间与默认属性。 | +| [`Device.line`](2_properties_device.md) / [`Device.ring`](2_properties_device.md) / [`Device.star`](2_properties_device.md) / [`Device.grid`](2_properties_device.md) | 按常见形态构建设备的静态方法。 | +| [`QubitProp`](2_properties_device.md) | 比特属性:读取误差、混淆概率、`t1` / `t2`、频率与原生指令属性。 | +| [`EdgeProp`](2_properties_device.md) | 边属性:该有向边上的原生指令属性。 | +| [`InstructionProp`](2_properties_device.md) | 指令属性:指令对象、误差率与可选时长。 | +| [`Device.validate_circuit`](2_properties_device.md) / [`Device.validate_operation`](2_properties_device.md) | 按原生能力与拓扑校验线路或单个操作。 | + +### 布局 + +| 名字 | 简介 | +| --- | --- | +| [`Layout`](3_layout.md) | 逻辑比特到物理比特的映射,含绑定、解绑与物理比特交换。 | +| [`Layout.from_pairs`](3_layout.md) | 由 `(逻辑编号, 物理编号)` 对与物理比特总数构造布局。 | +| [`LogicalQubit`](3_layout.md) / [`PhysicalQubit`](3_layout.md) | 逻辑比特与物理比特的类型,字符串形式分别为 `L0` 与 `P11`。 | + +### 噪声模型 + +| 名字 | 简介 | +| --- | --- | +| [`NoiseModel`](4_noise.md) | 按比特与操作登记噪声信道的容器。 | +| [`SingleQubitNoise`](4_noise.md) | 单比特噪声信道,含比特翻转、相位翻转、Pauli、去极化与两类阻尼。 | +| [`TwoQubitNoise`](4_noise.md) | 双比特噪声信道,含去极化、独立组合与关联 Pauli。 | +| [`ReadoutError`](4_noise.md) | 读出误差,分别记录 `P(0\|1)` 与 `P(1\|0)`。 | +| [`OperationKey`](4_noise.md) | 噪声查询的键,由门与比特下标组合而成。 | + +### 执行结果 + +| 名字 | 简介 | +| --- | --- | +| [`ExecutionResult`](5_result.md) | 任务执行结果,含计数、概率与时间戳。 | +| [`Status`](5_result.md) | 任务状态:排队、运行、完成、失败与取消。 | +| [`Outcome`](5_result.md) | 紧凑的测量结果比特串,支持按位访问与还原。 | + +--- + +## 快速示例 + +### 1. 构建设备并配置属性 + +```python +from cqlib import Qubit +from cqlib.circuit import Instruction, StandardGate +from cqlib.device import Device, EdgeProp, InstructionProp, QubitProp, Topology + +topo = Topology([0, 1, 2], [(0, 1, "G1"), (1, 2, "G2")]) +device = Device("mock_backend", [Qubit(0), Qubit(1), Qubit(2)], topo) +device.default_t1 = 50.0 +device.default_t2 = 35.0 +device.default_readout_error = 0.05 +device.native_gates = [ + Instruction.from_standard_gate(StandardGate.X), + Instruction.from_standard_gate(StandardGate.CX), +] + +qp0 = QubitProp(0.02) +qp0.t1 = 80.0 +qp0.t2 = 70.0 +device.add_qubit_properties(0, qp0) + +ep01 = EdgeProp() +cx_inst = Instruction.from_standard_gate(StandardGate.CX) +ep01.add_native_instruction(InstructionProp(cx_inst, 0.06)) +device.add_edge_properties(0, 1, ep01) +``` + +写入不存在的比特或边会被拒绝: + +```python +import pytest + +with pytest.raises(ValueError): + device.add_qubit_properties(9, QubitProp(0.01)) + +with pytest.raises(ValueError): + device.add_edge_properties(0, 9, EdgeProp()) +``` + +### 2. 建立布局并交换物理比特 + +```python +from cqlib import Qubit +from cqlib.device import Layout, PhysicalQubit + +layout = Layout( + logical=[0, 1], physical=[10, 11, 12], init_map={Qubit(0): Qubit(11)} +) +assert layout.num_logical == 2 +assert layout.num_physical == 3 +assert layout.num_vacant_physical == 1 +assert layout.get_physical(0) == PhysicalQubit(11) + +on_11 = layout.get_logical(11) +on_12 = layout.get_logical(12) + +layout.swap_physical(11, 12) +assert layout.get_logical(11) == on_12 +assert layout.get_logical(12) == on_11 +``` + +映射表可以整体读取: + +```python +from cqlib import Qubit +from cqlib.device import Layout, LogicalQubit, PhysicalQubit + +layout = Layout(logical=[0], physical=[10, 11], init_map={Qubit(0): Qubit(11)}) +assert layout.l2p_map == {LogicalQubit(0): PhysicalQubit(11)} +assert layout.p2l_map == {PhysicalQubit(11): LogicalQubit(0)} +``` + +### 3. 记录一次执行结果 + +```python +from cqlib import Qubit +from cqlib.device import ExecutionResult + +result = ExecutionResult( + task_id="task-1", + qubits=[Qubit(0), Qubit(1)], + shots=100, + num_qubits=2, + backend="sim", +) +assert result.status.kind == "queued" + +result.start() +assert result.status.kind == "running" + +result.finish({"00": 60, "11": 40}) +assert result.status.kind == "completed" +assert result.counts["00"] == 60 + +result.calc_probabilities() +assert result.probabilities["00"] == 0.6 +``` + +--- + +## 校验与错误处理 + +设备绑定层的失败统一抛 `ValueError`,没有自定义异常类型;异常消息直接来自底层的错误描述。 + +| 异常 | 触发场景 | +| --- | --- | +| `ValueError` | 比特或边不在设备/拓扑中,例如向不存在的比特添加属性、向不存在的方向添加边属性。 | +| `ValueError` | 拓扑与布局的构造或修改冲突:重复比特、重复耦合、自耦合、逻辑比特重复或已绑定、物理比特已被占用等。 | +| `ValueError` | 概率类参数不合法:误差率、读出误差、噪声信道参数超出 `[0, 1]` 或不是有限值;Pauli 信道的三个概率之和超过 1。 | +| `ValueError` | 时间类参数不合法:`t1` / `t2` / `frequency` 必须为有限正数。 | +| `ValueError` | 原生指令不合法:设备级原生门只接受至多两比特的标准门,比特级与边级分别只接受单比特与双比特标准门;被拒绝时设备保留原有能力。 | +| `ValueError` | `validate_operation` / `validate_circuit` 校验失败:比特不可用、缺少有向耦合、指令不在原生门集内或指令尚未分解。 | +| `ValueError` | 测量数据不合法:`Outcome` 的比特串含非 `0` / `1` 字符;`finish` 或 `from_counts` 的键长度不等于 `num_qubits`,或不同键折叠到同一个结果。 | +| `ValueError` | `set_calibration_time` 传入的时间超出可表示范围。 | +| `TypeError` / `OverflowError` | 参数类型或取值域不符,例如比特编号传入非整数或负数。 | + +概率类参数在写入时即校验,因此非法值不会落到设备或噪声模型对象上。 diff --git a/docs/api/cn/python/2_device/1_topology.md b/docs/api/cn/python/2_device/1_topology.md new file mode 100644 index 00000000..811d72db --- /dev/null +++ b/docs/api/cn/python/2_device/1_topology.md @@ -0,0 +1,125 @@ +# Topology + +`cqlib.device.Topology` 用于描述硬件物理比特与耦合边关系。 +`cqlib` 顶层也重导出了同一类型,可从 `cqlib` 直接导入。 + +## 导入 + +```python +from cqlib.device import Topology +``` + +--- + +## 构造 + +### `Topology(qubits, couplings)` + +参数: + +- `qubits` (`list[int | Qubit | PhysicalQubit]`):物理比特列表。 +- `couplings` (`list[tuple[control, target, name]]`):有向耦合列表,每项为 `(control, target, name)` 三元组。 + - `control -> target`:耦合方向。 + - `name`:耦合的名称标签,仅作标识,不参与连接性与重复判定。 + +说明: + +- 拓扑按有向边语义存储,`(control, target)` 与 `(target, control)` 是两条不同耦合。 +- 同一有序比特对只允许一条耦合,名称不同也算重复;重复比特与自耦合都会被拒绝。 + +### `Topology.line(qubits)` + +静态方法,按给定顺序构造有向直线拓扑 `qubits[0] -> qubits[1] -> ...`,耦合数为 `len(qubits) - 1`,耦合名为空串。 + +异常情况: + +- `ValueError`:比特列表中存在重复比特。 + +## 属性 + +- `num_qubits -> int` +- `num_couplings -> int` +- `qubits -> list[PhysicalQubit]` + +## 修改方法 + +### `add_qubits(qubits)` + +添加物理比特。 + +异常情况: + +- `ValueError`:待添加列表中比特重复,或与拓扑中已有比特重复。 + +### `add_couplings(couplings)` + +添加有向耦合,参数为 `(control, target, name)` 三元组列表。 + +异常情况: + +- `ValueError`:端点比特不在拓扑中、耦合已存在或出现自耦合。 + +### `remove_qubits(qubits)` + +删除物理比特(关联耦合边会被一并删除)。 + +异常情况: + +- `ValueError`:待删除比特不存在或重复出现。 + +### `remove_couplings(couplings)` + +删除有向耦合。参数格式为 `list[tuple[control, target]]`,只删除指定方向,反向耦合不受影响。 + +异常情况: + +- `ValueError`:待删除耦合不存在或重复出现。 + +## 查询方法 + +- `supports_directed_coupling(control, target) -> bool`:`control -> target` 方向是否存在耦合。 +- `supports_coupling_either_direction(a, b) -> bool`:两个比特之间任一方向是否存在耦合。 +- `successors(qubit) -> list[PhysicalQubit]`:出边邻居。 +- `predecessors(qubit) -> list[PhysicalQubit]`:入边邻居。 +- `neighbors_undirected(qubit) -> list[PhysicalQubit]`:无向邻居,两个方向只出现一次。 +- `undirected_edges() -> list[tuple[PhysicalQubit, PhysicalQubit]]`:无向边列表,每对按键值排序,方向相反的两条耦合折叠为一条。 +- `get_coupling_name(control, target) -> str | None`:耦合名,耦合不存在时为 `None`。 +- `contains_qubit(qubit) -> bool`:比特是否在拓扑中。 +- `out_degree(qubit) -> int`:出边数。 +- `in_degree(qubit) -> int`:入边数。 + +说明: + +- `successors` / `out_degree` 基于出边统计,`predecessors` / `in_degree` 基于入边统计。 +- 比特不在拓扑中时,邻居类查询返回空列表,度数查询返回 `0`,不抛异常。 + +## 其他行为 + +- 支持 `copy`、`deepcopy`。 +- `repr(topology) == "Topology(num_qubits=3, num_couplings=2)"`。 + +## 示例 + +```python +from cqlib.device import PhysicalQubit, Topology + +topo = Topology([0, 1, 2], [(0, 1, "G1"), (1, 2, "G2")]) + +assert topo.num_qubits == 3 +assert topo.num_couplings == 2 +assert topo.supports_directed_coupling(1, 2) is True +assert topo.supports_directed_coupling(2, 1) is False +assert topo.supports_coupling_either_direction(2, 1) is True +assert topo.get_coupling_name(1, 2) == "G2" +assert set(topo.successors(1)) == {PhysicalQubit(2)} +assert set(topo.neighbors_undirected(1)) == {PhysicalQubit(0), PhysicalQubit(2)} +assert topo.out_degree(1) == 1 +assert topo.in_degree(1) == 1 + +topo.add_qubits([3]) +topo.add_couplings([(2, 3, "G2")]) +assert topo.supports_coupling_either_direction(2, 3) is True +topo.remove_couplings([(2, 3)]) +topo.remove_qubits([3]) +assert topo.num_qubits == 3 +``` diff --git a/docs/api/cn/python/2_device/2_properties_device.md b/docs/api/cn/python/2_device/2_properties_device.md new file mode 100644 index 00000000..2b1dbe97 --- /dev/null +++ b/docs/api/cn/python/2_device/2_properties_device.md @@ -0,0 +1,219 @@ +# Device / Properties + +本页覆盖 `cqlib.device` 中用于设备标定建模的核心类型: + +- `InstructionProp` +- `QubitProp` +- `EdgeProp` +- `Device` + +## 导入 + +```python +from cqlib.circuit import Instruction, StandardGate +from cqlib.device import Device, EdgeProp, InstructionProp, QubitProp, Topology +``` + +--- + +## InstructionProp + +指令属性:一条指令、它的误差率与可选时长。 + +### `InstructionProp(instruction, error_rate)` + +参数: + +- `instruction` (`Instruction`):指令对象。 +- `error_rate` (`float`):误差率,取值需在 `[0, 1]` 内。 + +异常情况: + +- `ValueError`:`error_rate` 超出 `[0, 1]` 或不是有限值。 + +### 属性 + +- `instruction -> Instruction`:指令对象,可读写。 +- `error_rate -> float`:误差率,可读写,写入时按 `[0, 1]` 校验。 +- `length -> float | None`:指令时长,可读写,写入时要求非负有限值。 + +### 其他行为 + +- 支持 `copy`、`deepcopy`。 +- `repr(prop) == "InstructionProp(instruction=X, error_rate=0.01)"`,设置 `length` 后追加 `length=...`。 + +## QubitProp + +比特属性:读出误差、读出混淆概率、`t1` / `t2`、频率与原生指令属性。 + +### `QubitProp(readout_error)` + +参数: + +- `readout_error` (`float`):读出误差,取值需在 `[0, 1]` 内。 + +异常情况: + +- `ValueError`:`readout_error` 超出 `[0, 1]` 或不是有限值。 + +### 属性 + +- `readout_error -> float`:只读。 +- `prob_meas0_prep1 -> float | None`:可读写,取值需在 `[0, 1]` 内。 +- `prob_meas1_prep0 -> float | None`:可读写,取值需在 `[0, 1]` 内。 +- `t1 -> float | None`:可读写,要求有限正数。 +- `t2 -> float | None`:可读写,要求有限正数。 +- `frequency -> float | None`:可读写,要求有限正数。 +- `native_instructions -> list[InstructionProp]`:只读,本比特的原生指令属性。 + +### 方法 + +- `add_native_instruction(prop) -> None`:追加一条原生指令属性。 + +异常情况: + +- `ValueError`:概率类参数非法、`t1` / `t2` / `frequency` 不是有限正数,或追加的指令不是单比特标准门。 +- 写入失败时原有值保持不变。 + +## EdgeProp + +边属性:一条有向边上的原生指令属性。 + +### `EdgeProp()` + +无参构造。 + +### 方法 + +- `add_native_instruction(prop) -> None`:追加一条原生指令属性。 + +异常情况: + +- `ValueError`:追加的指令不是双比特标准门,或其误差率、时长非法。 + +### 属性 + +- `native_instructions -> list[InstructionProp]`:只读。 + +## Device + +设备对象:拓扑、已登记比特集合、原生门集、校准时间与设备级默认值。 + +### `Device(name, qubits, topology)` + +参数: + +- `name` (`str`):设备名。 +- `qubits` (`list[int | Qubit | PhysicalQubit]`):已登记的物理比特。 +- `topology` (`Topology`):设备拓扑。 + +异常情况: + +- `ValueError`:拓扑中存在未登记在 `qubits` 中的比特。 + +### 静态构造方法 + +- `Device.line(name, num_qubits) -> Device`:比特 `0..num_qubits` 组成的有向直线。 +- `Device.line_from_qubits(name, physical_qubits) -> Device`:按给定物理比特顺序连成有向直线。 +- `Device.bidirectional_line(name, num_qubits) -> Device`:双向直线。 +- `Device.ring(name, num_qubits) -> Device`:双向环。 +- `Device.star(name, num_qubits, center) -> Device`:以 `center` 为中心的双向星形。 +- `Device.grid(name, rows, cols) -> Device`:`rows × cols` 双向网格,比特编号按行优先排列。 +- `Device.from_edges(name, num_qubits, edges) -> Device`:比特 `0..num_qubits` 加显式有向边 `list[tuple[int, int]]`。 + +### 属性 + +- `name -> str`:只读。 +- `qubits -> list[PhysicalQubit]`:只读,已登记的物理比特。 +- `invalid_qubits -> list[PhysicalQubit]`:可读写;写入的值必须已登记在设备上,否则拒绝并保留原值。 +- `topology -> Topology`:只读。 +- `native_gates -> list[Instruction]`:可读写;写入时只接受至多两比特的标准门。 +- `usable_qubits -> list[PhysicalQubit]`:只读,已登记且未标记为不可用的比特。 +- `num_usable_qubits -> int`:只读。 +- `default_t1 -> float | None`:可读写,设备级默认 `t1`。 +- `default_t2 -> float | None`:可读写,设备级默认 `t2`。 +- `default_readout_error -> float | None`:可读写,设备级默认读出误差。 +- `default_single_qubit_error -> float | None`:可读写,设备级默认单比特误差率。 +- `default_two_qubit_error -> float | None`:可读写,设备级默认双比特误差率。 +- `calibration_time -> datetime | None`:只读,校准时间戳,纳秒精度。 + +### 写入方法 + +- `set_calibration_time(datetime) -> None`:写入校准时间戳。 +- `add_qubit_properties(qubit, props) -> None`:写入某个比特的属性。 +- `add_edge_properties(control, target, props) -> None`:写入某条有向边的属性。 + +异常情况: + +- `ValueError`:比特不在设备中或不在拓扑中、有向边不在拓扑中、默认值非法,或时间超出可表示范围。 +- 三类写入失败时设备保留原有值。 + +### 查询方法 + +- `qubit_properties(qubit) -> QubitProp | None` +- `edge_properties(control, target) -> EdgeProp | None` +- `get_t1(qubit) -> float | None` +- `get_t2(qubit) -> float | None` +- `get_readout_error(qubit) -> float | None` +- `single_qubit_error(qubit, instruction) -> float | None` +- `two_qubit_error(control, target, instruction) -> float | None` +- `edge_error(control, target) -> float | None` +- `is_usable_qubit(qubit) -> bool` +- `supports_native_instruction(instruction, qargs) -> bool` +- `validate_operation(operation) -> None` +- `validate_circuit(circuit) -> None` + +说明: + +- `get_t1` / `get_t2` / `get_readout_error` 优先返回局部属性,缺失时回退设备级默认值。 +- `single_qubit_error` / `two_qubit_error` 优先取局部原生指令属性,缺失时回退默认的单比特 / 双比特误差率;比特不可用或该方向不存在时返回 `None`。 +- `edge_error` 只读标定数据,取该有向边上原生指令的最小误差率。 +- `validate_operation` / `validate_circuit` 校验失败时抛 `ValueError`。 + +### 其他行为 + +- 支持 `copy`、`deepcopy`。 +- `repr(device) == "Device(name='mock_backend')"`。 + +## 示例 + +```python +from cqlib import Qubit +from cqlib.circuit import Instruction, StandardGate +from cqlib.device import Device, EdgeProp, InstructionProp, QubitProp, Topology + +topo = Topology([0, 1, 2], [(0, 1, "G1"), (1, 2, "G2")]) +device = Device("mock_backend", [Qubit(0), Qubit(1), Qubit(2)], topo) + +device.default_t1 = 50.0 +device.default_t2 = 35.0 +device.default_readout_error = 0.05 +device.native_gates = [ + Instruction.from_standard_gate(StandardGate.X), + Instruction.from_standard_gate(StandardGate.CX), +] + +x_inst = Instruction.from_standard_gate(StandardGate.X) +cx_inst = Instruction.from_standard_gate(StandardGate.CX) + +qp0 = QubitProp(0.02) +qp0.t1 = 80.0 +qp0.t2 = 70.0 +qp0.add_native_instruction(InstructionProp(x_inst, 0.001)) +device.add_qubit_properties(0, qp0) + +ep01 = EdgeProp() +ep01.add_native_instruction(InstructionProp(cx_inst, 0.02)) +device.add_edge_properties(0, 1, ep01) + +assert device.get_t1(0) == 80.0 +assert device.get_t1(2) == 50.0 # 回退到设备级默认值 +assert device.single_qubit_error(0, x_inst) == 0.001 +assert device.two_qubit_error(0, 1, cx_inst) == 0.02 +assert device.edge_error(0, 1) == 0.02 +assert device.num_usable_qubits == 3 + +device.invalid_qubits = [2] +assert device.is_usable_qubit(2) is False +assert device.num_usable_qubits == 2 +``` diff --git a/docs/api/cn/python/2_device/3_layout.md b/docs/api/cn/python/2_device/3_layout.md new file mode 100644 index 00000000..20057622 --- /dev/null +++ b/docs/api/cn/python/2_device/3_layout.md @@ -0,0 +1,115 @@ +# Layout + +`Layout` 用于维护逻辑比特到物理比特的映射。 + +## 导入 + +```python +from cqlib.device import Layout +``` + +--- + +## 构造函数 + +### `Layout(logical, physical, init_map=None)` + +参数: + +- `logical` (`list[int | Qubit | LogicalQubit]`):逻辑比特列表。 +- `physical` (`list[int | Qubit | PhysicalQubit]`):物理比特列表,可以多于逻辑比特。 +- `init_map` (`dict[LogicalQubit, PhysicalQubit] | None`):可选初始映射(逻辑 -> 物理),键值也可用 `int` 或 `Qubit`。 + +异常情况: + +- `ValueError`:逻辑比特多于物理比特、逻辑比特或物理比特重复、映射中的比特不属于布局,或同一逻辑比特、物理比特被重复映射。 + +### `Layout.from_pairs(pairs, physical_count)` + +静态方法,由 `(逻辑编号, 物理编号)` 对与物理比特总数构造布局。物理比特为 `0..physical_count`,未被 `pairs` 引用的物理比特是空闲物理比特。 + +异常情况: + +- `ValueError`:逻辑编号重复、物理编号重复,或物理编号超出 `0..physical_count`。 + +## 属性 + +- `num_logical -> int` +- `num_physical -> int` +- `num_vacant_physical -> int` +- `logical_qubits -> list[LogicalQubit]` +- `physical_qubits -> list[PhysicalQubit]` +- `vacant_physical_qubits -> list[PhysicalQubit]` +- `l2p_map -> dict[LogicalQubit, PhysicalQubit]` +- `p2l_map -> dict[PhysicalQubit, LogicalQubit]` + +## 方法 + +### `get_physical(logical_id) -> PhysicalQubit | None` + +根据逻辑比特查物理比特,未绑定时返回 `None`。 + +### `get_logical(physical_id) -> LogicalQubit | None` + +根据物理比特查逻辑比特,物理比特空闲时返回 `None`。 + +### `is_physical_vacant(physical_id) -> bool` + +物理比特是否空闲。 + +### `bind(logical_id, physical_id) -> None` + +把逻辑比特绑定到空闲物理比特。 + +异常情况: + +- `ValueError`:物理比特不属于布局、逻辑比特已绑定,或物理比特已被占用。 + +### `unbind(logical_id) -> PhysicalQubit` + +解除逻辑比特的绑定,返回释放出的物理比特。 + +异常情况: + +- `ValueError`:该逻辑比特未绑定。 + +### `swap_physical(phys_a, phys_b) -> None` + +交换两个物理比特上承载的逻辑比特,这是路由过程中移动逻辑比特的基本操作。任一物理比特可以空闲:与空闲物理比特交换会把逻辑比特搬到空闲位置。 + +异常情况: + +- `ValueError`:`phys_a` 或 `phys_b` 不在布局的物理比特集合中。 + +## 其他行为 + +- 支持 `copy`、`deepcopy`。 +- `repr(layout) == "Layout(num_logical=2, num_vacant_physical=1, num_physical=3)"`。 + +## 示例 + +```python +from cqlib import Qubit +from cqlib.device import Layout, LogicalQubit, PhysicalQubit + +layout = Layout(logical=[0, 1], physical=[10, 11, 12], init_map={Qubit(0): Qubit(11)}) + +print(layout.num_logical) # 2 +print(layout.num_physical) # 3 +print(layout.num_vacant_physical) # 1 + +assert layout.get_physical(0) == PhysicalQubit(11) +assert layout.get_logical(11) == LogicalQubit(0) +assert layout.get_logical(12) is None +assert layout.is_physical_vacant(12) is True + +before_11 = layout.get_logical(11) +before_12 = layout.get_logical(12) +layout.swap_physical(11, 12) +assert layout.get_logical(12) == before_11 +assert layout.get_logical(11) == before_12 + +released = layout.unbind(0) +assert released == PhysicalQubit(12) +assert layout.is_physical_vacant(12) is True +``` diff --git a/docs/api/cn/python/2_device/4_noise.md b/docs/api/cn/python/2_device/4_noise.md new file mode 100644 index 00000000..28930a04 --- /dev/null +++ b/docs/api/cn/python/2_device/4_noise.md @@ -0,0 +1,194 @@ +# NoiseModel / Noise Channels + +本页覆盖 `cqlib.device` 中噪声建模相关 API: + +- `SingleQubitNoise` +- `TwoQubitNoise` +- `ReadoutError` +- `OperationKey` +- `NoiseModel` + +## 导入 + +```python +from cqlib.circuit import StandardGate +from cqlib.device import ( + NoiseModel, + OperationKey, + ReadoutError, + SingleQubitNoise, + TwoQubitNoise, +) +``` + +--- + +## SingleQubitNoise + +### 静态构造方法 + +- `bit_flip(p) -> SingleQubitNoise`:比特翻转信道,以概率 `p` 作用 X。 +- `phase_flip(p) -> SingleQubitNoise`:相位翻转信道,以概率 `p` 作用 Z。 +- `pauli(px, py, pz) -> SingleQubitNoise`:一般 Pauli 信道,三个概率需满足 `px + py + pz <= 1`。 +- `depolarizing(p) -> SingleQubitNoise`:去极化信道,以概率 `p` 作用随机 Pauli 错误,三种 Pauli 各占 `p/3`。 +- `amplitude_damping(gamma) -> SingleQubitNoise`:振幅阻尼信道。 +- `phase_damping(lambda_) -> SingleQubitNoise`:相位阻尼信道。 + +异常情况: + +- `ValueError`:概率不在 `[0, 1]` 内或不是有限值;`pauli` 的三个概率之和超过 1。 + +### 方法与属性 + +- `is_valid() -> bool`:噪声参数是否物理有效。 +- `to_kraus() -> list[numpy.ndarray]`:Kraus 算符列表,每个为 2×2 复数矩阵。 + +说明: + +- 信道种类由静态构造方法决定,没有 `kind` 属性。 + +### 其他行为 + +- 支持 `==`:构造方法与参数相同的两个信道相等。 +- `repr(SingleQubitNoise.bit_flip(0.01)) == "SingleQubitNoise.bit_flip(0.01)"`。 + +## TwoQubitNoise + +### 静态构造方法 + +- `depolarizing(p) -> TwoQubitNoise`:双比特去极化信道。 +- `independent(q0_noise, q1_noise) -> TwoQubitNoise`:两个单比特信道的独立组合。 +- `correlated_pauli(op_q0, op_q1, p) -> TwoQubitNoise`:关联 Pauli 信道,`op_q0` / `op_q1` 为 `Pauli` 对象,例如 `Pauli.x()`、`Pauli.z()`。 + +异常情况: + +- `ValueError`:`p` 不在 `[0, 1]` 内或不是有限值。 +- `TypeError`:`op_q0` / `op_q1` 不是 `Pauli` 对象。 + +### 方法与属性 + +- `is_valid() -> bool` +- `to_kraus() -> list[numpy.ndarray]`:Kraus 算符列表,每个为 4×4 复数矩阵。 +- `kind -> str`:`depolarizing` / `independent` / `correlated_pauli`。 + +### 其他行为 + +- 支持 `==`。 + +## ReadoutError + +### `ReadoutError(p_0_given_1, p_1_given_0)` + +参数: + +- `p_0_given_1` (`float`):真实态为 1 却读成 0 的概率。 +- `p_1_given_0` (`float`):真实态为 0 却读成 1 的概率。 + +异常情况: + +- `ValueError`:概率不在 `[0, 1]` 内或不是有限值。 + +### 属性 + +- `p_0_given_1 -> float` +- `p_1_given_0 -> float` + +### 方法 + +- `is_valid() -> bool` + +### 其他行为 + +- 支持 `==`。 +- `repr(ReadoutError(0.1, 0.2)) == "ReadoutError(p_0_given_1=0.1, p_1_given_0=0.2)"`。 + +## OperationKey + +### 静态构造方法 + +- `new_single(gate, q0) -> OperationKey` +- `new_double(gate, q0, q1) -> OperationKey` +- `new_triple(gate, q0, q1, q2) -> OperationKey` + +异常情况: + +- `ValueError`:多比特门的比特有重复。 + +### 属性 + +- `gate -> StandardGate`:门类型。键只记录门类型,参数化门返回的对象参数一律为 0。 +- `qubits -> list[int]`:参与操作的比特下标。 + +说明: + +- 实现了 `__eq__` 与 `__hash__`,可作为字典键使用;门或比特组合不同则键不相等。 +- `repr(OperationKey.new_single(StandardGate.X, 0)) == "OperationKey(gate=X, qubits=[0])"`。 + +## NoiseModel + +### `NoiseModel()` + +### 写入方法 + +- `add_readout_error(qubit, error) -> None`:为某个比特登记读出误差。 +- `add_single_qubit_error(gate, qubit, noise) -> None`:为某个单比特门在指定比特上登记噪声信道。 +- `add_two_qubit_error(gate, q0, q1, noise) -> None`:为某个双比特门在指定比特对上登记噪声信道。 + +异常情况: + +- `ValueError`:噪声参数非法,或双比特门的两个比特相同。 + +### 查询方法 + +- `get_readout_error(qubit) -> ReadoutError | None` +- `get_single_qubit_errors(key) -> list[SingleQubitNoise] | None` +- `get_two_qubit_errors(key) -> list[TwoQubitNoise] | None` + +说明: + +- 同一个门与比特组合可以登记多条信道,查询返回列表;未登记的组合返回 `None`。 + +## 示例 + +```python +import pytest +from cqlib.circuit import StandardGate +from cqlib.device import ( + NoiseModel, + OperationKey, + ReadoutError, + SingleQubitNoise, + TwoQubitNoise, +) +from cqlib.qis import Pauli + +nm = NoiseModel() +nm.add_readout_error(0, ReadoutError(0.1, 0.2)) +nm.add_single_qubit_error(StandardGate.X, 0, SingleQubitNoise.bit_flip(0.01)) +nm.add_single_qubit_error(StandardGate.X, 0, SingleQubitNoise.phase_flip(0.02)) +nm.add_two_qubit_error(StandardGate.CX, 0, 1, TwoQubitNoise.depolarizing(0.02)) +nm.add_two_qubit_error( + StandardGate.CX, 0, 1, TwoQubitNoise.correlated_pauli(Pauli.x(), Pauli.z(), 0.05) +) + +skey = OperationKey.new_single(StandardGate.X, 0) +tkey = OperationKey.new_double(StandardGate.CX, 0, 1) + +assert len(nm.get_single_qubit_errors(skey)) == 2 +assert len(nm.get_two_qubit_errors(tkey)) == 2 +assert nm.get_readout_error(0) == ReadoutError(0.1, 0.2) +assert nm.get_readout_error(9) is None + +assert TwoQubitNoise.depolarizing(0.02).kind == "depolarizing" +assert TwoQubitNoise.correlated_pauli(Pauli.x(), Pauli.z(), 0.05).kind == "correlated_pauli" +assert SingleQubitNoise.depolarizing(0.1).is_valid() is True + +with pytest.raises(ValueError): + SingleQubitNoise.pauli(0.5, 0.5, 0.5) + +with pytest.raises(ValueError): + TwoQubitNoise.correlated_pauli(Pauli.x(), Pauli.z(), 1.5) + +with pytest.raises(TypeError): + TwoQubitNoise.correlated_pauli("X", Pauli.z(), 0.05) +``` diff --git a/docs/api/cn/python/2_device/5_result.md b/docs/api/cn/python/2_device/5_result.md new file mode 100644 index 00000000..e830f4ab --- /dev/null +++ b/docs/api/cn/python/2_device/5_result.md @@ -0,0 +1,170 @@ +# Outcome / Status / ExecutionResult + +本页覆盖量子任务结果相关 API: + +- `Outcome` +- `Status` +- `ExecutionResult` + +## 导入 + +```python +from cqlib.device import ExecutionResult, Outcome, Status +``` + +--- + +## Outcome + +### `Outcome(bitstring)` + +从二进制字符串构造测量结果对象。 + +异常情况: + +- `ValueError`:字符串含非法字符(非 `0/1`)。 + +### 静态方法 + +- `Outcome.from_bitstring(bitstring) -> Outcome`:从二进制字符串构造。 +- `Outcome.from_indices(width, indices) -> Outcome`:构造一个宽度为 `width`、仅在 `indices` 列出的位置上取值为 `1` 的结果。 + +### 方法与属性 + +- `is_one(index) -> bool`:指定位置是否为 `1`。 +- `to_bitstring(num_qubits) -> str`:按给定宽度输出比特串。 +- `chunks -> list[int]`:内部存储的分块。 + +说明: + +- 比特串一律按标准二进制形式书写(最高位在左),而内部按小端分块存储。 +- 位串宽度不由 `Outcome` 保存:解析 `"001"` 与解析 `"1"` 得到同一个 `Outcome`。因此输出宽度必须由调用方通过 `to_bitstring` 指定。 +- `Outcome` 支持 `==` 与 `hash(...)`。 + +### 其他行为 + +- 支持 `==`、`hash(...)`、`copy`、`deepcopy`。 +- `repr(Outcome("101")) == "Outcome(chunks=[5])"`。 + +## Status + +### 静态构造方法 + +- `Status.queued()` +- `Status.running()` +- `Status.completed()` +- `Status.failed(error_msg, error_code)` +- `Status.cancelled()` + +### 属性与方法 + +- `kind -> str`(`queued/running/completed/failed/cancelled`) +- `error_msg -> str | None` +- `error_code -> int | None` +- `is_terminal() -> bool` +- `is_success() -> bool` + +### 其他行为 + +- 支持 `==` 与 `hash(...)`:状态相同且错误信息、错误码相同时才相等;与非 `Status` 对象比较返回 `False`。 +- `repr(Status.completed()) == "Status(completed)"`,`str(Status.completed()) == "Completed"`。 + +## ExecutionResult + +### `ExecutionResult(task_id, qubits, shots, num_qubits, backend=None)` + +参数: + +- `task_id` (`str`) +- `qubits` (`list[int | Qubit]`):被测比特列表。 +- `shots` (`int`) +- `num_qubits` (`int`) +- `backend` (`str | None`) + +构造出的结果处于 `queued` 状态。 + +### `ExecutionResult.from_counts(task_id, qubits, shots, num_qubits, counts, backend=None)` + +静态方法,直接由测量计数构造已完成的结果,等价于依次执行构造方法、`start()`、`finish(counts)` 与 `calc_probabilities()`。 + +`counts` 为 `dict[str, int]`,key 必须是长度等于 `num_qubits` 的合法二进制比特串。 + +异常情况: + +- `ValueError`:`counts` 的比特串非法,或长度不等于 `num_qubits`。 + +### 生命周期方法 + +- `start() -> None` +- `finish(counts) -> None` +- `fail(msg, code) -> None` +- `cancel() -> None` +- `calc_probabilities() -> None` + +`finish(counts)` 中 `counts` 类型为 `dict[str, int]`,key 必须是长度等于 `num_qubits` 的合法二进制比特串;写入计数会清除已计算的概率,需要重新调用 `calc_probabilities()`。`calc_probabilities()` 在计数总数为 0 时不产生概率。 + +异常情况: + +- `ValueError`:`counts` 的比特串非法,或长度不等于 `num_qubits`。 + +### 属性 + +- `task_id -> str` +- `shots -> int` +- `num_qubits -> int` +- `qubits -> list[Qubit]`:被测比特列表。 +- `status -> Status` +- `created_at -> str`:创建时间,ISO 8601 字符串。 +- `started_at -> str | None`:开始时间,ISO 8601 字符串。 +- `finished_at -> str | None`:完成时间,ISO 8601 字符串。 +- `backend -> str | None` +- `counts -> dict[str, int]` +- `probabilities -> dict[str, float] | None`:调用 `calc_probabilities()` 后才可用。 + +说明: + +- `qubits[i]` 对应比特串第 `i` 位(权重 `2**i`);比特串按最高位在左书写,字符串顺序与 `qubits` 顺序相反。 + +### 其他行为 + +- 支持 `copy`、`deepcopy`。 +- `repr(result) == "ExecutionResult(task_id='task-1', status='queued', shots=100, num_qubits=2)"`。 + +## 示例 + +```python +import pytest +from cqlib import Qubit +from cqlib.device import ExecutionResult, Outcome, Status + +outcome = Outcome("101") +assert outcome.is_one(0) is True +assert outcome.is_one(1) is False +assert outcome.to_bitstring(3) == "101" +assert Outcome.from_bitstring("101") == outcome +assert Outcome.from_indices(3, [0, 2]) == outcome + +failed = Status.failed("backend down", 42) +assert failed.kind == "failed" +assert failed.error_code == 42 +assert failed.is_terminal() is True + +result = ExecutionResult("task-1", [Qubit(0), Qubit(1)], 100, 2, "sim") +print(result.status.kind) # queued + +result.start() +result.finish({"00": 60, "11": 40}) +result.calc_probabilities() + +print(result.status.kind) # completed +print(result.counts) # {'00': 60, '11': 40} +print(result.probabilities) # {'00': 0.6, '11': 0.4} + +done = ExecutionResult.from_counts("task-2", [Qubit(0), Qubit(1)], 100, 2, {"01": 60, "10": 40}) +print(done.status.kind) # completed +assert done.counts == {"01": 60, "10": 40} + +with pytest.raises(ValueError): + done.finish({"100": 1}) +``` + diff --git a/docs/api/cn/python/2_device/6_qubits.md b/docs/api/cn/python/2_device/6_qubits.md new file mode 100644 index 00000000..4bbdfaa3 --- /dev/null +++ b/docs/api/cn/python/2_device/6_qubits.md @@ -0,0 +1,156 @@ +# 量子比特标识 + +`cqlib.device` + +`LogicalQubit` 与 `PhysicalQubit` 是设备侧接口使用的强类型比特标识:前者标识线路中的逻辑比特,后者标识设备上的物理比特。两个类型都以数值标识为内部表示,但类型不同,因此数值相同的两个标识不相等。 + +## 导入 + +```python +from cqlib import Qubit +from cqlib.device import LogicalQubit, PhysicalQubit +``` + +--- + +## LogicalQubit + +逻辑比特标识,用于线路中的逻辑比特。 + +### LogicalQubit(id) + +参数: + +- `id` (`int`):数值标识,非负整数。 + +异常情况: + +- `TypeError`:`id` 不是整数。 +- `OverflowError`:`id` 为负数或超出 `u32` 范围。 + +### 属性 + +- `id -> int`:数值标识。 +- `index -> int`:数值标识的索引形式。 +- `qubit -> Qubit`:对应的线路比特。 + +### 其他行为 + +- 支持 `==`、`<`、`<=`、`>`、`>=`、`hash`、`copy`、`deepcopy`。 +- 比较只在本类型之间成立:与 `Qubit` 或 `PhysicalQubit` 比较时判定为不相等,不抛异常。 +- `str(LogicalQubit(0)) == "L0"`;`repr(LogicalQubit(0)) == "LogicalQubit(0)"`。 + +示例: + +```python +from cqlib import Qubit +from cqlib.device import LogicalQubit, PhysicalQubit + +logical = LogicalQubit(0) +assert logical.id == 0 +assert logical.index == 0 +assert logical.qubit == Qubit(0) +assert logical != Qubit(0) +assert logical != PhysicalQubit(0) +assert str(logical) == "L0" +assert repr(logical) == "LogicalQubit(0)" + +assert sorted([LogicalQubit(2), LogicalQubit(0)]) == [LogicalQubit(0), LogicalQubit(2)] +assert LogicalQubit(2) >= LogicalQubit(2) +``` + +--- + +## PhysicalQubit + +物理比特标识,用于设备上的比特位置。 + +### PhysicalQubit(id) + +参数: + +- `id` (`int`):数值标识,非负整数。 + +异常情况: + +- `TypeError`:`id` 不是整数。 +- `OverflowError`:`id` 为负数或超出 `u32` 范围。 + +### 属性 + +- `id -> int`:数值标识。 +- `index -> int`:数值标识的索引形式。 +- `qubit -> Qubit`:对应的线路比特。 + +### 其他行为 + +- 支持 `==`、`<`、`<=`、`>`、`>=`、`hash`、`copy`、`deepcopy`。 +- 比较只在本类型之间成立:与 `Qubit` 或 `LogicalQubit` 比较时判定为不相等,不抛异常。 +- `str(PhysicalQubit(11)) == "P11"`;`repr(PhysicalQubit(11)) == "PhysicalQubit(11)"`。 + +示例: + +```python +from cqlib import Qubit +from cqlib.device import LogicalQubit, PhysicalQubit + +physical = PhysicalQubit(11) +assert physical.id == 11 +assert physical.index == 11 +assert physical.qubit == Qubit(11) +assert physical == PhysicalQubit(11) +assert physical != Qubit(11) +assert physical != LogicalQubit(11) +assert str(physical) == "P11" +assert repr(physical) == "PhysicalQubit(11)" + +assert sorted([PhysicalQubit(2), PhysicalQubit(0)]) == [PhysicalQubit(0), PhysicalQubit(2)] +assert PhysicalQubit(1) < PhysicalQubit(2) +``` + +--- + +## 在布局中的角色 + +布局维护逻辑比特到物理比特的映射,是两个类型配合使用的主要场景:逻辑比特来自线路,物理比特来自设备,映射结果以强类型标识记录,避免两类数值混用。 + +示例: + +```python +from cqlib import Qubit +from cqlib.device import Layout, LogicalQubit, PhysicalQubit + +layout = Layout(logical=[0, 1], physical=[10, 11, 12], init_map={Qubit(0): Qubit(11)}) + +assert layout.num_logical == 2 +assert layout.num_physical == 3 +assert layout.num_vacant_physical == 1 + +assert set(layout.logical_qubits) == {LogicalQubit(0), LogicalQubit(1)} +assert set(layout.physical_qubits) == {PhysicalQubit(10), PhysicalQubit(11), PhysicalQubit(12)} + +assert layout.get_physical(0) == PhysicalQubit(11) +assert layout.get_logical(11) == LogicalQubit(0) +assert layout.l2p_map == {LogicalQubit(0): PhysicalQubit(11), LogicalQubit(1): PhysicalQubit(10)} +assert layout.p2l_map == {PhysicalQubit(10): LogicalQubit(1), PhysicalQubit(11): LogicalQubit(0)} + +before_11 = layout.get_logical(11) +before_12 = layout.get_logical(12) +layout.swap_physical(11, 12) +assert layout.get_logical(11) == before_12 +assert layout.get_logical(12) == before_11 + +layout.unbind(0) +assert layout.get_physical(0) is None +``` + +--- + +## 接受比特参数的接口 + +设备侧接口在接收比特参数时接受 `int` 或标识对象,也接受 `Qubit`,均按数值标识解析;返回的比特一律是强类型标识对象。逻辑比特参数位置接受 `int | LogicalQubit | Qubit`,物理比特参数位置接受 `int | PhysicalQubit | Qubit`。 + +- 布局 `Layout`:构造参数 `logical`、`physical`、`init_map`,以及 `get_physical`、`get_logical`、`bind`、`unbind`、`is_physical_vacant`、`swap_physical`。 +- 拓扑 `Topology`:构造参数 `qubits`、`couplings`,以及 `add_qubits`、`remove_qubits`、`add_couplings`、`remove_couplings`、`contains_qubit`、`successors`、`predecessors`、`neighbors_undirected`、`out_degree`、`in_degree`、`supports_directed_coupling`、`supports_coupling_either_direction`、`get_coupling_name`。 +- 设备 `Device`:构造参数 `qubits` 与 `line_from_qubits` 的参数,以及 `add_qubit_properties`、`add_edge_properties`、`qubit_properties`、`edge_properties`、`get_t1`、`get_t2`、`get_readout_error`、`is_usable_qubit`、`single_qubit_error`、`two_qubit_error`、`edge_error`、`supports_native_instruction`、`set_invalid_qubits`。 +- 噪声 `NoiseModel`:`add_readout_error`、`add_single_qubit_error`、`add_two_qubit_error`、`get_readout_error`;`OperationKey` 的 `new_single`、`new_double`、`new_triple`。 diff --git a/docs/api/cn/python/3_qis/0_overview.md b/docs/api/cn/python/3_qis/0_overview.md new file mode 100644 index 00000000..9339a0b1 --- /dev/null +++ b/docs/api/cn/python/3_qis/0_overview.md @@ -0,0 +1,218 @@ +# 量子信息 + +`cqlib.qis` + +`cqlib.qis` 是 Cqlib 的量子信息模块。它提供量子态的多种表示与模拟器、Pauli 算子代数、以 Hamiltonian 为核心的观测量体系,以及态保真度、迹距离、熵与纠缠度量等分析工具。这些设施既可以独立用于量子信息层面的计算,也可以直接消费 `cqlib.circuit` 中的线路来获得演化结果。 + +## Overview + +量子信息模块解决两类问题:一是**表示与演化**,即把量子态存下来并让门作用上去;二是**度量**,即从态或观测量中提取可比较的数值。二者共享同一套底层算子代数。 + +### 量子态的四种表示 + +四种模拟器的存储代价与适用范围互不相同,选择哪一种取决于线路的门集与需要观察的量: + +- 态矢量用 2^N 个复振幅表示纯态,是最紧凑的表示,但不能描述经典混合与噪声。 +- 密度矩阵用 2^N × 2^N 的矩阵表示混合态,代价高一个量级,但支持量子信道、部分迹与物理性校验。 +- 含噪密度矩阵在密度矩阵之上绑定噪声模型,把 Kraus 噪声按门自动施加,并提供读出噪声下的概率分布。 +- 稳定子表用 N 个对易 Pauli 生成元表示 Clifford 态,存储代价与比特数成线性关系,但只接受 Clifford 门。 + +前三种共享同一套门作用入口(`apply_circuit`、`apply_standard_gate`、单比特与双比特门方法),彼此之间的差异集中在状态存储与噪声处理上。 + +### 比特下标约定 + +四种表示使用同一套比特编号约定:比特 i 对应基态下标的第 i 位,比特 0 为最低位;在位串表示中,编号大的比特位于左侧。这一约定在全部页面中一致,不再重复说明。 + +### 从线路到状态 + +四种模拟器都提供 `from_circuit()` 静态方法,从 |0…0⟩ 出发模拟线路得到最终状态,输入线路保持不变。需要同时拿到最终状态与运行期经典数据时,使用 `StabilizerState.run_circuit()`。 + +### 观测量与时间演化 + +`PauliString` 与 `Hamiltonian` 描述可观测量,两者都可以给出期望值;`Hamiltonian` 还可以通过 Trotter 分解转成演化线路,回到 `cqlib.circuit` 的线路空间中执行。 + +--- + +## 常用入口 + +```python +import math + +from cqlib import Circuit +from cqlib.qis import PauliString +from cqlib.qis.state import Statevector + +circuit = Circuit(2) +circuit.h(0) +circuit.cx(0, 1) + +sv = Statevector.from_circuit(circuit) +assert math.isclose(sv.probabilities()[0], 0.5, abs_tol=1e-10) +assert math.isclose(sv.probabilities()[3], 0.5, abs_tol=1e-10) +assert math.isclose(sv.expectation(PauliString.from_str("ZZ")), 1.0, abs_tol=1e-10) +``` + +--- + +## 核心概念与术语 + +| 术语 | 说明 | +| --- | --- | +| **态矢量** | 用 2^N 个复振幅描述的 N 比特纯态,基态 \|i⟩ 的振幅位于下标 `i`。 | +| **密度矩阵** | 用 2^N × 2^N 的厄米半正定矩阵描述的 N 比特量子态,可表示纯态的经典混合。 | +| **含噪模拟** | 在密度矩阵之上按噪声模型施加 Kraus 噪声的模拟方式,门噪声在每次门作用后立即生效。 | +| **稳定子表** | 用 N 个对易 Pauli 生成元描述 Clifford 态的紧凑表示,存储代价随比特数线性增长。 | +| **Clifford 门** | 把 Pauli 群映射到自身的门,例如 H、S、X、Y、Z 与受控 Pauli 门。非 Clifford 门无法用稳定子表模拟。 | +| **反稳定子** | 与稳定子生成元配对的一组 Pauli 算子,用于确定测量结果并维持稳定子表的规范形式。 | +| **辛表示** | Pauli 字符串的紧凑存储形式,用 X、Z 两组比特掩码与一个相位位共同描述算子。 | +| **可观测量** | 可以计算期望值与方差的对象,例如 `PauliString` 与 `Hamiltonian`。 | +| **期望值** | 观测量在给定状态下的平均值,纯态为 ⟨ψ\|O\|ψ⟩,混合态为 Tr(ρ·O)。 | +| **部分迹** | 对部分比特求迹、保留其余比特的操作,用于得到子系统上的约化密度矩阵。 | +| **Trotter 分解** | 把时间演化算子 e^{-iHt} 近似为一串 Pauli 旋转的分解方式。 | +| **读出噪声** | 测量阶段引入的经典翻转,使读出结果与真实测量结果不一致。 | +| **运行时经典数据** | 线路执行过程中产生、并在执行结束后按句柄查询的经典值。 | + +--- + +## `cqlib.qis` API 概览 + +### 模块入口 + +| 名字 | 简介 | +| --- | --- | +| [`cqlib.qis`](0_overview.md) | 模块概览,即本页。 | + +### 状态模拟 + +| 名字 | 简介 | +| --- | --- | +| [`Statevector`](1_statevector.md) | 纯态模拟器,用 2^N 个复振幅表示 N 比特量子态。 | +| [`DensityMatrix`](2_density_matrix.md) | 混合态模拟器,支持量子信道、部分迹与物理性校验。 | +| [`DensityMatrixNoise`](3_density_matrix_noise.md) | 含噪混合态模拟器,按噪声模型施加门噪声与读出噪声。 | +| [`StabilizerState`](4_stabilizer.md) | Clifford 态模拟器,用稳定子表表示状态并支持生成元查询。 | +| [`StabilizerCircuitResult`](4_stabilizer.md) | `StabilizerState.run_circuit()` 的结果,携带最终状态与运行时经典数据。 | + +### 运行时经典数据 + +| 名字 | 简介 | +| --- | --- | +| [`RuntimeValue`](5_classical_state.md) | 单个经典值在一次执行中的带类型取值。 | +| [`ClassicalState`](5_classical_state.md) | 一次执行结束后全部运行时经典数据的快照,按句柄查询取值。 | + +### Pauli 代数与观测量 + +| 名字 | 简介 | +| --- | --- | +| [`Phase`](6_pauli.md) | Pauli 群元素相乘时产生的相位因子。 | +| [`Pauli`](6_pauli.md) | 单比特 Pauli 算子。 | +| [`PauliString`](6_pauli.md) | 多比特 Pauli 算子的张量积,采用辛表示存储。 | +| [`Observable`](7_hamiltonian.md) | 可观测量的协议,规定期望值与方差的计算入口。 | +| [`Hamiltonian`](7_hamiltonian.md) | 用 Pauli 字符串线性组合表示的观测量,可计算期望值并生成演化线路。 | + +### 时间演化 + +| 名字 | 简介 | +| --- | --- | +| [`TrotterMode`](8_evolution.md) | 时间演化算子的 Trotter 分解方式,包括一阶、二阶与随机化。 | +| [`Hamiltonian.to_trotter_circuit`](8_evolution.md) | 把 `Hamiltonian` 转成 Trotter 近似演化线路。 | +| [`Hamiltonian.to_evolution_circuit`](8_evolution.md) | 把 `Hamiltonian` 转成时间演化线路。 | + +### 度量与熵 + +| 名字 | 简介 | +| --- | --- | +| [`purity_pure`](9_metrics_entropy.md) / [`purity_mixed`](9_metrics_entropy.md) | 纯态与混合态的纯度。 | +| [`state_fidelity_pure`](9_metrics_entropy.md) / [`state_fidelity_pure_mixed`](9_metrics_entropy.md) / [`state_fidelity_mixed`](9_metrics_entropy.md) | 纯态之间、纯态与混合态之间、混合态之间的状态保真度。 | +| [`trace_distance_pure`](9_metrics_entropy.md) / [`trace_distance_mixed`](9_metrics_entropy.md) | 纯态与混合态的迹距离。 | +| [`entropy`](9_metrics_entropy.md) | 密度矩阵的冯诺依曼熵。 | +| [`partial_transpose`](9_metrics_entropy.md) / [`logarithmic_negativity`](9_metrics_entropy.md) | 部分转置与对数负度。 | +| [`linear_entropy`](9_metrics_entropy.md) / [`renyi_entropy`](9_metrics_entropy.md) | 线性熵与 Rényi 熵。 | +| [`entanglement_entropy_pure`](9_metrics_entropy.md) | 纯态在给定子系统上的纠缠熵。 | +| [`negativity`](9_metrics_entropy.md) / [`concurrence`](9_metrics_entropy.md) / [`entanglement_of_formation`](9_metrics_entropy.md) | 负度、共生与形成纠缠。 | + +--- + +## 快速示例 + +### 1. 同一线路的纯态与混合态模拟 + +```python +import math + +from cqlib import Circuit +from cqlib.qis import Hamiltonian, PauliString +from cqlib.qis.state import DensityMatrix, Statevector + +circuit = Circuit(2) +circuit.h(0) +circuit.cx(0, 1) + +# 纯态:用态矢量计算期望值 +sv = Statevector.from_circuit(circuit) +assert math.isclose(sv.expectation(PauliString.from_str("ZZ")), 1.0, abs_tol=1e-10) + +# 混合态:用密度矩阵计算期望值,并取子系统的约化态 +dm = DensityMatrix.from_circuit(circuit) +hamiltonian = Hamiltonian(2) +hamiltonian.add_term(PauliString.from_str("ZZ"), 0.5) +hamiltonian.add_term(PauliString.from_str("XX"), 0.5) +hamiltonian.simplify() +assert math.isclose(dm.expectation(hamiltonian), 1.0, abs_tol=1e-10) + +reduced = dm.partial_trace([0]) +assert math.isclose(reduced.probabilities()[0], 0.5, abs_tol=1e-10) +``` + +### 2. 带噪声的模拟 + +```python +import math + +from cqlib.circuit import StandardGate +from cqlib.device import NoiseModel, SingleQubitNoise +from cqlib.qis.state import DensityMatrixNoise + +noise_model = NoiseModel() +noise_model.add_single_qubit_error(StandardGate.X, 0, SingleQubitNoise.bit_flip(0.1)) + +sim = DensityMatrixNoise(1, noise_model) +sim.apply_x(0) + +probs = sim.probabilities() +assert math.isclose(probs[1], 0.9, abs_tol=0.01) +assert math.isclose(probs[0], 0.1, abs_tol=0.01) +``` + +### 3. Clifford 线路与稳定子表 + +```python +from cqlib import Circuit +from cqlib.qis.state import StabilizerState + +circuit = Circuit(2) +circuit.h(0) +circuit.cx(0, 1) + +state = StabilizerState.from_circuit(circuit) +assert state.probabilities()[0] == 0.5 +assert state.probabilities()[3] == 0.5 + +# 稳定子表的逐行文本表示 +assert state.to_stim_format().count("\n") == 2 +assert len(state.get_stabilizers()) == 2 +``` + +--- + +## 校验与错误处理 + +模拟器在构造、门作用与状态读写各阶段都会做校验。各页的异常表列出全部触发场景,共同的分类如下: + +| 异常 | 触发场景 | +| --- | --- | +| `IndexError` | 比特下标越界。 | +| `ValueError` | 维度或长度与比特数不符、振幅未归一化、密度矩阵违反物理约束、Kraus 算符非法、双比特门的目标比特重复、线路含非 Clifford 操作或控制流、可观测量类型或比特数不匹配。 | +| `TypeError` | 输入类型不受支持,例如构造时收到既非数组也非列表的初态,或校验方法的容差参数不是浮点数。 | +| `OverflowError` | `num_qubits` 为负数。 | + +各页的异常情况小节给出该页 API 的完整触发场景。 diff --git a/docs/api/cn/python/3_qis/1_statevector.md b/docs/api/cn/python/3_qis/1_statevector.md new file mode 100644 index 00000000..15445836 --- /dev/null +++ b/docs/api/cn/python/3_qis/1_statevector.md @@ -0,0 +1,263 @@ +# Statevector + +`cqlib.qis.state` + +`cqlib.qis.state` 中的纯态模拟器,覆盖 `Statevector` 类:状态构造、门作用、测量与采样、观测值计算。 + +## 导入 + +```python +from cqlib.qis.state import Statevector +``` + +--- + +## Statevector + +用 2^N 个复振幅描述 N 比特纯量子态 |ψ⟩ = Σᵢ αᵢ|i⟩ 的模拟器。 + +振幅在内存中按下标连续存放,基态 |i⟩ 的振幅位于下标 `i`。比特 i 对应该下标的第 i 位,比特 0 为最低位;在位串表示中,编号大的比特位于左侧。构造后所有振幅为 0,仅 |0…0⟩ 为 1。 + +### Statevector(num_qubits) + +创建一个处于 |0…0⟩ 的状态。 + +参数: + +- `num_qubits` (`int`):量子比特数。 + +示例: + +```python +import math + +from cqlib.qis.state import Statevector + +sv = Statevector(2) +assert sv.num_qubits == 2 + +probs = sv.probabilities() +assert len(probs) == 4 +assert math.isclose(probs[0], 1.0) +assert math.isclose(probs[1], 0.0) +``` + +--- + +### 静态方法 + +- `Statevector.from_state(num_qubits, initial_state) -> Statevector`:用给定振幅构造。`initial_state` 为长度 2^num_qubits 的 NumPy 复数组,或由复数、浮点数组成的列表。 + - 长度与 2^num_qubits 不符、或振幅未归一化时抛出 `ValueError`。 +- `Statevector.from_circuit(circuit) -> Statevector`:模拟线路从 |0…0⟩ 出发的演化,返回结果状态。输入线路不会被修改。 + - 线路含非酉操作、或线路与状态比特数不符时抛出 `ValueError`。 + +示例: + +```python +import math + +import numpy as np + +from cqlib import Circuit +from cqlib.qis.state import Statevector + +amps = np.array([1 / np.sqrt(2), 1 / np.sqrt(2)], dtype=complex) +sv = Statevector.from_state(1, amps) +assert math.isclose(sv.probabilities()[0], 0.5) +assert math.isclose(sv.probabilities()[1], 0.5) + +circuit = Circuit(2) +circuit.h(0) +circuit.cx(0, 1) + +bell = Statevector.from_circuit(circuit) +assert math.isclose(bell.probabilities()[0], 0.5, abs_tol=1e-10) +assert math.isclose(bell.probabilities()[3], 0.5, abs_tol=1e-10) +``` + +--- + +### 属性 + +- `num_qubits -> int`:量子比特数。 +- `data -> numpy.ndarray`:长度 2^num_qubits 的一维复振幅数组。每次读取返回副本,修改返回值不影响状态。 + +示例: + +```python +import numpy as np + +from cqlib.qis.state import Statevector + +sv = Statevector(2) +data = sv.data +assert len(data) == 4 +assert data[0] == 1.0 +assert np.allclose(data[1:], 0) + +# data 是副本,就地修改不会写回状态 +data[0] = 999.0 +assert sv.data[0] == 1.0 +``` + +--- + +### 方法 + +所有门作用与状态改写方法均就地修改当前状态,返回 `None`。 + +**电路作用** + +- `apply_circuit(circuit) -> None`:把线路作用到当前状态上。线路比特数不匹配时抛出 `ValueError`。 + +**标准门对象** + +- `apply_standard_gate(gate, qubits, params=None) -> None`:按 `StandardGate` 描述作用门。 + - `gate` (`StandardGate`):标准门。 + - `qubits` (`list[int]`):目标比特下标列表。 + - `params` (`list[float] | None`):参数化门的参数,默认空列表。 + +**单比特门(无参数)** + +`apply_x`、`apply_y`、`apply_z`、`apply_h`、`apply_s`、`apply_sdg`、`apply_t`、`apply_tdg`、`apply_x2p`、`apply_x2m`、`apply_y2p`、`apply_y2m`,签名均为 `(qubit) -> None`。 + +**单比特门(带角度)** + +- `apply_rx(qubit, theta)`、`apply_ry(qubit, theta)`、`apply_rz(qubit, theta)`:绕 X、Y、Z 轴旋转 `theta` 弧度。 +- `apply_phase(qubit, theta)`:相位门,`theta` 为相位角。 +- `apply_xy(qubit, theta)`:XY 门。 +- `apply_xy2p(qubit, theta)`:XY2P 门,等价于 `Rz(θ - π/2) Ry(π/2) Rz(π/2 - θ)`。 +- `apply_xy2m(qubit, theta)`:XY2M 门,等价于 `Rz(-θ + π/2) Ry(-π/2) Rz(-π/2 + θ)`。 +- `apply_rxy(qubit, theta, phi)`:RXY 门。 +- `apply_u(qubit, theta, phi, lambda_)`:一般单比特酉门,`U(θ, φ, λ) = Rz(φ) Ry(θ) Rz(λ)`。 + +**全局相位** + +- `apply_gphase(phi)`:给整个状态乘上全局相位 `e^{iφ}`。不改变 `probabilities()`。 + +**Pauli 字符串旋转** + +- `apply_pauli_rotation(pauli, theta)`:作用 `exp(-i θ/2 · P)`,要求 `P` 为厄米 Pauli 字符串且覆盖状态的全部比特。 + +**受控门与双比特门** + +- `apply_cx(control, target)`:受控 X(CNOT)门。 +- `apply_cy(control, target)`:受控 Y 门。 +- `apply_cz(q0, q1)`:受控 Z 门。 +- `apply_crx(control, target, theta)`:受控 RX 门。 +- `apply_cry(control, target, theta)`:受控 RY 门。 +- `apply_crz(control, target, theta)`:受控 RZ 门。 +- `apply_swap(q0, q1)`:SWAP 门。 +- `apply_rxx(q0, q1, theta)`:RXX(Ising XX)门。 +- `apply_ryy(q0, q1, theta)`:RYY(Ising YY)门。 +- `apply_rzz(q0, q1, theta)`:RZZ(Ising ZZ)门。 +- `apply_rzx(q0, q1, theta)`:RZX 门。 +- `apply_fsim(q0, q1, theta, phi)`:费米子模拟门,`theta` 为 iSWAP 角,`phi` 为受控相位角。 +- `apply_ccx(c0, c1, target)`:CCX(Toffoli)门。 + +双比特门的两个比特下标相同时抛出 `ValueError`。 + +**自定义矩阵门** + +- `apply_single_qubit_gate(qubit, matrix) -> None`:作用 `matrix` 给出的单比特门。 + - `matrix` (`numpy.ndarray`):2×2 复矩阵;形状不符时抛出 `ValueError`。 +- `apply_double_qubits_gate(q0, q1, matrix) -> None`:作用 `matrix` 给出的双比特门。 + - `matrix` (`numpy.ndarray`):4×4 复矩阵;形状不符时抛出 `ValueError`。 +- `apply_unitary_gate(qubits, matrix) -> None`:作用作用于 `qubits` 的任意比特数酉门。 + - `qubits` (`list[int]`):门作用的比特下标列表。 + - `matrix` (`numpy.ndarray`):2^n × 2^n 复矩阵,n 为 `qubits` 长度;形状不符时抛出 `ValueError`。 + +**观测值** + +- `expectation(observable) -> float`:计算 ⟨ψ|O|ψ⟩。 + - `observable` (`Hamiltonian | PauliString`):可观测量。比特数不匹配或类型不受支持时抛出 `ValueError`。 + +**测量与状态改写** + +- `probabilities() -> list[float]`:返回各基态的概率 p(i) = |αᵢ|²,长度为 2^num_qubits。不修改状态。 +- `reset(qubit) -> None`:测量指定比特,若结果为 1 则翻转为 |0⟩。 +- `measure(qubit) -> bool`:测量单个比特并坍缩状态,返回该比特的结果。 +- `measure_all() -> Outcome`:测量全部比特并坍缩状态,返回 `Outcome`。位串按比特编号从高到低排列。 +- `sample_shots(shots) -> list[Outcome]`:按当前概率分布采样 `shots` 次,不修改状态。 +- `sample(measurement, shots) -> ExecutionResult`:按测量回执采样,不修改状态。 + - `measurement` (`Measurement`):由线路的测量操作得到的测量回执。 + - `shots` (`int`):采样次数。 +- `probs(measurement) -> dict[Outcome, float]`:按测量回执计算边缘概率,不修改状态。 + +**复制** + +- `copy() -> Statevector`:返回独立副本,修改原状态不影响副本。 + +--- + +### 其他行为 + +- `repr(sv)` 返回形如 `Statevector(num_qubits=N, amplitudes=2^N)` 的字符串。 +- 未实现 `__eq__`:`==` 为默认的对象同一性比较,不比较状态内容。 +- 未实现 `__copy__`、`__deepcopy__`:标准库 `copy.copy()` 与 `copy.deepcopy()` 会抛出 `TypeError`。需要副本时使用 `copy()` 方法。 + +--- + +### 异常情况 + +- `IndexError`:比特下标越界。 +- `ValueError`:其他校验失败,包括振幅长度不符或未归一化、双比特门的目标比特重复、自定义门矩阵形状不符、可观测量类型或比特数不匹配、`apply_circuit` 的线路比特数不匹配。 + +--- + +示例: + +```python +import math + +import numpy as np + +from cqlib import Circuit +from cqlib.qis import PauliString +from cqlib.qis.state import Statevector + +# 用线路构造 Bell 态 +circuit = Circuit(2) +circuit.h(0) +circuit.cx(0, 1) + +sv = Statevector.from_circuit(circuit) +probs = sv.probabilities() +assert math.isclose(probs[0], 0.5, abs_tol=1e-10) +assert math.isclose(probs[3], 0.5, abs_tol=1e-10) + +# 期望值 +assert math.isclose(sv.expectation(PauliString.from_str("ZZ")), 1.0, abs_tol=1e-10) + +# 采样不改变状态,结果只落在关联基态上 +shots = sv.sample_shots(64) +assert len(shots) == 64 +assert {outcome.to_bitstring(2) for outcome in shots} <= {"00", "11"} +assert math.isclose(sv.probabilities()[0], 0.5, abs_tol=1e-10) + +# 测量并坍缩:先在基态上验证确定性结果 +basis = Statevector(1) +basis.apply_x(0) +assert basis.measure(0) is True +assert math.isclose(basis.probabilities()[1], 1.0, abs_tol=1e-10) + +# 全量测量后概率集中在单个基态上 +sv.measure_all() +assert math.isclose(sum(sv.probabilities()), 1.0, abs_tol=1e-10) + +# 副本独立:全局相位不改变概率分布 +clone = sv.copy() +clone.apply_gphase(np.pi / 2) +assert np.allclose(sv.probabilities(), clone.probabilities(), atol=1e-10) +``` + +--- + +## 异常 + +`Statevector` 的校验失败按来源分为两类: + +| 异常 | 触发场景 | +| --- | --- | +| `IndexError` | 比特下标越界。 | +| `ValueError` | 振幅长度与 2^num_qubits 不符、振幅未归一化、双比特门的目标比特重复、自定义门矩阵形状不符、可观测量类型或比特数不匹配、线路比特数不匹配、线路含非酉操作。 | diff --git a/docs/api/cn/python/3_qis/2_density_matrix.md b/docs/api/cn/python/3_qis/2_density_matrix.md new file mode 100644 index 00000000..1b053966 --- /dev/null +++ b/docs/api/cn/python/3_qis/2_density_matrix.md @@ -0,0 +1,292 @@ +# DensityMatrix + +`cqlib.qis.state` + +`cqlib.qis.state` 中的混合态模拟器,覆盖 `DensityMatrix` 类:状态构造、门作用与量子信道、部分迹、物理性校验、测量与采样、观测值计算。 + +## 导入 + +```python +from cqlib.qis.state import DensityMatrix +``` + +--- + +## DensityMatrix + +用 2^N × 2^N 的密度矩阵 ρ 描述 N 比特量子态的模拟器。与只能表示纯态的态矢量不同,密度矩阵可以表示纯态的经典混合。 + +数据按行优先顺序连续存放。比特 i 对应基态下标的第 i 位,比特 0 为最低位;在位串表示中,编号大的比特位于左侧。构造后状态为 |0…0⟩⟨0…0|。 + +### DensityMatrix(num_qubits) + +创建一个处于 |0…0⟩⟨0…0| 的纯态。 + +参数: + +- `num_qubits` (`int`):量子比特数。 + +示例: + +```python +import math + +from cqlib.qis.state import DensityMatrix + +dm = DensityMatrix(2) +assert dm.num_qubits == 2 + +probs = dm.probabilities() +assert len(probs) == 4 +assert math.isclose(probs[0], 1.0) +assert math.isclose(probs[1], 0.0) +``` + +--- + +### 静态方法 + +- `DensityMatrix.from_state(num_qubits, initial_state) -> DensityMatrix`:由纯态振幅构造,内部取外积 ρ = |ψ⟩⟨ψ|。`initial_state` 为长度 2^num_qubits 的 NumPy 复数组或复数列表。 + - 长度不符或振幅未归一化时抛出 `ValueError`。 +- `DensityMatrix.from_density_matrix(num_qubits, dm_state) -> DensityMatrix`:由展平的密度矩阵数据直接构造。 + - `dm_state` (`numpy.ndarray | list`):长度 4^num_qubits 的复数序列。 + - 长度不符、迹不为 1、非厄米或不满足半正定时抛出 `ValueError`。 +- `DensityMatrix.from_circuit(circuit) -> DensityMatrix`:模拟线路从 |0…0⟩⟨0…0| 出发的演化,返回结果状态。输入线路不会被修改。 + - 线路含不受支持的操作时抛出 `ValueError`。 +- `DensityMatrix.maximally_mixed(num_qubits) -> DensityMatrix`:构造最大混合态 I / 2^N,即对角元均为 1/2^N 的对角矩阵。`num_qubits` 为 0 时得到 1×1 的 `[1]`。 + +示例: + +```python +import math + +import numpy as np + +from cqlib import Circuit +from cqlib.qis.state import DensityMatrix + +amps = np.array([1 / np.sqrt(2), 1 / np.sqrt(2)], dtype=complex) +dm = DensityMatrix.from_state(1, amps) +assert math.isclose(dm.probabilities()[0], 0.5) +assert math.isclose(dm.probabilities()[1], 0.5) + +# |1><1| +data = np.array([0, 0, 0, 1], dtype=complex) +dm = DensityMatrix.from_density_matrix(1, data) +assert math.isclose(dm.probabilities()[1], 1.0) + +circuit = Circuit(2) +circuit.h(0) +circuit.cx(0, 1) + +bell = DensityMatrix.from_circuit(circuit) +assert math.isclose(bell.probabilities()[0], 0.5) +assert math.isclose(bell.probabilities()[3], 0.5) + +mixed = DensityMatrix.maximally_mixed(2) +assert np.allclose(mixed.data, np.eye(4, dtype=complex) / 4, atol=1e-12) +assert math.isclose(mixed.trace(), 1.0) +``` + +--- + +### 属性 + +- `num_qubits -> int`:量子比特数。 +- `data -> numpy.ndarray`:形状为 (2^num_qubits, 2^num_qubits) 的二维复矩阵。每次读取返回副本,修改返回值不影响状态。 + +示例: + +```python +import numpy as np + +from cqlib.qis.state import DensityMatrix + +dm = DensityMatrix(2) +data = dm.data +assert data.shape == (4, 4) +assert data[0, 0] == 1.0 +assert np.allclose(data[0, 1:], 0) + +# data 是副本,就地修改不会写回状态 +data[0, 0] = 999.0 +assert dm.data[0, 0] == 1.0 +``` + +--- + +### 方法 + +所有门作用与状态改写方法均就地修改当前状态,返回 `None`。 + +**电路作用** + +- `apply_circuit(circuit) -> None`:把线路作用到当前状态上。线路比特数与状态不匹配时抛出 `ValueError`。 + +**标准门对象** + +- `apply_standard_gate(gate, qubits, params=None) -> None`:按 `StandardGate` 描述作用门。 + - `gate` (`StandardGate`):标准门。 + - `qubits` (`list[int]`):目标比特下标列表。 + - `params` (`list[float] | None`):参数化门的参数,默认空列表。 + +**单比特门(无参数)** + +`apply_x`、`apply_y`、`apply_z`、`apply_h`、`apply_s`、`apply_sdg`、`apply_t`、`apply_tdg`、`apply_x2p`、`apply_x2m`、`apply_y2p`、`apply_y2m`,签名均为 `(qubit) -> None`。 + +**单比特门(带角度)** + +- `apply_rx(qubit, theta)`、`apply_ry(qubit, theta)`、`apply_rz(qubit, theta)`:绕 X、Y、Z 轴旋转 `theta` 弧度。 +- `apply_phase(qubit, theta)`:相位门。 +- `apply_xy(qubit, theta)`:XY 门。 +- `apply_xy2p(qubit, theta)`:XY2P 门。 +- `apply_xy2m(qubit, theta)`:XY2M 门。 +- `apply_rxy(qubit, theta, phi)`:RXY 门。 +- `apply_u(qubit, theta, phi, lambda_)`:一般单比特酉门,`U(θ, φ, λ) = Rz(φ) Ry(θ) Rz(λ)`。 + +**全局相位** + +- `apply_gphase(phi) -> None`:对密度矩阵没有可观测影响,调用后状态不变。 + +**受控门与双比特门** + +- `apply_cx(control, target)`:受控 X(CNOT)门。 +- `apply_cy(control, target)`:受控 Y 门。 +- `apply_cz(q0, q1)`:受控 Z 门。 +- `apply_crx(control, target, theta)`:受控 RX 门。 +- `apply_cry(control, target, theta)`:受控 RY 门。 +- `apply_crz(control, target, theta)`:受控 RZ 门。 +- `apply_swap(q0, q1)`:SWAP 门。 +- `apply_rxx(q0, q1, theta)`:RXX(Ising XX)门。 +- `apply_ryy(q0, q1, theta)`:RYY(Ising YY)门。 +- `apply_rzz(q0, q1, theta)`:RZZ(Ising ZZ)门。 +- `apply_rzx(q0, q1, theta)`:RZX 门。 +- `apply_fsim(q0, q1, theta, phi)`:费米子模拟门,`theta` 为 iSWAP 角,`phi` 为受控相位角。 +- `apply_ccx(c0, c1, target)`:CCX(Toffoli)门。 + +**自定义矩阵门** + +- `apply_single_qubit_gate(qubit, matrix) -> None`:作用 2×2 复矩阵给出的单比特门;形状不符时抛出 `ValueError`。 +- `apply_double_qubits_gate(q0, q1, matrix) -> None`:作用 4×4 复矩阵给出的双比特门;形状不符时抛出 `ValueError`。 +- `apply_unitary_gate(qubits, matrix) -> None`:作用作用于 `qubits` 的任意比特数酉门,演化形式为 ρ → U ρ U†。矩阵形状需为 2^n × 2^n,n 为 `qubits` 长度。 + - `qubits` (`list[int]`):门作用的比特下标列表。 + - `matrix` (`numpy.ndarray`):酉矩阵;形状与比特数不符时抛出 `ValueError`。 + +**量子信道** + +- `apply_kraus(qubits, ops) -> None`:作用 Kraus 算符给出的量子信道,演化形式为 ρ → Σ_k K_k ρ K_k†。 + - `qubits` (`list[int]`):信道作用的比特下标列表。 + - `ops` (`list[numpy.ndarray]`):Kraus 算符列表,每个算符是展平的复数数组。Kraus 算符非法时抛出 `ValueError`。 + +**约化与校验** + +- `partial_trace(keep) -> DensityMatrix`:对其余比特求部分迹,返回仅含 `keep` 给出比特的约化密度矩阵。 + - `keep` (`list[int]`):约化后保留的比特下标列表。下标越界时抛出 `IndexError`。 +- `trace() -> float`:返回迹(对角元之和)。物理态的迹恒为 1。取结果的实部。 +- `is_hermitian(tol=1e-10) -> bool`:判断是否在容差内满足 ρ = ρ†。 +- `is_positive_semidefinite(tol=1e-10) -> bool`:判断全部本征值是否满足 λᵢ ≥ -tol。对厄米矩阵该判定是确定的;非厄米矩阵或含 NaN、Inf 的矩阵返回 `False`。 +- `validate_physical(tol=1e-10) -> None`:一次性校验厄米性、半正定性与单位迹。任一约束不满足时抛出 `ValueError`。 + +**观测值** + +- `expectation(observable) -> float`:计算 Tr(ρ · O)。 + - `observable` (`Hamiltonian | PauliString`):可观测量。比特数不匹配或类型不受支持时抛出 `ValueError`。 + +**测量与采样** + +- `probabilities() -> list[float]`:返回各基态的概率,即密度矩阵的对角元 ρ_ii,长度为 2^num_qubits。不修改状态。 +- `reset(qubit) -> None`:测量指定比特,若结果为 1 则翻转为 |0⟩。 +- `measure(qubit) -> bool`:测量单个比特并坍缩状态,返回该比特的结果。 +- `measure_all() -> Outcome`:测量全部比特并坍缩状态,返回 `Outcome`。位串按比特编号从高到低排列。 +- `sample_shots(shots) -> list[Outcome]`:按当前概率分布采样 `shots` 次,不修改状态。 +- `sample(measurement, shots) -> ExecutionResult`:按测量回执采样,不修改状态。 + - `measurement` (`Measurement`):由线路的测量操作得到的测量回执。 + - `shots` (`int`):采样次数。 +- `probs(measurement) -> dict[Outcome, float]`:按测量回执计算边缘概率,不修改状态。 + +**复制** + +- `copy() -> DensityMatrix`:返回独立副本,修改原状态不影响副本。 + +--- + +### 其他行为 + +- `repr(dm)` 返回形如 `DensityMatrix(num_qubits=N, shape=(2^N, 2^N))` 的字符串。 +- 未实现 `__eq__`:`==` 为默认的对象同一性比较,不比较矩阵内容。 +- 未实现 `__copy__`、`__deepcopy__`:标准库 `copy.copy()` 与 `copy.deepcopy()` 会抛出 `TypeError`。需要副本时使用 `copy()` 方法。 + +--- + +### 异常情况 + +- `IndexError`:`partial_trace` 的比特下标越界。 +- `ValueError`:其他校验失败,包括振幅长度不符或未归一化、密度矩阵数据长度不符或违反物理约束、双比特门的目标比特重复、自定义门矩阵形状不符、Kraus 算符非法、可观测量类型或比特数不匹配、`validate_physical` 未通过。 +- `TypeError`:`from_state` 与 `from_density_matrix` 收到既非数组也非列表的输入;`is_hermitian`、`is_positive_semidefinite`、`validate_physical` 的 `tol` 不是浮点数。 + +--- + +示例: + +```python +import math + +import numpy as np + +from cqlib import Circuit +from cqlib.qis import Hamiltonian, PauliString +from cqlib.qis.state import DensityMatrix + +# 用线路构造 Bell 态 +circuit = Circuit(2) +circuit.h(0) +circuit.cx(0, 1) + +dm = DensityMatrix.from_circuit(circuit) +assert dm.is_hermitian() is True +assert dm.is_positive_semidefinite() is True +assert dm.validate_physical() is None +assert math.isclose(dm.trace(), 1.0, abs_tol=1e-10) + +# 观测量 +assert math.isclose(dm.expectation(PauliString.from_str("ZI")), 0.0, abs_tol=1e-10) + +hamiltonian = Hamiltonian(2) +hamiltonian.add_term(PauliString.from_str("ZZ"), 0.5) +hamiltonian.add_term(PauliString.from_str("XX"), 0.5) +hamiltonian.simplify() +assert math.isclose(dm.expectation(hamiltonian), 1.0, abs_tol=1e-10) + +# 部分迹:Bell 态的单比特约化是最大混合态 +reduced = dm.partial_trace([0]) +assert reduced.num_qubits == 1 +assert math.isclose(reduced.probabilities()[0], 0.5, abs_tol=1e-10) +assert math.isclose(reduced.probabilities()[1], 0.5, abs_tol=1e-10) + +# 量子信道:退极化信道保持迹为 1 +noisy = DensityMatrix(1) +p = 0.3 +k0 = np.sqrt(1 - p) * np.eye(2, dtype=complex) +k1 = np.sqrt(p / 3) * np.array([[0, 1], [1, 0]], dtype=complex) +k2 = np.sqrt(p / 3) * np.array([[0, -1j], [1j, 0]], dtype=complex) +k3 = np.sqrt(p / 3) * np.array([[1, 0], [0, -1]], dtype=complex) +noisy.apply_kraus([0], [k0.flatten(), k1.flatten(), k2.flatten(), k3.flatten()]) +assert np.isclose(noisy.trace(), 1.0, atol=1e-10) + +# 副本独立 +snapshot = dm.copy() +dm.apply_h(0) +assert math.isclose(snapshot.probabilities()[0], 0.5, abs_tol=1e-10) +``` + +--- + +## 异常 + +`DensityMatrix` 的校验失败按来源分为三类: + +| 异常 | 触发场景 | +| --- | --- | +| `IndexError` | `partial_trace` 的比特下标越界。 | +| `ValueError` | 振幅长度与 2^num_qubits 不符、振幅未归一化、密度矩阵数据长度不符或违反迹、厄米性、半正定约束、双比特门的目标比特重复、自定义门矩阵形状不符、Kraus 算符非法、可观测量类型或比特数不匹配、`validate_physical` 未通过。 | +| `TypeError` | `from_state`、`from_density_matrix` 收到不支持的类型;`tol` 参数类型错误。 | diff --git a/docs/api/cn/python/3_qis/3_density_matrix_noise.md b/docs/api/cn/python/3_qis/3_density_matrix_noise.md new file mode 100644 index 00000000..a2ec8681 --- /dev/null +++ b/docs/api/cn/python/3_qis/3_density_matrix_noise.md @@ -0,0 +1,278 @@ +# DensityMatrixNoise + +`cqlib.qis.state` + +`cqlib.qis.state` 中的含噪混合态模拟器,覆盖 `DensityMatrixNoise` 类:噪声模型绑定、带噪声的门作用、读出噪声下的概率与采样。 + +## 导入 + +```python +from cqlib.device import NoiseModel, ReadoutError, SingleQubitNoise +from cqlib.qis.state import DensityMatrixNoise +``` + +--- + +## DensityMatrixNoise + +在密度矩阵内核之上按噪声模型自动施加 Kraus 噪声的模拟器。每次门作用后,模型中对应该门的噪声立即作用到状态上;读出噪声则在概率计算与采样阶段生效。 + +数据按行优先顺序连续存放。比特 i 对应基态下标的第 i 位,比特 0 为最低位;在位串表示中,编号大的比特位于左侧。构造后状态为 |0…0⟩⟨0…0|。 + +### DensityMatrixNoise(num_qubits, noise_model=None) + +创建一个模拟器。省略噪声模型时等价于理想模拟。 + +参数: + +- `num_qubits` (`int`):量子比特数。 +- `noise_model` (`NoiseModel | None`):门噪声与读出噪声的定义,默认 `None`。 + +示例: + +```python +import math + +from cqlib.device import NoiseModel +from cqlib.qis.state import DensityMatrixNoise + +sim = DensityMatrixNoise(2) +assert sim.num_qubits == 2 +assert sim.state.shape == (4, 4) + +probs = sim.probabilities() +assert len(probs) == 4 +assert math.isclose(probs[0], 1.0) + +with_model = DensityMatrixNoise(2, NoiseModel()) +assert with_model.num_qubits == 2 +``` + +--- + +### 静态方法 + +- `DensityMatrixNoise.from_circuit(circuit, noise_model=None) -> DensityMatrixNoise`:模拟线路,返回演化后的模拟器。线路在执行前会被分解到基础门,噪声按模型在每个门之后施加。输入线路不会被修改。 + - `circuit` (`Circuit`):待模拟线路。 + - `noise_model` (`NoiseModel | None`):噪声模型,默认 `None`。 + - 线路含不受支持的操作时抛出 `ValueError`。 + +示例: + +```python +import math + +from cqlib import Circuit +from cqlib.device import NoiseModel +from cqlib.qis.state import DensityMatrixNoise + +circuit = Circuit(2) +circuit.h(0) +circuit.cx(0, 1) + +ideal = DensityMatrixNoise.from_circuit(circuit) +assert math.isclose(ideal.probabilities()[0], 0.5, abs_tol=1e-10) +assert math.isclose(ideal.probabilities()[3], 0.5, abs_tol=1e-10) + +noisy = DensityMatrixNoise.from_circuit(circuit, NoiseModel()) +assert noisy.num_qubits == 2 +``` + +--- + +### 属性 + +- `num_qubits -> int`:量子比特数。 +- `state -> numpy.ndarray`:形状为 (2^num_qubits, 2^num_qubits) 的二维复矩阵表示当前状态。每次读取返回副本,修改返回值不影响模拟器。 + +示例: + +```python +import math + +from cqlib.qis.state import DensityMatrixNoise + +sim = DensityMatrixNoise(2) +sim.apply_h(0) +sim.apply_cx(0, 1) + +rho = sim.state +assert rho.shape == (4, 4) +assert math.isclose(abs(rho[0, 3]), 0.5, abs_tol=1e-10) + +rho[0, 0] = 999.0 +assert sim.state[0, 0] != 999.0 +``` + +--- + +### 方法 + +所有门作用与状态改写方法均就地修改当前状态,返回 `None`。每个门方法在作用后会按噪声模型施加该门对应的噪声。 + +**电路作用** + +- `apply_circuit(circuit) -> None`:把线路作用到当前状态上,线路在执行前会被分解到基础门。线路比特数与状态不匹配时抛出 `ValueError`。 + +**标准门对象** + +- `apply_standard_gate_noise(gate, qubits, params=None) -> None`:按 `StandardGate` 描述作用门并施加对应噪声。 + - `gate` (`StandardGate`):标准门。 + - `qubits` (`list[int]`):目标比特下标列表。 + - `params` (`list[float] | None`):参数化门的参数,默认空列表。 + +**单比特门(无参数)** + +`apply_x`、`apply_y`、`apply_z`、`apply_h`、`apply_s`、`apply_sdg`、`apply_t`、`apply_tdg`、`apply_x2p`、`apply_x2m`、`apply_y2p`、`apply_y2m`,签名均为 `(q) -> None`。 + +**单比特门(带角度)** + +- `apply_rx(q, theta)`、`apply_ry(q, theta)`、`apply_rz(q, theta)`:绕 X、Y、Z 轴旋转 `theta` 弧度。 +- `apply_phase(q, theta)`:相位门。 +- `apply_xy(q, theta)`:XY 门。 +- `apply_xy2p(q, theta)`:XY2P 门。 +- `apply_xy2m(q, theta)`:XY2M 门。 +- `apply_rxy(q, theta, phi)`:布洛赫球上的任意旋转。 +- `apply_u(q, theta, phi, lambda_)`:一般单比特酉门,`U(θ, φ, λ) = Rz(φ) Ry(θ) Rz(λ)`。 + +**全局相位** + +- `apply_gphase(phi) -> None`:作用全局相位门。 + +**受控门与双比特门** + +- `apply_cx(control, target)`:受控 X(CNOT)门。 +- `apply_cy(control, target)`:受控 Y 门。 +- `apply_cz(q0, q1)`:受控 Z 门。 +- `apply_crx(control, target, theta)`:受控 RX 门。 +- `apply_cry(control, target, theta)`:受控 RY 门。 +- `apply_crz(control, target, theta)`:受控 RZ 门。 +- `apply_swap(q0, q1)`:SWAP 门。 +- `apply_rxx(q0, q1, theta)`:RXX(Ising XX)门。 +- `apply_ryy(q0, q1, theta)`:RYY(Ising YY)门。 +- `apply_rzz(q0, q1, theta)`:RZZ(Ising ZZ)门。 +- `apply_rzx(q0, q1, theta)`:RZX 门。 +- `apply_fsim(q0, q1, theta, phi)`:费米子模拟门,`theta` 为 iSWAP 角,`phi` 为受控相位角。 +- `apply_ccx(c1, c2, t)`:CCX(Toffoli)门,`c1`、`c2` 为控制位,`t` 为目标位。 + +**自定义矩阵门** + +- `apply_unitary_gate(qubits, matrix) -> None`:作用作用于 `qubits` 的任意比特数酉门。该入口不施加噪声。 + - `qubits` (`list[int]`):门作用的比特下标列表。 + - `matrix` (`numpy.ndarray`):2^n × 2^n 复矩阵,n 为 `qubits` 长度;形状与比特数不符时抛出 `ValueError`。 + +**观测值** + +- `expectation(observable) -> float`:计算带噪声状态下的观测值。 + - `observable` (`Hamiltonian | PauliString`):可观测量。比特数不匹配或类型不受支持时抛出 `ValueError`。 + +**概率** + +- `probabilities() -> list[float]`:返回不含读出噪声的各基态概率,长度为 2^num_qubits。不修改状态。 +- `probabilities_with_readout(qubits) -> list[float]`:返回在指定比特上施加读出噪声后的概率分布,长度为 2^n,n 为 `qubits` 长度。 + - `qubits` (`list[int]`):参与读出噪声建模的比特下标列表。下标越界时抛出 `IndexError`。 + +**测量与采样** + +- `measure(qubit) -> bool`:测量单个比特并坍缩状态,返回该比特的结果。 +- `measure_all() -> Outcome`:测量全部比特并坍缩状态,返回 `Outcome`。位串按比特编号从高到低排列。 +- `sample_shots(shots) -> list[Outcome]`:按含读出噪声的分布采样 `shots` 次,不修改状态。 +- `sample(measurement, shots) -> ExecutionResult`:按测量回执采样,不修改状态。 + - `measurement` (`Measurement`):由线路的测量操作得到的测量回执。 + - `shots` (`int`):采样次数。 +- `probs(measurement) -> dict[Outcome, float]`:按测量回执计算不含读出噪声的边缘概率,不修改状态。 +- `probs_with_readout(measurement) -> dict[Outcome, float]`:按测量回执计算施加读出噪声后的边缘概率,不修改状态。 + +**复制** + +- `copy() -> DensityMatrixNoise`:返回独立副本,噪声模型一并复制。修改原模拟器不影响副本。 + +--- + +### 其他行为 + +- `repr(sim)` 返回形如 `DensityMatrixNoise(num_qubits=N, state_shape=(2^N, 2^N))` 的字符串。 +- 未实现 `__eq__`:`==` 为默认的对象同一性比较,不比较状态内容。 +- 未实现 `__copy__`、`__deepcopy__`:标准库 `copy.copy()` 与 `copy.deepcopy()` 会抛出 `TypeError`。需要副本时使用 `copy()` 方法。 + +--- + +### 异常情况 + +- `IndexError`:比特下标越界。 +- `ValueError`:其他校验失败,包括自定义门矩阵形状与比特数不符、可观测量类型或比特数不匹配、线路含不受支持的操作、线路比特数与状态不匹配。 +- `OverflowError`:`num_qubits` 为负数。 + +--- + +示例: + +```python +import math + +import numpy as np + +from cqlib.circuit import StandardGate +from cqlib.device import NoiseModel, ReadoutError, SingleQubitNoise +from cqlib.qis.state import DensityMatrixNoise + +# 理想模拟:不带噪声模型时与纯密度矩阵结果一致 +ideal = DensityMatrixNoise(2) +ideal.apply_h(0) +ideal.apply_cx(0, 1) +assert math.isclose(ideal.probabilities()[0], 0.5, abs_tol=1e-10) +assert math.isclose(ideal.probabilities()[3], 0.5, abs_tol=1e-10) + +# 比特翻转噪声:X 门后以 p=0.1 翻转 +noise_model = NoiseModel() +noise_model.add_single_qubit_error(StandardGate.X, 0, SingleQubitNoise.bit_flip(0.1)) + +sim = DensityMatrixNoise(1, noise_model) +sim.apply_x(0) +probs = sim.probabilities() +assert math.isclose(probs[1], 0.9, abs_tol=0.01) +assert math.isclose(probs[0], 0.1, abs_tol=0.01) + +# 理想概率与读出噪声下的概率相互独立 +circuit_readout = NoiseModel() +circuit_readout.add_readout_error(0, ReadoutError(p_0_given_1=0.1, p_1_given_0=0.2)) + +readout_sim = DensityMatrixNoise(1, circuit_readout) +readout_sim.apply_x(0) +assert readout_sim.probabilities() == [0.0, 1.0] +readout_probs = readout_sim.probabilities_with_readout([0]) +assert math.isclose(readout_probs[0], 0.1, abs_tol=1e-10) +assert math.isclose(readout_probs[1], 0.9, abs_tol=1e-10) + +# 副本独立,且保留噪声行为 +snapshot = sim.copy() +sim.apply_h(0) +assert math.isclose(snapshot.probabilities()[1], 0.9, abs_tol=0.01) + +# 自定义酉门入口不施加噪声 +matrix = np.array( + [ + [1 / np.sqrt(2), 0, 1 / np.sqrt(2), 0], + [0, 1 / np.sqrt(2), 0, 1 / np.sqrt(2)], + [0, 1 / np.sqrt(2), 0, -1 / np.sqrt(2)], + [1 / np.sqrt(2), 0, -1 / np.sqrt(2), 0], + ], + dtype=complex, +) +plain = DensityMatrixNoise(2) +plain.apply_unitary_gate([0, 1], matrix) +assert math.isclose(plain.probabilities()[0], 0.5, abs_tol=1e-10) +assert math.isclose(np.trace(plain.state @ plain.state).real, 1.0, abs_tol=1e-10) +``` + +--- + +## 异常 + +`DensityMatrixNoise` 的校验失败按来源分为三类: + +| 异常 | 触发场景 | +| --- | --- | +| `IndexError` | 比特下标越界,包括 `probabilities_with_readout` 的 `qubits` 超出范围。 | +| `ValueError` | 自定义门矩阵形状与比特数不符、可观测量类型或比特数不匹配、线路比特数与状态不匹配、线路含不受支持的操作。 | +| `OverflowError` | `num_qubits` 为负数。 | diff --git a/docs/api/cn/python/3_qis/4_stabilizer.md b/docs/api/cn/python/3_qis/4_stabilizer.md new file mode 100644 index 00000000..0dd5443d --- /dev/null +++ b/docs/api/cn/python/3_qis/4_stabilizer.md @@ -0,0 +1,267 @@ +# Stabilizer + +`cqlib.qis.state` + +`cqlib.qis.state` 中的稳定子模拟器,覆盖 `StabilizerState` 与 `StabilizerCircuitResult` 两个类:Clifford 线路的模拟、稳定子生成元查询、测量采样,以及执行后运行期经典数据的读取。 + +## 导入 + +```python +from cqlib.qis.state import StabilizerCircuitResult, StabilizerState +``` + +--- + +## StabilizerState + +用稳定子表描述 N 比特 Clifford 态的模拟器。Clifford 门把 Pauli 群映射到自身,因此状态可以用 N 个对易 Pauli 生成元紧凑表示,而无需存储全部振幅。 + +该类只接受 Clifford 门。含 T 门、一般角度旋转等非 Clifford 操作的线路无法模拟,此时抛出 `ValueError`。 + +比特 i 对应基态下标的第 i 位,比特 0 为最低位;在位串表示中,编号大的比特位于左侧。构造后状态为 |0…0⟩。 + +### StabilizerState(num_qubits) + +创建一个处于 |0…0⟩ 的状态。 + +参数: + +- `num_qubits` (`int`):量子比特数。 + +示例: + +```python +from cqlib.qis.state import StabilizerState + +state = StabilizerState(3) +assert state.num_qubits == 3 +assert state.probabilities() == [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] +assert "StabilizerState" in repr(state) +``` + +--- + +### 静态方法 + +- `StabilizerState.from_circuit(circuit) -> StabilizerState`:模拟 Clifford 线路的演化,返回最终状态。输入线路不会被修改。线路含非 Clifford 操作时抛出 `ValueError`。 +- `StabilizerState.run_circuit(circuit) -> StabilizerCircuitResult`:执行 Clifford 线路,返回同时包含最终状态与运行期经典数据的结果对象。输入线路不会被修改。 + - 线路含控制流、或含非 Clifford 操作时抛出 `ValueError`。 + +示例: + +```python +from cqlib import Circuit +from cqlib.qis.state import StabilizerState + +circuit = Circuit(2) +circuit.h(0) +circuit.cx(0, 1) + +bell = StabilizerState.from_circuit(circuit) +assert bell.num_qubits == 2 +assert bell.probabilities()[0] == 0.5 +assert bell.probabilities()[3] == 0.5 +``` + +--- + +### 属性 + +- `num_qubits -> int`:量子比特数。 + +--- + +### 方法 + +所有门作用与状态改写方法均就地修改当前状态,返回 `None`。 + +**电路作用** + +- `apply_circuit(circuit) -> None`:把 Clifford 线路作用到当前状态上。线路含非 Clifford 操作或比特数不匹配时抛出 `ValueError`。 + +**单比特 Clifford 门** + +`apply_h`、`apply_s`、`apply_sdg`、`apply_x`、`apply_y`、`apply_z`、`apply_x2p`、`apply_x2m`、`apply_y2p`、`apply_y2m`,签名均为 `(qubit) -> None`。 + +**双比特 Clifford 门** + +- `apply_cx(control, target)`:受控 X 门。 +- `apply_cy(control, target)`:受控 Y 门。 +- `apply_cz(q0, q1)`:受控 Z 门。 +- `apply_swap(q0, q1)`:SWAP 门。 + +两个比特下标相同时抛出 `ValueError`。 + +**稳定子表查询** + +- `get_stabilizers() -> list[PauliString]`:返回 N 个稳定子生成元。 +- `get_destabilizers() -> list[PauliString]`:返回 N 个反稳定子生成元。 +- `to_stim_format() -> str`:返回稳定子生成元的逐行文本表示,每行一个生成元并带符号前缀,例如 `+XX`。 + +**观测值** + +- `pauli_expectation(pauli) -> int`:返回 Pauli 字符串的期望值,取值 `-1`、`0` 或 `1`。比特数不匹配时抛出 `ValueError`。 + +**概率** + +- `probabilities() -> list[float]`:返回各基态的概率分布,长度为 2^num_qubits。不修改状态。 +- `probability_of(bits) -> float`:返回给定计算基位串的概率。 + - `bits` (`list[bool]`):按比特编号给出各比特取值。长度与 `num_qubits` 不符时抛出 `ValueError`。 + +**测量与采样** + +- `reset(qubit) -> None`:测量指定比特,若结果为 1 则翻转为 |0⟩。 +- `measure(qubit) -> bool`:测量单个比特并坍缩状态,返回该比特的结果。 +- `measure_all() -> Outcome`:测量全部比特并坍缩状态,返回 `Outcome`。位串按比特编号从高到低排列。 +- `sample_shots(shots) -> list[Outcome]`:按当前概率分布采样 `shots` 次,不修改状态。 +- `sample(measurement, shots) -> ExecutionResult`:按测量回执采样,不修改状态。 + - `measurement` (`Measurement`):由线路的测量操作得到的测量回执。 + - `shots` (`int`):采样次数。 +- `probs(measurement) -> dict[Outcome, float]`:按测量回执计算边缘概率,不修改状态。 + +**复制** + +- `copy() -> StabilizerState`:返回独立副本,修改原状态不影响副本。 + +--- + +### 其他行为 + +- `repr(state)` 返回形如 `StabilizerState(num_qubits=N)` 的字符串。 +- 未实现 `__eq__`:`==` 为默认的对象同一性比较,不比较稳定子表内容。 +- 未实现 `__copy__`、`__deepcopy__`:标准库 `copy.copy()` 与 `copy.deepcopy()` 会抛出 `TypeError`。需要副本时使用 `copy()` 方法。 + +--- + +### 异常情况 + +- `IndexError`:比特下标越界。 +- `ValueError`:非 Clifford 操作、线路含控制流、双比特门的目标比特重复、`probability_of` 的位串长度与 `num_qubits` 不符、`pauli_expectation` 的比特数不匹配、线路比特数与状态不匹配。 +- `OverflowError`:`num_qubits` 为负数。 + +--- + +示例: + +```python +import math + +from cqlib.qis import PauliString +from cqlib.qis.state import StabilizerState + +state = StabilizerState(2) +state.apply_h(0) +state.apply_cx(0, 1) + +# 概率与单位置概率 +probs = state.probabilities() +assert probs[0] == 0.5 +assert probs[3] == 0.5 +assert math.isclose(state.probability_of([False, False]), 0.5, abs_tol=1e-10) +assert math.isclose(state.probability_of([True, False]), 0.0, abs_tol=1e-10) + +# 稳定子表 +stabilizers = state.get_stabilizers() +destabilizers = state.get_destabilizers() +assert len(stabilizers) == 2 +assert len(destabilizers) == 2 +assert all(isinstance(pauli, PauliString) for pauli in stabilizers) +assert state.to_stim_format().count("\n") == 2 + +# 观测值 +assert state.pauli_expectation(PauliString.from_str("ZZ")) == 1 +assert state.pauli_expectation(PauliString.from_str("ZI")) == 0 + +# 采样不改变状态,结果只落在关联基态上 +shots = state.sample_shots(64) +assert len(shots) == 64 +assert {outcome.to_bitstring(2) for outcome in shots} <= {"00", "11"} +assert state.probabilities()[0] == 0.5 + +# 副本独立:快照保留副本创建时的分布 +snapshot = state.copy() +state.apply_x(0) +assert snapshot.probabilities()[3] == 0.5 +assert state.probabilities()[1] == 0.5 +assert state.probabilities()[2] == 0.5 + +# 全量测量后状态坍缩到单个基态 +outcome = state.measure_all() +assert outcome.to_bitstring(2) in {"01", "10"} + +# 测量与复位:在确定性基态上结果可预期 +basis = StabilizerState(1) +basis.apply_x(0) +assert basis.measure(0) is True +assert basis.probabilities()[1] == 1.0 + +basis.reset(0) +assert basis.probabilities()[0] == 1.0 +``` + +--- + +## StabilizerCircuitResult + +`StabilizerState.run_circuit()` 的返回对象,同时携带最终稳定子状态与执行过程中产生的运行期经典数据。 + +该对象没有公开构造函数,只能由 `StabilizerState.run_circuit()` 返回。 + +### 属性 + +- `state -> StabilizerState`:线路执行结束后的最终稳定子状态。 +- `classical -> ClassicalState`:执行过程中产生的运行期经典值集合,见 [Classical State](5_classical_state.md)。 + +### 其他行为 + +- `repr(result)` 返回形如 `StabilizerCircuitResult(num_qubits=N)` 的字符串。 +- 未实现 `__eq__`:`==` 为默认的对象同一性比较。 +- 未实现 `__copy__`、`__deepcopy__`:标准库 `copy.copy()` 与 `copy.deepcopy()` 会抛出 `TypeError`。需要保留结果时重新调用 `StabilizerState.run_circuit()`。 + +--- + +示例: + +```python +from cqlib import Circuit +from cqlib.qis.state import ( + ClassicalState, + RuntimeValue, + StabilizerCircuitResult, + StabilizerState, +) + +circuit = Circuit(2) +circuit.x(0) +measurement = circuit.measure(0) +circuit.reset(0) +circuit.h(1) + +result = StabilizerState.run_circuit(circuit) +assert isinstance(result, StabilizerCircuitResult) +assert "StabilizerCircuitResult" in repr(result) + +# 最终状态 +assert result.state.num_qubits == 2 +assert result.state.probabilities()[0] == 0.5 +assert result.state.probabilities()[2] == 0.5 + +# 运行期经典数据 +assert isinstance(result.classical, ClassicalState) +measured = result.classical.value(measurement.value) +assert isinstance(measured, RuntimeValue) +assert measured.kind == "bit" +assert measured.as_bit() is True +``` + +--- + +## 异常 + +两个类的校验失败按来源分为三类: + +| 异常 | 触发场景 | +| --- | --- | +| `IndexError` | 比特下标越界。 | +| `ValueError` | 线路含非 Clifford 操作或控制流、双比特门的目标比特重复、`probability_of` 的位串长度不符、`pauli_expectation` 的比特数不匹配、线路比特数与状态不匹配。 | +| `OverflowError` | `num_qubits` 为负数。 | diff --git a/docs/api/cn/python/3_qis/5_classical_state.md b/docs/api/cn/python/3_qis/5_classical_state.md new file mode 100644 index 00000000..241dead9 --- /dev/null +++ b/docs/api/cn/python/3_qis/5_classical_state.md @@ -0,0 +1,131 @@ +# 运行时经典状态 + +`cqlib.qis.state` + +`ClassicalState` 与 `RuntimeValue` 描述线路执行过程中产生的运行时经典数据。`RuntimeValue` 是单个经典值在一次执行中的带类型取值;`ClassicalState` 是一次执行结束后全部运行时经典数据的快照,按句柄查询取值。两者都由模拟器在执行完成后给出,没有公开的构造方法。 + +## 导入 + +```python +from cqlib.qis.state import ClassicalState, RuntimeValue +``` + +--- + +## RuntimeValue + +单个经典值的运行时取值。取值种类由 `kind` 区分,静态类型由 `ty` 给出。 + +### 属性 + +- `kind -> str`:取值种类,为 `"bit"`、`"bool"`、`"uint"`、`"bit_vec"` 之一。 +- `ty -> ClassicalType`:该取值对应的经典类型。 + +### 方法 + +- `to_bitstring() -> str | None`:取值为 `bit` 或 `bit_vec` 时返回比特串,其余种类返回 `None`。 +- `as_bit() -> bool`:按 `bit` 读出取值。 +- `as_bool() -> bool`:按 `bool` 读出取值。 +- `as_uint() -> int`:按 `uint` 读出取值。 +- `as_bitvec_outcome() -> Outcome`:按 `bit_vec` 读出取值,返回测量回执。 + +### 其他行为 + +- 相等性按值判定:种类、位宽与载荷都相同才相等;与其它类型比较一律为假。 +- 相等的取值哈希相同,可直接放入集合去重。 +- 支持 `copy`、`deepcopy`。 +- `repr` 形如 `RuntimeValue.bit(true)`、`RuntimeValue.uint(width=8, value=3)`、`RuntimeValue.bit_vec(width=2, bits='10')`。 + +### 异常情况 + +- `TypeError`:把 `as_bit`、`as_bool`、`as_uint`、`as_bitvec_outcome` 用于不匹配的取值种类时抛出。 + +--- + +## ClassicalState + +一次线路执行结束后的运行时经典状态快照,按经典值句柄或经典变量句柄查询取值。 + +### 方法 + +- `value(value) -> RuntimeValue | None`:查询某个不可变经典值本次执行产生的运行时取值;没有取值时返回 `None`。`value` 为 `ClassicalValue`,通常取自测量回执。 +- `var(var) -> RuntimeValue | None`:查询某个可变经典变量的当前运行时取值;没有取值时返回 `None`。`var` 为 `ClassicalVar`。 + +### 其他行为 + +- 没有公开构造方法,只能从执行结果取得。 +- 支持 `copy`、`deepcopy`。 +- `repr` 固定为 `ClassicalState()`。 + +--- + +## 示例 + +### 查询测量产生的取值 + +```python +from cqlib.circuit import Circuit +from cqlib.qis.state import StabilizerState + +circuit = Circuit(2) +circuit.x(0) +measurement = circuit.measure(0) +circuit.reset(0) +circuit.h(1) + +result = StabilizerState.run_circuit(circuit) +measured = result.classical.value(measurement.value) +assert measured.kind == "bit" +assert measured.as_bit() is True +assert measured.to_bitstring() == "1" +``` + +### 查询可变经典变量 + +```python +from cqlib.circuit import Circuit, ClassicalExpr, ClassicalType +from cqlib.qis.state import StabilizerState + +circuit = Circuit(1) +flag = circuit.var(ClassicalType.bool()) +circuit.store(flag, ClassicalExpr.bool_literal(True)) + +result = StabilizerState.run_circuit(circuit) +stored = result.classical.var(flag) +assert stored.kind == "bool" +assert stored.as_bool() is True +assert stored.to_bitstring() is None +``` + +### 取值比较与去重 + +同类同载荷的取值相等且哈希一致,可直接用于集合去重;不同种类的取值即使语义相近也不相等。 + +```python +from cqlib.circuit import Circuit +from cqlib.qis.state import StabilizerState + + +def measured_bit(): + circuit = Circuit(1) + circuit.x(0) + measurement = circuit.measure(0) + return StabilizerState.run_circuit(circuit).classical.value(measurement.value) + + +first = measured_bit() +second = measured_bit() +assert first == second +assert hash(first) == hash(second) +assert len({first, second}) == 1 +``` + +--- + +## 校验与错误处理 + +| 异常 | 触发场景 | +| --- | --- | +| `TypeError` | 用不匹配的读出方法读取 `RuntimeValue`,例如对 `bit` 取值调用 `as_uint()`。 | + +经典类型、经典变量与经典值句柄的定义见 [经典数据与控制流](../0_circuit/9_classical_control_flow.md);`Outcome` 的定义见 [结果与执行](../2_device/5_result.md)。 diff --git a/docs/api/cn/python/3_qis/6_pauli.md b/docs/api/cn/python/3_qis/6_pauli.md new file mode 100644 index 00000000..79658710 --- /dev/null +++ b/docs/api/cn/python/3_qis/6_pauli.md @@ -0,0 +1,247 @@ +# Pauli 算子 + +`cqlib.qis` + +`Phase`、`Pauli` 与 `PauliString` 是 Pauli 群的三个层次:`Phase` 表示群元素相乘时产生的相位因子,`Pauli` 是单比特 Pauli 算子,`PauliString` 是多比特 Pauli 算子的张量积。`PauliString` 采用辛表示存储,同时提供字符形式、比特掩码与矩阵形式之间的转换。 + +## 导入 + +```python +from cqlib.qis import Phase, Pauli, PauliString +``` + +--- + +## Phase + +Pauli 群中的相位因子,同构于 4 阶循环群,表示 $i^n$ 且 $n \in \{0, 1, 2, 3\}$。 + +### Phase(val) + +参数: + +- `val` (`int`):相位指数,按 4 取模,可取值范围为 `0`–`255`。 + +### 静态方法 + +- `Phase.plus()`:返回 $+1$,即 $i^0$。 +- `Phase.i()`:返回 $+i$,即 $i^1$。 +- `Phase.minus()`:返回 $-1$,即 $i^2$。 +- `Phase.minus_i()`:返回 $-i$,即 $i^3$。 + +### 属性 + +- `exponent -> int`:相位指数,取值 `0`–`3`。 + +### 方法 + +- `to_complex() -> complex`:转换为 Python 复数。 + +### 其他行为 + +- `+` 与 `*` 都表示群乘法,结果按 4 取模。 +- 支持 `==`、`hash`。 +- `str` 为 `1`、`i`、`-1`、`-i`;`repr` 形如 `Phase(1)`。 + +### 异常情况 + +- `OverflowError`:`val` 超出 `0`–`255` 时抛出。 + +--- + +## Pauli + +单比特 Pauli 算子。 + +### 静态方法 + +- `Pauli.x()`、`Pauli.y()`、`Pauli.z()`、`Pauli.i()`:四个 Pauli 算子的唯一构造入口。 + +### 方法 + +- `to_symplectic() -> tuple[int, int]`:返回辛表示 $(x, z)$。`I` 为 `(0, 0)`,`X` 为 `(1, 0)`,`Y` 为 `(1, 1)`,`Z` 为 `(0, 1)`。 +- `to_matrix() -> numpy.ndarray`:返回 $2 \times 2$ 复数矩阵。 +- `mul_with_phase(other) -> tuple[Pauli, Phase]`:与另一个单比特 Pauli 算子相乘,返回结果算子与相位因子。 + +### 其他行为 + +- 没有公开构造方法。 +- `*` 表示相乘,但不返回相位因子;需要相位时用 `mul_with_phase`。 +- 支持 `==`、`hash`。 +- `str` 为算符名;`repr` 形如 `Pauli.X`。 + +--- + +## PauliString + +多比特 Pauli 算子,即 $P = \bigotimes_{i=0}^{N-1} P_i$,其中 $P_i \in \{I, X, Y, Z\}$。 + +### PauliString(num_qubits) + +参数: + +- `num_qubits` (`int`):量子比特数。构造出的是全 `I` 字符串,相位为 $+1$。 + +### PauliString.from_str(s) + +从字符形式构造。格式为 `[+|-][i|j]<算符序列>`,算符序列由 `I`、`X`、`Y`、`Z` 组成;量子比特按下标逆序排列,首个字符对应最高比特下标。`j` 与 `i` 等价,内部统一为 `i`。 + +参数: + +- `s` (`str`):字符形式,例如 `"XZI"`、`"-iZII"`、`"+XYZ"`。 + +返回: + +- `PauliString`:新的 Pauli 字符串。 + +异常情况: + +- `ValueError`:字符串为空、含有非法字符、或只给出相位而没有算符时抛出。 + +### 属性 + +- `num_qubits -> int`:量子比特数。 +- `phase -> Phase`:全局相位,可读可写。 +- `x_bits -> list[bool]`:$X$ 分量比特向量,下标为比特下标。 +- `z_bits -> list[bool]`:$Z$ 分量比特向量。 +- `x_mask -> int`:$X$ 分量的整数掩码。 +- `z_mask -> int`:$Z$ 分量的整数掩码。 + +### 方法 + +- `get_pauli(idx) -> Pauli`:读取指定比特上的算符。 +- `set_pauli(idx, pauli) -> None`:写入指定比特上的算符。 +- `y_phase() -> complex`:返回 $Y$ 算符贡献的相位因子。由 $Y = iXZ$,$n$ 个 $Y$ 贡献 $i^n$。 +- `commutes_with(other) -> bool`:判断两个 Pauli 字符串是否对易,依据是辛内积模 2 是否为零。 +- `support() -> list[int]`:返回取了非 `I` 算符的比特下标,升序排列。 +- `to_matrix() -> numpy.ndarray`:返回稠密矩阵,按小端张量序展开(比特 `0` 是最低有效张量因子,即 $P_{N-1} \otimes \cdots \otimes P_0$),并计入全局相位。形状为 $(2^N, 2^N)$,`complex128` 类型。空间复杂度为 $O(4^N)$,适用于小规模系统的分析校验。 +- `copy() -> PauliString`:返回副本。 +- `expectation(probs) -> float`:由概率分布计算期望值 $\langle P \rangle = \sum_s p(s)\langle s | P | s \rangle$。若字符串含 `X` 或 `Y` 等非对角算符,对任意计算基概率分布的期望值均为 `0`。 +- `expectation_statevector(sv) -> float`:由态矢量计算期望值。 +- `expectation_density_matrix(dm) -> float`:由密度矩阵计算期望值。 +- `expectation_probs(measurements) -> float`:由测量概率计算期望值。 +- `variance_statevector(sv) -> float`:由态矢量计算方差。 + +### 其他行为 + +- `len(ps)` 等于 `num_qubits`;零比特字符串的布尔值为假。 +- 可直接迭代,按比特下标升序逐个给出 `Pauli`,包含 `I`。迭代器采用快照语义:创建迭代器之后对原字符串的修改不影响本次迭代。迭代器提供 `__length_hint__`,给出剩余元素个数。 +- `ps[i]` 读取指定比特上的算符,支持负下标,`-1` 指最高比特下标。 +- `*` 与 `*=` 表示相乘,结果的相位按群乘法累加。 +- 支持 `==`;作为可变类型不定义 `hash`,不可哈希,也不能用作字典键。 +- `str` 形如 `+XYZ`、`-iZII`;`repr` 形如 `PauliString(num_qubits=3, phase=1, x_bits=[...], z_bits=[...])`。 + +### 异常情况 + +- `ValueError`:`from_str` 解析失败;`commutes_with` 或 `*` 的两个操作数比特数不一致。 +- `IndexError`:`get_pauli`、`set_pauli` 的下标越界;`ps[i]` 的下标越界。 + +### 期望值参数说明 + +`expectation(probs)` 的 `probs` 是状态串到概率的字典。状态串按小端约定书写:最右字符对应比特 `0`,例如 `"01"` 表示比特 `0` 为 `1`、比特 `1` 为 `0`。状态串长度必须等于 `num_qubits`。 + +`expectation_probs(measurements)` 的 `measurements` 是若干 `(PauliString, dict)` 元组的列表,每个元组给出一组测量基与对应的概率分布。若待求算符无法由给出的测量基推出,则报错。 + +### 示例 + +```python +from cqlib.qis import Pauli, PauliString, Phase + +# 乘法与相位 +x, y, z, i = Pauli.x(), Pauli.y(), Pauli.z(), Pauli.i() +res, phase = x.mul_with_phase(y) # XY = iZ +assert res == z +assert phase == Phase.i() +assert x * y == z +assert x.to_symplectic() == (1, 0) + +# 字符形式与属性 +ps = PauliString.from_str("-iZII") +assert ps.num_qubits == 3 +assert str(ps) == "-iZII" +assert ps.phase == Phase.minus_i() +assert PauliString.from_str("+jZZ").phase == Phase.i() + +# 掩码、支撑集与迭代 +ps = PauliString.from_str("XIZ") +assert ps.x_mask == 0b100 +assert ps.z_mask == 0b001 +assert ps.support() == [0, 2] +assert len(ps) == 3 +assert list(ps) == [Pauli.z(), Pauli.i(), Pauli.x()] # 按比特下标升序,含 I +``` + +```python +from cqlib.qis import Pauli, PauliString + +# 构造与逐比特读写 +ps = PauliString(3) +assert str(ps) == "+III" +ps.set_pauli(0, Pauli.x()) +ps.set_pauli(1, Pauli.z()) +ps.set_pauli(2, Pauli.y()) +assert str(ps) == "+YZX" +assert ps.get_pauli(0) == Pauli.x() +assert ps[0] == Pauli.x() +assert ps[-1] == Pauli.y() +``` + +```python +from cqlib.qis import PauliString + +# 乘法与对易性 +ps1 = PauliString.from_str("X") +ps2 = PauliString.from_str("Z") +assert str(ps1 * ps2) == "-iY" + +ps3 = PauliString.from_str("XZ") +ps4 = PauliString.from_str("YX") +assert str(ps3 * ps4) == "-ZY" + +ps5 = PauliString.from_str("XZI") +assert ps5.commutes_with(PauliString.from_str("ZXI")) is True +assert ps5.commutes_with(PauliString.from_str("YII")) is False +``` + +```python +import math + +from cqlib.qis import PauliString, Statevector + +# 期望值与方差 +sv = Statevector(1) +ps_z = PauliString.from_str("Z") +assert math.isclose(ps_z.expectation_statevector(sv), 1.0) + +sv.apply_h(0) +ps_x = PauliString.from_str("X") +assert math.isclose(ps_x.expectation_statevector(sv), 1.0) +assert math.isclose(ps_z.variance_statevector(sv), 1.0, abs_tol=1e-10) + +# 由概率分布求期望值 +assert math.isclose(ps_z.expectation({"0": 0.5, "1": 0.5}), 0.0) +``` + +```python +import numpy as np + +from cqlib.qis import PauliString + +# 矩阵形式,小端张量序:比特 0 是最低有效张量因子 +ps = PauliString.from_str("ZX") +matrix = ps.to_matrix() +assert matrix.shape == (4, 4) +assert matrix.dtype == np.complex128 +``` + +--- + +## 校验与错误处理 + +| 异常 | 触发场景 | +| --- | --- | +| `ValueError` | `PauliString.from_str` 的字符串为空、含非法字符或缺少算符;`commutes_with`、`*`、`*=` 的两个操作数比特数不一致。 | +| `IndexError` | `get_pauli`、`set_pauli` 或 `ps[i]` 的比特下标越界。 | +| `OverflowError` | `Phase(val)` 的指数超出可取值范围。 | + +`expectation_statevector`、`expectation_density_matrix` 与 `variance_statevector` 的输入类型见 [Statevector](1_statevector.md) 与 [DensityMatrix](2_density_matrix.md)。 diff --git a/docs/api/cn/python/3_qis/7_hamiltonian.md b/docs/api/cn/python/3_qis/7_hamiltonian.md new file mode 100644 index 00000000..8e5627ee --- /dev/null +++ b/docs/api/cn/python/3_qis/7_hamiltonian.md @@ -0,0 +1,222 @@ +# Hamiltonian 观测量 + +`cqlib.qis` + +`Hamiltonian` 用 Pauli 字符串的线性组合表示算子 $H = \sum_k c_k P_k$,其中 $c_k$ 为复系数、$P_k$ 为多比特 Pauli 字符串。它是 $2^N \times 2^N$ 矩阵的稀疏表示,用于描述系统能量、作为观测量计算期望值,以及生成时间演化线路。 + +本页同时覆盖 `Observable` 协议,它规定了观测量需要提供哪些成员。 + +## 导入 + +```python +from cqlib.qis import Hamiltonian, Observable +``` + +--- + +## Observable + +观测量协议。它是由 `cqlib.qis` 定义的纯 Python `typing.Protocol`,不是原生类,也不提供运行时检查。 + +定义: + +```python +from typing import Dict, List, Protocol, Tuple + + +class Observable(Protocol): + def expectation_statevector(self, sv: Statevector) -> float: ... + def expectation_density_matrix(self, dm: DensityMatrix) -> float: ... + def expectation_probs( + self, measurements: List[Tuple[PauliString, Dict[str, float]]] + ) -> float: ... + def variance_statevector(self, sv: Statevector) -> float: ... + @property + def num_qubits(self) -> int: ... +``` + +实现该协议需要提供以下成员: + +- `expectation_statevector(sv) -> float`:由态矢量计算期望值 $\langle \psi | O | \psi \rangle$。 +- `expectation_density_matrix(dm) -> float`:由密度矩阵计算期望值 $\mathrm{Tr}(\rho O)$。 +- `expectation_probs(measurements) -> float`:由测量概率计算期望值,`measurements` 是若干 `(PauliString, dict)` 元组。 +- `variance_statevector(sv) -> float`:由态矢量计算方差。 +- `num_qubits -> int`:观测量作用的量子比特数,用于计算前的维数校验。 + +`Hamiltonian` 与 `PauliString` 都具备上述全部成员,可直接作为观测量传给接受 `Observable` 的接口。 + +--- + +## Hamiltonian + +### Hamiltonian(num_qubits) + +参数: + +- `num_qubits` (`int`):算符作用的量子比特数。构造出的是该比特数上的零算子。 + +### Hamiltonian.from_pauli(pauli) + +用单个 Pauli 字符串构造,系数为 `1.0`。 + +参数: + +- `pauli` (`PauliString`):作为唯一项的 Pauli 字符串。 + +返回: + +- `Hamiltonian`:表示 $H = 1.0 \cdot P$ 的算符。 + +### Hamiltonian.from_list(terms) + +由 `(PauliString, 系数)` 元组列表构造。 + +参数: + +- `terms` (`list[tuple[PauliString, float | int | complex | tuple[float, float]]]`):项列表。系数可以是 `float`、`int`、`complex`,或 `(实部, 虚部)` 二元组。 + +返回: + +- `Hamiltonian`:新的算符。 + +异常情况: + +- `ValueError`:存在项的 Pauli 字符串比特数不一致;或列表元素不是二元组。 + +### 属性 + +- `num_qubits -> int`:量子比特数。 +- `num_terms -> int`:当前项数,即 `terms` 列表在 `simplify()` 之前的长度。 +- `terms -> list[tuple[PauliString, complex]]`:项列表,系数以 Python 复数给出。 + +### 方法 + +- `add_term(op, coeff) -> None`:追加一个 Pauli 项及其系数。系数取值形式与 `from_list` 相同。 +- `simplify() -> None`:化简。先把 Pauli 字符串的内部相位吸收进复系数,再合并 Pauli 字符串相同的项、删除系数接近零的项。 +- `scale(factor) -> None`:用复系数整体缩放所有项。系数取值形式与 `from_list` 相同。 +- `all_terms_commute() -> bool`:判断所有 Pauli 项是否两两对易。 +- `to_matrix() -> numpy.ndarray`:返回稠密矩阵 $H = \sum_k c_k P_k$,按小端比特序展开并计入 Pauli 相位。形状为 $(2^N, 2^N)$,`complex128` 类型;空算符得到零矩阵。空间复杂度为 $O(4^N)$,适用于小规模系统的分析校验。 +- `to_trotter_circuit(time, steps, mode) -> Circuit`:生成 Trotter-Suzuki 分解给出的时间演化线路,用一串 Pauli 旋转逼近 $U(t) = e^{-iHt}$。 +- `to_evolution_circuit(time, steps, mode) -> Circuit`:生成时间演化线路。所有项对易时走精确的单遍分解;存在非对易项时回退到指定的 Trotter 模式与步数。 +- `copy() -> Hamiltonian`:返回副本。 +- `expectation_statevector(sv) -> float`:由态矢量计算期望值。 +- `expectation_density_matrix(dm) -> float`:由密度矩阵计算期望值。 +- `expectation_probs(measurements) -> float`:由测量概率计算期望值。 +- `variance_statevector(sv) -> float`:由态矢量计算方差。 + +### 其他行为 + +- `+` 与 `+=` 把两个算符的项列表惰性拼接,不自动合并同类项;需要合并时调用 `simplify()`。 +- 支持 `==`。 +- 不定义 `hash`,不可哈希。 +- `str` 给出算符的可读形式;`repr` 形如 `Hamiltonian(num_qubits=2, num_terms=2)`。 + +### 异常情况 + +- `ValueError`:两个算符比特数不一致(`+`、`+=`)、项的比特数与算符不一致(`from_list`、`add_term`)、系数类型不受支持、观测量与量子态比特数不一致(`expectation_*`、`variance_statevector`)、测量概率中找不到可用的测量基(`expectation_probs`)。 +- `ValueError`:`to_trotter_circuit` 与 `to_evolution_circuit` 在 `steps` 为 `0`、算符为空、系数非 Hermitian、或 Pauli 字符串相位非 Hermitian 时抛出。 + +### 示例 + +```python +import math + +from cqlib.qis import Hamiltonian, PauliString, Statevector + +# 从空算符逐项构造 +h = Hamiltonian(2) +h.add_term(PauliString.from_str("ZZ"), 0.5) +h.add_term(PauliString.from_str("XX"), 0.5) +h.simplify() + +# 在 Bell 态上求期望值 +sv = Statevector(2) +sv.apply_h(0) +sv.apply_cx(0, 1) +assert math.isclose(h.expectation_statevector(sv), 1.0) +``` + +```python +import math + +from cqlib.qis import Hamiltonian, PauliString + +# 由项列表构造,系数支持 float、complex 与 (实部, 虚部) 二元组 +terms = [ + (PauliString.from_str("XX"), 0.5), + (PauliString.from_str("YY"), -0.3 + 0.0j), + (PauliString.from_str("ZZ"), (0.0, 0.2)), +] +h = Hamiltonian.from_list(terms) +assert h.num_qubits == 2 +assert h.num_terms == 3 +``` + +```python +import math + +from cqlib.qis import Hamiltonian, PauliString + +# 化简:合并同类项、删除近零项、把内部相位并入系数 +h = Hamiltonian(2) +h.add_term(PauliString.from_str("XX"), 0.5) +h.add_term(PauliString.from_str("XX"), 0.3) +h.add_term(PauliString.from_str("ZZ"), 1.0) +h.add_term(PauliString.from_str("ZZ"), -1.0) +h.add_term(PauliString.from_str("+iYY"), 0.5) + +assert h.num_terms == 5 +h.simplify() +assert h.num_terms == 2 + +xx_term = next(t for t in h.terms if str(t[0]) == "+XX") +assert math.isclose(xx_term[1].real, 0.8) +``` + +```python +from cqlib.qis import Hamiltonian, PauliString, TrotterMode + +# 时间演化线路 +h = Hamiltonian(2) +h.add_term(PauliString.from_str("ZZ"), 0.5) +h.add_term(PauliString.from_str("XX"), 0.3) + +circuit = h.to_trotter_circuit(1.0, 10, TrotterMode.first_order()) +assert circuit.num_qubits == 2 +assert len(circuit) > 0 +``` + +```python +from cqlib.qis import Hamiltonian, PauliString, TrotterMode + +# 所有项对易时 to_evolution_circuit 走精确分解 +h_commuting = Hamiltonian(2) +h_commuting.add_term(PauliString.from_str("ZZ"), 0.5) +h_commuting.add_term(PauliString.from_str("IZ"), 0.3) +assert h_commuting.all_terms_commute() is True + +exact = h_commuting.to_evolution_circuit(1.0, 1, TrotterMode.first_order()) +assert exact.num_qubits == 2 + +# 存在非对易项时回退到 Trotter +h_non_commuting = Hamiltonian(1) +h_non_commuting.add_term(PauliString.from_str("X"), 1.0) +h_non_commuting.add_term(PauliString.from_str("Z"), 1.0) +assert h_non_commuting.all_terms_commute() is False + +trotterized = h_non_commuting.to_evolution_circuit(0.5, 4, TrotterMode.second_order()) +assert trotterized.num_qubits == 1 +``` + +--- + +## 校验与错误处理 + +| 异常 | 触发场景 | +| --- | --- | +| `ValueError` | 比特数不一致:`from_list` 的项之间、`add_term` 的项与算符之间、`+` 与 `+=` 的两个算符之间、观测量与量子态之间。 | +| `ValueError` | 系数不是 `float`、`int`、`complex` 或 `(实部, 虚部)` 二元组。 | +| `ValueError` | `to_trotter_circuit` 与 `to_evolution_circuit`:`steps` 为 `0`、算符为空、系数或 Pauli 字符串相位非 Hermitian。 | +| `ValueError` | `expectation_probs` 无法从给定的测量基推出待求算符。 | + +观测量计算的输入类型见 [Statevector](1_statevector.md) 与 [DensityMatrix](2_density_matrix.md);Trotter 模式的定义见 [Trotter 演化](8_evolution.md)。 diff --git a/docs/api/cn/python/3_qis/8_evolution.md b/docs/api/cn/python/3_qis/8_evolution.md new file mode 100644 index 00000000..9607ec6b --- /dev/null +++ b/docs/api/cn/python/3_qis/8_evolution.md @@ -0,0 +1,148 @@ +# Trotter 演化 + +`cqlib.qis` + +`TrotterMode` 描述如何把时间演化算子 $U(t) = e^{-iHt}$ 近似为一串 Pauli 旋转,是把 `Hamiltonian` 转成演化线路时必须指定的分解方式。本页覆盖 `TrotterMode` 本身,以及消费它的两个演化入口。 + +## 导入 + +```python +from cqlib.qis import Hamiltonian, PauliString, TrotterMode +``` + +--- + +## TrotterMode + +Trotter-Suzuki 分解模式。没有公开构造方法,三种模式分别由静态方法创建;相等性按模式与随机种子判定。 + +### 静态方法 + +- `TrotterMode.first_order()`:一阶 Lie-Trotter 分解。对 $H = \sum_k c_k P_k$,单步按项顺序逐个作用 + $U(t) \approx \left[ \prod_k e^{-i c_k (t/n) P_k} \right]^n$, + 误差量级为 $O(t^2/n)$。 +- `TrotterMode.second_order()`:二阶 Strang 对称分解。单步先按项顺序、再按相反顺序各作用一次 + $U(t) \approx \left[ \prod_k e^{-i c_k (t/2n) P_k} \prod_k e^{-i c_k (t/2n) P_k} \right]^n$, + 误差量级为 $O(t^3/n^2)$。 +- `TrotterMode.randomized(seed)`:随机化一阶分解。每个 Trotter 步内随机打乱 Pauli 项的顺序,用于削弱系统性误差。 + +参数: + +- `seed` (`int`):随机种子,无符号整数,相同种子给出相同结果。 + +返回: + +- `TrotterMode`:对应的分解模式对象。 + +### 其他行为 + +- 支持 `==` 与 `hash`。`TrotterMode.randomized(s)` 之间只有种子相同才相等;`first_order()` 与 `second_order()` 分别与各自的新实例相等。 +- `str` 为 `first-order`、`second-order`、`randomized (seed=N)`。 +- `repr` 为 `TrotterMode.FirstOrder`、`TrotterMode.SecondOrder`、`TrotterMode.Randomized(seed=N)`。 + +--- + +## 演化入口 + +`TrotterMode` 由 `Hamiltonian` 的两个演化方法消费。二者都返回抽象线路,参数含义一致。 + +### Hamiltonian.to_trotter_circuit(time, steps, mode) -> Circuit + +按指定模式与步数生成 Trotter 化的时间演化线路。 + +参数: + +- `time` (`float`):总演化时间 $t$。 +- `steps` (`int`):Trotter 步数 $n$,必须大于 `0`。 +- `mode` (`TrotterMode`):分解模式。 + +返回: + +- `Circuit`:逼近 $U(t) = e^{-iHt}$ 的线路。 + +### Hamiltonian.to_evolution_circuit(time, steps, mode) -> Circuit + +生成时间演化线路。所有项对易时走精确的单遍分解 $\prod_k e^{-i c_k t P_k}$;存在非对易项时按指定的模式与步数做 Trotter 近似。`steps` 与 `mode` 只在非对易时起作用,但 `steps` 仍须大于 `0`。 + +参数: + +- `time` (`float`):总演化时间 $t$。 +- `steps` (`int`):Trotter 步数,必须大于 `0`。仅在对易性不成立时参与分解。 +- `mode` (`TrotterMode`):分解模式。仅在对易性不成立时参与分解。 + +返回: + +- `Circuit`:精确或近似的演化线路。 + +### 异常情况 + +两个入口在以下情况抛出 `ValueError`: + +- `steps` 为 `0`。 +- `Hamiltonian` 为空。 +- 系数不是 Hermitian 的(虚部超出容差)。 +- Pauli 字符串的相位不是 Hermitian 的,即相位为 $\pm i$ 而非 $\pm 1$。 + +### 示例 + +```python +from cqlib.qis import TrotterMode + +# 三种模式 +mode = TrotterMode.first_order() +assert str(mode) == "first-order" +assert "FirstOrder" in repr(mode) + +assert str(TrotterMode.second_order()) == "second-order" + +mode_random = TrotterMode.randomized(42) +assert "seed=42" in repr(mode_random) +assert mode_random != TrotterMode.randomized(123) +assert mode_random == TrotterMode.randomized(42) +``` + +```python +from cqlib.qis import Hamiltonian, PauliString, TrotterMode + +h = Hamiltonian(2) +h.add_term(PauliString.from_str("ZZ"), 0.5) +h.add_term(PauliString.from_str("XX"), 0.3) + +first = h.to_trotter_circuit(1.0, 10, TrotterMode.first_order()) +second = h.to_trotter_circuit(1.0, 10, TrotterMode.second_order()) +randomized = h.to_trotter_circuit(1.0, 10, TrotterMode.randomized(42)) + +assert first.num_qubits == 2 +assert second.num_qubits == 2 +assert randomized.num_qubits == 2 +``` + +```python +from cqlib.qis import Hamiltonian, PauliString, TrotterMode + +# 对易项走精确分解,步数与模式不参与分解 +h_commuting = Hamiltonian(2) +h_commuting.add_term(PauliString.from_str("ZZ"), 0.5) +h_commuting.add_term(PauliString.from_str("IZ"), 0.3) + +exact = h_commuting.to_evolution_circuit(1.0, 1, TrotterMode.first_order()) +assert exact.num_qubits == 2 + +# 非对易项回退到 Trotter +h_non_commuting = Hamiltonian(1) +h_non_commuting.add_term(PauliString.from_str("X"), 1.0) +h_non_commuting.add_term(PauliString.from_str("Z"), 1.0) + +trotterized = h_non_commuting.to_evolution_circuit(0.5, 4, TrotterMode.second_order()) +assert trotterized.num_qubits == 1 +``` + +--- + +## 校验与错误处理 + +| 异常 | 触发场景 | +| --- | --- | +| `ValueError` | 演化入口的 `steps` 为 `0`、`Hamiltonian` 为空、系数或 Pauli 字符串相位非 Hermitian。 | + +需要在参数化的演化线路构造中指定分解方式时,使用线路层的演化策略接口,见 [Ansatz](../0_circuit/11_ansatz.md)。`Hamiltonian` 的完整接口见 [Hamiltonian 观测量](7_hamiltonian.md)。 diff --git a/docs/api/cn/python/3_qis/9_metrics_entropy.md b/docs/api/cn/python/3_qis/9_metrics_entropy.md new file mode 100644 index 00000000..70ed16c9 --- /dev/null +++ b/docs/api/cn/python/3_qis/9_metrics_entropy.md @@ -0,0 +1,536 @@ +# 度量与熵 + +`cqlib.qis.metrics` + +`cqlib.qis.entropy` + +`cqlib.qis.metrics` 提供量子态的度量:纯度、保真度、迹距离、冯·诺依曼熵,以及部分转置与对数负度。`cqlib.qis.entropy` 提供熵与纠缠度量:线性熵、Rényi 熵、纠缠熵、负度、concurrence 与纠缠生成熵。两个子模块都由函数组成,输入为 `Statevector` 或 `DensityMatrix`。 + +## 导入 + +```python +from cqlib.qis import entropy, metrics +``` + +--- + +## `cqlib.qis.metrics` + +### `purity_pure(sv) -> float` + +计算纯态的纯度。 + +参数: + +- `sv` (`Statevector`):态矢量。 + +返回: + +- `float`:纯度。归一化的纯态为 `1.0`。 + +异常情况: + +- 无。 + +示例: + +```python +import math + +from cqlib.qis import Statevector, metrics + +sv = Statevector(2) +assert math.isclose(metrics.purity_pure(sv), 1.0, abs_tol=1e-10) +``` + +### `purity_mixed(dm) -> float` + +计算混合态的纯度。 + +参数: + +- `dm` (`DensityMatrix`):密度矩阵。 + +返回: + +- `float`:纯度 $\mathrm{Tr}(\rho^2)$,取值区间为 $[1/2^N, 1.0]$,最大混合态取到下界。 + +异常情况: + +- 无。 + +示例: + +```python +import math + +from cqlib.qis import DensityMatrix, metrics + +dm_pure = DensityMatrix(2) +assert math.isclose(metrics.purity_mixed(dm_pure), 1.0, abs_tol=1e-10) +``` + +### `state_fidelity_pure(sv1, sv2) -> float` + +计算两个纯态之间的保真度。 + +参数: + +- `sv1` (`Statevector`):第一个态矢量。 +- `sv2` (`Statevector`):第二个态矢量。 + +返回: + +- `float`:保真度 $F(\psi, \phi) = |\langle \psi | \phi \rangle|^2$,取值区间为 `[0.0, 1.0]`。 + +异常情况: + +- `ValueError`:两个态矢量比特数不一致。 + +示例: + +```python +import math + +from cqlib.qis import Statevector, metrics + +sv1 = Statevector(1) # |0> +sv2 = Statevector(1) +sv2.apply_x(0) # |1> + +assert math.isclose(metrics.state_fidelity_pure(sv1, sv1), 1.0, abs_tol=1e-10) +assert math.isclose(metrics.state_fidelity_pure(sv1, sv2), 0.0, abs_tol=1e-10) +``` + +### `trace_distance_pure(sv1, sv2) -> float` + +计算两个纯态之间的迹距离。 + +参数: + +- `sv1` (`Statevector`):第一个态矢量。 +- `sv2` (`Statevector`):第二个态矢量。 + +返回: + +- `float`:迹距离 $D(\psi, \phi) = \sqrt{1 - |\langle \psi | \phi \rangle|^2}$,取值区间为 `[0.0, 1.0]`。 + +异常情况: + +- `ValueError`:两个态矢量比特数不一致。 + +示例: + +```python +import math + +from cqlib.qis import Statevector, metrics + +sv1 = Statevector(1) # |0> +sv2 = Statevector(1) +sv2.apply_x(0) # |1> + +assert math.isclose(metrics.trace_distance_pure(sv1, sv1), 0.0, abs_tol=1e-10) +assert math.isclose(metrics.trace_distance_pure(sv1, sv2), 1.0, abs_tol=1e-10) +``` + +### `state_fidelity_pure_mixed(sv, dm) -> float` + +计算纯态与混合态之间的保真度。 + +参数: + +- `sv` (`Statevector`):纯态。 +- `dm` (`DensityMatrix`):混合态。 + +返回: + +- `float`:保真度 $F(\psi, \rho) = \langle \psi | \rho | \psi \rangle$,取值区间为 `[0.0, 1.0]`。 + +异常情况: + +- `ValueError`:态矢量与密度矩阵比特数不一致。 + +示例: + +```python +import math + +import numpy as np + +from cqlib.qis import DensityMatrix, Statevector, metrics + +sv = Statevector(1) # |0> +dm = DensityMatrix(1) # |0><0| +assert math.isclose(metrics.state_fidelity_pure_mixed(sv, dm), 1.0, abs_tol=1e-10) + +# |0><0| 与 I/2 的保真度为 0.5 +mixed_data = np.array([0.5, 0, 0, 0.5], dtype=complex) +dm_mixed = DensityMatrix.from_density_matrix(1, mixed_data) +assert math.isclose(metrics.state_fidelity_pure_mixed(sv, dm_mixed), 0.5, abs_tol=1e-10) +``` + +### `entropy(dm) -> float` + +计算混合态的冯·诺依曼熵。 + +参数: + +- `dm` (`DensityMatrix`):密度矩阵。 + +返回: + +- `float`:熵,以比特为单位(底为 2 的对数)。 + +异常情况: + +- `ValueError`:特征分解失败。 + +示例: + +```python +import math + +import numpy as np + +from cqlib.qis import DensityMatrix, metrics + +dm = DensityMatrix(2) +assert math.isclose(metrics.entropy(dm), 0.0, abs_tol=1e-10) + +# 最大混合单比特态 I/2 的熵为 1 比特 +mixed_data = np.array([0.5, 0, 0, 0.5], dtype=complex) +dm_mixed = DensityMatrix.from_density_matrix(1, mixed_data) +assert math.isclose(metrics.entropy(dm_mixed), 1.0, abs_tol=1e-10) +``` + +### `trace_distance_mixed(dm1, dm2) -> float` + +计算两个混合态之间的迹距离。 + +参数: + +- `dm1` (`DensityMatrix`):第一个密度矩阵。 +- `dm2` (`DensityMatrix`):第二个密度矩阵。 + +返回: + +- `float`:迹距离 $D(\rho, \sigma) = \frac{1}{2}\mathrm{Tr}|\rho - \sigma|$。 + +异常情况: + +- `ValueError`:两个密度矩阵比特数不一致,或特征分解失败。 + +示例: + +```python +import math + +from cqlib.qis import DensityMatrix, metrics + +dm1 = DensityMatrix(1) +dm2 = DensityMatrix(1) +dm2.apply_x(0) + +assert math.isclose(metrics.trace_distance_mixed(dm1, dm1), 0.0, abs_tol=1e-10) +assert math.isclose(metrics.trace_distance_mixed(dm1, dm2), 1.0, abs_tol=1e-10) +``` + +### `state_fidelity_mixed(dm1, dm2) -> float` + +计算两个混合态之间的保真度。 + +参数: + +- `dm1` (`DensityMatrix`):第一个密度矩阵。 +- `dm2` (`DensityMatrix`):第二个密度矩阵。 + +返回: + +- `float`:保真度 $F(\rho, \sigma) = \left( \mathrm{Tr}\sqrt{\sqrt{\rho}\,\sigma\sqrt{\rho}} \right)^2$。 + +异常情况: + +- `ValueError`:两个密度矩阵比特数不一致,或特征分解失败。 + +示例: + +```python +import math + +from cqlib.qis import DensityMatrix, metrics + +dm1 = DensityMatrix(1) +dm2 = DensityMatrix(1) +dm2.apply_x(0) + +assert math.isclose(metrics.state_fidelity_mixed(dm1, dm1), 1.0, abs_tol=1e-10) +assert math.isclose(metrics.state_fidelity_mixed(dm1, dm2), 0.0, abs_tol=1e-10) +``` + +### `partial_transpose(dm, target_qubits) -> DensityMatrix` + +对密度矩阵的指定子系统做部分转置。 + +参数: + +- `dm` (`DensityMatrix`):输入的密度矩阵。 +- `target_qubits` (`list[int]`):要转置的子系统比特下标。 + +返回: + +- `DensityMatrix`:部分转置后的密度矩阵。转置保持迹不变,但可能不再半正定。 + +异常情况: + +- `IndexError`:`target_qubits` 中有比特下标越界。 + +示例: + +```python +import math + +from cqlib.qis import DensityMatrix, metrics + +# Bell 态 |Φ+> = (|00> + |11>) / √2 +dm = DensityMatrix(2) +dm.apply_h(0) +dm.apply_cx(0, 1) + +pt_dm = metrics.partial_transpose(dm, [0]) +data = pt_dm.data +assert math.isclose(data[0, 0].real, 0.5, abs_tol=1e-10) +assert math.isclose(data[3, 3].real, 0.5, abs_tol=1e-10) +``` + +### `logarithmic_negativity(dm, sys_a) -> float` + +计算双分划量子态的对数负度。 + +参数: + +- `dm` (`DensityMatrix`):密度矩阵。 +- `sys_a` (`list[int]`):子系统 A 的比特下标。 + +返回: + +- `float`:对数负度 $\log_2 \|\rho^{T_A}\|_1$,取值不小于 `0`。 + +异常情况: + +- `IndexError`:`sys_a` 中有比特下标越界。 +- `ValueError`:特征分解失败。 + +示例: + +```python +import math + +from cqlib.qis import DensityMatrix, metrics + +# Bell 态的对数负度为 1.0 +dm = DensityMatrix(2) +dm.apply_h(0) +dm.apply_cx(0, 1) +assert math.isclose(metrics.logarithmic_negativity(dm, [0]), 1.0, abs_tol=1e-10) + +# 可分态 |00> 的对数负度为 0.0 +dm_sep = DensityMatrix(2) +assert math.isclose(metrics.logarithmic_negativity(dm_sep, [0]), 0.0, abs_tol=1e-10) +``` + +--- + +## `cqlib.qis.entropy` + +### `linear_entropy(dm) -> float` + +计算线性熵。线性熵是冯·诺依曼熵的计算上更省事的近似,用于刻画混合程度。 + +参数: + +- `dm` (`DensityMatrix`):密度矩阵。 + +返回: + +- `float`:线性熵 $1 - \mathrm{Tr}(\rho^2)$,取值区间为 `[0, 1)`。 + +异常情况: + +- 无。 + +示例: + +```python +from cqlib.qis import DensityMatrix, entropy + +dm_pure = DensityMatrix(2) +assert abs(entropy.linear_entropy(dm_pure) - 0.0) < 1e-10 +``` + +### `renyi_entropy(dm, alpha) -> float` + +计算阶数为 `alpha` 的 Rényi 熵。 + +参数: + +- `dm` (`DensityMatrix`):密度矩阵。 +- `alpha` (`float`):阶数,必须为正。取 `1.0` 时回退到冯·诺依曼熵。 + +返回: + +- `float`:Rényi 熵,以比特为单位(底为 2 的对数)。 + +异常情况: + +- `ValueError`:`alpha` 不大于零,或特征分解失败。 + +示例: + +```python +from cqlib.qis import DensityMatrix, entropy + +dm = DensityMatrix(2) # 纯态 +assert abs(entropy.renyi_entropy(dm, 2.0) - 0.0) < 1e-10 +assert abs(entropy.renyi_entropy(dm, 1.0) - 0.0) < 1e-10 # 回退到冯·诺依曼熵 +``` + +### `entanglement_entropy_pure(sv, subsys_a) -> float` + +计算双分划纯态的纠缠熵,即子系统 A 约化密度矩阵的冯·诺依曼熵。 + +参数: + +- `sv` (`Statevector`):纯态。 +- `subsys_a` (`list[int]`):子系统 A 的比特下标。其余比特构成子系统 B。 + +返回: + +- `float`:纠缠熵,以比特为单位。 + +异常情况: + +- `ValueError`:`subsys_a` 为空、包含全部比特,或含重复下标。 +- `IndexError`:`subsys_a` 中有比特下标越界。 + +示例: + +```python +from cqlib.qis import Statevector, entropy + +# Bell 态 |Φ+> = (|00> + |11>) / √2 +sv = Statevector(2) +sv.apply_h(0) +sv.apply_cx(0, 1) +assert abs(entropy.entanglement_entropy_pure(sv, [0]) - 1.0) < 1e-10 + +# 直积态 |00> 的纠缠熵为 0 +sv_prod = Statevector(2) +assert abs(entropy.entanglement_entropy_pure(sv_prod, [0]) - 0.0) < 1e-10 +``` + +### `negativity(dm, subsys_a) -> float` + +计算负度,依据部分转置判据。 + +参数: + +- `dm` (`DensityMatrix`):双分划态的密度矩阵。 +- `subsys_a` (`list[int]`):子系统 A 的比特下标。 + +返回: + +- `float`:负度,即部分转置后负特征值绝对值之和,取值不小于 `0`。 + +异常情况: + +- `IndexError`:`subsys_a` 中有比特下标越界。 +- `ValueError`:特征分解失败。 + +示例: + +```python +from cqlib.qis import DensityMatrix, entropy + +dm = DensityMatrix(2) +dm.apply_h(0) +dm.apply_cx(0, 1) + +# Bell 态的负度为 0.5 +assert abs(entropy.negativity(dm, [0]) - 0.5) < 1e-10 +``` + +### `concurrence(dm) -> float` + +计算 2 比特量子态的 concurrence。 + +参数: + +- `dm` (`DensityMatrix`):2 比特态的密度矩阵。 + +返回: + +- `float`:concurrence,取值区间为 `[0, 1]`。可分态为 `0`,最大纠缠态为 `1`。 + +异常情况: + +- `ValueError`:态的比特数不是 2,或计算失败。 + +示例: + +```python +from cqlib.qis import DensityMatrix, entropy + +dm = DensityMatrix(2) +dm.apply_h(0) +dm.apply_cx(0, 1) + +assert abs(entropy.concurrence(dm) - 1.0) < 1e-10 + +dm_sep = DensityMatrix(2) +assert abs(entropy.concurrence(dm_sep) - 0.0) < 1e-10 +``` + +### `entanglement_of_formation(dm) -> float` + +计算 2 比特态的纠缠生成熵。 + +参数: + +- `dm` (`DensityMatrix`):2 比特态的密度矩阵。 + +返回: + +- `float`:纠缠生成熵,以比特为单位。 + +异常情况: + +- `ValueError`:态的比特数不是 2。 + +示例: + +```python +from cqlib.qis import DensityMatrix, entropy + +dm = DensityMatrix(2) +dm.apply_h(0) +dm.apply_cx(0, 1) + +assert abs(entropy.entanglement_of_formation(dm) - 1.0) < 1e-10 + +dm_sep = DensityMatrix(2) +assert abs(entropy.entanglement_of_formation(dm_sep) - 0.0) < 1e-10 +``` + +--- + +## 校验与错误处理 + +| 异常 | 触发场景 | +| --- | --- | +| `ValueError` | 参与比较的两个量子态比特数不一致;`renyi_entropy` 的 `alpha` 不大于零;`entanglement_entropy_pure` 的子系统为空、包含全部比特或含重复下标;`concurrence` 与 `entanglement_of_formation` 的态不是 2 比特;特征分解失败。 | +| `IndexError` | `partial_transpose`、`logarithmic_negativity`、`negativity`、`entanglement_entropy_pure` 的比特下标越界。 | + +注意 `cqlib.qis.entropy` 是子模块名,`cqlib.qis.metrics` 中另有一个同名函数 `entropy`,二者互不相干。 + +本页各函数的输入类型见 [Statevector](1_statevector.md) 与 [DensityMatrix](2_density_matrix.md)。 diff --git a/docs/api/cn/python/4_compile/0_overview.md b/docs/api/cn/python/4_compile/0_overview.md new file mode 100644 index 00000000..b9b7a75c --- /dev/null +++ b/docs/api/cn/python/4_compile/0_overview.md @@ -0,0 +1,230 @@ +# 编译优化 + +`cqlib.compile` + +`cqlib.compile` 是 Cqlib 的编译管线模块。它以一条端到端的 `compile()` 入口覆盖从逻辑线路到满足目标约束的线路(设备目标下即物理线路)的完整流程,同时把各个编译阶段拆成可独立调用的 transform 子模块,供需要精细控制编译过程的调用方使用。 + +## Overview + +编译是把给定输入线路改写为匹配特定量子设备拓扑、并针对执行目标优化的过程。多数线路都必须经过一系列变换才能兼容目标设备,并降低噪声对结果的影响。Cqlib 的推荐入口是 `compile()` 函数:一次调用完成规范化、知识规则优化、门分解、可选布局与路由、目标门集翻译和最终校验。 + +### 编译管线 + +`compile()` 按固定顺序编排以下阶段(阶段名对应 `CompileResult.steps` 中每条记录的 `stage` 字段): + +1. **目标解析与资源校验**(`pre_init`)— 解析目标约束(`resolve.target`),校验辅助比特资源策略(`validate.resources`)。 +2. **输入规范化**(`init`)— 对输入线路做生产级规范化(`canonicalize.input`)。 +3. **定义展开与预分解优化**(`optimization`)— 先展开用户自定义门定义,让知识规则看到复合门内部的操作;随后执行预分解知识规则重写(`optimize.pre_decomposition`)。 +4. **门分解**(`optimization`)— 矩阵酉门合成、多控制门分解(按资源策略申请 ancilla)、分解后规范化、基于对易性的自反门抵消(`optimize.commutative_cancellation`)、双比特块重综合与单比特优化。 +5. **分解后优化**(`optimization`)— 知识规则重写迭代至不动点(`optimize.post_decomposition`)。 +6. **路由门集降级**(`translation`)— 把超出 SABRE 门元数约束的门(如 `CCX`)降级为至多两比特的操作。 +7. **布局与路由**(`routing`)— 在设备目标下选择初始布局,插入 SWAP 满足拓扑邻接约束(`route.sabre`),路由后执行清理与重综合。 +8. **目标门集转换**(`translation`)— 把线路降级到目标门集,设备目标进一步降级到原生指令集。 +9. **原生优化与最终校验**(`optimization` / `validation`)— 对原生指令做不动点优化(`optimize.native_fixed_point`),设备目标最后执行设备校验(`validate.device`)。 + +没有设备目标时,布局、路由与设备校验步骤会被跳过,并在 `steps` 中记录跳过原因。每个阶段都可以跳过 `compile()` 单独调用,见 [Transform](2_transform.md) 及后续页面。 + +### 选择优化力度 + +`CompileMode.normal()` 是保守的逻辑优化,使用生产默认 pass 参数;`CompileMode.enhanced()` 使用更强的分阶段工作流、更大的 pass 预算,并在有目标约束时执行目标感知清理。对大多数用例,`normal` 已足够;当编译时间预算充裕、希望换取更好的路由后清理效果时使用 `enhanced`。 + +### 选择目标约束 + +- `CompileTarget.logical()`:只做逻辑优化,产物不绑定任何硬件。适合算法层面的线路化简。 +- `CompileTarget.basis([...])`:降级到显式标准门集,不涉及设备拓扑。 +- `CompileTarget.device(device)`:针对具体设备的路由与降级,产物必须匹配设备原生能力,并执行最终设备校验。 +- `CompileTarget.topology_basis(device, [...])`:在设备拓扑上路由,同时降级到显式门集;设备只提供容量、布局与路由约束,不强制门集匹配设备原生能力。 + +### 可复现性 + +编译中的布局与路由包含随机启发式。传入 `seed` 参数(`compile()`、`CompileTarget.device()`、`CompileTarget.topology_basis()` 均支持)可获得确定性结果:相同线路、相同配置与相同 seed 产生相同 cqlib 结果。不同 cqlib 版本之间的产物不保证一致;seed 只保证同版本内的可重复性。 + +--- + +## 常用入口 + +```python +from cqlib import Circuit +from cqlib.compile import CompileMode, compile +from cqlib.device import Device + +circuit = Circuit(3) +circuit.h(0) +circuit.cx(0, 2) + +device = Device.line("line-3", 3) + +result = compile( + circuit, + mode=CompileMode.enhanced(), + device=device, + target_basis=["H", "CX", "RZ"], + seed=42, +) + +print("changed:", result.changed) +for step in result.steps: + if step.changed and not step.skipped: + print(step.stage, step.name) +``` + +--- + +## 核心概念与术语 + +| 术语 | 说明 | +| --- | --- | +| **编译管线** | 把抽象线路逐步转换为满足目标约束的线路的流程,包括逻辑优化、分解、布局、路由和目标门集翻译等阶段。 | +| **Pass** | 编译管线中的一个独立变换步骤,例如规范化、知识规则重写或路由。多数 pass 既可以由 `compile()` 自动编排,也可以单独调用。 | +| **Transform** | 一个可复用的变换对象,例如 `Canonicalizer`、`KnowledgeRewriter`。调用 `run()` 得到结果对象,输入线路保持不变。 | +| **目标约束** | 编译产物需要满足的条件,包括逻辑空间(无约束)、标准门集(`CompileTarget.basis`)、物理设备(`CompileTarget.device`)以及设备拓扑加门集(`CompileTarget.topology_basis`)。 | +| **初始布局** | 编译开始前逻辑比特到物理比特的映射。可由调用方提供,也可由布局算法自动选择。 | +| **路由** | 在受限拓扑上通过插入 SWAP 等移动操作使所有二体交互满足邻接约束的过程。 | +| **门分解** | 把复合门、自定义酉门或多控制门展开为更基础的门序列。 | +| **知识规则** | 描述相邻操作模式及其替换形式的规则,用于模式化简、抵消和硬件原生门匹配。 | +| **Ancilla** | 分解过程中按需申请的辅助量子比特。干净 ancilla 要求进出变换时处于 `\|0>`,脏 ancilla 要求完整还原其原状态。 | + +--- + +## `cqlib.compile` API 概览 + +### 端到端编译入口 + +| 名字 | 简介 | +| --- | --- | +| [`compile`](1_compiler.md) | 推荐入口,一次调用完成配置、执行和结果报告。 | +| [`CompileConfig`](1_compiler.md) | 编译配置快照,包含模式、目标约束和资源策略。 | +| [`CompileMode`](1_compiler.md) | 优化力度枚举,包括 `normal` 与 `enhanced`。 | +| [`CompileTarget`](1_compiler.md) | 目标约束枚举,包括逻辑、门集、设备与拓扑加门集。 | +| [`DeviceCompileTarget`](1_compiler.md) | 设备目标输入,包含设备、可选初始布局与随机种子。 | +| [`CompileResult`](1_compiler.md) | 编译结果,包含优化后线路、分步报告和设备布局元数据。 | +| [`WorkflowStepReport`](1_compiler.md) | 单个工作流步骤的执行记录。 | +| [`DeviceCompilationMetadata`](1_compiler.md) | 设备编译产生的初始与最终布局。 | +| [`CompilerWorkflow`](1_compiler.md) | 可复用的工作流对象,按配置反复执行编译。 | + +### 基础 transform pass + +| 名字 | 简介 | +| --- | --- | +| [`TransformResult`](2_transform.md) | transform 通用结果,包含输出线路与是否变更标志。 | +| [`CircuitAnalysis`](2_transform.md) | 线路特征快照,例如是否含测量、控制流、复合门等。 | +| [`Canonicalizer`](2_transform.md) / [`canonicalize_circuit`](2_transform.md) | 规范化 pass,统一指令形式、消除空操作、折叠全局相位。 | +| [`KnowledgeRewriter`](2_transform.md) / [`rewrite_circuit`](2_transform.md) | 基于知识规则的模式重写 pass。 | +| [`OptimizeOneQubitRuns`](2_transform.md) | 单比特连续门串优化。 | +| [`CommutativeCancellation`](2_transform.md) | 基于对易性的自反门抵消。 | +| [`LowerToRoutingBasis`](2_transform.md) / [`lower_to_routing_basis`](2_transform.md) | 路由门集降级,保证路由阶段只面对可移动的门。 | + +### 布局与路由 + +| 名字 | 简介 | +| --- | --- | +| [`trivial_layout`](3_layout.md) / [`greedy_layout`](3_layout.md) / [`vf2_perfect_layout`](3_layout.md) / [`sabre_layout`](3_layout.md) | 四种初始布局算法。 | +| [`LayoutObjective`](3_layout.md) / [`LayoutScore`](3_layout.md) | 布局目标权重与打分结果。 | +| [`CircuitLayoutAnalysis`](3_layout.md) / [`InteractionGraph`](3_layout.md) | 线路交互图分析,布局算法的输入。 | +| [`PhysicalLayoutGraph`](3_layout.md) / [`DistanceTable`](3_layout.md) | 设备物理图与最短距离表。 | +| [`route_with_layout`](4_routing.md) / [`route_sabre`](4_routing.md) | 变换层路由入口。 | +| [`RoutedCircuit`](4_routing.md) / [`SabreRouteResult`](4_routing.md) | 路由结果对象。 | +| [`SabreConfig`](5_sabre.md) / [`SabreHeuristicConfig`](5_sabre.md) / [`SabreVf2PrepassConfig`](5_sabre.md) | SABRE 算法配置。 | +| [`sabre_route`](5_sabre.md) | 已知初始布局条件下的 SABRE 路由。 | + +### 分解、重综合与目标门集 + +| 名字 | 简介 | +| --- | --- | +| [`expand_definitions`](6_decompose_resynthesis.md) / [`decompose_unitaries`](6_decompose_resynthesis.md) / [`decompose_mc_gates`](6_decompose_resynthesis.md) | 定义展开、酉门合成与多控制门分解。 | +| [`UnitaryDecomposeConfig`](6_decompose_resynthesis.md) / [`McGateDecomposeConfig`](6_decompose_resynthesis.md) | 分解配置。 | +| [`ResynthesizeTwoQubitBlocks`](6_decompose_resynthesis.md) / [`resynthesize_two_qubit_blocks`](6_decompose_resynthesis.md) | 双比特块重综合。 | +| [`TargetBasisLowerer`](6_decompose_resynthesis.md) / [`TargetBasisCostModel`](6_decompose_resynthesis.md) | 目标门集转换与代价评估。 | +| [`DeviceLowerer`](6_decompose_resynthesis.md) | 设备原生指令降级。 | + +### 知识规则与资源管理 + +| 名字 | 简介 | +| --- | --- | +| [`Rule`](7_knowledge.md) / [`RuleItem`](7_knowledge.md) / [`Condition`](7_knowledge.md) | 知识规则构造。 | +| [`RuleLibrary`](7_knowledge.md) / [`RuleMetadata`](7_knowledge.md) / [`RuleKind`](7_knowledge.md) | 规则库验证、分类与查询。 | +| [`loads`](7_knowledge.md) / [`load`](7_knowledge.md) / [`dumps`](7_knowledge.md) / [`dump`](7_knowledge.md) | 规则 DSL 读写。 | +| [`rule_matches_operations`](7_knowledge.md) / [`MatchBindings`](7_knowledge.md) | 规则结构匹配。 | +| [`ResourceManager`](8_resource.md) / [`ResourcePolicy`](8_resource.md) / [`ResourceLimits`](8_resource.md) | Ancilla 资源规划与租借。 | +| [`Commutation`](9_commutation.md) / [`CommutationChecker`](9_commutation.md) / [`check_commutation`](9_commutation.md) | 对易性保守证明。 | + +--- + +## 快速示例 + +### 1. 端到端编译 + +```python +from cqlib import Circuit +from cqlib.compile import CompileMode, compile +from cqlib.device import Device + +circuit = Circuit(3) +circuit.h(0) +circuit.cx(0, 2) + +device = Device.line("line-3", 3) + +result = compile( + circuit, + mode=CompileMode.enhanced(), + device=device, + target_basis=["H", "CX", "RZ"], + seed=42, +) + +print("changed:", result.changed) +for step in result.steps: + if step.changed and not step.skipped: + print(step.stage, step.name) +``` + +### 2. 单独执行一个 transform + +```python +from cqlib import Circuit +from cqlib.compile.transform import canonicalize_circuit + +circuit = Circuit(1) +circuit.i(0) + +result = canonicalize_circuit(circuit) +assert result.circuit is not circuit +assert len(circuit.operations) == 1 +assert len(result.circuit.operations) == 0 +assert result.changed +``` + +### 3. SABRE 路由 + +```python +from cqlib import Circuit +from cqlib.compile.sabre import SabreConfig, sabre_route +from cqlib.device import Device, Layout + +circuit = Circuit(2) +circuit.cx(0, 1) + +result = sabre_route( + circuit, + Device.line("line-3", 3), + Layout.from_pairs([(0, 0), (1, 2)], physical_count=3), + SabreConfig(routing_trials=1, seed=7), +) +assert result.swap_count == 1 +``` + +--- + +## 校验与错误处理 + +编译过程在配置解析、变换执行和设备校验阶段都可能失败。常见异常包括: + +| 异常 | 触发场景 | +| --- | --- | +| `CompilerConfigError` | 编译配置非法,例如 `target` 与 `target_basis` / `device` 同时提供、未知的 mode 名称、空目标门集。 | +| `CompilerTransformError` | 某个变换 pass 执行失败,例如分解或路由无法在给定约束下完成。 | +| `CompilerInternalError` | 编译内部一致性检查失败。 | +| `CompilerError` | 上述异常的公共基类。 | + +设备相关错误请参考设备模块文档;知识规则解析错误请参考 [Knowledge](7_knowledge.md);资源不足错误请参考 [Resource](8_resource.md)。 diff --git a/docs/api/cn/python/4_compile/1_compiler.md b/docs/api/cn/python/4_compile/1_compiler.md new file mode 100644 index 00000000..8e92e65a --- /dev/null +++ b/docs/api/cn/python/4_compile/1_compiler.md @@ -0,0 +1,302 @@ +# Compiler + +`cqlib.compile` 的端到端编译入口,包括 `compile()` 函数、编译配置对象、编译结果对象与可复用工作流对象。 + +## 导入 + +```python +from cqlib.compile import ( + CompileMode, + CompileTarget, + DeviceCompileTarget, + CompileConfig, + CompileResult, + WorkflowStepReport, + DeviceCompilationMetadata, + CompilerWorkflow, + compile, +) +``` + +--- + +## 函数 + +### compile(circuit, *, mode=None, target=None, target_basis=None, device=None, initial_layout=None, resource_policy=None, seed=None) + +运行配置好的编译工作流,返回编译结果。输入线路不会被修改。 + +顶层包以 `compile_circuit` 之名重导出同一函数,避免与内建名及子模块名冲突;`from cqlib import compile_circuit` 与 `from cqlib.compile import compile` 拿到的是同一个函数,两者行为完全一致。 + +参数: + +- `circuit` (`Circuit`):待编译线路。 +- `mode` (`CompileMode | str | None`):优化力度。接受 `CompileMode.normal()` / `CompileMode.enhanced()`,或字符串 `"normal"` / `"enhanced"`(大小写不敏感)。默认 `CompileMode.normal()`。 +- `target` (`CompileTarget | None`):显式目标约束。与 `target_basis`、`device` 互斥,同时提供会抛出 `CompilerConfigError`。 +- `target_basis` (`list[str | Instruction] | None`):目标标准门集的便捷写法,等价于 `CompileTarget.basis(...)`。条目为大小写不敏感的标准门名字符串或 `Instruction` 对象;多控制门没有字符串形式,必须以 `Instruction` 传入。 +- `device` (`Device | None`):目标设备的便捷写法。单独提供时等价于 `CompileTarget.device(device, ...)`;与 `target_basis` 同时提供时等价于 `CompileTarget.topology_basis(device, target_basis, ...)`。 +- `initial_layout` (`Layout | None`):逻辑到物理的初始布局,仅在提供 `device` 时生效。 +- `resource_policy` (`ResourcePolicy | None`):预布局分解阶段的辅助比特策略,见 [Resource](8_resource.md)。 +- `seed` (`int | None`):设备布局与路由的确定性随机种子,仅在提供 `device` 时生效。 + +返回: + +- `CompileResult` + +异常情况: + +- `CompilerConfigError`:配置非法(`target` 与便捷参数互斥、未知 mode 名称、空目标门集等)。 +- `CompilerTransformError`:变换执行失败。 +- `CompilerInternalError`:内部一致性检查失败。 + +示例: + +```python +from cqlib import Circuit +from cqlib.compile import CompileMode, compile +from cqlib.device import Device + +circuit = Circuit(3) +circuit.h(0) +circuit.cx(0, 2) + +device = Device.line("line-3", 3) + +result = compile( + circuit, + mode=CompileMode.enhanced(), + device=device, + target_basis=["H", "CX", "RZ"], + seed=42, +) + +print("changed:", result.changed) +for step in result.steps: + if step.changed and not step.skipped: + print(step.stage, step.name, step.reason) +``` + +--- + +## CompileMode + +优化力度枚举。 + +### 静态方法 + +- `CompileMode.normal()`:保守的逻辑优化,使用生产默认 pass 参数。 +- `CompileMode.enhanced()`:更强的分阶段工作流,使用更大的 pass 预算,并在有目标约束时执行目标感知清理。 + +### 其他行为 + +- `str(CompileMode.normal()) == "normal"`,`str(CompileMode.enhanced()) == "enhanced"`。 +- 支持 `==`、`hash`、`copy`、`deepcopy`。 + +--- + +## CompileConfig + +编译配置快照,不可变。 + +### CompileConfig(*, mode=None, target=None, resource_policy=None) + +参数: + +- `mode` (`CompileMode | str | None`):默认 `CompileMode.normal()`。 +- `target` (`CompileTarget | None`):默认 `CompileTarget.logical()`。 +- `resource_policy` (`ResourcePolicy | None`):默认 `ResourcePolicy()`。 + +### 属性 + +- `mode -> CompileMode` +- `target -> CompileTarget` +- `resource_policy -> ResourcePolicy` + +示例: + +```python +from cqlib.compile import CompileConfig, CompileMode, CompileTarget + +config = CompileConfig(mode=CompileMode.enhanced(), target=CompileTarget.logical()) +assert config.mode == CompileMode.enhanced() +assert config.target.kind == "logical" +``` + +--- + +## CompileTarget + +目标约束枚举。实例通过静态方法构造。 + +### 静态方法 + +- `CompileTarget.logical()`:在逻辑比特空间编译,不做目标相关降级。 +- `CompileTarget.basis(instructions)`:降级到显式标准门集。 + - `instructions` (`list[str | Instruction]`):非空门集条目列表,条目要求同 `compile()` 的 `target_basis`。 +- `CompileTarget.device(device, *, initial_layout=None, seed=None)`:针对具体设备的路由与降级,要求产物匹配设备原生能力并做最终设备校验。`device` 必须已经配置原生门集(`device.native_gates` 非空),否则构造失败;只关心拓扑与布局、不要求产物落到设备原生门集时,改用 `topology_basis`。 +- `CompileTarget.topology_basis(device, instructions, *, initial_layout=None, seed=None)`:在设备拓扑上路由,同时降级到显式门集;设备只用于容量、布局与路由,不要求门集匹配设备原生能力,因此不要求设备配置原生门集。 + +### 属性 + +- `kind -> str`:目标类别,取值 `"logical"`、`"basis"`、`"device"`、`"topology_basis"`。 + +异常情况: + +- `CompilerConfigError`:门集为空、门集条目不是已知的标准门,或 `device` 目标所用的设备没有原生门集。 + +示例: + +```python +from cqlib.circuit import Instruction, StandardGate +from cqlib.compile import CompileTarget +from cqlib.device import Device + +logical = CompileTarget.logical() +basis = CompileTarget.basis(["H", "CX", "RZ"]) + +device = Device.line("line-3", 3) +device.native_gates = [ + Instruction.from_standard_gate(StandardGate.H), + Instruction.from_standard_gate(StandardGate.CX), +] +device_target = CompileTarget.device(device, seed=42) + +topo_basis = CompileTarget.topology_basis(Device.line("line-3", 3), ["H", "CX", "RZ"]) +``` + +--- + +## DeviceCompileTarget + +设备编译输入快照。 + +### DeviceCompileTarget(device, *, initial_layout=None, seed=None) + +参数: + +- `device` (`Device`):约束产物的有序原生能力设备。 +- `initial_layout` (`Layout | None`):调用方提供的逻辑到物理初始布局。 +- `seed` (`int | None`):设备布局与路由的确定性随机种子。 + +### 属性 + +- `device -> Device` +- `initial_layout -> Layout | None` +- `seed -> int | None` + +--- + +## CompileResult + +`compile()` 与 `CompilerWorkflow.run()` 的返回对象。 + +### 属性 + +- `circuit -> Circuit`:优化后的线路。 +- `changed -> bool`:是否有任何步骤改变了输入表示。 +- `mode -> CompileMode`:本次运行使用的模式。 +- `steps -> list[WorkflowStepReport]`:按执行顺序排列的分步报告。 +- `device_metadata -> DeviceCompilationMetadata | None`:使用设备拓扑路由时的物理布局数据;非设备目标时为 `None`。 + +### 方法 + +- `step(name) -> WorkflowStepReport | None`:返回第一个名字匹配的分步报告。 +- `step_changed(name) -> bool`:该名字的报告中是否有未被跳过且确实变更了线路的记录。 + +### 其他行为 + +- 支持 `==`、`copy`、`deepcopy`。 + +示例: + +```python +from cqlib import Circuit +from cqlib.compile import compile + +circuit = Circuit(2) +circuit.h(0) +circuit.cx(0, 1) + +result = compile(circuit) +assert result.circuit is not circuit +assert result.step_changed("canonicalize.input") or not result.changed +``` + +--- + +## WorkflowStepReport + +单个工作流步骤的执行记录。 + +### 属性 + +- `stage -> str`:粗粒度阶段名,例如 `"pre_init"`、`"init"`、`"optimization"`、`"translation"`、`"routing"`、`"output"`、`"validation"`。 +- `name -> str`:工作流内步骤名。 +- `changed -> bool`:该步骤是否改变了线路表示。 +- `skipped -> bool`:该步骤是否被有意跳过。 +- `reason -> str | None`:跳过或配置说明。 + +### 其他行为 + +- 支持 `copy`、`deepcopy`。 + +--- + +## DeviceCompilationMetadata + +设备编译产生的物理布局信息。 + +### 属性 + +- `initial_layout -> Layout`:路由开始前的逻辑到物理布局。 +- `final_layout -> Layout`:所有路由 SWAP 之后的逻辑到物理布局。 + +### 其他行为 + +- 支持 `==`、`copy`、`deepcopy`。 + +--- + +## CompilerWorkflow + +可复用的编译工作流对象。 + +### CompilerWorkflow(config=None) + +参数: + +- `config` (`CompileConfig | None`):配置快照,默认使用 `CompileConfig()`。 + +### 属性 + +- `config -> CompileConfig` + +### 方法 + +- `run(circuit) -> CompileResult`:按配置执行工作流,不修改输入线路。 + +示例: + +```python +from cqlib import Circuit +from cqlib.compile import CompileMode, CompilerWorkflow, CompileConfig + +workflow = CompilerWorkflow(CompileConfig(mode=CompileMode.enhanced())) + +for _ in range(3): + circuit = Circuit(2) + circuit.h(0) + circuit.cx(0, 1) + result = workflow.run(circuit) +``` + +--- + +## 异常 + +- `CompilerError`:编译错误基类。 +- `CompilerConfigError`:配置错误。 +- `CompilerTransformError`:变换执行错误。 +- `CompilerInternalError`:内部错误。 + +以上异常均从 `cqlib.compile` 导出。 diff --git a/docs/api/cn/python/4_compile/2_transform.md b/docs/api/cn/python/4_compile/2_transform.md new file mode 100644 index 00000000..16e9d7ce --- /dev/null +++ b/docs/api/cn/python/4_compile/2_transform.md @@ -0,0 +1,325 @@ +# Transform + +`cqlib.compile.transform` 提供可复用的编译 pass。每个 pass 返回结果对象,不修改输入线路。端到端 `compile()` 自动编排这些 pass;需要单独观察或调试某个阶段时可以直接调用。 + +## 导入 + +```python +from cqlib.compile.transform import ( + TransformResult, + CircuitAnalysis, + CanonicalizeConfig, + Canonicalizer, + CanonicalizeResult, + canonicalize_circuit, + RewriteMode, + RewriteConfig, + KnowledgeRewriter, + KnowledgeRewriteResult, + KnowledgeRewriteStats, + rewrite_circuit, + OptimizeOneQubitRuns, + CommutativeCancellation, + LowerToRoutingBasis, + lower_to_routing_basis, +) +``` + +--- + +## TransformResult + +transform 通用结果对象。 + +### 属性 + +- `circuit -> Circuit`:输出线路。 +- `changed -> bool`:pass 是否改变了线路。 + +### 其他行为 + +- 支持 `==`、`copy`、`deepcopy`。 + +--- + +## CircuitAnalysis + +线路特征快照,供工作流决定执行哪些 pass。该类的完整模块路径为 `cqlib.compile.transform.analysis`。 + +### CircuitAnalysis.analyze(circuit) + +静态方法,分析线路并返回快照。 + +### 属性 + +- `has_classical_data -> bool`:是否包含经典数据。 +- `has_classical_control -> bool`:是否包含经典控制流。 +- `has_measurement -> bool`:是否包含测量。 +- `has_classical_values -> bool`:是否包含经典值(如测量结果)。 +- `has_classical_vars -> bool`:是否包含经典变量。 +- `has_runtime_classical -> bool`:是否存在运行时经典语义(测量值被后续条件使用)。 +- `needs_classical_handle_preservation -> bool`:变换是否必须保持经典句柄。 +- `has_circuit_gate_definitions -> bool`:是否包含电路门定义。 +- `has_unitary_circuit_definitions -> bool`:是否包含酉电路定义。 +- `has_unitary_gates -> bool`:是否包含自定义酉门。 +- `has_mc_gates -> bool`:是否包含多控制门。 + +示例: + +```python +from cqlib import Circuit +from cqlib.compile.transform import CircuitAnalysis + +circuit = Circuit(2) +circuit.h(0) +m = circuit.measure(0) + +analysis = CircuitAnalysis.analyze(circuit) +assert analysis.has_measurement +``` + +--- + +## Canonicalizer / canonicalize_circuit + +规范化 pass,统一指令形式、折叠全局相位、消除空操作。 + +### CanonicalizeConfig(*, round_limit=8, recurse_control_flow=true, fold_gphase=true, canonicalize_instruction_form=true, drop_noops=true, canonicalize_barriers=true) + +参数: + +- `round_limit` (`int`):规范化最大轮数。 +- `recurse_control_flow` (`bool`):是否递归进入控制流块。 +- `fold_gphase` (`bool`):是否折叠全局相位。 +- `canonicalize_instruction_form` (`bool`):是否统一指令形式。 +- `drop_noops` (`bool`):是否删除空操作。 +- `canonicalize_barriers` (`bool`):是否规范化 barrier。 + +### 静态方法 + +- `CanonicalizeConfig.production()`:生产默认配置。 + +### 属性 + +`round_limit`、`recurse_control_flow`、`fold_gphase`、`canonicalize_instruction_form`、`drop_noops`、`canonicalize_barriers`,与构造参数一一对应。 + +### Canonicalizer(config=None) + +参数: + +- `config` (`CanonicalizeConfig | None`):默认 `CanonicalizeConfig.production()`。 + +### 静态方法 + +- `Canonicalizer.production()`:使用生产默认配置构造。 + +### 属性与方法 + +- `config -> CanonicalizeConfig` +- `run(circuit) -> CanonicalizeResult` + +### CanonicalizeResult + +属性: + +- `circuit -> Circuit` +- `changed -> bool` +- `rounds -> int`:实际执行的规范化轮数。 + +### canonicalize_circuit(circuit, config=None) + +函数式入口,等价于 `Canonicalizer(config).run(circuit)`。 + +示例: + +```python +from cqlib import Circuit +from cqlib.compile.transform import canonicalize_circuit + +circuit = Circuit(1) +circuit.i(0) + +result = canonicalize_circuit(circuit) +assert result.circuit is not circuit +assert len(circuit.operations) == 1 +assert len(result.circuit.operations) == 0 +assert result.changed +``` + +--- + +## KnowledgeRewriter / rewrite_circuit + +基于知识规则的模式重写 pass,是编译管线逻辑优化的核心。 + +### RewriteMode + +重写模式枚举: + +- `RewriteMode.optimize()`:保守优化,接受的替换必须严格改善局部代价。 +- `RewriteMode.lowering()`:显式降级,分解与硬件原生规则可以局部扩张。 +- `RewriteMode.optimize().name -> "optimize"`,`RewriteMode.lowering().name -> "lowering"`。 + +### RewriteConfig(*, max_rounds=8, max_window_ops=16, max_pattern_len=8, recurse_control_flow=true, skip_labeled_ops=true, enabled_kinds=None, mode=None, target_instructions=None) + +参数: + +- `max_rounds` (`int`):最大重写轮数。 +- `max_window_ops` (`int`):滑动窗口最大操作数。 +- `max_pattern_len` (`int`):模式最大长度。 +- `recurse_control_flow` (`bool`):是否递归进入控制流块。 +- `skip_labeled_ops` (`bool`):是否跳过带标签的操作。 +- `enabled_kinds` (`list[RuleKind] | None`):启用的规则类别,`None` 表示全部。 +- `mode` (`RewriteMode | None`):默认 `RewriteMode.optimize()`。 +- `target_instructions` (`list[str | Instruction] | None`):可选目标指令基,用于目标感知代价评估。 + +### 静态方法 + +- `RewriteConfig.production()`:生产优化配置。 +- `RewriteConfig.lowering()`:降级配置。 + +### 属性 + +`max_rounds`、`max_window_ops`、`max_pattern_len`、`recurse_control_flow`、`skip_labeled_ops`、`enabled_kinds`、`mode`、`target_instructions`,与构造参数一一对应。 + +### KnowledgeRewriter(config=None) + +参数: + +- `config` (`RewriteConfig | None`):默认 `RewriteConfig.production()`。 + +### 静态方法 + +- `KnowledgeRewriter.production()`、`KnowledgeRewriter.lowering()`:使用对应预置配置构造。 + +### 属性与方法 + +- `config -> RewriteConfig` +- `run(circuit) -> KnowledgeRewriteResult` + +### KnowledgeRewriteResult + +属性: + +- `circuit -> Circuit` +- `changed -> bool` +- `stats -> KnowledgeRewriteStats` + +### KnowledgeRewriteStats + +属性: + +- `rounds_executed -> int`:实际执行轮数。 +- `rules_applied -> int`:应用规则次数。 +- `changed_sequences -> int`:发生变化的操作序列数。 +- `reached_fixpoint -> bool`:是否达到不动点。 + +### rewrite_circuit(circuit, config=None) + +函数式入口,等价于 `KnowledgeRewriter(config).run(circuit)`。 + +示例: + +```python +from cqlib import Circuit +from cqlib.compile.transform import rewrite_circuit + +circuit = Circuit(2) +circuit.h(0) +circuit.h(0) +circuit.cx(0, 1) + +result = rewrite_circuit(circuit) +print(result.stats.rules_applied) +``` + +--- + +## OptimizeOneQubitRuns + +单比特连续门串优化。该类的完整模块路径为 `cqlib.compile.transform.one_qubit_optimization`。 + +### 静态方法 + +- `OptimizeOneQubitRuns.logical()`:目标中立的优化器,使用严格逻辑代价。 +- `OptimizeOneQubitRuns.basis(target_basis)`:按精确降级到 `target_basis` 后的代价优化。`target_basis` 为大小写不敏感的标准门名字符串或 `Instruction` 对象列表(多控制门必须用 `Instruction`)。 + +### 属性与方法 + +- `policy -> str`:当前接受策略,取值 `"logical"` 或 `"basis"`。 +- `target_basis -> list[Instruction] | None`:显式目标门集;逻辑优化时为 `None`。 +- `run(circuit) -> TransformResult` + +示例: + +```python +from cqlib import Circuit, Parameter +from cqlib.compile.transform import OptimizeOneQubitRuns + +circuit = Circuit(1) +circuit.rx(0, Parameter("a")) +circuit.rx(0, Parameter("b")) + +result = OptimizeOneQubitRuns.logical().run(circuit) +``` + +--- + +## CommutativeCancellation + +基于对易性的自反门抵消 pass。该类的完整模块路径为 `cqlib.compile.transform.commutative_cancellation`。 + +### CommutativeCancellation() + +无参构造。 + +### 方法 + +- `run(circuit) -> TransformResult` + +示例: + +```python +from cqlib import Circuit +from cqlib.compile.transform import CommutativeCancellation + +circuit = Circuit(2) +circuit.x(0) +circuit.cx(0, 1) +circuit.x(0) + +result = CommutativeCancellation().run(circuit) +``` + +--- + +## LowerToRoutingBasis / lower_to_routing_basis + +路由门集降级 pass,把线路转换为适合路由阶段处理的形式。 + +### LowerToRoutingBasis(preferred_basis=None) + +参数: + +- `preferred_basis` (`list[Instruction] | None`):偏好门集;`None` 使用默认路由门集。 + +### 方法 + +- `run(circuit) -> TransformResult` + +### lower_to_routing_basis(circuit, preferred_basis=None) + +函数式入口。 + +示例: + +```python +from cqlib import Circuit +from cqlib.compile.transform import lower_to_routing_basis + +circuit = Circuit(2) +circuit.h(0) +circuit.cx(0, 1) + +result = lower_to_routing_basis(circuit) +``` diff --git a/docs/api/cn/python/4_compile/3_layout.md b/docs/api/cn/python/4_compile/3_layout.md new file mode 100644 index 00000000..28a91830 --- /dev/null +++ b/docs/api/cn/python/4_compile/3_layout.md @@ -0,0 +1,293 @@ +# Layout + +`cqlib.compile.transform.layout` 提供初始布局算法与布局分析工具。布局算法把逻辑比特映射到物理比特,为后续路由阶段提供起点。 + +## 导入 + +```python +from cqlib.compile.transform.layout import ( + trivial_layout, + greedy_layout, + vf2_perfect_layout, + sabre_layout, + analyze_circuit_for_layout, + prepare_sabre_circuit, + prepare_sabre_device_target, + trivial_layout_prepared, + greedy_layout_prepared, + vf2_perfect_layout_prepared, + sabre_layout_prepared, + LayoutObjective, + LayoutScore, + LayoutDiagnostics, + LayoutResult, + Vf2EdgeRequirement, + Vf2LayoutConfig, + Interaction, + InteractionGraph, + CircuitLayoutAnalysis, + DistanceTable, + PhysicalLayoutGraph, + PreparedSabreCircuit, + PreparedSabreTarget, +) +``` + +--- + +## 布局函数 + +### trivial_layout(circuit, device, objective=None) + +平凡布局:按编号顺序把逻辑比特映射到物理比特。 + +### greedy_layout(circuit, device, objective=None) + +贪心布局:按交互强度顺序把逻辑比特放到物理比特上。 + +### vf2_perfect_layout(circuit, device, objective=None, config=None) + +VF2 精确布局:在线路交互图能精确嵌入设备拓扑时给出完美布局。 + +参数: + +- `config` (`Vf2LayoutConfig | None`):VF2 搜索配置。 + +### sabre_layout(circuit, device, objective=None, config=None) + +SABRE 布局:使用 SABRE 启发式细化候选布局。 + +参数: + +- `config` (`SabreConfig | None`):SABRE 配置,见 [SABRE](5_sabre.md)。 + +以上函数通用参数: + +- `circuit` (`Circuit`):待布局线路。 +- `device` (`Device`):目标设备。 +- `objective` (`LayoutObjective | None`):布局目标权重,默认 `LayoutObjective.topology_only()`。 + +返回: + +- `LayoutResult` + +### 预计算入口 + +对反复布局的场景,可先做一次线路分析或设备准备,再复用中间结果: + +- `analyze_circuit_for_layout(circuit) -> CircuitLayoutAnalysis`:线路交互图分析。 +- `prepare_sabre_circuit(circuit) -> PreparedSabreCircuit`:SABRE 线路预处理。 +- `prepare_sabre_device_target(device) -> PreparedSabreTarget`:设备物理图预处理。 +- `trivial_layout_prepared(analysis, physical, objective=None) -> LayoutResult` +- `greedy_layout_prepared(analysis, physical, objective=None) -> LayoutResult` +- `vf2_perfect_layout_prepared(analysis, physical, objective=None, config=None) -> LayoutResult` +- `sabre_layout_prepared(prepared, prepared_target, objective=None, config=None) -> LayoutResult` + +其中 `analysis` 为 `CircuitLayoutAnalysis`,`physical` 为 `PhysicalLayoutGraph`,`prepared` 为 `PreparedSabreCircuit`,`prepared_target` 为 `PreparedSabreTarget`。 + +示例: + +```python +from cqlib import Circuit +from cqlib.compile.transform.layout import sabre_layout +from cqlib.device import Device + +circuit = Circuit(3) +circuit.cx(0, 2) + +device = Device.line("line-3", 3) +result = sabre_layout(circuit, device) +print(result.layout) +``` + +--- + +## LayoutObjective + +布局目标权重,用于给候选布局打分。 + +### LayoutObjective(*, distance_weight=1.0, direction_weight=1.0, two_qubit_error_weight=0.0, readout_error_weight=0.0) + +参数: + +- `distance_weight` (`float`):最短距离项权重。 +- `direction_weight` (`float`):方向项权重。 +- `two_qubit_error_weight` (`float`):双比特门错误率项权重。 +- `readout_error_weight` (`float`):读出错误率项权重。 + +### 静态方法 + +- `LayoutObjective.topology_only()`:纯拓扑目标。 +- `LayoutObjective.fidelity_aware()`:默认保真度感知目标。 +- `LayoutObjective.auto_from_device(device)`:设备有可用校准数据时选择保真度感知目标,否则退回拓扑目标。 +- `LayoutObjective.auto_from_physical(physical)`:同上,作用于已准备的物理图。 +- `LayoutObjective.fidelity_required(device)`:保真度感知目标;设备没有可用校准数据时抛错。 +- `LayoutObjective.fidelity_required_from_physical(physical)`:同上,作用于已准备的物理图。 + +### 属性 + +- `distance_weight`、`direction_weight`、`two_qubit_error_weight`、`readout_error_weight`(`float`),与构造参数一一对应。 +- `uses_fidelity -> bool`:是否使用保真度项。 + +### 方法 + +- `score_layout(analysis, physical, layout) -> LayoutScore`:用线路分析、物理图和一个完整布局计算分数。 + +### 其他行为 + +- 支持 `==`、`copy`、`deepcopy`。 + +--- + +## LayoutScore + +布局打分结果。 + +### 属性 + +- `total -> float`:总分。 +- `distance -> float`:距离项。 +- `direction -> float`:方向项。 +- `two_qubit_error -> float`:双比特错误项。 +- `readout_error -> float`:读出错误项。 +- `used_fidelity -> bool`:是否使用保真度项。 + +--- + +## LayoutDiagnostics + +布局过程诊断信息。 + +### 属性 + +- `is_perfect -> bool`:是否找到完美布局。 +- `candidates_evaluated -> int`:评估的候选数。 +- `used_fidelity -> bool`:是否使用保真度项。 +- `notes -> list[str]`:诊断说明。 + +--- + +## LayoutResult + +布局函数返回对象。 + +### 属性 + +- `layout -> Layout`:选出的逻辑到物理布局。 +- `score -> LayoutScore | None`:打分结果,无打分时为 `None`。 +- `diagnostics -> LayoutDiagnostics`:诊断信息。 + +--- + +## Vf2EdgeRequirement + +VF2 搜索的边要求。 + +### 静态方法 + +- `Vf2EdgeRequirement.positive_interactions()`:只要求正交互边匹配。 +- `Vf2EdgeRequirement.all_interactions()`:要求所有交互边匹配。 + +--- + +## Vf2LayoutConfig + +VF2 搜索配置。 + +### Vf2LayoutConfig(*, candidate_limit=10, call_limit=None, edge_requirement=None) + +参数: + +- `candidate_limit` (`int`):候选布局数量上限。 +- `call_limit` (`int | None`):搜索调用次数上限。 +- `edge_requirement` (`Vf2EdgeRequirement | None`):边匹配要求。 + +### 属性 + +`candidate_limit`、`call_limit`、`edge_requirement`,与构造参数一一对应。 + +--- + +## 布局分析对象 + +### Interaction + +一条逻辑比特对交互。 + +属性: + +- `left -> Qubit`、`right -> Qubit`:交互两端逻辑比特。 +- `weight -> float`:交互总权重。 +- `directed_weight_left_to_right -> float`:从左到右的有向权重。 +- `directed_weight_right_to_left -> float`:从右到左的有向权重。 +- `first_seen_order -> int`:首次出现顺序。 + +### InteractionGraph + +线路交互图。 + +属性与方法: + +- `interactions -> list[Interaction]`:交互列表。 +- `is_empty() -> bool`:是否无交互。 +- `logical_activity() -> list[tuple[Qubit, float]]`:每个逻辑比特的总交互权重。 + +### CircuitLayoutAnalysis + +线路布局分析结果,由 `analyze_circuit_for_layout()` 返回。 + +属性: + +- `logical_qubits -> list[Qubit]`:逻辑比特列表。 +- `interactions -> InteractionGraph`:交互图。 + +### DistanceTable + +物理图最短距离表。 + +属性与方法: + +- `qubits -> list[Qubit]`:物理比特列表。 +- `distance(a, b) -> int | None`:两物理比特间最短距离,不可达时为 `None`。 + +### PhysicalLayoutGraph + +设备物理布局图,由 `PhysicalLayoutGraph.from_device(device)` 构造。 + +属性与方法: + +- `physical_qubits -> list[Qubit]` +- `distances -> DistanceTable` +- `has_fidelity_data -> bool`:是否有保真度数据。 +- `has_readout_error_data -> bool`:是否有读出错误率数据。 +- `has_two_qubit_error_data -> bool`:是否有双比特门错误率数据。 +- `distance(a, b) -> int | None` +- `is_adjacent_undirected(a, b) -> bool` +- `readout_error(qubit) -> float | None` +- `supports_two_qubit_gate_directed(a, b) -> bool` +- `two_qubit_gate_error_directed(a, b) -> float | None` +- `supports_directed_coupling(a, b) -> bool` + +### PreparedSabreCircuit + +SABRE 线路预处理结果,由 `prepare_sabre_circuit()` 返回。 + +属性: + +- `analysis -> CircuitLayoutAnalysis` +- `logical_qubits -> list[Qubit]` + +### PreparedSabreTarget + +设备物理图预处理结果,由 `prepare_sabre_device_target()` 返回。 + +属性: + +- `physical -> PhysicalLayoutGraph` + +--- + +## 异常情况 + +- `CompilerConfigError`:配置非法,例如保真度目标在无校准数据设备上使用。 +- `CompilerTransformError`:布局无法完成,例如交互无法在拓扑上满足。 diff --git a/docs/api/cn/python/4_compile/4_routing.md b/docs/api/cn/python/4_compile/4_routing.md new file mode 100644 index 00000000..f0898c2c --- /dev/null +++ b/docs/api/cn/python/4_compile/4_routing.md @@ -0,0 +1,117 @@ +# Routing + +`cqlib.compile.transform.routing` 提供变换层路由入口。路由在受限拓扑上插入 SWAP,使线路中所有二体交互满足设备邻接约束。 + +## 导入 + +```python +from cqlib.compile.transform.routing import ( + route_with_layout, + route_sabre, + RoutedCircuit, + SabreRouteResult, +) +``` + +--- + +## 函数 + +### route_with_layout(circuit, device, initial_layout, config=None) + +已知初始布局条件下的确定性路由。 + +参数: + +- `circuit` (`Circuit`):待路由线路。 +- `device` (`Device`):目标设备。 +- `initial_layout` (`Layout`):逻辑到物理初始布局。 +- `config` (`SabreConfig | None`):SABRE 配置,默认 `SabreConfig()`。 + +返回: + +- `RoutedCircuit` + +### route_sabre(circuit, device, objective=None, config=None) + +带布局选择的 SABRE 路由:先按 `objective` 选出初始布局,再执行路由。 + +参数: + +- `circuit` (`Circuit`):待路由线路。 +- `device` (`Device`):目标设备。 +- `objective` (`LayoutObjective | None`):布局目标,默认 `LayoutObjective.topology_only()`。 +- `config` (`SabreConfig | None`):SABRE 配置,默认 `SabreConfig()`。 + +返回: + +- `SabreRouteResult` + +示例: + +```python +from cqlib import Circuit +from cqlib.compile.transform.routing import route_sabre +from cqlib.device import Device + +circuit = Circuit(3) +circuit.cx(0, 2) + +device = Device.line("line-3", 3) +result = route_sabre(circuit, device) +print("swap_count:", result.swap_count) +``` + +--- + +## RoutedCircuit + +已知初始布局条件下的路由结果。 + +### 属性 + +- `circuit -> Circuit`:路由后的线路。 +- `initial_layout -> Layout`:路由开始前的布局。 +- `final_layout -> Layout`:路由结束后的布局。 +- `swap_count -> int`:插入的 SWAP 数量。 +- `diagnostics -> SabreRoutingDiagnostics`:路由诊断信息,见 [SABRE](5_sabre.md)。 +- `changed -> bool`:路由是否改变了线路。 + +--- + +## SabreRouteResult + +带布局选择的路由结果,在 `RoutedCircuit` 的基础上附带布局打分。 + +### 属性 + +- `routed -> RoutedCircuit`:路由结果。 +- `layout_score -> LayoutScore | None`:选中布局的分数。 +- `layout_diagnostics -> LayoutDiagnostics`:布局过程诊断信息。 +- `circuit`、`initial_layout`、`final_layout`、`swap_count`、`diagnostics`、`changed`:同 `RoutedCircuit`。 + +示例: + +```python +from cqlib import Circuit +from cqlib.compile.transform.routing import route_sabre +from cqlib.device import Device + +circuit = Circuit(2) +circuit.cx(0, 1) + +device = Device.line("line-3", 3) +result = route_sabre(circuit, device) + +print("selected layout:", result.routed.initial_layout) +print("score:", result.layout_score.total if result.layout_score else None) +``` + +--- + +## 异常情况 + +- `CompilerConfigError`:SABRE 配置非法,例如 `routing_trials` 为零。 +- `CompilerTransformError`:路由失败,例如初始布局缺失或不可达、设备容量不足。 + +低层 `sabre_route` 与 `SabreConfig` 的细节见 [SABRE](5_sabre.md)。 diff --git a/docs/api/cn/python/4_compile/5_sabre.md b/docs/api/cn/python/4_compile/5_sabre.md new file mode 100644 index 00000000..abcb6893 --- /dev/null +++ b/docs/api/cn/python/4_compile/5_sabre.md @@ -0,0 +1,193 @@ +# SABRE + +`cqlib.compile.sabre` 提供 SWAP 路由的底层入口与配置。当初始布局已知、线路必须满足设备拓扑约束时使用 `sabre_route`。固定随机种子可复现多轮试探的平局裁决与选择。 + +## 导入 + +```python +from cqlib.compile.sabre import ( + SabreConfig, + SabreHeuristicConfig, + SabreVf2PrepassConfig, + SabreRoutingResult, + SabreRoutingDiagnostics, + sabre_route, + normalize_initial_layout, + validate_reachable_interactions, +) +``` + +--- + +## sabre_route(circuit, device, initial_layout, config=None) + +已知初始布局条件下的 SABRE 路由。 + +参数: + +- `circuit` (`Circuit`):待路由线路。 +- `device` (`Device`):目标设备。 +- `initial_layout` (`Layout`):逻辑到物理初始布局。 +- `config` (`SabreConfig | None`):默认 `SabreConfig()`。 + +返回: + +- `SabreRoutingResult` + +示例: + +```python +from cqlib import Circuit +from cqlib.compile.sabre import SabreConfig, sabre_route +from cqlib.device import Device, Layout + +circuit = Circuit(2) +circuit.cx(0, 1) + +result = sabre_route( + circuit, + Device.line("line-3", 3), + Layout.from_pairs([(0, 0), (1, 2)], physical_count=3), + SabreConfig(routing_trials=1, seed=7), +) +assert result.swap_count == 1 +``` + +--- + +## 辅助函数 + +### normalize_initial_layout(logical_qubits, device, initial_layout) + +把完整逻辑到物理布局对设备可用比特做规范化。 + +参数: + +- `logical_qubits` (`list[Qubit]`):线路逻辑比特列表。 +- `device` (`Device`):目标设备。 +- `initial_layout` (`Layout`):待规范化的初始布局。 + +返回: + +- `Layout` + +### validate_reachable_interactions(circuit, device, initial_layout) + +在不执行路由的前提下验证原生移动可达性。 + +参数: + +- `circuit` (`Circuit`):待检查线路。 +- `device` (`Device`):目标设备。 +- `initial_layout` (`Layout`):初始布局。 + +返回: + +- `None`;不可达时抛出 `CompilerError`。 + +--- + +## SabreConfig + +SABRE 布局细化与路由共用的配置。 + +### SabreConfig(*, layout_trials=10, layout_assignment_budget=1_000_000, vf2_prepass=SabreVf2PrepassConfig(candidate_limit=10, call_limit=1_000_000), refinement_iterations=1, routing_trials=1, seed=None, heuristic=None) + +参数: + +- `layout_trials` (`int`):在确定性与交互感知候选之外增加的随机化初始布局数量。 +- `layout_assignment_budget` (`int`):布局组件分配状态的最大探索量;耗尽会报告预算耗尽,区别于已证明不可行。 +- `vf2_prepass` (`SabreVf2PrepassConfig | None`):有界 VF2 预检查,用于添加拓扑完美候选;`None` 关闭预检查。 +- `refinement_iterations` (`int`):每个布局试探的正向/反向细化轮数。 +- `routing_trials` (`int`):每个完整细化布局执行的完整路由试探数。搜索直接返回最佳路由,不为中间细化状态执行路由。 +- `seed` (`int | None`):确定性随机种子。相同种子产生相同 cqlib 结果。 +- `heuristic` (`SabreHeuristicConfig | None`):交换选择启发式配置,默认 `SabreHeuristicConfig()`。 + +### 静态方法 + +- `SabreConfig.deterministic_seeded(seed)`:紧凑的确定性配置,适合测试与示例。所有试探数较小、随机种子固定、交换尝试有界。 + +### 属性 + +`layout_trials`、`layout_assignment_budget`、`vf2_prepass`、`refinement_iterations`、`routing_trials`、`seed`、`heuristic`,与构造参数一一对应。 + +### 异常情况 + +- `CompilerConfigError`:`routing_trials` 为零等路由字段非法(布局细化字段不影响路由校验)。 + +--- + +## SabreHeuristicConfig + +SABRE 交换选择启发式配置。主分数组合当前前沿层和、按活跃层缩放的前瞻与乘法拥塞;有原生方案时在窄结构窗口内用精确原生双比特代价裁决候选。 + +### SabreHeuristicConfig(*, basic_weight=1.0, lookahead_weights=None, decay_increment=0.002, decay_reset=10, attempt_limit=1000, best_epsilon=1e-10) + +参数: + +- `basic_weight` (`float`):当前前沿层总距离的权重。 +- `lookahead_weights` (`list[float] | None`):每个活跃层缩放前瞻层总和的权重;默认 `[0.5, 0.25, 0.125, 0.0625, 0.03125]`。 +- `decay_increment` (`float | None`):启发式 SWAP 使用某个物理比特后,其拥塞乘数的增量;`None` 关闭拥塞控制。 +- `decay_reset` (`int`):衰减值重置前的启发式 SWAP 尝试次数。 +- `attempt_limit` (`int`):未路由前沿节点时允许的连续启发式 SWAP 次数,超过后回退到最短路径逃逸。 +- `best_epsilon` (`float`):候选 SWAP 分数视为平局的浮点容差。 + +### 属性 + +`basic_weight`、`lookahead_weights`、`decay_increment`、`decay_reset`、`attempt_limit`、`best_epsilon`,与构造参数一一对应。 + +--- + +## SabreVf2PrepassConfig + +用于给 SABRE 布局候选播种的有界 VF2 预检查。 + +### SabreVf2PrepassConfig(*, candidate_limit=10, call_limit=1_000_000) + +参数: + +- `candidate_limit` (`int`):VF2 打分的完整完美映射数量上限。 +- `call_limit` (`int`):VF2 尝试的部分映射扩展数量上限。 + +### 属性 + +`candidate_limit`、`call_limit`,与构造参数一一对应。 + +--- + +## SabreRoutingResult + +`sabre_route` 的返回对象。 + +### 属性 + +- `circuit -> Circuit`:路由后的线路。 +- `initial_layout -> Layout`:路由开始前的布局。 +- `final_layout -> Layout`:路由结束后的布局。 +- `swap_count -> int`:插入的 SWAP 数量。 +- `diagnostics -> SabreRoutingDiagnostics`:路由诊断信息。 + +--- + +## SabreRoutingDiagnostics + +路由过程诊断信息。 + +### 属性 + +- `trials_evaluated -> int`:评估的路由试探数。 +- `selected_trial_index -> int`:选中试探的索引。 +- `fallback_count -> int`:回退次数。 +- `control_flow_blocks_routed -> int`:已路由的控制流块数量。 +- `two_qubit_depth -> int`:路由后线路的双比特深度。 +- `operation_count -> int`:操作总数。 +- `native_two_qubit_count -> int`:原生双比特门数量。 +- `native_two_qubit_depth -> int`:原生双比特深度。 +- `native_total_depth -> int`:原生总深度。 +- `native_operation_count -> int`:原生操作总数。 +- `unknown_loop_count -> int`:未知循环数量。 +- `requirement_signature_count -> int`:需求签名数量。 +- `eager_pair_state_count -> int`:急切对状态数量。 +- `lazy_pair_l1_lookup_count -> int`:惰性对一级查找次数。 +- `lazy_pair_l1_hit_count -> int`:惰性对一级命中次数。 +- `lazy_pair_l1_cached_count -> int`:惰性对一级缓存数量。 diff --git a/docs/api/cn/python/4_compile/6_decompose_resynthesis.md b/docs/api/cn/python/4_compile/6_decompose_resynthesis.md new file mode 100644 index 00000000..56b9f9bc --- /dev/null +++ b/docs/api/cn/python/4_compile/6_decompose_resynthesis.md @@ -0,0 +1,570 @@ +# Decompose / Resynthesis + +`cqlib.compile.transform.decompose` + +门分解、双比特块重综合、目标门集转换与设备降级,位于 `cqlib.compile.transform` 的 `decompose`、`resynthesis`、`target_basis` 与 `device_lowering` 子模块。 + +门分解按操作可用的表示分层:定义展开处理自带实现线路的门,数值合成处理带固定矩阵的酉门,多控制门分解处理受控操作。定义展开先于数值合成执行,使带实现线路的酉门在进入矩阵合成之前已经展开。 + +## 导入 + +```python +from cqlib.compile.transform.decompose import ( + DecompositionRuleStats, + McGateDecomposeConfig, + TwoQubitUnitaryDecomposeBasis, + UnitaryDecomposeConfig, + decompose_mc_gates, + decompose_mc_gates_for_device, + decompose_mc_gates_with_rule_stats, + decompose_unitaries, + decompose_unitaries_with_rule_stats, + expand_definitions, + mc_gate, + unitary, +) +from cqlib.compile.transform.resynthesis import ( + ResynthesizeTwoQubitBlocks, + TwoQubitBlockResynthesisConfig, + resynthesize_two_qubit_blocks, +) +from cqlib.compile.transform.target_basis import ( + TargetBasisCost, + TargetBasisCostModel, + TargetBasisLowerer, + TargetBasisSignature, +) +from cqlib.compile.transform import DeviceLowerer +``` + +--- + +## 定义展开 + +### expand_definitions(circuit) + +展开电路门定义,不修改输入线路。 + +参数: + +- `circuit` (`Circuit`):待展开线路。 + +返回: + +- `TransformResult`;`circuit` 字段为展开后的线路,`changed` 表示是否发生改写。 + +--- + +## 酉门合成 + +线路级入口位于 `cqlib.compile.transform.decompose`,对应的数值合成原语位于 `cqlib.compile.transform.decompose.unitary`。 + +### decompose_unitaries(circuit, config=None) + +用矩阵合成单比特与双比特酉门。 + +参数: + +- `circuit` (`Circuit`):待合成线路。 +- `config` (`UnitaryDecomposeConfig | None`):分解配置,默认 `UnitaryDecomposeConfig()`。 + +返回: + +- `TransformResult` + +异常情况: + +- `CompilerTransformError`:`UnitaryGate` 没有矩阵表示,或矩阵的比特数与门的比特数不符。 + +### decompose_unitaries_with_rule_stats(circuit, config=None) + +同上,并返回 pass 内规则缓存统计。 + +返回: + +- `(TransformResult, DecompositionRuleStats)` + +### UnitaryDecomposeConfig(*, two_qubit_basis=None, target_basis=None, recurse_control_flow=True) + +参数: + +- `two_qubit_basis` (`TwoQubitUnitaryDecomposeBasis | None`):双比特合成门集。 +- `target_basis` (`list[str | Instruction] | None`):目标门集约束,条目形式同 `compile()` 的 `target_basis`。 +- `recurse_control_flow` (`bool`):是否递归进入控制流块。 + +`two_qubit_basis` 与 `target_basis` 互斥,同时给出会被拒绝。 + +属性: + +- `two_qubit_basis -> TwoQubitUnitaryDecomposeBasis | None` +- `target_basis -> list[Instruction] | None` +- `recurse_control_flow -> bool` + +异常情况: + +- `CompilerConfigError`:`two_qubit_basis` 与 `target_basis` 同时给出,或目标门集为空。 + +### TwoQubitUnitaryDecomposeBasis + +双比特合成门集,取值为以下静态方法之一: + +- `TwoQubitUnitaryDecomposeBasis.pauli_rotations()`:局部 `U` 门加 `RXX`、`RYY`、`RZZ`。 +- `TwoQubitUnitaryDecomposeBasis.cx()`:局部 `U` 门加 `CX`。 +- `TwoQubitUnitaryDecomposeBasis.cy()`:局部 `U` 门加 `CY`。 +- `TwoQubitUnitaryDecomposeBasis.cz()`:局部 `U` 门加 `CZ`。 +- `TwoQubitUnitaryDecomposeBasis.rzz()`:局部 `U`、`H`、`RX` 加 `RZZ`。 + +### DecompositionRuleStats + +pass 内规则缓存统计: + +- `hits -> int`:缓存命中次数。 +- `misses -> int`:缓存未命中次数。 +- `inserts -> int`:缓存写入次数。 + +--- + +## 数值酉门合成 + +`cqlib.compile.transform.decompose.unitary` 提供矩阵到门序列的直接合成,不经过线路遍历。 + +### synthesize_numeric_1q_unitary(matrix) + +把数值单比特酉矩阵合成 `U` 门角度与全局相位。 + +参数: + +- `matrix` (`list[list[complex]]`):2×2 酉矩阵,可转换为二维 `complex128` 数组。 + +返回: + +- `OneQubitUnitaryDecomposition` + +异常情况: + +- `CompilerConfigError`:矩阵不是 2×2。 +- `TypeError`:输入无法转换为二维数组。 + +### synthesize_numeric_2q_unitary(matrix, first, second, basis=None) + +把数值双比特酉矩阵合成为门序列。 + +参数: + +- `matrix` (`list[list[complex]]`):4×4 酉矩阵,可转换为二维 `complex128` 数组。 +- `first`、`second` (`int | Qubit`):作用比特,须互不相同。 +- `basis` (`TwoQubitUnitaryDecomposeBasis | None`):合成门集,默认 `TwoQubitUnitaryDecomposeBasis.pauli_rotations()`。 + +返回: + +- `TwoQubitUnitarySynthesisResult` + +异常情况: + +- `CompilerConfigError`:矩阵不是 4×4,或 `first` 与 `second` 指向同一比特。 + +### kak_decompose(matrix) + +计算数值 4×4 酉矩阵的规范 KAK 分解。 + +参数: + +- `matrix` (`list[list[complex]]`):4×4 酉矩阵,可转换为二维 `complex128` 数组。 + +返回: + +- `KakDecomposition` + +异常情况: + +- `TypeError`:输入无法转换为二维数组。 + +### OneQubitUnitaryDecomposition + +单比特酉矩阵的 `U` 门分解结果。 + +属性: + +- `theta -> float`:合成的 `U` 门的极角。 +- `phi -> float`:合成的 `U` 门的第一个方位角。 +- `lambda_ -> float`:合成的 `U` 门的第二个方位角。 +- `global_phase -> float`:乘在合成的 `U` 门上的标量相位。 + +其他行为: + +- 支持 `copy`、`deepcopy`,可按值比较。 + +### TwoQubitUnitarySynthesisResult + +双比特酉矩阵的合成结果。 + +属性: + +- `operations -> list[ValueOperation]`:实现该酉门的标准门操作序列,精确到 `global_phase`。 +- `global_phase -> float`:乘在操作序列上的标量相位。 + +其他行为: + +- 支持 `copy`、`deepcopy`。 + +### KakDecomposition + +双比特酉矩阵的规范 KAK 分解。 + +属性: + +- `global_phase -> float`:乘在完整分解上的标量相位。 +- `k1l -> numpy.ndarray`:作用在 Cartan 相互作用之后的左局部因子。 +- `k1r -> numpy.ndarray`:作用在 Cartan 相互作用之后的右局部因子。 +- `k2l -> numpy.ndarray`:作用在 Cartan 相互作用之前的左局部因子。 +- `k2r -> numpy.ndarray`:作用在 Cartan 相互作用之前的右局部因子。 +- `a -> float`:规范 Pauli-XX 相互作用坐标。 +- `b -> float`:规范 Pauli-YY 相互作用坐标。 +- `c -> float`:规范 Pauli-ZZ 相互作用坐标。 + +其他行为: + +- 各 `k` 因子为独立副本,修改返回值不影响分解对象。 +- 支持 `copy`、`deepcopy`。 + +--- + +## 多控制门分解 + +### decompose_mc_gates(circuit, config=None) + +用配置的辅助比特资源分解多控制门。此入口没有目标设备,因此不检查设备容量;需要容量检查时用 `decompose_mc_gates_for_device`。 + +参数: + +- `circuit` (`Circuit`):待分解线路。 +- `config` (`McGateDecomposeConfig | None`):分解配置,默认 `McGateDecomposeConfig()`。 + +返回: + +- `TransformResult` + +### decompose_mc_gates_with_rule_stats(circuit, config=None) + +同上,并返回规则缓存统计。 + +返回: + +- `(TransformResult, DecompositionRuleStats)` + +### decompose_mc_gates_for_device(circuit, device, resource_policy=None) + +在设备容量约束下分解多控制门。此入口限制线路的逻辑比特数不超过设备可用比特数,不检查耦合拓扑。 + +参数: + +- `circuit` (`Circuit`):待分解线路。 +- `device` (`Device`):提供可用物理比特容量约束。 +- `resource_policy` (`ResourcePolicy | None`):辅助比特策略。 + +返回: + +- `TransformResult` + +异常情况: + +- `CompilerConfigError`:线路宽度超出设备可用比特数。 + +### McGateDecomposeConfig(*, resource_policy=None, resource_limits=None) + +参数: + +- `resource_policy` (`ResourcePolicy | None`):辅助比特策略。 +- `resource_limits` (`ResourceLimits | None`):资源上限。 + +属性: + +- `resource_policy -> ResourcePolicy` +- `resource_limits -> ResourceLimits` + +--- + +## 多控制门原语 + +`cqlib.compile.transform.decompose.mc_gate` 提供按门族划分的精确合成原语,直接给出操作序列,不做线路遍历、不选择算法、不申请辅助比特。**正常编译流程应使用 `decompose_mc_gates` 或 `decompose_mc_gates_for_device`**;只有在需要自行控制辅助比特分配或实现自定义降级策略时,才直接调用这些原语。 + +原语按门族分组,每组通常提供「无辅助比特」「若干干净辅助比特」「若干脏辅助比特」以及针对单辅助比特的固定深度变体: + +| 门族 | 覆盖范围 | +| --- | --- | +| MCX | 多控制 X 门。 | +| 多控制 SU(2) | 多控制特殊酉旋转,旋转轴由 `Su2RotationAxis` 给出。 | +| Pauli | 多控制 Pauli 门族。 | +| RZZ | 多控制 RZZ。 | +| Pauli 旋转 | 多控制 Pauli 旋转。 | +| 旋转 | 多控制 `RX` / `RY` / `RZ` 及其固有受控形态。 | +| 相位 | 多控制 `S` / `SDG` / `T` / `TDG` / `Phase`。 | +| QCIS | 面向 QCIS 门集的合成原语。 | +| Hadamard | 多控制 Hadamard。 | +| SWAP | 多控制 SWAP。 | +| FSIM | 多控制 FSIM。 | +| 酉门 | 多控制通用酉门。 | + +共同参数: + +- `controls` (`list[int | Qubit]`):控制比特,须为展开后的全部控制,含门本身固有的控制。 +- `target`,或双比特原语的 `first`、`second`:目标比特。 +- `clean_ancillas` / `clean_ancilla`:干净辅助比特,须以 `|0>` 进入并恢复为 `|0>`。 +- `dirty_ancillas` / `dirty_ancilla`:脏辅助比特,可以任意未知态进入,但须精确恢复。 + +所有原语返回 `list[ValueOperation]`,可用 `Circuit.from_operations` 组装成线路。超出消耗前缀的额外辅助比特被忽略;辅助比特必须与所有控制比特和目标比特互不相同。完整清单见 `cqlib.compile.transform.decompose.mc_gate` 的公开面。 + +--- + +## 双比特块重综合 + +### resynthesize_two_qubit_blocks(circuit, config=None) + +重综合双比特门块,函数式入口。 + +参数: + +- `circuit` (`Circuit`):待重综合线路。 +- `config` (`TwoQubitBlockResynthesisConfig | None`):重综合配置。 + +返回: + +- `TransformResult` + +### ResynthesizeTwoQubitBlocks(config=None) + +可复用 pass 对象。 + +参数: + +- `config` (`TwoQubitBlockResynthesisConfig | None`):重综合配置。 + +属性与方法: + +- `config -> TwoQubitBlockResynthesisConfig` +- `run(circuit) -> TransformResult` + +### TwoQubitBlockResynthesisConfig(*, two_qubit_basis=None, target_basis=None, enhanced=False, max_block_ops=None, max_crossed_ops=None, max_scan_span=None, skip_labeled_ops=True, recurse_control_flow=True, commutation=None) + +参数: + +- `two_qubit_basis` (`TwoQubitUnitaryDecomposeBasis | None`):双比特合成门集。 +- `target_basis` (`list[str | Instruction] | None`):目标门集约束。 +- `enhanced` (`bool`):是否使用增强预算,以编译时间换更好的路由后清理。 +- `max_block_ops` (`int | None`):块内最大操作数。 +- `max_crossed_ops` (`int | None`):块内最大交叉操作数;被跨越操作保持原位,且必须与合成替换对易。 +- `max_scan_span` (`int | None`):双比特锚点单侧的收集预算。 +- `skip_labeled_ops` (`bool`):是否把带标签操作视为硬边界。 +- `recurse_control_flow` (`bool`):是否递归进入控制流块。 +- `commutation` (`CommutationConfig | None`):对易检查配置。 + +--- + +## 目标门集转换 + +### TargetBasisLowerer(target_basis) + +把线路翻译到目标门集。 + +参数: + +- `target_basis` (`list[str | Instruction]`):非空目标门集,条目形式同 `compile()` 的 `target_basis`。 + +属性与方法: + +- `target_basis -> list[Instruction]` +- `run(circuit) -> TransformResult` + +### TargetBasisSignature + +目标门集签名: + +- `TargetBasisSignature.from_standard_gates(gates)`:从标准门列表构造签名。 + +### TargetBasisCost + +目标门集代价统计: + +- `two_qubit_ops -> int`:双比特门数量。 +- `depth -> int`:深度。 +- `total_ops -> int`:操作总数。 +- `parameterized_ops -> int`:参数化操作数量。 + +### TargetBasisCostModel + +目标门集代价模型: + +- `signature -> TargetBasisSignature` +- `target_basis -> list[Instruction]` +- `cost_of_fixed_operations(qubits, operations) -> TargetBasisCost`:计算固定操作集合的代价。`qubits` 为比特编号或 `Qubit` 对象列表,`operations` 为 `ValueOperation` 列表。 + +--- + +## 设备降级 + +### DeviceLowerer(device) + +按设备原生指令集降级线路。 + +参数: + +- `device` (`Device`):目标设备。 + +属性与方法: + +- `device -> Device` +- `run(circuit) -> TransformResult` + +--- + +## 示例 + +### 1. 定义展开与酉门合成 + +```python +import numpy as np + +from cqlib.circuit import Circuit, UnitaryGate +from cqlib.compile.transform.decompose import ( + decompose_unitaries_with_rule_stats, + expand_definitions, +) + +definition = Circuit(1) +definition.h(0) +circuit = Circuit(1) +circuit.append_circuit_gate(definition.to_gate("custom_h"), [0]) + +expanded = expand_definitions(circuit) +assert expanded.changed is True +assert [op.instruction.instruction.name for op in circuit.operations] == ["custom_h"] +assert [op.instruction.instruction.name for op in expanded.circuit.operations] == ["H"] + +matrix = np.array([[0, 1], [1, 0]], dtype=np.complex128) +gate = UnitaryGate("x_matrix", 1).with_matrix(matrix) +circuit = Circuit(2) +circuit.append_unitary_gate(gate, [0]) +circuit.append_unitary_gate(gate, [1]) + +result, stats = decompose_unitaries_with_rule_stats(circuit) +assert result.changed is True +assert all( + operation.instruction.instruction.name == "U" + for operation in result.circuit.operations +) +assert (stats.hits, stats.misses, stats.inserts) == (1, 1, 1) +``` + +### 2. 数值酉门合成 + +```python +import numpy as np + +from cqlib.circuit import Circuit, Qubit, UnitaryGate +from cqlib.compile.transform.decompose import TwoQubitUnitaryDecomposeBasis +from cqlib.compile.transform.decompose.unitary import ( + kak_decompose, + synthesize_numeric_1q_unitary, + synthesize_numeric_2q_unitary, +) + +source = ( + np.exp(0.37j) + * np.array([[1, 1], [1, -1]], dtype=np.complex128) + / np.sqrt(2) +) +decomposition = synthesize_numeric_1q_unitary(source) +circuit = Circuit(1) +circuit.u(0, decomposition.theta, decomposition.phi, decomposition.lambda_) +reconstructed = np.exp(1j * decomposition.global_phase) * circuit.to_matrix() +np.testing.assert_allclose(reconstructed, source, atol=1e-10) + +swap = np.array( + [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]], + dtype=np.complex128, +) +synthesis = synthesize_numeric_2q_unitary( + swap, 0, Qubit(1), TwoQubitUnitaryDecomposeBasis.cx() +) +expected = Circuit(2) +expected.append_unitary_gate(UnitaryGate("source", 2).with_matrix(swap), [0, 1]) +synthesized = Circuit.from_operations([Qubit(0), Qubit(1)], synthesis.operations) +synthesized.set_global_phase(synthesis.global_phase) +np.testing.assert_allclose( + synthesized.to_matrix(), expected.to_matrix(), atol=1e-8 +) + +kak = kak_decompose(swap) +assert kak.k1l.shape == (2, 2) +assert isinstance(kak.global_phase, float) +``` + +### 3. 多控制门分解 + +```python +from cqlib.circuit import Circuit, MCGate, StandardGate +from cqlib.compile.resource import ResourceLimits, ResourcePolicy +from cqlib.compile.transform.decompose import ( + McGateDecomposeConfig, + decompose_mc_gates_with_rule_stats, +) + +circuit = Circuit(4) +gate = MCGate(3, StandardGate.X) +circuit.append_mc_gate(gate, [0, 1, 2, 3]) +circuit.append_mc_gate(gate, [0, 1, 2, 3]) + +config = McGateDecomposeConfig( + resource_policy=ResourcePolicy( + max_pre_layout_clean_ancillas=2, + allow_dirty_borrowing=True, + ), + resource_limits=ResourceLimits(max_total_qubits=8), +) +result, stats = decompose_mc_gates_with_rule_stats(circuit, config) +assert result.changed is True +assert all( + not operation.instruction.instruction.is_mcgate + for operation in result.circuit.operations +) +assert stats.inserts >= 1 +``` + +### 4. 双比特块重综合与目标门集转换 + +```python +from cqlib.circuit import Circuit +from cqlib.compile.transform.decompose import TwoQubitUnitaryDecomposeBasis +from cqlib.compile.transform.resynthesis import ( + ResynthesizeTwoQubitBlocks, + TwoQubitBlockResynthesisConfig, + resynthesize_two_qubit_blocks, +) +from cqlib.compile.transform.target_basis import TargetBasisLowerer + +circuit = Circuit(2) +circuit.cx(0, 1) +circuit.cx(0, 1) +config = TwoQubitBlockResynthesisConfig( + two_qubit_basis=TwoQubitUnitaryDecomposeBasis.cx() +) + +result = resynthesize_two_qubit_blocks(circuit, config) +transformer = ResynthesizeTwoQubitBlocks(config) +assert transformer.config == config +assert result.changed is True +assert len(result.circuit.operations) == 0 + +lowerer = TargetBasisLowerer(["h", "CZ"]) +assert [instruction.name for instruction in lowerer.target_basis] == ["H", "CZ"] +``` + +--- + +## 异常情况 + +- `CompilerTransformError`:分解或合成失败,例如矩阵不是酉矩阵、目标门集无法覆盖输入操作、辅助比特数量不足或比特重复。 +- `CompilerConfigError`:配置非法,例如空目标门集、`two_qubit_basis` 与 `target_basis` 同时给出、线路宽度超出设备容量。 +- `ParameterError`:门参数非有限值。 +- `ValueError`:要求恰好两个辅助比特的原语收到其他数量。 +- `ResourceError`:多控制门分解申请辅助比特失败,见 [Resource](8_resource.md)。 diff --git a/docs/api/cn/python/4_compile/7_knowledge.md b/docs/api/cn/python/4_compile/7_knowledge.md new file mode 100644 index 00000000..6bdea984 --- /dev/null +++ b/docs/api/cn/python/4_compile/7_knowledge.md @@ -0,0 +1,284 @@ +# Knowledge + +`cqlib.compile.knowledge` 提供编译知识规则、验证后的规则库与结构匹配能力。知识规则描述一个相邻操作模式、可选符号参数约束以及替换该模式的操作序列,是知识规则重写与门分解的规则来源。 + +## 导入 + +```python +from cqlib.compile.knowledge import ( + Rule, + RuleItem, + Condition, + RuleId, + RuleKind, + RuleMetadata, + RuleLibrary, + MatchBindings, + loads, + load, + dumps, + dump, + match_rule_item, + conditions_hold, + instantiate_target, + rule_matches_operations, +) +``` + +--- + +## 规则构造 + +规则内量子比特标签是非负整数且必须从零开始连续排列。它们是占位符:标签 `0` 在匹配时可以绑定任意具体 `Qubit`。 + +### RuleItem + +规则模式或替换目标中的单条操作。 + +#### 静态方法 + +- `RuleItem.standard(gate, qubits)`:标准门条目,保留绑定在门上的参数。`qubits` 为规则内比特标签列表。 +- `RuleItem.mc_gate(gate, qubits)`:多控制门条目,保留绑定参数。 + +#### 属性与方法 + +- `instruction -> Instruction`:条目指令。 +- `qubits -> list[int]`:规则内比特标签。 +- `params -> list[...]`:参数。 +- `symbols() -> set[str]`:条目中出现的符号。 +- `validate()`:校验条目,非法时抛出 `ValueError`。 +- `equivalent_to(other) -> bool`:条目等价判断。 + +### Condition + +规则匹配的符号参数约束。 + +#### 静态方法 + +- `Condition.equal(lhs, rhs)`:`lhs == rhs` 约束。 +- `Condition.equal_mod(lhs, rhs, modulus)`:`lhs == rhs (mod modulus)` 约束。 + +#### 属性 + +- `kind -> str`:约束类型,取值 `"equal"` 或 `"equal_mod"`。 +- `lhs`、`rhs`、`modulus`:约束操作数。 + +#### 方法 + +- `symbols() -> set[str]`:约束中出现的符号。 + +### Rule(name, operations, target, conditions=None) + +参数: + +- `name` (`str`):规则名。 +- `operations` (`list[RuleItem]`):被匹配的相邻操作模式。 +- `target` (`list[RuleItem]`):替换目标,可为空列表(表示抵消删除)。 +- `conditions` (`list[Condition] | None`):符号参数约束。 + +#### 属性与方法 + +- `name -> str` +- `operations -> list[RuleItem]` +- `conditions -> list[Condition]` +- `target -> list[RuleItem]` +- `num_qubits -> int`:规则涉及比特标签数。 +- `validate()`:结构校验,非法时抛出 `ValueError`。 +- `verify() -> VerifyResult`:规则验证结果。 +- `verify_by_sampling(num_bindings, tolerance) -> VerifyResult`:采样验证,`num_bindings` 必须大于零。 +- `needs_sampling_fallback() -> bool`:是否需要采样回退。 +- `free_symbols() -> set[str]` +- `operation_qubits() -> set[int]` +- `target_qubits() -> set[int]` + +#### VerifyResult + +属性: + +- `status -> str`:验证状态。 +- `passed -> bool`:是否通过。 +- `num_bindings -> int`:验证的绑定数量。 +- `reason -> str | None`:未通过原因。 + +规则构造示例: + +```python +from cqlib.circuit import StandardGate +from cqlib.compile.knowledge import Rule, RuleItem + +cancel_h = Rule( + "cancel_h", + operations=[ + RuleItem.standard(StandardGate.H, [0]), + RuleItem.standard(StandardGate.H, [0]), + ], + target=[], +) +cancel_h.validate() +assert cancel_h.verify().passed +``` + +--- + +## RuleKind + +规则类别: + +- `RuleKind.simplify()`:化简。 +- `RuleKind.cancel()`:抵消。 +- `RuleKind.merge()`:合并。 +- `RuleKind.commute()`:对易。 +- `RuleKind.decompose()`:分解。 +- `RuleKind.canonicalize()`:规范化。 +- `RuleKind.hardware_native()`:硬件原生。 +- `RuleKind.other()`:其他。 + +属性与方法: + +- `name -> str`:类别名。 +- `label() -> str`:类别标签。 + +--- + +## RuleLibrary + +规则库,负责规则的加载、验证、分类与查询。 + +### 构造与加载 + +- `RuleLibrary()`:空规则库。 +- `RuleLibrary.builtin()`:内置规则库。 +- `RuleLibrary.from_rules(rules, kind)`:从规则列表构造,所有规则归入 `kind` 类别。 +- `RuleLibrary.from_dsl(source, kind)`:从 `.rule` DSL 字符串解析并构造。 +- `RuleLibrary.from_dsl_file(path, kind)`:从 `.rule` DSL 文件解析并构造。 + +### 修改 + +- `add_rule(rule, kind) -> RuleId`:添加规则,返回其 ID。 +- `extend_rules(rules, kind) -> list[RuleId]`:批量添加规则。 + +### 查询 + +- `rules() -> list[Rule]` +- `get(id) -> Rule | None` +- `metadata(id) -> RuleMetadata | None` +- `id_by_name(name) -> RuleId | None` +- `get_by_name(name) -> Rule | None` +- `name in library -> bool`(`__contains__`) +- `len(library) -> int` +- `candidates_for_first_instruction(instruction) -> list[RuleId]`:按首指令筛选候选规则。 +- `rules_by_kind(kind) -> list[RuleId]` +- `filter_rule_ids_by_instruction_keys(op_instructions, target_instructions) -> list[RuleId]`:按操作与目标指令键筛选规则 ID。 + +### RuleId + +- `index -> int`:库内索引,支持 `int(rule_id)`。 + +### RuleMetadata + +规则元数据: + +- `id -> RuleId` +- `kind -> RuleKind` +- `pattern_len -> int`:模式长度。 +- `rewrite_len -> int`:替换长度。 +- `qubit_count -> int`:涉及比特数。 +- `first_instruction -> Instruction`:首条指令。 +- `cost_delta -> int`:代价变化。 +- `has_conditions -> bool`:是否带约束。 + +--- + +## DSL 读写 + +### loads(source) -> list[Rule] + +从 `.rule` DSL 字符串解析规则列表。 + +### load(path) -> list[Rule] + +从 `.rule` DSL 文件解析规则列表。 + +### dumps(rules) -> str + +把规则列表序列化为 DSL 字符串。 + +### dump(rules, path) + +把规则列表写入 DSL 文件。 + +示例: + +```python +from cqlib.compile.knowledge import loads + +rule = loads(''' +rule cancel_x { + match { X 0, X 0 } + rewrite {} +} +''')[0] +rule.validate() +assert rule.verify().passed +``` + +--- + +## 结构匹配 + +### MatchBindings + +匹配绑定结果。 + +属性与方法: + +- `qubits() -> dict[int, Qubit]`:规则比特标签到具体比特的绑定。 +- `qubit(rule_qubit) -> Qubit | None`:单个标签的绑定。 +- `params() -> dict[str, Parameter]`:符号参数绑定。 +- `param(symbol) -> Parameter | None`:单个符号的绑定。 + +### rule_matches_operations(rule, operations) -> MatchBindings | None + +把规则匹配到相邻 `ValueOperation` 序列;不匹配返回 `None`。 + +### match_rule_item(item, operation, bindings) -> bool + +把单条 `RuleItem` 匹配到一个 `ValueOperation`,就地更新绑定。 + +### conditions_hold(conditions, bindings) -> bool + +检查约束在给定绑定下是否成立。 + +### instantiate_target(target, bindings) -> list[ValueOperation] + +用绑定实例化替换目标。 + +示例: + +```python +from cqlib import Circuit, Qubit +from cqlib.circuit import StandardGate, ValueOperation +from cqlib.compile.knowledge import loads, rule_matches_operations + +rule = loads(''' +rule cancel_x { + match { X 0, X 0 } + rewrite {} +} +''')[0] + +operations = [ + ValueOperation.from_standard_gate(StandardGate.X, [Qubit(3)]), + ValueOperation.from_standard_gate(StandardGate.X, [Qubit(3)]), +] + +bindings = rule_matches_operations(rule, operations) +assert bindings is not None +assert bindings.qubit(0) == Qubit(3) +``` + +--- + +## 异常情况 + +- `ValueError`:规则结构非法、DSL 解析失败、规则库构造或验证失败(错误信息带 `Rule library error: ...` 等前缀)、采样验证参数非法。 diff --git a/docs/api/cn/python/4_compile/8_resource.md b/docs/api/cn/python/4_compile/8_resource.md new file mode 100644 index 00000000..72dd37b3 --- /dev/null +++ b/docs/api/cn/python/4_compile/8_resource.md @@ -0,0 +1,149 @@ +# Resource + +`cqlib.compile.resource` 提供 ancilla 辅助比特的申请、规划、租借与回收管理。 + +干净资源与脏资源是恢复契约,不是态模拟结果。干净资源进出变换时必须处于 `|0>`;脏资源可能处于未知或纠缠态,变换释放租约前必须完整还原其输入状态。 + +## 导入 + +```python +from cqlib.compile.resource import ( + AncillaRequirement, + ResourcePolicy, + ResourceLimits, + ResourceRequest, + ResourcePlan, + ResourceLease, + ResourceManager, + ResourceError, + ResourceUnavailableError, +) +``` + +--- + +## 资源契约对象 + +### AncillaRequirement + +资源种类: + +- `AncillaRequirement.clean_zero()`:干净 ancilla,进出变换时处于 `|0>`。 +- `AncillaRequirement.dirty()`:脏 ancilla,释放前必须完整还原原状态。 + +支持 `==`、`hash`、`copy`、`deepcopy`。 + +### ResourceRequest(requirement, count, *, excluded=None) + +资源申请: + +- `requirement` (`AncillaRequirement`):资源种类。 +- `count` (`int`):申请数量。 +- `excluded` (`list[Qubit] | None`):不可使用的比特。 + +属性: + +- `requirement`、`count`、`excluded`,与构造参数一一对应。 + +### ResourcePlan + +申请预览结果,由 `ResourceManager.preview()` 返回,不实际占用资源。 + +属性: + +- `qubits -> list[Qubit]`:规划到的比特。 +- `requirement -> AncillaRequirement`:资源种类。 +- `num_new_qubits -> int`:规划引入的新比特数。 + +### ResourceLease + +已生效的租约,由 `ResourceManager.commit()` 返回。 + +属性: + +- `id -> int`:租约 ID。 +- `qubits -> list[Qubit]`:租借的比特。 +- `requirement -> AncillaRequirement`:资源种类。 + +--- + +## ResourcePolicy / ResourceLimits + +### ResourcePolicy(*, max_pre_layout_clean_ancillas=0, allow_dirty_borrowing=false) + +预布局分解阶段的辅助比特权限: + +- `max_pre_layout_clean_ancillas` (`int`):允许分配的逻辑干净 ancilla 数量上限。 +- `allow_dirty_borrowing` (`bool`):是否允许借用脏输入比特。 + +设备目标会根据可用物理比特推导硬容量,不受此策略限制。 + +### ResourceLimits(*, max_total_qubits=None) + +资源上限: + +- `max_total_qubits` (`int | None`):总比特数上限。 + +--- + +## ResourceManager + +资源管理器,把无副作用的规划与实际变更分离。 + +### ResourceManager.from_circuit(circuit, *, policy=None, limits=None) + +静态方法,从线路构造管理器。管理器须与线路保持同步演进。 + +参数: + +- `circuit` (`Circuit`):管理器跟踪的线路。 +- `policy` (`ResourcePolicy | None`):默认 `ResourcePolicy()`。 +- `limits` (`ResourceLimits | None`):资源上限。 + +### 方法 + +- `preview(request) -> ResourcePlan`:预览申请。不占用资源、不改变线路。 +- `commit(circuit, plan) -> ResourceLease`:按规划实际占用资源并修改线路。 +- `release(lease)`:释放租约。 +- `enter_post_layout(circuit)`:进入布局后阶段。 +- `verify_consistency(circuit)`:校验管理器与线路一致。 +- `verify_idle(circuit)`:校验所有租约已释放。 + +### 用法约定 + +消耗资源的变换在租约生效期间使用 `lease.qubits`,并须在释放前完成恢复(干净 ancilla 回到 `|0>`,脏 ancilla 还原原状态)。普通编译只需给 `compile()` 传一个 `ResourcePolicy`,管理器生命周期由编译器内部维护。 + +示例: + +```python +from cqlib import Circuit +from cqlib.compile.resource import ( + AncillaRequirement, + ResourceManager, + ResourcePolicy, + ResourceRequest, +) + +circuit = Circuit(2) +manager = ResourceManager.from_circuit( + circuit, + policy=ResourcePolicy(max_pre_layout_clean_ancillas=1), +) + +request = ResourceRequest(AncillaRequirement.clean_zero(), 1) + +# 预览不占用资源、不改变线路 +plan = manager.preview(request) +lease = manager.commit(circuit, plan) + +# 消耗资源的变换在此使用 lease.qubits,之后须恢复 |0> +manager.release(lease) +manager.verify_idle(circuit) +``` + +--- + +## 异常情况 + +- `ResourceError`:资源管理错误基类。 +- `ResourceUnavailableError`:资源不足或租约状态非法。 diff --git a/docs/api/cn/python/4_compile/9_commutation.md b/docs/api/cn/python/4_compile/9_commutation.md new file mode 100644 index 00000000..613ad787 --- /dev/null +++ b/docs/api/cn/python/4_compile/9_commutation.md @@ -0,0 +1,100 @@ +# Commutation + +`cqlib.compile.commutation` 提供量子操作交换的保守证明。该模块证明两个具体 `ValueOperation` 对象交换后是否保持线路语义。返回 `None` 表示配置的证明源无法建立对易性,并不证明两个操作不对易。 + +## 导入 + +```python +from cqlib.compile.commutation import ( + Commutation, + CommutationConfig, + CommutationChecker, + check_commutation, + algebraic_commutation, +) +``` + +--- + +## 函数 + +### check_commutation(lhs, rhs) -> Commutation | None + +使用共享内置检查器判断两个自包含操作是否对易。 + +### algebraic_commutation(lhs, rhs) -> Commutation | None + +只使用符号代数证明源判断对易。 + +示例: + +```python +from cqlib import Circuit, Qubit +from cqlib.circuit import StandardGate, ValueOperation +from cqlib.compile.commutation import check_commutation + +lhs = ValueOperation.from_standard_gate(StandardGate.H, [Qubit(0)]) +rhs = ValueOperation.from_standard_gate(StandardGate.X, [Qubit(1)]) + +proof = check_commutation(lhs, rhs) +assert proof is not None and proof.is_exact() +``` + +--- + +## Commutation + +对易证明结果。 + +### 静态方法 + +- `Commutation.exact()`:精确对易证明。 +- `Commutation.up_to_global_phase(phase)`:相差全局相位的对易证明。 + +### 属性与方法 + +- `phase -> Parameter`:证明携带的相位。 +- `is_exact() -> bool`:是否为精确对易。 + +--- + +## CommutationConfig + +对易检查器配置。 + +### CommutationConfig(*, enable_rule_oracle=true, enable_matrix_fallback=true, max_matrix_qubits=4) + +参数: + +- `enable_rule_oracle` (`bool`):启用规则预言机(来自知识库的 commute 规则)。 +- `enable_matrix_fallback` (`bool`):启用矩阵回退。 +- `max_matrix_qubits` (`int`):矩阵回退允许的最大比特数。 + +### 属性 + +`enable_rule_oracle`、`enable_matrix_fallback`、`max_matrix_qubits`,与构造参数一一对应。 + +--- + +## CommutationChecker + +可配置的对易检查器。 + +### 静态方法 + +- `CommutationChecker.builtin()`:内置规则与默认配置。 +- `CommutationChecker.with_config(config)`:内置规则与显式配置。 +- `CommutationChecker.from_library(library, config=None)`:使用已加载规则库中的 commute 规则(即 `RuleKind.commute()` 类别)参与证明。`config=None` 时使用默认配置,与 `builtin()` 一致。当 `enable_rule_oracle` 关闭时抛出 `ValueError`,否则规则库会被静默忽略。 + +### 属性与方法 + +- `config -> CommutationConfig`:当前配置副本。 +- `check(lhs, rhs) -> Commutation | None`:判断两个自包含操作值是否对易。 + +--- + +## 注意事项 + +- 证明结果保守:`None` 只代表无法证明,不代表不对易。 +- 经典控制流相关操作不在本检查器的证明范围内。 +- 带符号参数的操作参与证明时保持符号参数不变。 diff --git a/docs/api/cn/python/5_visualization/0_overview.md b/docs/api/cn/python/5_visualization/0_overview.md new file mode 100644 index 00000000..5c9af0ea --- /dev/null +++ b/docs/api/cn/python/5_visualization/0_overview.md @@ -0,0 +1,208 @@ +# 可视化 + +`cqlib.visualization` + +`cqlib.visualization` 负责把量子线路、量子态和测量结果渲染为可视化产物。它以 SVG 为首要输出格式:每个渲染入口都在内存中生成 SVG 文本并作为返回值,并可在给出输出路径时把同一份 SVG 落盘;对需要在终端或纯文本环境中查看的场景,另提供文本线路图。 + +`cqlib.visualization` 主要覆盖以下能力: + +- **线路渲染**:把线路绘制成 SVG 线路图或文本线路图,用于核对门序、比特连接、参数取值与折叠效果。 +- **量子态渲染**:以单比特 Bloch 向量、每比特约化 Bloch 向量、态矩阵和 Pauli 期望值等形式呈现量子态信息。 +- **测量结果渲染**:把执行结果的计数渲染成计数直方图或归一化概率分布图。 +- **统一输出契约**:全部渲染入口共享 SVG 文本返回值与输出路径落盘约定。 + +## 导入 + +```python +from cqlib.visualization import ( + draw_text, + draw_figure, + plot_histogram, + plot_distribution, + plot_bloch_vector, + plot_bloch_multivector, + plot_state_city, + plot_state_paulivec, +) +``` + +--- + +## Overview + +渲染入口按输入对象分为三组,各组的输入类型与产物如下: + +| 输入 | 渲染入口 | 产物 | +| --- | --- | --- | +| 线路(`Circuit`) | `draw_text`、`draw_figure` | 文本线路图、SVG 线路图 | +| 量子态(`Statevector` 或 `DensityMatrix`) | `plot_bloch_vector`、`plot_bloch_multivector`、`plot_state_city`、`plot_state_paulivec` | SVG 状态图 | +| 执行结果(`ExecutionResult`) | `plot_histogram`、`plot_distribution` | SVG 计数直方图、SVG 概率分布图 | + +### 统一的输出契约 + +所有渲染入口遵循同一套输出流程: + +1. 调用渲染入口得到返回值,即渲染产物的文本内容。除 `draw_text` 返回文本线路图外,其余入口返回 SVG 文本,类型是 `str` 的子类 `_InlineSvg`。 +2. 在 notebook 前端中,把返回值用作单元格的最后一个表达式即可内联显示;`_InlineSvg` 通过 SVG 富显示协议完成这一步。详见 [渲染输出与落盘](5_render_to_file.md)。 +3. 传入 `output_path` 时,同一份 SVG 同时写入该路径。扩展名为 `.png` 时按同一份 SVG 光栅化为 PNG,其余扩展名直接写入 SVG 文本。 + +### 选项的分组 + +三组入口的选项集各自独立,签名中位于 `*` 之后的参数全部为关键字参数: + +- **线路选项**:`initial_state`、`reverse_bits`、`show_params`、`decompose_circuit_gates` 为两个线路入口共有;`fold` 仅 `draw_figure` 提供,`line_width` 仅 `draw_text` 提供。 +- **状态选项**:`title`、`color`、`alpha`、`reverse_bits`、`figsize`,四个状态渲染入口签名一致。 +- **结果选项**:`figsize`、`color`、`number_to_keep`、`sort`、`target_string`、`legend`、`bar_labels`、`title`,两个结果渲染入口签名一致。 + +同名选项在三组之间含义一致:`reverse_bits` 控制是否反转计算基比特序的显示,`figsize` 控制绘图尺寸,`title` 控制标题文本,`output_path` 控制是否额外落盘。 + +--- + +## 常用入口 + +```python +from cqlib import Circuit +from cqlib.qis.state import Statevector +from cqlib.visualization import draw_text, plot_bloch_multivector + +circuit = Circuit(2) +circuit.h(0) +circuit.cx(0, 1) + +print(draw_text(circuit)) + +state = Statevector(2) +state.apply_h(0) +state.apply_cx(0, 1) + +svg = plot_bloch_multivector(state) +assert svg.startswith("`。 +- `reverse_bits` (`bool`):是否反转量子比特的显示顺序。 +- `show_params` (`bool`):是否在门标签后附上门的参数。 +- `decompose_circuit_gates` (`bool`):是否先把线路门展开为内部操作后再绘制。 + +`line_width` 之后的参数全部为关键字参数。 + +返回: + +- `str`:文本线路图。该入口直接转发底层实现,返回值是普通字符串,不提供内联显示;需要把渲染结果内联显示时使用 [SVG 线路图](2_draw_figure.md)。 + +异常情况: + +- `TypeError`:`circuit` 不是线路对象。 +- `ValueError`:线路预处理失败,或线路中的操作引用了不存在的量子比特。 + +--- + +## 渲染行为 + +- 每条量子比特线路用框线字符(如 `─`、`│`)绘制,门以字符框的形式标注在线路上。 +- `initial_state=True` 时,线路起始处显示 `|0>`,用于强调线路的初始态假设。 +- `show_params=True` 时,带参数的旋转门在标签中附带参数值;关闭后只显示门名。 +- `reverse_bits=True` 时,最高编号的量子比特显示在最上方,便于与按位书写的比特串对齐阅读。 +- `decompose_circuit_gates=True` 时,线路门先展开为内部操作,文本图中不再出现线路门这一层。 + +--- + +## 示例 + +### 1. 渲染线路的文本图 + +```python +from cqlib import Circuit +from cqlib.visualization import draw_text + +circuit = Circuit(2) +circuit.h(0) +circuit.cx(0, 1) +circuit.measure(0) +circuit.measure(1) + +text = draw_text(circuit) +assert "H" in text +``` + +### 2. 控制折行与显示内容 + +```python +from cqlib import Circuit +from cqlib.visualization import draw_text + +circuit = Circuit(3) +circuit.h(0) +circuit.rx(1, 0.25) +circuit.cx(0, 2) +circuit.barrier([0, 1, 2]) +circuit.swap(1, 2) +circuit.measure(0) +circuit.measure(1) +circuit.measure(2) + +text = draw_text( + circuit, + line_width=40, + initial_state=True, + reverse_bits=True, + show_params=False, +) +``` + +该示例只渲染文本线路图,不产生落盘文件;需要把同一线路渲染成可落盘的 SVG 时使用 [draw_figure](2_draw_figure.md)。 diff --git a/docs/api/cn/python/5_visualization/2_draw_figure.md b/docs/api/cn/python/5_visualization/2_draw_figure.md new file mode 100644 index 00000000..a937bc9f --- /dev/null +++ b/docs/api/cn/python/5_visualization/2_draw_figure.md @@ -0,0 +1,102 @@ +# SVG 线路图 + +`cqlib.visualization.draw_figure` + +`draw_figure` 把线路渲染成 SVG 线路图。与文本线路图相比,它以图形符号表示量子门、连接线、屏障与测量等操作,适合在文档、网页与 notebook 中展示,并可通过输出路径直接落盘。 + +## 导入 + +```python +from cqlib.visualization import draw_figure +``` + +--- + +## 函数 + +### draw_figure(circuit, *, fold=None, initial_state=False, reverse_bits=False, show_params=True, decompose_circuit_gates=False, output_path=None) + +渲染线路的 SVG 线路图。 + +参数: + +- `circuit` (`Circuit`):待渲染的线路。 +- `fold` (`int | None`):每行最多绘制的列数,负值表示不折叠;不传时使用默认值 18。 +- `initial_state` (`bool`):是否在比特标签中显示 `|0>`。 +- `reverse_bits` (`bool`):是否反转量子比特的显示顺序。 +- `show_params` (`bool`):是否在门标签后附上门的参数。 +- `decompose_circuit_gates` (`bool`):是否先把线路门展开为内部操作后再绘制。 +- `output_path` (`str | None`):输出路径。给出时,同一份 SVG 写入该路径;扩展名为 `.png` 时按该 SVG 光栅化为 PNG,其余扩展名直接写入 SVG 文本。 + +`circuit` 之后的参数全部为关键字参数。 + +返回: + +- `_InlineSvg`:SVG 文本,类型为 `str` 的子类,在 notebook 前端中作为单元格的最后一个表达式时内联显示。是否给出 `output_path` 都不影响返回值内容。 + +异常情况: + +- `TypeError`:`circuit` 不是线路对象。 +- `ValueError`:线路预处理失败、操作引用了不存在的量子比特,或 PNG 光栅化失败。 +- `IOError`:`output_path` 指向的路径写入失败;`IOError` 即 `OSError`。 + +--- + +## 渲染行为 + +- `fold` 控制折叠:按列把线路切成多行,每行不超过给定列数;负值表示不折叠,整条线路绘制在一行;不传时使用默认列数。 +- `initial_state=True` 时,比特标签形如 `q2 |0>`;`reverse_bits=True` 时显示顺序反转,编号最大的量子比特显示在最上方。 +- `show_params=False` 时,门标签中不出现参数取值。 +- `decompose_circuit_gates=True` 时,线路门先展开为内部操作,图中不再出现线路门这一层。 + +--- + +## 示例 + +### 1. 渲染默认样式的线路图 + +```python +from cqlib import Circuit +from cqlib.visualization import draw_figure + +circuit = Circuit(2) +circuit.h(0) +circuit.cx(0, 1) +circuit.measure(0) +circuit.measure(1) + +svg = draw_figure(circuit, output_path="bell.svg") +assert svg._repr_svg_() == str(svg) +assert "sim" in svg +assert ">rest" in svg +``` + +### 3. 概率分布图 + +```python +from cqlib.device import ExecutionResult +from cqlib.visualization import plot_distribution + +result = ExecutionResult.from_counts( + "vis-test", + [0], + 100, + 1, + {"0": 25, "1": 75}, +) + +svg = plot_distribution( + result, + figsize=(3.2, 2.4), + color=["#0f766e"], + legend=["probability"], + bar_labels=False, + title="Distribution options", + output_path="distribution.svg", +) +assert 'width="320"' in svg +assert 'height="240"' in svg +assert "#0f766e" in svg +assert ">probability" in svg +``` diff --git a/docs/api/cn/python/5_visualization/5_render_to_file.md b/docs/api/cn/python/5_visualization/5_render_to_file.md new file mode 100644 index 00000000..1e4791e6 --- /dev/null +++ b/docs/api/cn/python/5_visualization/5_render_to_file.md @@ -0,0 +1,108 @@ +# 渲染输出与落盘 + +`cqlib.visualization` + +本页说明 `cqlib.visualization` 各渲染入口共享的输出约定:SVG 文本返回值与 `output_path` 落盘的关系、`FigureSize` 与 `QuantumState` 两个类型别名的含义,以及返回值在 notebook 中的内联显示行为。 + +## 导入 + +```python +from cqlib.visualization import FigureSize, QuantumState +``` + +--- + +## 输出路径与返回值 + +每个渲染入口都在内存中先生成产物文本,再按需写盘: + +- **返回值**:渲染产物的文本内容。除 `draw_text` 返回文本线路图外,其余入口返回 SVG 文本。 +- **`output_path`**:可选的关键字参数。给出时,把同一份 SVG 写入该路径。落盘动作不改变返回值的内容,两者来自同一份渲染结果。 +- **扩展名规则**:扩展名为 `.png`(不区分大小写)时,按同一份 SVG 光栅化为 PNG 文件;其余扩展名(含 `.svg` 与无扩展名)直接写入 SVG 文本。 + +```python +from cqlib import Circuit +from cqlib.visualization import draw_figure + +circuit = Circuit(2) +circuit.h(0) +circuit.cx(0, 1) + +# 只取返回值:SVG 文本留在内存中,不产生文件 +svg = draw_figure(circuit) + +# 同时落盘:返回值不变,同一份 SVG 另写入 "bell.svg" +svg = draw_figure(circuit, output_path="bell.svg") + +# 扩展名为 .png:返回值不变,同一份 SVG 另光栅化为 "bell.png" +svg = draw_figure(circuit, output_path="bell.png") +``` + +异常情况: + +- `IOError`:`output_path` 指向的路径写入失败;`IOError` 即 `OSError`。 +- `ValueError`:PNG 光栅化失败,例如 SVG 无法解析或目标尺寸超出可表示范围。 + +--- + +## FigureSize + +```python +FigureSize = tuple[float, float] +``` + +绘图尺寸类型别名,用于各渲染入口的 `figsize` 参数。两个元素依次为宽与高,单位为英寸。 + +- 不传 `figsize` 时,使用该绘图族的默认尺寸。 +- 渲染时把英寸换算为 SVG 画布的像素尺寸;各绘图族的默认尺寸与换算下限不同,因此同一组取值在不同绘图族中得到的画布尺寸可能不同。 +- 取值过小时按该绘图族的下限处理,不会得到退化的画布。 + +```python +from cqlib.device import ExecutionResult +from cqlib.visualization import plot_distribution + +result = ExecutionResult.from_counts("vis-test", [0], 100, 1, {"0": 25, "1": 75}) + +svg = plot_distribution(result, figsize=(3.2, 2.4)) +assert 'width="320"' in svg +assert 'height="240"' in svg +``` + +--- + +## QuantumState + +```python +QuantumState = Statevector | DensityMatrix +``` + +状态渲染入口接受的状态类型别名,取值为 `cqlib.qis.Statevector` 或 `cqlib.qis.DensityMatrix`。两个状态类型可互换使用,同一量子态用两种输入渲染得到相同结果,详见 [量子态可视化](3_state_plots.md)。 + +传入既不是 `Statevector` 也不是 `DensityMatrix` 的对象时,状态渲染入口以 `ValueError` 报出。 + +--- + +## 内联显示 + +各渲染入口的 SVG 返回值不是普通字符串,而是 `str` 的子类 `_InlineSvg`。它保持字符串的全部行为,同时实现 SVG 富显示协议: + +- `_repr_svg_()` 返回 SVG 文本本身,notebook 前端据此把返回值作为图形直接渲染。 +- 内联显示只在返回值被用作单元格的最后一个表达式时触发;把返回值赋给变量或存入容器时不会自动显示,需要显式求值该变量。 +- 由于返回值是 `str` 的子类,可以直接参与字符串比较、拼接与写文件。 + +```python +from cqlib import Circuit +from cqlib.visualization import draw_figure + +circuit = Circuit(2) +circuit.h(0) +circuit.cx(0, 1) +circuit.measure(0) +circuit.measure(1) + +svg = draw_figure(circuit, output_path="bell.svg") +assert svg._repr_svg_() == str(svg) +assert " U (U† U)^level`,使线路深度随等级增长;选择性折叠只对指令名匹配给定门集的操作执行同样的展开,其余操作原样保留。等级为 `0` 时缓解线路即输入线路的副本。 +- 虚拟蒸馏把线路复制 `copies` 份并按位插入 SWAP,得到一条宽度为 `copies` 倍的 copy-swap 线路。分子与分母在同一线路上执行,区别只在传给回调的观测量:分母为 `None`,分子为按拷贝数扩展后的 `Hamiltonian`。 + +### 数据流 + +```text +输入线路 + 观测量 + ↓ +构造缓解线路族(折叠线路 / copy-swap 线路) + ↓ +回调逐条估计,返回 (期望值, 方差) + ↓ +后处理:外推到零噪声点 / 分子分母求比值 + ↓ +MitigatedResult(expectation, variance) +``` + +两种后处理对 `variance` 的处理不同:零噪声外推只使用期望值序列,结果的 `variance` 为 `None`;虚拟蒸馏同时使用期望值与方差,结果的 `variance` 有值。 + +--- + +## 常用入口 + +```python +from cqlib.circuit import Circuit +from cqlib.error_mitigation import ( + ErrorMitigation, + ExtrapolateMethod, + MitigationMethod, + ProcessArgs, + RunArgs, + ZneConfig, +) +from cqlib.qis import Hamiltonian, PauliString + +circuit = Circuit(1) +circuit.x(0) + +hamiltonian = Hamiltonian.from_list([(PauliString.from_str("Z"), 1.0)]) + + +def estimator(run_circuit, observable, shots): + # 替换为后端或模拟器的期望值估计 + return (len(run_circuit.operations) + 0.5, 0.0) + + +mitigation = ErrorMitigation( + circuit, + MitigationMethod.zne(ZneConfig([0, 1, 2])), +) + +mitigation.run(hamiltonian, RunArgs.zne(shots=256), estimator) +result = mitigation.get_mitigated( + ProcessArgs.zne(ExtrapolateMethod.polynomial(), degree=1) +) + +print("expectation:", result.expectation) +print("variance:", result.variance) +``` + +调用顺序由对象自身约束:每个 `ErrorMitigation` 实例只能 `run()` 一次、`get_mitigated()` 一次,且必须先 `run()` 再 `get_mitigated()`。 + +--- + +## 核心概念与术语 + +| 术语 | 说明 | +| --- | --- | +| **错误缓解** | 在期望值层面降低观测量系统性偏差的处理,不改变线路的酉语义,也不等同于量子纠错。 | +| **折叠等级** | 零噪声外推中控制线路展开次数的非负整数,由 `fold_levels` 给出。 | +| **噪声因子** | 由折叠等级导出的噪声放大倍数,满足 `noise_factor = 2 * fold_level + 1`。 | +| **全局折叠** | `gate_set` 为 `None` 时对整条线路做 `U -> U (U† U)^level` 的展开。 | +| **选择性折叠** | 只对指令名匹配 `gate_set` 的操作做折叠,其余操作原样保留。 | +| **外推方法** | 把不同噪声因子下的期望值外推到零噪声点的拟合模型,包括多项式与指数两种。 | +| **拷贝数** | 虚拟蒸馏中密度矩阵的拷贝份数,由 `copies` 给出,最小为 2。 | +| **copy-swap 线路** | 由输入线路复制 `copies` 份并按位插入 SWAP 得到的缓解线路。 | +| **分子线路** | 用于估计 `Tr(O ρ^M)` 的 copy-swap 线路执行,观测量为按拷贝数扩展后的 `Hamiltonian`。 | +| **分母线路** | 用于估计 `Tr(ρ^M)` 的 copy-swap 线路执行,观测量为 `None`。 | +| **estimator** | 调用方提供的回调,执行一条缓解线路并返回 `(期望值, 方差)` 二元组。 | +| **统一流水线** | 由 `ErrorMitigation` 承载的顺序式流程:`run()` 收集估计值,`get_mitigated()` 做后处理。 | + +--- + +## `cqlib.error_mitigation` API 概览 + +### 零噪声外推 + +| 名字 | 简介 | +| --- | --- | +| [`ZNEMitigation`](1_zne.md) | 低层零噪声外推辅助类,负责折叠、执行与外推。 | +| [`ZneConfig`](1_zne.md) | 零噪声外推配置,包含折叠等级。 | +| [`ExtrapolateMethod`](1_zne.md) | 外推方法,包括 `polynomial` 与 `exponential`。 | +| [`Estimator`](1_zne.md) | 回调类型别名,约定三参数与二元组返回。 | + +### 虚拟蒸馏 + +| 名字 | 简介 | +| --- | --- | +| [`VirtualDistillation`](2_virtual_distillation.md) | 低层虚拟蒸馏辅助类,负责构造 copy-swap 线路与求比值。 | +| [`VirtualDistillationConfig`](2_virtual_distillation.md) | 虚拟蒸馏配置,包含拷贝数。 | + +### 统一流水线 + +| 名字 | 简介 | +| --- | --- | +| [`ErrorMitigation`](3_unified.md) | 统一流水线,按 `run()` → `get_mitigated()` 顺序完成一次缓解。 | +| [`MitigationMethod`](3_unified.md) | 缓解方法选择,包括 `zne` 与 `virtual_distillation`。 | +| [`RunArgs`](3_unified.md) | 执行参数,与所选方法一一对应。 | +| [`ProcessArgs`](3_unified.md) | 后处理参数,与所选方法一一对应。 | +| [`MitigatedResult`](3_unified.md) | 缓解结果,包含期望值与可选的方差。 | +| [`ErrorMitigationError`](3_unified.md) | 错误缓解异常的基类。 | + +--- + +## 快速示例 + +### 1. 低层零噪声外推 + +```python +from cqlib.circuit import Circuit +from cqlib.error_mitigation import ExtrapolateMethod, ZNEMitigation +from cqlib.qis import Hamiltonian, PauliString + +circuit = Circuit(1) +circuit.x(0) + +hamiltonian = Hamiltonian.from_list([(PauliString.from_str("Z"), 1.0)]) + +zne = ZNEMitigation(circuit, [0, 1, 2]) +print(zne.fold_levels) # [0, 1, 2] +print(zne.noise_factors) # [1, 3, 5] + +folded = zne.fold_circuits() +print([len(item.operations) for item in folded]) # [1, 3, 5] + + +def estimator(run_circuit, observable, shots): + return (0.5 * len(run_circuit.operations), 0.0) + + +noisy = zne.run_em_sequence_with_shots(None, hamiltonian, 256, estimator) +print(noisy) # [0.5, 1.5, 2.5] + +mitigated = zne.extrapolate(noisy, ExtrapolateMethod.polynomial(), 1) +print(mitigated) # 0.0 +``` + +### 2. 低层虚拟蒸馏 + +```python +from cqlib.circuit import Circuit +from cqlib.error_mitigation import VirtualDistillation +from cqlib.qis import Hamiltonian, PauliString + +circuit = Circuit(1) +circuit.x(0) + +hamiltonian = Hamiltonian.from_list([(PauliString.from_str("Z"), 1.0)]) + +vd = VirtualDistillation(circuit, 2) +copy_swap = vd.build_copy_swap_circuit() +print(copy_swap.width) # 2 + + +def estimator(run_circuit, observable, shots): + if observable is None: + return (2.0, 1.0) # 分母:Tr(ρ^M) + return (1.5, 0.25) # 分子:Tr(O ρ^M) + + +expectation, variance = vd.run_vd( + hamiltonian, + shots_numerator=3, + shots_denominator=2, + estimator=estimator, +) +print(expectation) # 0.75 +print(variance) # 0.203125 +``` + +### 3. 统一流水线 + +```python +from cqlib.circuit import Circuit +from cqlib.error_mitigation import ( + ErrorMitigation, + MitigationMethod, + VirtualDistillationConfig, + ProcessArgs, + RunArgs, +) +from cqlib.qis import Hamiltonian, PauliString + +circuit = Circuit(1) +circuit.x(0) + +hamiltonian = Hamiltonian.from_list([(PauliString.from_str("Z"), 1.0)]) + + +def estimator(run_circuit, observable, shots): + if observable is None: + return (2.0, 1.0) + return (1.5, 0.25) + + +mitigation = ErrorMitigation( + circuit, + MitigationMethod.virtual_distillation(VirtualDistillationConfig(2)), +) + +mitigation.run( + hamiltonian, + RunArgs.virtual_distillation(shots_numerator=3, shots_denominator=2), + estimator, +) +result = mitigation.get_mitigated(ProcessArgs.virtual_distillation()) + +print(result.expectation) # 0.75 +print(result.variance) # 0.203125 +``` + +--- + +## 校验与错误处理 + +模块中的校验分两类:构造期的配置校验(折叠等级、拷贝数)与执行期的一致性校验(观测量比特数、流水线调用顺序、参数与方法是否匹配)。 + +| 异常 | 触发场景 | +| --- | --- | +| `ErrorMitigationError` | 配置或调用顺序非法,例如折叠等级为负、拷贝数小于 2、观测量比特数与线路不符、未 `run()` 就调用 `get_mitigated()`、重复 `run()` 或 `get_mitigated()`、`RunArgs` / `ProcessArgs` 与实际方法不匹配,以及外推输入非法(见 [零噪声外推](1_zne.md))。 | +| `CircuitError` | 缓解线路构造失败,例如在 `ZNEMitigation` 上以负折叠等级调用 `fold_circuits()`。 | +| `ValueError` | 观测量相关的运算失败,例如按拷贝数扩展 `Hamiltonian` 时出错。 | +| `TypeError` | 传入的 `estimator` 不可调用。 | + +`ErrorMitigationError` 与 `CircuitError` 同为 `cqlib.circuit.CqlibError` 的子类。回调内部抛出的异常不会被替换,会在缓解流程返回后原样重新抛出。 diff --git a/docs/api/cn/python/6_error_mitigation/1_zne.md b/docs/api/cn/python/6_error_mitigation/1_zne.md new file mode 100644 index 00000000..3e2fc5fb --- /dev/null +++ b/docs/api/cn/python/6_error_mitigation/1_zne.md @@ -0,0 +1,226 @@ +# 零噪声外推 + +`cqlib.error_mitigation.zne` + +`cqlib.error_mitigation.zne` 提供零噪声外推的低层接口:按折叠等级构造缓解线路族,用回调逐条估计期望值,再把结果外推到零噪声点。本页覆盖 `ZNEMitigation`、`ZneConfig`、`ExtrapolateMethod` 以及回调类型别名 `Estimator`。 + +## 导入 + +```python +from cqlib.error_mitigation.zne import ( + Estimator, + ExtrapolateMethod, + ZneConfig, + ZNEMitigation, +) +``` + +上述符号同时由 `cqlib.error_mitigation` 重导出。 + +--- + +## Estimator + +回调类型别名,约定缓解方法如何调用调用方提供的估计函数。 + +### `Estimator = Callable[[Circuit, Hamiltonian | None, int | None], tuple[float, float]]` + +调用约定:以位置参数传入三个实参。 + +| 参数 | 类型 | 含义 | +| --- | --- | --- | +| `circuit` | `Circuit` | 本次待执行的缓解线路。零噪声外推传入的是按某个折叠等级展开后的线路。 | +| `hamiltonian` | `Hamiltonian \| None` | 待估计的观测量。零噪声外推始终传入配置的 `Hamiltonian`。 | +| `shots` | `int \| None` | 本次执行的采样次数。 | + +返回约定:必须返回一个二元组 `(期望值, 方差)`,两个元素都会按浮点数取用。 + +| 返回位置 | 含义 | 零噪声外推是否使用 | +| --- | --- | --- | +| 第 1 个元素 | 该线路在观测量上的期望值。 | 使用,构成外推的输入序列。 | +| 第 2 个元素 | 该期望值的方差。 | 不使用,仅在虚拟蒸馏中参与后处理。 | + +`shots` 的取值取决于调用路径: + +- `run_em_sequence()` 始终传入 `None`。 +- `run_em_sequence_with_shots()` 传入给定的采样次数。 +- 统一流水线的 `RunArgs.zne(shots=...)` 传入该值;未提供时为 `None`。 + +当 `shots` 为 `None` 时,表示由回调自行决定采样方式。回调必须可调用,否则在流程开始前抛出 `TypeError`。回调内部抛出的异常会在流程返回后原样重新抛出,不会被替换为其他异常类型。 + +同一别名在 `cqlib.error_mitigation.virtual_distillation` 与 `cqlib.error_mitigation.unified` 中同样定义,三者含义一致;[虚拟蒸馏](2_virtual_distillation.md) 与 [统一流水线](3_unified.md) 中回调形式相同,区别仅在于传入的线路与观测量。 + +--- + +## ZNEMitigation + +零噪声外推的低层辅助类,持有基础线路的副本,负责折叠、执行与外推。 + +### ZNEMitigation(circuit, fold_levels) + +参数: + +- `circuit` (`Circuit`):基础线路。构造时复制保存,折叠不会修改传入的线路。 +- `fold_levels` (`Sequence[int]`):折叠等级序列,每个等级对应噪声因子 `2 * level + 1`。构造时不做校验,负值在折叠时才会失败。 + +### 属性 + +- `circuit -> Circuit`:基础线路的副本。 +- `fold_levels -> list[int]`:配置的折叠等级。 +- `noise_factors -> list[int]`:由折叠等级导出的噪声因子,满足 `noise_factor = 2 * fold_level + 1`。 + +### 方法 + +- `fold_circuits(gate_set=None) -> list[Circuit]`:按每个折叠等级各返回一条缓解线路,返回顺序与 `fold_levels` 一致,长度与 `fold_levels` 相同。 +- `run_em_sequence(gate_set, hamiltonian, estimator) -> list[float]`:逐条执行折叠线路,返回期望值列表。等价于以 `shots=None` 调用下一个方法。 +- `run_em_sequence_with_shots(gate_set, hamiltonian, shots, estimator) -> list[float]`:同上,并把 `shots` 传给回调。 +- `extrapolate(noisy_results, method, degree) -> float`:按 `method` 选择拟合方式,返回零噪声点的外推值。 +- `poly_extrapolate(noisy_results, degree) -> float`:用多项式拟合外推。 +- `exp_extrapolate(noisy_results) -> float`:用指数衰减模型拟合外推。 + +### 其他行为 + +- 支持 `copy`、`deepcopy`,副本与原对象各自独立。 +- 不提供值相等比较;两个 `ZNEMitigation` 实例仅在为同一对象时相等。 +- `repr()` 形如 `ZNEMitigation(fold_levels=[0, 1, 2], noise_factors=[1, 3, 5])`。 + +--- + +## 折叠行为 + +`fold_circuits()` 的 `gate_set` 决定折叠范围: + +- `gate_set` 为 `None`(默认):全局折叠,把线路整体改写为 `U -> U (U† U)^level`。展开后的操作数随等级线性增长。 +- `gate_set` 为指令序列:选择性折叠,只对指令名匹配其中任一指令的操作做同样的展开,其余操作原样保留。 + +指令名取自传入的 `Instruction`,因此可先按标准门名构造: + +下文示例中的 `zne` 为 `ZNEMitigation` 实例。 + +```python +from cqlib.circuit import Instruction, StandardGate + +gate_set = [Instruction.from_standard_gate(StandardGate.X)] +folded = zne.fold_circuits(gate_set) +``` + +折叠等级为 `0` 时不展开,返回基础线路的副本。等级为负时折叠失败并抛出 `CircuitError`。 + +--- + +## 执行约定 + +`run_em_sequence()` 与 `run_em_sequence_with_shots()` 都要求观测量比特数与基础线路宽度一致,否则抛出 `ErrorMitigationError`。两者的 `gate_set` 都需要显式传入:全局折叠时传 `None`,选择性折叠时传指令序列。 + +两个方法对每个折叠等级各调用一次回调,返回的列表长度与 `fold_levels` 相同,列表元素为回调返回二元组的第 1 个元素: + +```python +noisy = zne.run_em_sequence(None, hamiltonian, estimator) +``` + +`run_em_sequence()` 传入的 `shots` 始终为 `None`;需要把采样次数交给回调时使用 `run_em_sequence_with_shots()`。 + +--- + +## 外推 + +三个外推方法都以 `noise_factors` 作为自变量、以 `noisy_results` 作为观测值,返回自变量为 `0` 处的拟合值。 + +### `extrapolate(noisy_results, method, degree) -> float` + +按 `method` 选择拟合方式: + +- `ExtrapolateMethod.polynomial()`:使用 `degree`,等价于 `poly_extrapolate(noisy_results, degree)`。 +- `ExtrapolateMethod.exponential()`:忽略 `degree`,等价于 `exp_extrapolate(noisy_results)`。 + +参数: + +- `noisy_results` (`Sequence[float]`):各个折叠等级下的期望值,长度必须与 `noise_factors` 相同。 +- `method` (`ExtrapolateMethod`):拟合方法。 +- `degree` (`int`):多项式阶数。指数拟合时该参数不参与计算,仍需传入。 + +返回: + +- `float`:零噪声点的外推值。 + +### `poly_extrapolate(noisy_results, degree) -> float` + +对数据点做 `degree` 阶多项式最小二乘拟合,返回常数项。`degree` 必须严格小于噪声因子个数;`noisy_results` 必须与噪声因子一一对应,且不能为空。 + +### `exp_extrapolate(noisy_results) -> float` + +按 `y(x) = A * exp(-x / tau)` 做对数空间线性回归,返回零噪声点的值 `A`。调用前所有 `noisy_results` 必须为正值——对数空间要求非负,而取值为 `0` 的项同样会被拒绝。 + +示例: + +```python +from cqlib.circuit import Circuit +from cqlib.error_mitigation.zne import ZNEMitigation + +circuit = Circuit(1) +circuit.x(0) + +zne = ZNEMitigation(circuit, [0, 1, 2]) +print(zne.noise_factors) # [1, 3, 5] + +noisy_results = [2.75, 6.75, 10.75] +print(zne.poly_extrapolate(noisy_results, 1)) # 0.75 +``` + +上例中的数据点满足 `y = 0.75 + 2x`,一阶拟合的常数项即零噪声点的值。 + +--- + +## ExtrapolateMethod + +外推方法,通过静态方法构造。实例为不可变值对象,可传给 `ZNEMitigation.extrapolate()` 与统一流水线的 `ProcessArgs.zne()`。 + +### 静态方法 + +- `ExtrapolateMethod.polynomial()`:多项式拟合,需要阶数。 +- `ExtrapolateMethod.exponential()`:对数空间的指数衰减拟合,不使用阶数。 + +### 其他行为 + +- `str()` 返回 `"polynomial"` 或 `"exponential"`;`repr()` 返回 `ExtrapolateMethod.polynomial()` 或 `ExtrapolateMethod.exponential()`。 +- 支持 `==`、`hash`、`copy`、`deepcopy`。 + +--- + +## ZneConfig + +零噪声外推的配置对象,可在低层类之外独立构造,并传给统一流水线。 + +### ZneConfig(fold_levels) + +参数: + +- `fold_levels` (`Sequence[int]`):折叠等级序列,负值不在构造时校验。 + +### 属性 + +- `fold_levels -> list[int]`:配置的折叠等级。 + +### 其他行为 + +- 支持 `==`、`copy`、`deepcopy`;不提供 `hash`。 +- `repr()` 形如 `ZneConfig(fold_levels=[0, 1, 2])`。 + +示例: + +```python +from cqlib.error_mitigation.zne import ZneConfig + +config = ZneConfig([0, 1, 2]) +print(config.fold_levels) # [0, 1, 2] +``` + +--- + +## 异常情况 + +| 异常 | 触发场景 | +| --- | --- | +| `ErrorMitigationError` | 观测量比特数与基础线路宽度不符;外推输入为空;外推输入长度与噪声因子个数不符;多项式阶数不小于数据点个数;指数外推的输入存在非正值;拟合的正规方程或回归系统奇异。 | +| `CircuitError` | 折叠等级为负,构造折叠线路失败。 | +| `TypeError` | 传入的 `estimator` 不可调用。 | diff --git a/docs/api/cn/python/6_error_mitigation/2_virtual_distillation.md b/docs/api/cn/python/6_error_mitigation/2_virtual_distillation.md new file mode 100644 index 00000000..88b2c8bd --- /dev/null +++ b/docs/api/cn/python/6_error_mitigation/2_virtual_distillation.md @@ -0,0 +1,164 @@ +# 虚拟蒸馏 + +`cqlib.error_mitigation.virtual_distillation` + +`cqlib.error_mitigation.virtual_distillation` 提供虚拟蒸馏的低层接口:把基础线路复制若干份并插入 SWAP 构造 copy-swap 线路,再分别估计分子与分母,最后求比值得到缓解后的期望值。本页覆盖 `VirtualDistillation` 与 `VirtualDistillationConfig`。 + +## 导入 + +```python +from cqlib.error_mitigation.virtual_distillation import ( + Estimator, + VirtualDistillationConfig, + VirtualDistillation, +) +``` + +`VirtualDistillation` 与 `VirtualDistillationConfig` 同时由 `cqlib.error_mitigation` 重导出;回调约定见 [零噪声外推](1_zne.md) 中的 `Estimator`。 + +虚拟蒸馏估计的比值是 `Tr(O ρ^M) / Tr(ρ^M)`,其中 `M` 为拷贝数,`O` 为 `Hamiltonian` 表示的观测量。 + +--- + +## VirtualDistillationConfig + +虚拟蒸馏的配置对象,可在低层类之外独立构造,并传给统一流水线。 + +### VirtualDistillationConfig(copies) + +参数: + +- `copies` (`int`):密度矩阵的拷贝份数。构造时不做校验,小于 2 时会在真正使用该配置时失败。 + +### 属性 + +- `copies -> int`:配置的拷贝数。 + +### 其他行为 + +- 支持 `==`、`copy`、`deepcopy`;不提供 `hash`。 +- `repr()` 形如 `VirtualDistillationConfig(copies=2)`。 + +--- + +## VirtualDistillation + +虚拟蒸馏的低层辅助类,持有基础线路的副本,负责构造 copy-swap 线路、执行分子与分母、以及求比值。 + +### VirtualDistillation(circuit, copies) + +参数: + +- `circuit` (`Circuit`):基础线路。构造时复制保存,不会修改传入的线路。 +- `copies` (`int`):拷贝份数,必须不小于 2,否则抛出 `ErrorMitigationError`。拷贝数决定缓解线路的宽度:copy-swap 线路的宽度为基础线路宽度的 `copies` 倍。 + +### 属性 + +- `copies -> int`:当前配置的拷贝数。 + +### 方法 + +- `set_copies(copies)`:更新拷贝数。校验在赋值之前完成,校验失败时对象的拷贝数保持不变。 +- `build_copy_swap_circuit() -> Circuit`:构造 copy-swap 线路。基础线路先做分解,再逐份复制到各自比特区间,最后按位插入 SWAP。 +- `run_denominator_circuit(shots, estimator) -> tuple[float, float]`:执行分母线路,返回回调给出的二元组。 +- `run_numerator_circuit(hamiltonian, shots, estimator) -> tuple[float, float]`:执行分子线路,返回回调给出的二元组。 +- `run_vd(hamiltonian, shots_numerator, shots_denominator, estimator) -> tuple[float, float]`:依次执行分子与分母并求比值,返回 `(缓解后的期望值, 方差)`。 + +### 其他行为 + +- 支持 `copy`、`deepcopy`,副本与原对象各自独立。 +- 不提供值相等比较;两个 `VirtualDistillation` 实例仅在为同一对象时相等。 +- `repr()` 形如 `VirtualDistillation(copies=2)`。 + +--- + +## copy-swap 线路 + +`build_copy_swap_circuit()` 的输出由三部分构成: + +1. 基础线路先做分解,保证复制的是一组已展开的操作。 +2. 基础线路按 `copies` 份复制,第 `i` 份落在偏移 `i` 倍基础宽度的比特区间上。 +3. 对第 1 份与其余每一份,逐位插入 SWAP。SWAP 只在第 1 份与其他各份之间插入,其余份之间不直接相连。 + +因此拷贝数为 2 时,基础线路每条操作出现 2 次,另加按位 SWAP;拷贝数增加时 SWAP 数量按份数线性增长,而不随拷贝数两两配对增长。 + +```python +from cqlib.circuit import Circuit +from cqlib.error_mitigation.virtual_distillation import VirtualDistillation + +circuit = Circuit(1) +circuit.x(0) + +vd = VirtualDistillation(circuit, 2) +copy_swap = vd.build_copy_swap_circuit() +print(copy_swap.width) # 2 +``` + +--- + +## 执行约定 + +虚拟蒸馏的缓解线路只有一条,分子与分母都在该线路上执行,区别在于传给回调的观测量与采样次数: + +| 方法 | 回调收到的线路 | 回调收到的观测量 | 回调收到的采样次数 | +| --- | --- | --- | --- | +| `run_denominator_circuit(shots, estimator)` | copy-swap 线路 | `None` | `shots` | +| `run_numerator_circuit(hamiltonian, shots, estimator)` | copy-swap 线路 | 按拷贝数扩展后的 `Hamiltonian` | `shots` | +| `run_vd(...)` | copy-swap 线路 | 先分子后分母,同上 | 分别为 `shots_numerator` 与 `shots_denominator` | + +分子所用的观测量由传入的 `Hamiltonian` 扩展得到:原有 Pauli 项保留在各自比特上,更高位的比特补 `Z`,使观测量比特数与 copy-swap 线路宽度一致。扩展由类内部完成,调用方只需传入作用在基础线路上的观测量。 + +`run_vd()` 依次完成两步:先按 `shots_numerator` 执行分子,再按 `shots_denominator` 执行分母,然后按下式合成结果: + +```text +期望值 = 分子期望值 / 分母期望值 +方差 = 分子方差 / 分母期望值^2 + + 分子期望值^2 * 分母方差 / 分母期望值^4 +``` + +分母期望值为 `0` 时无法求比值,抛出 `ErrorMitigationError`。`run_vd()` 要求观测量比特数与基础线路宽度一致,不符时在调用回调之前就抛出 `ErrorMitigationError`。 + +`run_denominator_circuit()` 与 `run_numerator_circuit()` 不做合成,直接返回回调给出的 `(期望值, 方差)` 二元组,适合需要分别查看分子与分母结果的场景。 + +示例: + +```python +from cqlib.circuit import Circuit +from cqlib.error_mitigation.virtual_distillation import VirtualDistillation +from cqlib.qis import Hamiltonian, PauliString + +circuit = Circuit(1) +circuit.x(0) + +hamiltonian = Hamiltonian.from_list([(PauliString.from_str("Z"), 1.0)]) + +vd = VirtualDistillation(circuit, 2) + + +def estimator(run_circuit, observable, shots): + if observable is None: + return (2.0, 1.0) # 分母:Tr(ρ^M) + return (1.5, 0.25) # 分子:Tr(O ρ^M) + + +expectation, variance = vd.run_vd( + hamiltonian, + shots_numerator=3, + shots_denominator=2, + estimator=estimator, +) +print(expectation) # 0.75 +print(variance) # 0.203125 +``` + +上例中的分子与分母为定值,故 `0.75 = 1.5 / 2.0`,`0.203125 = 0.25 / 4 + 2.25 / 16`。 + +--- + +## 异常情况 + +| 异常 | 触发场景 | +| --- | --- | +| `ErrorMitigationError` | 拷贝数小于 2(构造时或 `set_copies()` 时);`run_vd()` 的观测量比特数与基础线路宽度不符;分母期望值为 `0`。 | +| `CircuitError` | copy-swap 线路构造失败,或观测量扩展后的比特数与缓解线路宽度不符。 | +| `TypeError` | 传入的 `estimator` 不可调用。 | diff --git a/docs/api/cn/python/6_error_mitigation/3_unified.md b/docs/api/cn/python/6_error_mitigation/3_unified.md new file mode 100644 index 00000000..74e91907 --- /dev/null +++ b/docs/api/cn/python/6_error_mitigation/3_unified.md @@ -0,0 +1,230 @@ +# 统一流水线 + +`cqlib.error_mitigation.unified` + +`cqlib.error_mitigation.unified` 提供顺序式的统一流水线:先按选定方法执行缓解线路并收集回调节果,再做方法相关的后处理。本页覆盖 `ErrorMitigation` 及其配套的 `MitigationMethod`、`RunArgs`、`ProcessArgs`、`MitigatedResult` 与 `ErrorMitigationError`。 + +## 导入 + +```python +from cqlib.error_mitigation.unified import ( + Estimator, + ErrorMitigationError, + MitigationMethod, + RunArgs, + ProcessArgs, + MitigatedResult, + ErrorMitigation, +) +``` + +除 `Estimator` 外,上述符号同时由 `cqlib.error_mitigation` 重导出。回调约定见 [零噪声外推](1_zne.md) 中的 `Estimator`。 + +--- + +## MitigationMethod + +缓解方法选择,通过静态方法构造,在构造 `ErrorMitigation` 时确定本实例使用哪种方法。 + +### 静态方法 + +- `MitigationMethod.zne(config)`:配置为零噪声外推。参数 `config`(`ZneConfig`)给出折叠等级。 +- `MitigationMethod.virtual_distillation(config)`:配置为虚拟蒸馏。参数 `config`(`VirtualDistillationConfig`)给出拷贝数。 + +### 属性 + +- `method_type -> str`:方法标识,取值为 `"zne"` 或 `"virtual_distillation"`。 + +### 其他行为 + +- 支持 `==`、`copy`、`deepcopy`;不提供 `hash`。 +- `repr()` 形如 `MitigationMethod.zne(ZneConfig(fold_levels=[0, 1, 2]))`。 + +--- + +## RunArgs + +一次执行所需的运行参数,与所选方法一一对应。方法选 `zne` 时只能用 `RunArgs.zne(...)`,否则 `run()` 抛出 `ErrorMitigationError`。 + +### 静态方法 + +- `RunArgs.zne(gate_set=None, shots=None)`: + - `gate_set` (`Sequence[Instruction] | None`):选择性折叠的门集。默认 `None`,表示全局折叠。 + - `shots` (`int | None`):传给回调的采样次数。默认 `None`。 +- `RunArgs.virtual_distillation(shots_numerator, shots_denominator)`: + - `shots_numerator` (`int`):分子的采样次数。 + - `shots_denominator` (`int`):分母的采样次数。 + +### 属性 + +- `method_type -> str`:方法标识。 + +### 其他行为 + +- 支持 `==`、`copy`、`deepcopy`;不提供 `hash`。比较 `zne` 参数时按指令名比对 `gate_set` 中的元素,而非按对象同一性。 +- `repr()` 形如 `RunArgs.zne(gate_set=None, shots=Some(256))`。 + +示例: + +```python +from cqlib.circuit import Instruction, StandardGate +from cqlib.error_mitigation.unified import RunArgs + +global_folding = RunArgs.zne(shots=256) +selective_folding = RunArgs.zne( + gate_set=[Instruction.from_standard_gate(StandardGate.X)], + shots=256, +) + +vd_args = RunArgs.virtual_distillation(shots_numerator=3, shots_denominator=2) +``` + +--- + +## ProcessArgs + +后处理参数,与所选方法一一对应,在 `get_mitigated()` 时传入。 + +### 静态方法 + +- `ProcessArgs.zne(method, degree=None)`: + - `method` (`ExtrapolateMethod`):外推方法。 + - `degree` (`int | None`):多项式阶数。默认 `None`,此时取 `min(数据点个数 - 1, 1)`。指数拟合时该参数不参与计算。 +- `ProcessArgs.virtual_distillation()`:虚拟蒸馏无额外后处理参数。 + +### 属性 + +- `method_type -> str`:方法标识。 + +### 其他行为 + +- 支持 `==`、`copy`、`deepcopy`;不提供 `hash`。 +- `repr()` 形如 `ProcessArgs.zne(method=polynomial, degree=Some(1))`。 + +--- + +## MitigatedResult + +缓解结果,由 `get_mitigated()` 返回,不可直接构造。属性为只读。 + +### 属性 + +- `expectation -> float`:缓解后的期望值。 +- `variance -> float | None`:缓解后的方差。零噪声外推的结果为 `None`;虚拟蒸馏的结果有值。 + +### 其他行为 + +- 支持 `==`、`copy`、`deepcopy`;不提供 `hash`。 +- `repr()` 形如 `MitigatedResult(expectation=0.5, variance=None)`。 + +--- + +## ErrorMitigation + +统一流水线,按 `run()` → `get_mitigated()` 的顺序完成一次缓解。 + +### ErrorMitigation(circuit, method) + +参数: + +- `circuit` (`Circuit`):基础线路。构造时复制保存。 +- `method` (`MitigationMethod`):缓解方法。 + +构造时校验方法配置:零噪声外推的折叠等级含负值、或虚拟蒸馏的拷贝数小于 2 时抛出 `ErrorMitigationError`。 + +### 方法 + +- `run(hamiltonian, run_args, estimator) -> None`:按 `run_args` 执行方法对应的缓解线路,并把回调节果记录下来。 +- `get_mitigated(process_args) -> MitigatedResult`:对已记录的结果做后处理,返回缓解结果。 + +### 其他行为 + +- 支持 `copy`、`deepcopy`,副本携带当前状态。 +- 不提供值相等比较。 +- `repr()` 固定为 `ErrorMitigation()`。 + +### 状态机 + +每个实例只完成一次缓解,调用顺序与参数匹配都由实例自身约束: + +| 当前状态 | 调用 | 结果 | +| --- | --- | --- | +| 已构造 | `run()` | 执行并转入已运行状态。 | +| 已构造 | `get_mitigated()` | 抛出 `ErrorMitigationError`,要求先完成 `run()`。 | +| 已运行 | `get_mitigated()` | 返回结果并转入已缓解状态。 | +| 已运行 | `run()` | 抛出 `ErrorMitigationError`,不允许重复 `run()`。 | +| 已缓解 | `run()` 或 `get_mitigated()` | 抛出 `ErrorMitigationError`,不允许再次调用。 | + +若 `run()` 传入的 `RunArgs` 与构造时的方法不匹配,抛出 `ErrorMitigationError`,此时回调不会被调用,状态也不改变。`get_mitigated()` 传入的 `ProcessArgs` 与方法不匹配时同样抛出 `ErrorMitigationError`。 + +### 执行约定 + +零噪声外推:`run()` 按配置的折叠等级构造缓解线路,对每条线路调用一次回调,观测量为传入的 `Hamiltonian`,采样次数取自 `RunArgs.zne(shots=...)`。回调返回的二元组只取第 1 个元素,方差被丢弃。`get_mitigated()` 对外推输入做拟合,结果的 `variance` 为 `None`。 + +虚拟蒸馏:`run()` 先构造 copy-swap 线路,再在同一条线路上调用两次回调——先分子,后分母。分子传入按拷贝数扩展后的观测量与 `shots_numerator`,分母传入 `None` 与 `shots_denominator`。`get_mitigated()` 按 [虚拟蒸馏](2_virtual_distillation.md) 的合成公式求比值,结果的 `variance` 有值。 + +### 示例 + +```python +from cqlib.circuit import Circuit +from cqlib.error_mitigation.unified import ( + ErrorMitigation, + MitigationMethod, + ProcessArgs, + RunArgs, +) +from cqlib.error_mitigation.zne import ExtrapolateMethod, ZneConfig +from cqlib.qis import Hamiltonian, PauliString + +circuit = Circuit(1) +circuit.x(0) + +hamiltonian = Hamiltonian.from_list([(PauliString.from_str("Z"), 1.0)]) + + +def estimator(run_circuit, observable, shots): + return (len(run_circuit.operations) + 0.5, 0.0) + + +mitigation = ErrorMitigation( + circuit, + MitigationMethod.zne(ZneConfig([0, 1, 2])), +) + +mitigation.run(hamiltonian, RunArgs.zne(shots=256), estimator) +result = mitigation.get_mitigated( + ProcessArgs.zne(ExtrapolateMethod.polynomial(), degree=1) +) + +print(result.expectation) # 0.5 +print(result.variance) # None +``` + +上例中折叠线路的操作数依次为 `[1, 3, 5]`,回调给出 `[1.5, 3.5, 5.5]`,一阶拟合的零噪声点取值为 `0.5`。 + +--- + +## ErrorMitigationError + +错误缓解异常的基类,继承自 `cqlib.circuit.CqlibError`。 + +| 异常 | 触发场景 | +| --- | --- | +| `ErrorMitigationError` | 见下表所列情形。 | +| `CircuitError` | 折叠线路或 copy-swap 线路构造失败。 | +| `ValueError` | 观测量相关的运算失败,例如按拷贝数扩展 `Hamiltonian` 时出错。 | +| `TypeError` | 传入的 `estimator` 不可调用。 | + +`ErrorMitigationError` 覆盖的常见情形: + +| 触发场景 | 说明 | +| --- | --- | +| 构造时折叠等级含负值 | 在 `ErrorMitigation()` 构造时报告,`ZneConfig` 本身不报错。 | +| 构造时拷贝数小于 2 | 在 `ErrorMitigation()` 构造时报告。 | +| 观测量比特数与基础线路宽度不符 | 在 `run()` 中报告,回调不会被调用。 | +| `RunArgs` 与实际方法不匹配 | 在 `run()` 中报告,回调不会被调用。 | +| `ProcessArgs` 与实际方法不匹配 | 在 `get_mitigated()` 中报告。 | +| 未 `run()` 就调用 `get_mitigated()` | 要求先完成 `run()`。 | +| 重复 `run()` 或重复 `get_mitigated()` | 每个实例只允许各调用一次。 | +| 外推输入非法或拟合失败 | 见 [零噪声外推](1_zne.md)。 | +| 虚拟蒸馏的分母期望值为 `0` | 无法求比值,见 [虚拟蒸馏](2_virtual_distillation.md)。 | diff --git a/docs/api/rust/0_circuit/0_overview.md b/docs/api/cn/rust/0_circuit/0_overview.md similarity index 83% rename from docs/api/rust/0_circuit/0_overview.md rename to docs/api/cn/rust/0_circuit/0_overview.md index 6f8611ef..faf1359f 100644 --- a/docs/api/rust/0_circuit/0_overview.md +++ b/docs/api/cn/rust/0_circuit/0_overview.md @@ -4,6 +4,8 @@ `cqlib_core::circuit` 是 Cqlib Rust core 中的量子电路中间表示模块,负责提供类型安全的线路构造、量子比特管理、参数表达式、门定义、经典数据、结构化控制流、控制流图、矩阵转换以及参数化线路模板等基础能力。 +## Overview + `cqlib_core::circuit` 主要提供以下能力: - **量子线路构造**:通过 `Circuit` 创建逻辑量子比特集合,并追加标准门、自定义门、复合门、测量、reset、barrier 等操作。 @@ -11,12 +13,29 @@ - **操作级 IR**:通过 `Instruction`、`Operation`、`ValueInstruction` 和 `ValueOperation` 表示线路中的指令和操作,支持构造层 IR 与存储层 IR 分离。 - **门定义体系**:提供 `StandardGate`、`UnitaryGate`、`MCGate`、`CircuitGate` 和 `FrozenCircuit` 等门定义类型,覆盖标准门、自定义酉门、多控制门和子线路复合门。 - **经典数据与控制流**:通过 `ClassicalType`、`ClassicalExpr`、`ClassicalControlOp` 等类型描述测量结果、经典变量和结构化控制流。 -- **控制流图分析**:通过 `CircuitCFG`、`BasicBlock`、`Terminator` 等类型为编译器 pass 提供控制流图视图。 +- **控制流图分析**:通过 `CircuitCFG`、`BasicBlock`、`Terminator` 等类型为编译器 pass 提供控制流图视图,并通过 `CircuitDag` 提供线路的有向无环图视图。 - **矩阵与符号矩阵转换**:支持将小规模纯量子线路转换为数值矩阵或保留参数的符号矩阵,并支持全局相位等价检查。 - **参数化线路模板**:通过 `ansatz` 模块提供 VQE、QAOA、feature map 和 Hamiltonian evolution 等常见模板。 --- +## 常用入口 + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; + +let mut circuit = Circuit::new(2); +circuit.h(Qubit::new(0))?; +circuit.cx(Qubit::new(0), Qubit::new(1))?; + +assert_eq!(circuit.qubits().len(), 2); +assert_eq!(circuit.operations().len(), 2); + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +--- + ## API 导航 | 分类 | 页面 | 主要对象 | 说明 | @@ -33,11 +52,26 @@ | 符号矩阵 | [Symbolic Matrix](10_symbolic_matrix.md) | `SymbolicComplex`, `SymbolicMatrix` | 保留 `Parameter` 的密集符号矩阵和等价检查。 | | Ansatz | [Ansatz](11_ansatz.md) | `Ansatz`, `TwoLocal`, `QAOAAnsatz`, `PauliEvolutionAnsatz` | 参数化线路模板。 | | 控制流图 | [CFG](12_cfg.md) | `CircuitCFG`, `BasicBlock`, `Terminator` | 线路控制流图视图、分析和重建。 | +| 线路 DAG | [线路 DAG](14_circuit_dag.md) | `CircuitDag`, `DagNode`, `DagWire` | 线路有向无环图视图、依赖查询和图结构重建。 | | 矩阵转换 | [Circuit To Matrix](13_circuit_to_matrix.md) | `circuit_to_matrix`, `Circuit::to_matrix` | 数值矩阵转换、量子比特顺序和全局相位。 | --- -## 核心 IR 分层 +## 核心概念与术语 + +| 术语 | 说明 | +| --- | --- | +| **抽象线路** | 面向算法描述的量子线路,使用逻辑量子比特和高层操作表示量子程序,不直接绑定具体硬件拓扑或原生门集。 | +| **物理线路** | 已根据目标设备约束完成布局、路由和门集转换的线路,通常只包含后端支持的物理量子比特连接和原生操作。 | +| **量子比特** | 量子信息的基本逻辑单位,由 `Qubit` 表示,内部是 `u32` 句柄。 | +| **指令** | 线路中的操作类型,由 `Instruction` 表示,可以是量子门,也可以是 `Barrier`、`Measure`、`Reset`、`Delay`、经典数据操作或控制流结构。 | +| **操作** | 指令在线路中的一次具体应用,包含指令类型、作用量子比特、参数列表和可选标签。 | +| **门定义** | 与具体应用分离的门描述,由 `StandardGate`、`UnitaryGate`、`MCGate`、`CircuitGate` 等类型表示。 | +| **参数** | 构造期符号表达式,由 `Parameter` 表示,可在后续通过 `assign_parameters()` 绑定为数值。 | +| **全局相位** | 线路整体相位因子。它通常不影响单独测量概率,但在线路组合、受控操作、矩阵等价检查和编译重写中仍然具有意义。 | +| **Ansatz** | 参数化线路模板,由 `ansatz` 模块提供,常用于变分算法和量子机器学习。 | + +### 两层 IR 的对照 Rust core 中的 circuit IR 分为构造层和存储层。理解这一区分对于编写导入器、序列化工具和编译器 pass 很重要。 diff --git a/docs/api/rust/0_circuit/10_symbolic_matrix.md b/docs/api/cn/rust/0_circuit/10_symbolic_matrix.md similarity index 85% rename from docs/api/rust/0_circuit/10_symbolic_matrix.md rename to docs/api/cn/rust/0_circuit/10_symbolic_matrix.md index 06c6a03f..cf9264a0 100644 --- a/docs/api/rust/0_circuit/10_symbolic_matrix.md +++ b/docs/api/cn/rust/0_circuit/10_symbolic_matrix.md @@ -128,6 +128,19 @@ let matrix = SymbolicMatrix::from_shape_vec( | `symbolic_matrices_equivalent(lhs, rhs)` | 判断两个符号矩阵是否能证明只差全局相位。 | | `circuits_equivalent(lhs, rhs, qubits_order)` | 判断两条线路是否能证明全局相位等价。 | +除上述函数外,模块还导出一组就地将门矩阵写入符号矩阵的底层函数,主要用于矩阵分块构造和自定义门实现。 + +| 函数 | 说明 | +| --- | --- | +| `apply_standard_gate_to_matrix(matrix, gate, bits, params)` | 将标准门就地作用到指定比特上。 | +| `apply_gate_to_matrix(matrix, gate, bits)` | 将符号门矩阵就地作用到指定比特上。 | +| `apply_gate_to_matrix_num(matrix, gate, bits)` | 将数值门矩阵就地作用到指定比特上。 | +| `apply_single_qubit_gate(matrix, gate, bit)` / `apply_single_qubit_gate_num(matrix, gate, bit)` | 单比特门就地作用,分别接收符号门矩阵与数值门矩阵。 | +| `apply_two_qubit_gate(matrix, gate, b0, b1)` / `apply_two_qubit_gate_num(matrix, gate, b0, b1)` | 双比特门就地作用。 | +| `apply_general_gate(matrix, gate, bits)` / `apply_general_gate_num(matrix, gate, bits)` | 任意比特数的门就地作用。 | +| `apply_symbolic_permutation_gate(matrix, permutation, bits)` / `apply_numeric_permutation_gate(matrix, permutation, bits)` | 就地作用置换型门。 | +| `apply_symbolic_diagonal_gate(matrix, diagonal, bits)` / `apply_numeric_diagonal_gate(matrix, diagonal, bits)` | 就地作用对角型门。 | + --- ## 电路符号矩阵转换 @@ -135,7 +148,7 @@ let matrix = SymbolicMatrix::from_shape_vec( ```rust pub fn circuit_to_symbolic_matrix( circuit: &Circuit, - qubits_order: Option<&[Qubit]>, + qubits_order: Option<&[usize]>, ) -> Result ``` @@ -178,7 +191,7 @@ assert_eq!(numeric.shape(), &[2, 2]); - 显式顺序必须与线路中的量子比特集合完全一致,不能遗漏、重复或包含未知量子比特; - 顺序中的第一个量子比特对应 basis index 的 least-significant bit。 -例如,如果线路量子比特为 `[Qubit(0), Qubit(2)]`,显式顺序可以写为 `[Qubit(0), Qubit(2)]` 或 `[Qubit(2), Qubit(0)]`,二者会得到不同轴顺序下的矩阵表示。 +例如,如果线路中的量子比特编号为 `0` 和 `2`,显式顺序可以写为 `[0, 2]` 或 `[2, 0]`,二者会得到不同轴顺序下的矩阵表示。 --- diff --git a/docs/api/rust/0_circuit/11_ansatz.md b/docs/api/cn/rust/0_circuit/11_ansatz.md similarity index 83% rename from docs/api/rust/0_circuit/11_ansatz.md rename to docs/api/cn/rust/0_circuit/11_ansatz.md index 4609c961..827b914e 100644 --- a/docs/api/rust/0_circuit/11_ansatz.md +++ b/docs/api/cn/rust/0_circuit/11_ansatz.md @@ -18,6 +18,10 @@ use cqlib_core::circuit::ansatz::{ QAOAAnsatz, EvolutionStrategy, PauliEvolutionAnsatz, + real_amplitudes, + efficient_su2, + zz_feature_map, + pauli_feature_map, }; ``` @@ -124,11 +128,9 @@ pub enum EntanglementTopology { use cqlib_core::circuit::ansatz::EntanglementTopology; let topology = EntanglementTopology::Linear; -let pairs = topology.generate_pairs(4)?; +let pairs = topology.generate_pairs(4); assert_eq!(pairs, vec![(0, 1), (1, 2), (2, 3)]); - -# Ok::<(), cqlib_core::circuit::CircuitError>(()) ``` --- @@ -193,6 +195,44 @@ let circuit = fm.build_circuit("x")?; --- +## 快捷构造函数 + +对于使用频率较高的几种模板,`ansatz` 模块提供了一组快捷构造函数,用于按模板名直接创建实例。它们返回的仍是上表中的模板对象,可以继续使用 builder 方法调整配置。 + +| 函数 | 返回 | 说明 | +| --- | --- | --- | +| `real_amplitudes(num_qubits, reps, entanglement)` | `TwoLocal` | 由 `RY` 旋转层与纠缠层交替组成的模板。 | +| `efficient_su2(num_qubits, reps, entanglement)` | `TwoLocal` | 由 `RY`、`RZ` 旋转层与纠缠层交替组成的模板。 | +| `zz_feature_map(num_qubits, reps, entanglement)` | `ZZFeatureMap` | 二阶 ZZ feature map。 | +| `pauli_feature_map(num_qubits, reps, paulis, entanglement)` | `PauliFeatureMap` | 按给定 Pauli string 模板构造的 feature map。 | + +其中 `paulis` 是 `(PauliString, String)` 列表,前者指定相互作用类型,后者是该 Pauli 项的标签。 + +```rust +use cqlib_core::circuit::ansatz::{ + efficient_su2, + real_amplitudes, + zz_feature_map, + Ansatz, + EntanglementTopology, +}; + +let ansatz = real_amplitudes(3, 2, EntanglementTopology::Linear); + +assert_eq!(ansatz.num_qubits(), 3); +assert_eq!(ansatz.num_parameters(), 9); + +let su2 = efficient_su2(2, 1, EntanglementTopology::Full); + +assert_eq!(su2.num_parameters(), 8); + +let fm = zz_feature_map(3, 1, EntanglementTopology::Full); + +assert_eq!(fm.num_parameters(), 3); +``` + +--- + ## Layer 模板 除通用 `TwoLocal` 外,`ansatz` 模块还提供常见层状模板,用于快速构造规律性较强的可训练线路。 @@ -274,11 +314,11 @@ Hamiltonian evolution 模板用于构造形如 `exp(-i H t)` 的参数化时间 `EvolutionStrategy` 用于指定 Hamiltonian 演化如何转换为量子门序列。 -| 变体 / 工厂方法 | 说明 | +| 变体 | 说明 | | --- | --- | -| `exact()` | 对两两对易的 Hamiltonian 使用精确 Pauli rotation 分解;若非对易,构建时返回错误。 | -| `auto(steps)` | 自动选择策略:对易时使用 exact,非对易时使用一阶 Trotter。 | -| `trotter(mode, steps)` | 显式使用指定 Trotter-Suzuki 分解模式和步数。 | +| `Exact` | 对两两对易的 Hamiltonian 使用精确 Pauli rotation 分解;若非对易,构建时返回错误。 | +| `Auto { steps }` | 自动选择策略:对易时使用 exact,非对易时使用一阶 Trotter,步数为 `steps`。 | +| `Trotter { mode, steps }` | 显式使用指定 Trotter-Suzuki 分解模式和步数。 | ### 2. `EvolutionInfo` diff --git a/docs/api/cn/rust/0_circuit/12_cfg.md b/docs/api/cn/rust/0_circuit/12_cfg.md new file mode 100644 index 00000000..24267fda --- /dev/null +++ b/docs/api/cn/rust/0_circuit/12_cfg.md @@ -0,0 +1,381 @@ +# CircuitCFG + +`cqlib_core::circuit::CircuitCFG` + +```rust +use cqlib_core::circuit::CircuitCFG; +use cqlib_core::circuit::cfg::{ + BasicBlock, + ControlFlowRegion, + FlowEdge, + OperationMetadata, + SwitchRegionCase, + Terminator, +}; +``` + +`CircuitCFG` 是 Rust core 中用于表示量子线路控制流图(Control Flow Graph, CFG)的分析视图。与 `Circuit` 面向用户和结构化线路构造不同,`CircuitCFG` 更偏向编译器内部使用,用于分析、重写和验证包含结构化控制流的线路。 + +--- + +## 核心概念 + +| 类型 | 说明 | +| --- | --- | +| `CircuitCFG` | 线路的控制流图视图,包含基本块、控制流边、入口块、量子比特、经典数据表、符号表、参数表和全局相位。 | +| `BasicBlock` | 基本块,保存一段顺序执行的 `Operation`,在合法 CFG 中必须恰好带有一个终结符。 | +| `Terminator` | 基本块末尾的控制转移描述,例如顺序跳转、分支、循环退出等。 | +| `FlowEdge` | 控制流图中的边类型,用于描述块与块之间的执行流关系。 | +| `ControlFlowRegion` | 结构化控制流区域元数据,用于记录某个分支或循环对应的结构化区域。 | +| `OperationMetadata` | 操作级元数据,记录操作作用的量子比特、参数和标签。 | +| `SwitchRegionCase` | `switch` 控制流区域中的 case 元数据,记录 case 值与对应入口块。 | + +在这些类型中,只有 `CircuitCFG` 可以从 `cqlib_core::circuit` 模块根导入,其余类型位于 `cqlib_core::circuit::cfg` 子模块。符号表、参数表和全局相位由 CFG 内部保存,没有公开访问接口,但会在 `to_circuit()` 时一并还原。 + +--- + +## `BasicBlock` + +`BasicBlock` 表示一段顺序执行的操作序列。 + +常用方法如下: + +| 方法 | 说明 | +| --- | --- | +| `new()` | 创建一个空基本块。 | +| `with_label(label)` | 消费自身并返回带标签的新基本块,需要链式调用或重新绑定。 | +| `push_operation(op)` | 向基本块末尾添加一条操作。 | +| `extend_operations(ops)` | 批量追加操作。 | +| `set_terminator(terminator)` | 设置基本块的终结符。 | +| `is_empty()` | 判断基本块是否既无操作也无终结符。 | +| `has_terminator()` | 判断基本块是否已经设置终结符。 | +| `len()` | 返回基本块中的操作数量。 | +| `label()` | 读取基本块标签。 | +| `terminator()` | 读取基本块终结符。 | +| `operations()` | 读取基本块中的操作切片。 | + +此外,`BasicBlock` 实现了 `Default`,其行为等价于 `BasicBlock::new()`。 + +--- + +## `Terminator` + +```rust +pub enum Terminator { + Branch(ClassicalExpr), + ForLoop { + var: ClassicalVar, + start: ClassicalExpr, + stop: ClassicalExpr, + step: ClassicalExpr, + }, + Switch(ClassicalExpr), + Jump(NodeIndex), + Break(NodeIndex), + Continue(NodeIndex), + Return, +} +``` + +| 变体 | 说明 | +| --- | --- | +| `Branch(condition)` | 条件分支,条件必须是 `Bool` 表达式,出边类型分别为 `FlowEdge::TrueBranch` 与 `FlowEdge::FalseBranch`。 | +| `ForLoop { var, start, stop, step }` | 半开区间循环,四个表达式必须是相同位宽的 `UInt`。 | +| `Switch(target)` | 多分支选择,目标必须是 `UInt` 表达式,出边数量必须等于 case 数量加一。 | +| `Jump(target)` | 无条件跳转到目标基本块,出边类型为 `FlowEdge::Unconditional`。 | +| `Break(target)` | 跳出循环或 `switch`,出边类型为 `FlowEdge::Break`。 | +| `Continue(target)` | 进入下一轮循环,出边类型为 `FlowEdge::Continue`。 | +| `Return` | 结束当前线路分支,不允许有出边。 | + +在合法 CFG 中,每个基本块都必须有且仅有一个终结符,且其出边数量与类型必须与终结符一致。由 `from_circuit()` 构造的线性线路,其入口块终结符为 `Terminator::Return`。 + +--- + +## `FlowEdge` + +| 变体 | 说明 | +| --- | --- | +| `TrueBranch` | 条件为真时经过的边。 | +| `FalseBranch` | 条件为假时经过的边。 | +| `Unconditional` | 无条件跳转边。 | +| `Case(value)` | `switch` 中匹配到 `value` 时经过的边。 | +| `DefaultCase` | `switch` 默认分支的边;即使源 `switch` 没有 default 体也会存在。 | +| `Break` | `break` 跳出边。 | +| `Continue` | `continue` 继续边。 | + +--- + +## 创建 `CircuitCFG` + +`CircuitCFG` 提供以下创建接口: + +```rust +pub fn new(num_qubits: usize) -> Self +pub fn from_qubits(qubits: Vec) -> Self +pub fn from_circuit(circuit: &Circuit) -> Result +``` + +| 接口 | 说明 | +| --- | --- | +| `CircuitCFG::new(num_qubits)` | 创建包含连续逻辑量子比特的空 CFG;此时尚未设置入口块,需要编辑后才能通过 `validate()`。 | +| `CircuitCFG::from_qubits(qubits)` | 根据指定逻辑量子比特集合创建空 CFG,适合稀疏逻辑编号;重复的量子比特会被去除,插入顺序会被保留。 | +| `CircuitCFG::from_circuit(circuit)` | 从已有结构化 `Circuit` 构造 CFG,是最常用的入口;构造结束前会在内部调用一次 `validate()`,无法结构化的控制流会直接返回错误。 | + +`CircuitCFG` 还实现了 `TryFrom<&Circuit>`,其行为等同于 `from_circuit()`;反向的 `TryFrom<&CircuitCFG> for Circuit` 等同于 `to_circuit()`。两者的错误类型都是 `CircuitError`。 + +```rust +use cqlib_core::circuit::{Circuit, CircuitCFG, Qubit}; + +let mut circuit = Circuit::new(2); +circuit.h(Qubit::new(0))?; + +let cfg = CircuitCFG::from_circuit(&circuit)?; + +assert_eq!(cfg.num_qubits(), 2); + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +--- + +## 图编辑接口 + +`CircuitCFG` 提供基本的图编辑能力,用于添加基本块、添加边、设置入口块和维护结构化控制流区域元数据。 + +| 方法 | 说明 | +| --- | --- | +| `add_block(block)` | 添加一个 `BasicBlock`,并返回对应的 `NodeIndex`。 | +| `add_edge(source, target, flow)` | 在 `source` 与 `target` 之间添加控制流边,成功时返回边索引;任一端点不存在时不修改图并返回 `None`。 | +| `entry_block()` | 读取当前 CFG 的入口基本块,未设置时返回 `None`。 | +| `set_entry_block(index)` | 设置 CFG 的入口基本块;该接口不校验索引是否有效,合法性由 `validate()` 判定。 | +| `set_control_flow_region(branch_block, region)` | 为某个分支或控制流入口块设置结构化区域元数据。 | +| `control_flow_region(branch_block)` | 读取某个基本块关联的结构化控制流区域元数据,没有记录时返回 `None`。 | +| `is_loop_header(block)` | 判断某个基本块是否为循环头;只有 `While`、`For` 区域的入口块返回 `true`,`Switch` 区域入口不算循环头。 | +| `block_mut(index)` | 可变读取指定基本块,索引不存在时返回 `None`。 | +| `outgoing_edges(source)` | 遍历指定基本块的出边,产出 `(NodeIndex, FlowEdge)` 二元组。 | + +--- + +## 查询接口 + +`CircuitCFG` 提供以下查询接口,用于遍历图结构和读取线路基础信息。 + +| 方法 | 说明 | +| --- | --- | +| `blocks()` | 遍历 CFG 中的基本块,产出 `(NodeIndex, &BasicBlock)` 二元组。 | +| `num_blocks()` | 返回基本块数量。 | +| `num_qubits()` | 返回量子比特数量。 | +| `qubits()` | 按插入顺序返回逻辑量子比特列表,返回拥有所有权的 `Vec`。 | +| `classical_vars()` | 返回经典变量类型表。 | +| `classical_values()` | 返回经典值类型表。 | + +--- + +## 验证与重建 + +`CircuitCFG` 提供两个关键接口用于检查和还原图结构: + +```rust +pub fn validate(&self) -> Result<(), CircuitError> +pub fn to_circuit(&self) -> Result +``` + +### `validate()` + +`validate()` 用于检查 CFG 结构是否自洽。典型检查内容包括: + +- 是否存在有效入口块; +- 边引用的基本块是否存在; +- 每个基本块是否都有终结符,且终结符与出边数量、出边类型是否匹配,`Return` 是否没有出边; +- 基本块内是否残留未展开的 `Instruction::ClassicalControl`; +- 是否存在不可达或未被消费的基本块,即全部基本块是否都从入口块可达; +- 控制流区域元数据是否与图结构一致; +- 循环头、循环回边和退出边是否满足约束; +- `Branch` 的条件是否为 `Bool` 表达式; +- `Switch` 的目标是否为 `UInt` 表达式,出边数量是否等于 case 数量加一; +- 经典变量和值的作用域和依赖关系是否可被正确解释; +- `break` / `continue` 终结符是否各自恰好有一条同类型出边并指向目标块。 + +其中,`break` 与 `continue` 是否出现在合法的循环或 `switch` 区域内,由 `from_circuit()` 在构造期判定;`validate()` 只检查终结符与出边的对应关系。 + +### `to_circuit()` + +`to_circuit()` 用于将 CFG 重新还原为结构化 `Circuit`。它要求 CFG 不仅是一个合法图,还必须能够对应回 Cqlib 支持的结构化控制流形式。 + +如果 CFG 的图结构已经被破坏,或某些控制流区域无法映射回结构化 `if`、`while`、`for`、`switch` 等结构,`to_circuit()` 会返回错误。该方法内部会先调用一次 `validate()`,因此上一节的约束在此处同样生效。 + +建议工作流如下: + +```rust +use cqlib_core::circuit::{Circuit, CircuitCFG, Qubit}; + +let mut circuit = Circuit::new(2); +circuit.h(Qubit::new(0))?; +circuit.cx(Qubit::new(0), Qubit::new(1))?; + +let cfg = CircuitCFG::from_circuit(&circuit)?; + +// 在这里执行 CFG 分析或转换 pass + +cfg.validate()?; +let new_circuit = cfg.to_circuit()?; + +assert_eq!(new_circuit.num_qubits(), 2); + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +--- + +## `ControlFlowRegion` 元数据 + +`ControlFlowRegion` 用于记录结构化控制流区域的边界和语义。对于 `if`、`while`、`for`、`switch` 等结构,仅有 CFG 边并不足以完整恢复原始结构化语义,还需要区域元数据描述哪些基本块属于同一个控制流结构、入口和出口在哪里、switch case 如何对应等。 + +```rust +pub enum ControlFlowRegion { + If { + then_entry: NodeIndex, + else_entry: NodeIndex, + merge_block: NodeIndex, + has_else: bool, + outer: OperationMetadata, + }, + While { + body_entry: NodeIndex, + exit_block: NodeIndex, + outer: OperationMetadata, + }, + For { + body_entry: NodeIndex, + exit_block: NodeIndex, + outer: OperationMetadata, + }, + Switch { + cases: Vec, + default_entry: NodeIndex, + merge_block: NodeIndex, + has_default: bool, + outer: OperationMetadata, + }, +} +``` + +| 变体 | 字段 | 说明 | +| --- | --- | --- | +| `If` | `then_entry`, `else_entry`, `merge_block`, `has_else`, `outer` | 条件分支区域的入口块、`else` 分支入口块、汇合块,以及源线路是否存在 `else` 分支。 | +| `While` | `body_entry`, `exit_block`, `outer` | `while` 循环体入口块与退出块。 | +| `For` | `body_entry`, `exit_block`, `outer` | `for` 循环体入口块与退出块。 | +| `Switch` | `cases`, `default_entry`, `merge_block`, `has_default`, `outer` | 各 case 的元数据、默认分支入口块、汇合块,以及源 `switch` 是否存在 default 分支。 | + +其中 `outer` 是记录该控制流结构本身的操作元数据,用于在重建时还原被展开的控制流操作。相关类型如下: + +```rust +pub struct OperationMetadata { + pub qubits: SmallVec<[Qubit; 3]>, + pub params: SmallVec<[CircuitParam; 1]>, + pub label: Option>, +} + +pub struct SwitchRegionCase { + pub value: u128, + pub entry: NodeIndex, +} +``` + +| 字段 | 说明 | +| --- | --- | +| `OperationMetadata::qubits` | 该控制流操作作用的量子比特。 | +| `OperationMetadata::params` | 该控制流操作的参数。 | +| `OperationMetadata::label` | 该控制流操作的标签。 | +| `SwitchRegionCase::value` | case 匹配的 `UInt` 取值。 | +| `SwitchRegionCase::entry` | 该 case 对应分支的入口块。 | + +这些元数据没有独立的访问接口,pass 在修改图结构时需要自行维护;否则 `to_circuit()` 重建线路时可能丢失参数或标签。 + +因此,编写 CFG pass 时需要特别注意: + +- 如果修改了分支结构,应同步更新对应的 `ControlFlowRegion`; +- 如果删除或合并了基本块,应检查区域元数据中是否仍引用旧 block; +- 如果调整循环边,应检查循环头和区域边界是否仍然正确; +- 如果修改 switch case,应同步维护 `SwitchRegionCase` 等元数据; +- 如果只是对基本块内部操作做局部优化,通常不需要修改区域元数据。 + +保持区域元数据与图边一致,是 `to_circuit()` 能否成功重建结构化线路的关键。 + +--- + +## 典型使用流程 + +### 1. 从结构化线路进入 CFG + +```rust +use cqlib_core::circuit::{Circuit, CircuitCFG, Qubit}; + +let mut circuit = Circuit::new(2); +circuit.h(Qubit::new(0))?; +circuit.cx(Qubit::new(0), Qubit::new(1))?; + +let cfg = CircuitCFG::from_circuit(&circuit)?; + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +### 2. 遍历基本块 + +```rust +use cqlib_core::circuit::{Circuit, CircuitCFG, Qubit}; + +let mut circuit = Circuit::new(2); +circuit.h(Qubit::new(0))?; + +let cfg = CircuitCFG::from_circuit(&circuit)?; + +for (node, block) in cfg.blocks() { + for _op in block.operations() { + // 分析每条 Operation + } + + if block.has_terminator() { + // 分析控制转移 + } +} + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +### 3. 修改后验证并重建 + +```rust +use cqlib_core::circuit::cfg::{BasicBlock, Terminator}; +use cqlib_core::circuit::{Circuit, CircuitCFG}; + +let mut cfg = CircuitCFG::new(1); + +let mut block = BasicBlock::new().with_label("entry"); +block.set_terminator(Terminator::Return); +let entry = cfg.add_block(block); +cfg.set_entry_block(entry); + +cfg.validate()?; +let circuit = cfg.to_circuit()?; + +assert_eq!(circuit.num_qubits(), 1); + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +--- + +## 适用的编译 pass 类型 + +`CircuitCFG` 适合用于需要显式控制流结构的分析和转换任务,例如: + +| pass 类型 | 说明 | +| --- | --- | +| 控制流可达性分析 | 检查不可达基本块、分支路径和循环结构。 | +| 分支内局部门优化 | 在每个基本块内部做门合并、门抵消或局部替换。 | +| 循环体资源估计 | 统计循环体内的门数量、深度或测量使用情况。 | +| 经典数据作用域检查 | 分析 `ClassicalValue` 是否越作用域使用。 | +| 动态线路合法性检查 | 检查控制流、测量和经典数据是否符合后端约束。 | +| 后端控制流降级 | 将结构化控制流转换为目标后端支持的形式。 | +| 分支展开或静态化 | 在条件可静态确定时,将控制流简化为线性操作。 | \ No newline at end of file diff --git a/docs/api/rust/0_circuit/13_circuit_to_matrix.md b/docs/api/cn/rust/0_circuit/13_circuit_to_matrix.md similarity index 96% rename from docs/api/rust/0_circuit/13_circuit_to_matrix.md rename to docs/api/cn/rust/0_circuit/13_circuit_to_matrix.md index 414b4dd7..1e4e070a 100644 --- a/docs/api/rust/0_circuit/13_circuit_to_matrix.md +++ b/docs/api/cn/rust/0_circuit/13_circuit_to_matrix.md @@ -103,9 +103,14 @@ let matrix = circuit_to_matrix(&bound, None)?; ## 符号矩阵转换 ```rust +use cqlib_core::circuit::{Circuit, Parameter, Qubit}; use cqlib_core::circuit::symbolic_matrix::circuit_to_symbolic_matrix; +let mut circuit = Circuit::new(1); +circuit.rx(Qubit::new(0), Parameter::symbol("theta"))?; + let symbolic = circuit_to_symbolic_matrix(&circuit, None)?; + # Ok::<(), cqlib_core::circuit::CircuitError>(()) ``` diff --git a/docs/api/cn/rust/0_circuit/14_circuit_dag.md b/docs/api/cn/rust/0_circuit/14_circuit_dag.md new file mode 100644 index 00000000..01f31ee8 --- /dev/null +++ b/docs/api/cn/rust/0_circuit/14_circuit_dag.md @@ -0,0 +1,741 @@ +# 线路 DAG + +`cqlib_core::circuit` + +```rust +use cqlib_core::circuit::{CircuitDag, DagControlFlow, DagNode, DagSwitchCase, DagWire}; +``` + +`CircuitDag` 是线路在操作依赖层面的算法视图:节点表示一条操作,边表示两条操作因共享同一份资源而存在的先后顺序。可共享的资源包括量子比特、可变经典变量、不可变经典值,以及用于承载没有具体数据资源之操作的全局序。该视图不改变线路本身的存储结构,主要用于依赖分析、分层调度和子图替换。 + +本页覆盖 `CircuitDag`、`DagWire`、`DagNode`、`DagControlFlow` 与 `DagSwitchCase`。 + +--- + +## 核心概念 + +| 概念 | 说明 | +| --- | --- | +| `CircuitDag` | 线路的操作级依赖图,操作节点之外每条线还带输入与输出哨兵节点。 | +| `DagWire` | 边承载的资源,取值为量子比特、经典变量、经典值或全局序。 | +| `DagNode` | 节点种类,分为输入哨兵、输出哨兵与操作三类。 | +| `DagControlFlow` | 结构化控制流操作携带的递归 DAG 载荷。 | +| `DagSwitchCase` | `switch` 中的单个精确整数匹配分支。 | + +--- + +## CircuitDag + +### 构造 + +```rust +pub fn from_circuit(circuit: &Circuit) -> Result +``` + +`CircuitDag::from_circuit()` 从一条已有线路构造依赖图,同时复制线路的量子比特、参数表、符号名、经典变量与经典值类型表以及全局相位。构造结束后会自动调用 `validate()`。 + +```rust +use cqlib_core::circuit::{Circuit, CircuitDag, Qubit}; + +let mut circuit = Circuit::new(2); +circuit.h(Qubit::new(0))?; +circuit.cx(Qubit::new(0), Qubit::new(1))?; + +let dag = CircuitDag::from_circuit(&circuit)?; + +assert_eq!(dag.num_qubits(), 2); +assert_eq!(dag.num_ops(), 2); +assert_eq!(dag.depth()?, 2); + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +```rust +pub fn from_operations( + qubits: impl IntoIterator, + operations: &[Operation], +) -> Result +``` + +`CircuitDag::from_operations()` 从一段自包含的操作切片构造依赖图,是该视图面向 pass 局部分析的窄入口。它不接受参数表、经典声明与全局相位:每条操作都必须恰好对给定的量子比特集合有效,并且不依赖任何线路级元数据。当操作流中包含索引参数、经典变量、经典值或控制流体时,应改用 `from_circuit()`;有意忽略这些资源的分析 pass 应先将其归一化,例如解析参数、或把非量子操作替换为依赖屏障。 + +`qubits` 中出现重复量子比特、操作引用了集合之外的量子比特、或操作切片并非自包含时返回错误。 + +```rust +use cqlib_core::circuit::{Circuit, CircuitDag, Qubit}; + +let mut circuit = Circuit::new(2); +circuit.h(Qubit::new(0))?; +circuit.cx(Qubit::new(0), Qubit::new(1))?; + +let dag = CircuitDag::from_operations(circuit.qubits(), circuit.operations())?; + +assert_eq!(dag.num_ops(), 2); +assert_eq!(dag.parameters().len(), 0); + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +### 导出回线路 + +```rust +pub fn to_circuit(&self) -> Result +``` + +`to_circuit()` 按 DAG 的确定性拓扑序重建线路,并恢复量子比特集合、经典类型表与全局相位。图中含环、引用了非法参数索引、或重建出的操作序列被 `Circuit::from_operations()` 拒绝时返回错误。 + +```rust +use cqlib_core::circuit::{Circuit, CircuitDag, ParameterValue, Qubit}; + +let mut circuit = Circuit::new(1); +circuit.rx(Qubit::new(0), ParameterValue::from("theta"))?; + +let dag = CircuitDag::from_circuit(&circuit)?; +let recovered = dag.to_circuit()?; + +assert_eq!(recovered.operations().len(), 1); +assert!(dag.symbols().contains("theta")); + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +### 校验 + +```rust +pub fn validate(&self) -> Result<(), CircuitError> +``` + +`validate()` 检查图与元数据的一致性,包括:图是否含环、每条线的输入输出哨兵是否合法、操作节点序号是否唯一且与顺序索引一致、操作引用的量子比特与参数索引是否有效、经典变量与经典值是否属于当前线路、控制流载荷是否与所属操作匹配、边的资源是否合法。 + +从外部导入、自动生成或手动修改过的 DAG,在进入后续阶段前应显式校验: + +```rust +use cqlib_core::circuit::{Circuit, CircuitDag, Qubit}; + +let mut circuit = Circuit::new(2); +circuit.h(Qubit::new(0))?; +circuit.cx(Qubit::new(0), Qubit::new(1))?; +circuit.measure(Qubit::new(1))?; + +let dag = CircuitDag::from_circuit(&circuit)?; +dag.validate()?; + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +### 类型转换 + +DAG 与线路之间还提供 `TryFrom` 转换,语义与 `from_circuit()`、`to_circuit()` 一致。 + +| 实现 | 说明 | +| --- | --- | +| `impl TryFrom<&Circuit> for CircuitDag` | 等价于 `CircuitDag::from_circuit()`。 | +| `impl TryFrom<&CircuitDag> for Circuit` | 等价于 `CircuitDag::to_circuit()`。 | + +```rust +use cqlib_core::circuit::{Circuit, CircuitDag, Qubit}; + +let mut circuit = Circuit::new(2); +circuit.h(Qubit::new(0))?; +circuit.cx(Qubit::new(0), Qubit::new(1))?; + +let dag = CircuitDag::try_from(&circuit)?; +let recovered = Circuit::try_from(&dag)?; +assert_eq!(recovered, circuit); + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +### 元数据与统计 + +| 方法 | 返回 | 说明 | +| --- | --- | --- | +| `num_qubits()` | `usize` | 量子比特数量。 | +| `num_ops()` | `usize` | 操作节点数量。 | +| `is_empty()` | `bool` | 是否没有操作节点。 | +| `qubits()` | `Vec` | 按插入顺序返回量子比特。 | +| `parameters()` | `&IndexSet` | 参数表。 | +| `symbols()` | `&IndexSet` | 自由符号名集合。 | +| `classical_vars()` | `&[ClassicalType]` | 可变经典变量类型表。 | +| `classical_values()` | `&[ClassicalType]` | 不可变经典值类型表。 | +| `add_parameter(param)` | `(usize, bool)` | 将参数驻留进参数表,返回表内索引与是否新插入。 | + +`add_parameter()` 的行为与 `Circuit::add_parameter()` 一致:参数不存在时插入,并登记其全部符号名。 + +```rust +use cqlib_core::circuit::{Circuit, ClassicalType, CircuitDag, Qubit}; + +let mut circuit = Circuit::new(1); +let _flag = circuit.var(ClassicalType::Bool); +let _value = circuit.measure(Qubit::new(0))?; + +let dag = CircuitDag::from_circuit(&circuit)?; + +assert_eq!(dag.classical_vars(), circuit.classical_vars()); +assert_eq!(dag.classical_values(), circuit.classical_values()); + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +### 属性 + +- 实现 `Debug` 与 `Clone`。 + +--- + +## DagWire + +```rust +pub enum DagWire { + Qubit(Qubit), + ClassicalVar(ClassicalVar), + ClassicalValue(ClassicalValue), + GlobalOrder, +} +``` + +`DagWire` 是边承载的资源。它同时也是线的身份:每条线对应图中一个输入哨兵节点和一个输出哨兵节点。 + +| 变体 | 说明 | +| --- | --- | +| `DagWire::Qubit(Qubit)` | 量子比特线。 | +| `DagWire::ClassicalVar(ClassicalVar)` | 可变经典变量线,由 `store` 写入。 | +| `DagWire::ClassicalValue(ClassicalValue)` | 不可变经典值线,通常由测量产生。 | +| `DagWire::GlobalOrder` | 全局序,用于承载没有具体数据资源的操作之间的稳定顺序。 | + +`DagWire` 实现 `Debug`、`Clone`、`Copy`、`PartialEq`、`Eq` 与 `Hash`。 + +### 线查询 + +| 方法 | 返回 | 说明 | +| --- | --- | --- | +| `wires()` | `impl Iterator` | 遍历当前 DAG 中已物化的全部线。 | +| `has_wire(wire)` | `bool` | 判断某条线是否已在当前 DAG 中物化。 | +| `is_wire_idle(wire)` | `Result` | 判断某条线上是否没有任何操作。 | +| `wire_in(wire)` | `Option` | 返回该线的输入哨兵节点。 | +| `wire_out(wire)` | `Option` | 返回该线的输出哨兵节点。 | +| `nodes_on_wire(wire)` | `Result, CircuitError>` | 按线上顺序返回该线上的操作节点。 | + +线不是当前 DAG 的合法资源、或线上时间线断裂或分叉时,`is_wire_idle()` 与 `nodes_on_wire()` 返回错误。 + +```rust +use cqlib_core::circuit::{Circuit, CircuitDag, DagWire, Qubit}; + +let mut circuit = Circuit::new(1); +circuit.h(Qubit::new(0))?; +circuit.x(Qubit::new(0))?; +circuit.z(Qubit::new(0))?; + +let dag = CircuitDag::from_circuit(&circuit)?; +let nodes = dag.nodes_on_wire(DagWire::Qubit(Qubit::new(0)))?; + +assert_eq!(nodes.len(), 3); +assert_eq!(dag.num_ops(), 3); + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +量子比特线在构造时全部物化,而经典变量线与经典值线只在被操作引用时才物化。下面这条未写入的经典变量线既不在 `wires()` 中,也不参与任何依赖;`DagWire::GlobalOrder` 则始终存在,用于承载不涉及任何具体数据资源的操作。 + +```rust +use cqlib_core::circuit::{Circuit, CircuitDag, ClassicalType, DagWire, Qubit}; + +let mut circuit = Circuit::new(2); +let unused_var = circuit.var(ClassicalType::Bool); +circuit.h(Qubit::new(1))?; + +let dag = CircuitDag::from_circuit(&circuit)?; + +assert!(dag.has_wire(DagWire::Qubit(Qubit::new(0)))); +assert!(!dag.has_wire(DagWire::ClassicalVar(unused_var))); +assert!(dag.is_wire_idle(DagWire::Qubit(Qubit::new(0)))?); + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +--- + +## DagNode + +```rust +pub enum DagNode { + WireIn(DagWire), + WireOut(DagWire), + Operation { + operation: Operation, + order: usize, + }, +} +``` + +`DagNode` 是图中的节点载荷。每个操作节点保存存储层操作与一个源顺序位置,该位置用于确定性遍历。 + +| 变体 | 说明 | +| --- | --- | +| `DagNode::WireIn(DagWire)` | 某条线的输入哨兵。 | +| `DagNode::WireOut(DagWire)` | 某条线的输出哨兵。 | +| `DagNode::Operation { operation, order }` | 一条操作,`order` 为其源顺序位置。 | + +节点种类可以用 `node_kind()` 查询,它返回 `"wire_in"`、`"wire_out"` 或 `"operation"`,节点不存在时返回 `None`。 + +--- + +## 节点标识 + +节点标识类型为 `NodeIndex`,由 `rustworkx_core::petgraph::prelude::NodeIndex` 提供,可按 `node.index()` 取回其 `usize` 值。节点标识由以下入口取得: + +| 取得方式 | 说明 | +| --- | --- | +| `op_nodes()` | 按源顺序遍历全部操作节点。 | +| `topological_op_nodes()` | 按拓扑序返回全部操作节点。 | +| `front_layer()`、`layers()`、`node_layers()` | 分层结果中的节点。 | +| `wire_in()` / `wire_out()` | 某条线的输入与输出哨兵节点。 | +| `apply_operation_back()` 等变更方法 | 返回新节点标识。 | + +| 方法 | 返回 | 说明 | +| --- | --- | --- | +| `operation(node)` | `Option<&Operation>` | 返回节点对应的存储层操作;节点不是操作节点时为 `None`。 | +| `is_operation(node)` | `bool` | 判断节点是否为操作节点。 | +| `node_kind(node)` | `Option<&'static str>` | 返回节点种类。 | +| `control_flow(node)` | `Option<&DagControlFlow>` | 返回该节点携带的控制流载荷,没有时为 `None`。 | +| `op_nodes()` | `impl Iterator` | 按源顺序遍历操作节点。 | +| `topological_op_nodes()` | `Result, CircuitError>` | 按确定性拓扑序返回操作节点。 | + +```rust +use cqlib_core::circuit::{Circuit, CircuitDag, DagWire, Instruction, Qubit, StandardGate}; + +let mut circuit = Circuit::new(1); +circuit.h(Qubit::new(0))?; + +let dag = CircuitDag::from_circuit(&circuit)?; +let node = dag.topological_op_nodes()?[0]; +let operation = dag.operation(node).expect("operation node"); + +assert!(matches!( + operation.instruction, + Instruction::Standard(StandardGate::H) +)); +assert_eq!(operation.qubits[0], Qubit::new(0)); + +let wire_in = dag.wire_in(DagWire::Qubit(Qubit::new(0))).expect("qubit wire"); +assert!(dag.operation(wire_in).is_none()); + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +--- + +## 图遍历 + +前驱与后继的查询分三个层次:不限资源的全部邻居、限定在某一条线上的邻居、以及按量子或经典资源分类的邻居。 + +| 方法 | 返回 | 说明 | +| --- | --- | --- | +| `predecessors(node)` | `impl Iterator` | 遍历全部前驱节点,可能包含哨兵节点。 | +| `successors(node)` | `impl Iterator` | 遍历全部后继节点,可能包含哨兵节点。 | +| `predecessors_on_wire(node, wire)` | `Result, CircuitError>` | 仅通过指定线相连的操作前驱。 | +| `successors_on_wire(node, wire)` | `Result, CircuitError>` | 仅通过指定线相连的操作后继。 | +| `quantum_predecessors(node)` | `Result, CircuitError>` | 通过量子比特线相连的操作前驱。 | +| `quantum_successors(node)` | `Result, CircuitError>` | 通过量子比特线相连的操作后继。 | +| `classical_predecessors(node)` | `Result, CircuitError>` | 通过经典变量线或经典值线相连的操作前驱。 | +| `classical_successors(node)` | `Result, CircuitError>` | 通过经典变量线或经典值线相连的操作后继。 | + +`predecessors()` 与 `successors()` 返回的邻居可能包含线的哨兵节点,需要过滤时可以用 `operation(node).is_some()` 判断。其余方法只返回操作节点,结果按拓扑序排列。节点标识不存在、或线不是当前 DAG 的合法资源时返回错误。 + +```rust +use cqlib_core::circuit::{Circuit, CircuitDag, DagWire, Qubit}; + +let mut circuit = Circuit::new(2); +circuit.h(Qubit::new(0))?; +circuit.x(Qubit::new(1))?; +circuit.cx(Qubit::new(0), Qubit::new(1))?; +circuit.z(Qubit::new(0))?; +circuit.h(Qubit::new(1))?; + +let dag = CircuitDag::from_circuit(&circuit)?; +let nodes = dag.topological_op_nodes()?; +let (h0, x1, cx, z0, h1) = (nodes[0], nodes[1], nodes[2], nodes[3], nodes[4]); + +assert_eq!( + dag.predecessors_on_wire(cx, DagWire::Qubit(Qubit::new(0)))?, + vec![h0] +); +assert_eq!( + dag.successors_on_wire(cx, DagWire::Qubit(Qubit::new(1)))?, + vec![h1] +); +assert_eq!(dag.quantum_predecessors(cx)?, vec![h0, x1]); +assert_eq!(dag.quantum_successors(cx)?, vec![z0, h1]); + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +经典资源上的依赖使用同一组接口: + +```rust +use cqlib_core::circuit::{Circuit, CircuitDag, ClassicalExpr, ClassicalType, Qubit}; + +let mut circuit = Circuit::new(1); +let measured = circuit.measure(Qubit::new(0))?; +let dest = circuit.var(ClassicalType::Bool); +circuit.store(dest, ClassicalExpr::bit_to_bool(measured.expr())?)?; + +let dag = CircuitDag::from_circuit(&circuit)?; +let nodes = dag.topological_op_nodes()?; +let (measure, store) = (nodes[0], nodes[1]); + +assert_eq!(dag.classical_predecessors(store)?, vec![measure]); +assert!(dag.quantum_predecessors(store)?.is_empty()); + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +--- + +## 分层与深度 + +| 方法 | 返回 | 说明 | +| --- | --- | --- | +| `front_layer()` | `Result, CircuitError>` | 没有操作前驱的操作节点,即最早可执行的一层。 | +| `layers()` | `Result>, CircuitError>` | 按尽早调度方式分层的操作节点。 | +| `node_layers()` | `Result, CircuitError>` | 每个操作节点到所在层号的映射。 | +| `depth()` | `Result` | 依赖深度,即层数。 | + +`layers()` 与 `node_layers()` 使用同一套层号:某一层的层号为其全部操作前驱层号的最大值加一。`depth()` 等于最大层号加一,空图为 `0`。图中含环,或操作前驱的层号与拓扑序不一致时返回错误。 + +```rust +use cqlib_core::circuit::{Circuit, CircuitDag, Qubit}; + +let mut circuit = Circuit::new(2); +circuit.h(Qubit::new(0))?; +circuit.x(Qubit::new(1))?; +circuit.cx(Qubit::new(0), Qubit::new(1))?; + +let dag = CircuitDag::from_circuit(&circuit)?; +let layers = dag.layers()?; + +assert_eq!(layers.len(), 2); +assert_eq!(layers[0].len(), 2); +assert_eq!(layers[1].len(), 1); +assert_eq!(dag.front_layer()?.len(), 2); +assert_eq!(dag.depth()?, 2); + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +`node_layers()` 返回的映射与 `layers()` 一致,可用于按节点反查层号。 + +```rust +use cqlib_core::circuit::{Circuit, CircuitDag, Qubit}; + +let mut circuit = Circuit::new(2); +circuit.h(Qubit::new(0))?; +circuit.x(Qubit::new(1))?; +circuit.cx(Qubit::new(0), Qubit::new(1))?; + +let dag = CircuitDag::from_circuit(&circuit)?; +let node_layers = dag.node_layers()?; + +assert_eq!(node_layers.len(), dag.num_ops()); +for (layer_index, layer) in dag.layers()?.iter().enumerate() { + for node in layer { + assert_eq!(node_layers.get(node).copied(), Some(layer_index)); + } +} + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +--- + +## 控制流与测量 + +当线路中包含结构化控制流时,控制流操作节点会额外携带一份递归的 DAG 载荷。控制流本身的构造接口与低层 IR 见 [Classical Data / Control Flow](9_classical_control_flow.md)。 + +| 方法 | 返回 | 说明 | +| --- | --- | --- | +| `has_control_flow()` | `bool` | 顶层操作中是否存在结构化控制流。 | +| `has_nested_control_flow()` | `bool` | 控制流体内部是否还包含结构化控制流。 | +| `has_measurement()` | `bool` | 顶层操作中是否直接或递归包含测量。 | +| `control_flow(node)` | `Option<&DagControlFlow>` | 返回该节点携带的控制流载荷。 | + +`has_control_flow()` 与 `has_measurement()` 都只遍历顶层操作节点,区别在于 `has_measurement()` 会对每条操作内部递归判断,因此控制流体内的测量同样会被计入。`has_nested_control_flow()` 则深入控制流体,判断其中是否还有控制流。 + +### DagControlFlow + +```rust +pub enum DagControlFlow { + If { + then_body: Box, + else_body: Option>, + }, + While { body: Box }, + For { body: Box }, + Switch { + cases: Vec, + default: Option>, + }, + Break, + Continue, +} +``` + +| 变体 | 说明 | +| --- | --- | +| `If { then_body, else_body }` | `if` 结构,`else_body` 在缺少该分支时为 `None`。 | +| `While { body }` | `while` 循环。 | +| `For { body }` | 基于 `UInt` 变量的半开区间循环。 | +| `Switch { cases, default }` | `switch` 多分支选择。 | +| `Break` | 跳出跳转,没有子图。 | +| `Continue` | 继续跳转,没有子图。 | + +各分支体本身就是 `CircuitDag`,可以继续调用 `num_ops()`、`topological_op_nodes()`、`control_flow()` 等接口递归分析。 + +```rust +use cqlib_core::circuit::{Circuit, CircuitDag, ClassicalExpr, DagControlFlow, Qubit}; + +let mut circuit = Circuit::new(2); +let measured = circuit.measure(Qubit::new(0))?; +let condition = ClassicalExpr::bit_to_bool(measured.expr())?; +circuit.if_(condition, |body| { + body.x(Qubit::new(1))?; + Ok(()) +})?; + +let dag = CircuitDag::from_circuit(&circuit)?; +let control_node = dag.topological_op_nodes()?[1]; + +match dag.control_flow(control_node).expect("if payload") { + DagControlFlow::If { + then_body, + else_body, + } => { + assert_eq!(then_body.num_ops(), 1); + assert!(else_body.is_none()); + } + _ => panic!("expected if control-flow payload"), +} + +assert!(dag.has_control_flow()); +assert!(!dag.has_nested_control_flow()); + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +循环体的取法一致。`For` 变体只保存循环体,不保存循环变量、起止与步长,这些信息保留在所属操作的 `ClassicalControlOp::For` 中。 + +```rust +use cqlib_core::circuit::{Circuit, CircuitDag, ClassicalExpr, ClassicalType, DagControlFlow, Qubit}; + +let mut circuit = Circuit::new(1); +let counter = circuit.var(ClassicalType::uint(8).expect("valid width")); +circuit.for_uint( + counter, + ClassicalExpr::uint_literal(8, 0)?, + ClassicalExpr::uint_literal(8, 3)?, + ClassicalExpr::uint_literal(8, 1)?, + |body, _| { + body.h(Qubit::new(0))?; + Ok(()) + }, +)?; + +let dag = CircuitDag::from_circuit(&circuit)?; +let control_node = dag + .topological_op_nodes()? + .into_iter() + .find(|node| dag.control_flow(*node).is_some()) + .expect("control-flow node"); + +match dag.control_flow(control_node).expect("for payload") { + DagControlFlow::For { body } => { + assert_eq!(body.num_ops(), 1); + } + _ => panic!("expected for control-flow payload"), +} + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +### DagSwitchCase + +```rust +pub struct DagSwitchCase { + pub value: u128, + pub body: Box, +} +``` + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| `value` | `u128` | 该分支匹配的整数值。 | +| `body` | `Box` | 命中该分支时执行的控制流体。 | + +```rust +use cqlib_core::circuit::{Circuit, ClassicalType, CircuitDag, DagControlFlow, Qubit}; + +let mut circuit = Circuit::new(1); +let state = circuit.var(ClassicalType::uint(2).expect("valid width")); +circuit.switch(state.expr(), |cases| { + cases.value(0, |body| { + body.x(Qubit::new(0))?; + Ok(()) + })?; + cases.value(1, |body| { + body.z(Qubit::new(0))?; + Ok(()) + })?; + cases.default(|body| { + body.h(Qubit::new(0))?; + Ok(()) + })?; + Ok(()) +})?; + +let dag = CircuitDag::from_circuit(&circuit)?; +let control_node = dag + .topological_op_nodes()? + .into_iter() + .find(|node| dag.control_flow(*node).is_some()) + .expect("switch node"); + +match dag.control_flow(control_node).expect("switch payload") { + DagControlFlow::Switch { cases, default } => { + assert_eq!(cases.len(), 2); + assert_eq!(cases[0].value, 0); + assert_eq!(cases[0].body.num_ops(), 1); + assert!(default.is_some()); + } + _ => panic!("expected switch control-flow payload"), +} + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +--- + +## 操作计数与连续段 + +| 方法 | 返回 | 说明 | +| --- | --- | --- | +| `operation_count_by_name()` | `IndexMap` | 按指令名统计顶层操作数量。 | +| `operation_count_by_name_recursive()` | `IndexMap` | 按指令名统计顶层与各控制流体内部的操作数量。 | +| `collect_runs_on_wire(wire, predicate)` | `Result>, CircuitError>` | 在指定线上收集由 `predicate` 接受的连续操作段。 | +| `collect_1q_runs()` | `Result>, CircuitError>` | 在每条量子比特线上收集连续的单量子比特门段。 | +| `collect_2q_runs()` | `Result>, CircuitError>` | 在每条量子比特线上收集连续的双量子比特门段。 | + +计数结果保持插入顺序,键为指令名。连续段以源码顺序在线上收集:`predicate` 返回 `false` 的操作会打断当前段。 + +`collect_runs_on_wire()` 面向具体的量子比特线与经典线;`DagWire::GlobalOrder` 虽然也被接受,但它只对没有具体数据资源的操作排序,通常不适合作为优化段的划分依据。 + +`collect_1q_runs()` 与 `collect_2q_runs()` 的内部判据是:操作作用于一个或两个量子比特,且指令为标准门、多控制门、自定义酉门或复合门。非门操作、测量、复位、屏障和多量子比特操作会中断段。同一个双量子比特操作可能同时出现在其两个量子比特线的段中。 + +```rust +use cqlib_core::circuit::{Circuit, CircuitDag, DagWire, Instruction, Qubit}; + +let mut circuit = Circuit::new(2); +circuit.h(Qubit::new(0))?; +circuit.x(Qubit::new(0))?; +circuit.cx(Qubit::new(0), Qubit::new(1))?; +circuit.h(Qubit::new(0))?; +circuit.reset(Qubit::new(0))?; +circuit.z(Qubit::new(0))?; +circuit.x(Qubit::new(1))?; + +let dag = CircuitDag::from_circuit(&circuit)?; +let q0_runs = dag.collect_runs_on_wire(DagWire::Qubit(Qubit::new(0)), |operation| { + matches!(operation.instruction, Instruction::Standard(_)) && operation.qubits.len() == 1 +})?; + +assert_eq!( + q0_runs.iter().map(Vec::len).collect::>(), + vec![2, 1, 1] +); + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +--- + +## 变更图 + +| 方法 | 返回 | 说明 | +| --- | --- | --- | +| `apply_operation_back(operation)` | `Result` | 在 DAG 后端追加一条存储层操作。 | +| `apply_value_operation_back(operation)` | `Result` | 先降级再在 DAG 后端追加一条构造层操作。 | +| `apply_operation_front(operation)` | `Result` | 在 DAG 前端追加一条存储层操作。 | +| `apply_value_operation_front(operation)` | `Result` | 先降级再在 DAG 前端追加一条构造层操作。 | +| `remove_op_node(node)` | `Result` | 删除一个操作节点,返回被删除的操作。 | +| `substitute_node(node, operation)` | `Result<(), CircuitError>` | 用一条存储层操作替换指定节点。 | +| `substitute_value_node(node, operation)` | `Result<(), CircuitError>` | 先降级再用一条构造层操作替换指定节点。 | +| `substitute_node_with_dag(node, replacement)` | `Result<(), CircuitError>` | 用另一个 `CircuitDag` 替换指定节点。 | + +`apply_value_operation_*` 与 `substitute_value_node()` 会把 `ValueOperation` 中的符号参数驻留进 DAG 参数表,因此调用后 `symbols()` 与 `parameters()` 也会随之更新。降级过程中若出现非有限数值参数或参数索引非法,则返回错误。 + +```rust +use cqlib_core::circuit::{Circuit, CircuitDag, Parameter, ParameterValue, Qubit, StandardGate, ValueOperation}; + +let mut circuit = Circuit::new(1); +circuit.h(Qubit::new(0))?; +let mut dag = CircuitDag::from_circuit(&circuit)?; +let theta = Parameter::symbol("theta"); + +let rx = ValueOperation::from_standard( + StandardGate::RX, + [Qubit::new(0)], + [ParameterValue::Param(theta.clone())], +); + +let front = dag.apply_value_operation_front(rx)?; + +assert!(dag.symbols().contains("theta")); +assert_eq!(dag.num_ops(), 2); + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +`substitute_node_with_dag()` 会先校验替换 DAG,再检查替换后读取或写入的资源是否落在被替换节点的资源范围内。替换控制流节点时,替换内容必须只包含一条同类型的控制流操作。任一校验不通过则返回错误。 + +```rust +use cqlib_core::circuit::{Circuit, CircuitDag, Qubit}; + +let mut circuit = Circuit::new(1); +circuit.x(Qubit::new(0))?; +let mut dag = CircuitDag::from_circuit(&circuit)?; + +let mut replacement = Circuit::new(1); +replacement.h(Qubit::new(0))?; +let replacement_dag = CircuitDag::from_circuit(&replacement)?; + +let node = dag.topological_op_nodes()?[0]; +dag.substitute_node_with_dag(node, replacement_dag)?; + +assert_eq!(dag.num_ops(), 1); + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +--- + +## 典型用途 + +编译 pass 与路由算法通常在 DAG 上完成依赖分析:用 `front_layer()` 与 `node_layers()` 决定哪些操作已经就绪,用 `predecessors()` 与 `successors()` 判断两条操作之间是否存在顺序约束,用 `collect_1q_runs()` 这类连续段收集接口定位可以整体处理的门序列,再用 `substitute_node()` 系列接口把变换结果写回图中。 + +`from_operations()` 适合 pass 内部的局部视图:当一段操作已经解析完参数、并且只涉及量子资源时,可以用它构造一个不携带线路级元数据的轻量依赖图。 + +--- + +## 错误处理 + +`CircuitDag` 的多数接口返回 `Result<_, CircuitError>`。常见错误来源如下: + +| 错误 | 触发场景 | +| --- | --- | +| `CircuitError::InvalidDag` | 图含环、操作节点序号重复或与顺序索引不一致、线的哨兵非法、线的时间线断裂或分叉、节点标识不存在、控制流载荷与所属操作不匹配、替换 DAG 的资源范围或控制流形态非法。 | +| `CircuitError::QubitNotFound` | 操作引用了不在 DAG 量子比特集合中的量子比特。 | +| `CircuitError::DuplicateQubits` | `from_operations()` 的 `qubits` 中含有重复量子比特。 | +| `CircuitError::ForeignClassicalHandle` | 操作或线引用了属于其他线路的经典变量或经典值。 | +| `CircuitError::InvalidParameterIndex` | 操作引用了超出参数表范围的参数索引,或全局相位参数无法解析。 | +| `CircuitError::InvalidParameterValue` | 降级构造层操作时出现非有限数值参数。 | +| `CircuitError::InvalidOperation` | 构造层指令中嵌套了不合法的控制流表示。 | +| `CircuitError::DuplicateClassicalValueDefinition` | 同一个不可变经典值出现了多个产生者。 | diff --git a/docs/api/rust/0_circuit/1_circuit.md b/docs/api/cn/rust/0_circuit/1_circuit.md similarity index 94% rename from docs/api/rust/0_circuit/1_circuit.md rename to docs/api/cn/rust/0_circuit/1_circuit.md index 6d469c69..ef33ea2b 100644 --- a/docs/api/rust/0_circuit/1_circuit.md +++ b/docs/api/cn/rust/0_circuit/1_circuit.md @@ -73,6 +73,8 @@ pub fn from_operations( | `operations()` | `&[Operation]` | 返回内部存储层操作序列。 | | `parameters()` | `&IndexSet` | 返回已驻留的参数表达式集合。 | | `symbols()` | `&IndexSet` | 返回线路中出现的自由符号名集合。 | +| `used_symbols()` | `IndexSet` | 返回可执行 IR 实际引用的符号名集合。 | +| `uses_symbol(symbol)` | `bool` | 判断指定符号是否被可执行 IR 引用。 | | `classical_vars()` | `&[ClassicalType]` | 返回已分配的可变经典变量类型表。 | | `classical_values()` | `&[ClassicalType]` | 返回不可变经典值类型表,通常由测量产生。 | | `global_phase()` | `Parameter` | 返回线路全局相位的参数表达式。 | @@ -240,9 +242,9 @@ c.rzz(q0, q1, theta)?; | 方法 | 说明 | | --- | --- | | `barrier(qubits)` | 插入 barrier,用于约束编译器或调度器不要跨该边界重排相关量子比特上的操作。 | -| `reset(qubit)` | 将量子比特复位到 `|0>`。 | -| `delay(qubit, duration)` | 在指定量子比特上插入空闲时间。 | -| `multi_control(controls, targets, gate, params)` | 构造多控制标准门。 | +| `reset(qubit)` | 将量子比特复位到 `\|0>`。 | +| `delay(qubit, delay)` | 在指定量子比特上插入空闲时间。 | +| `multi_control(instruction, controls, targets, params)` | 构造多控制标准门。 | | `unitary(gate, qubits)` | 追加无额外位置参数的 `UnitaryGate`。 | | `unitary_with_params(gate, qubits, params)` | 追加带位置参数的 `UnitaryGate`。 | | `circuit_gate(gate, qubits, params)` | 追加 `CircuitGate`。 | @@ -277,6 +279,8 @@ c.rzz(q0, q1, theta)?; | 方法 | 说明 | | --- | --- | | `depth(recurse)` | 计算线路深度。若线路包含控制流且 `recurse = false`,通常返回 `ControlFlowPresent`。 | +| `remove_operation(index)` | 删除单条顶层操作并返回被删除的操作。 | +| `remove_operations(indices)` | 删除多条顶层操作,按原始下标升序返回被删除的操作。 | | `validate()` | 验证经典句柄、控制流作用域、数据依赖和线路结构不变量。 | | `inverse()` | 返回当前线路的反线路。若包含不可逆操作则返回错误。 | | `decompose()` | 展开由线路定义的复合门。 | diff --git a/docs/api/rust/0_circuit/2_qubit.md b/docs/api/cn/rust/0_circuit/2_qubit.md similarity index 88% rename from docs/api/rust/0_circuit/2_qubit.md rename to docs/api/cn/rust/0_circuit/2_qubit.md index 435491bf..c123d2d5 100644 --- a/docs/api/rust/0_circuit/2_qubit.md +++ b/docs/api/cn/rust/0_circuit/2_qubit.md @@ -63,7 +63,7 @@ assert_eq!(b, Qubit::new(10)); assert_eq!(c, Qubit::new(20)); ``` -对于可能为负数或可能超出 `u32` 范围的整数类型,应使用 `TryFrom`。 +对于可能为负数或可能超出 `u32` 范围的整数类型,应使用 `TryFrom`:有符号类型 `i8`、`i16`、`i32`、`isize`,以及无符号类型 `usize`。其中 `usize` 与 `isize` 在 64 位系统上可以表示超出 `u32` 的值,转换失败时不会被静默截断,而是返回错误。 ```rust use cqlib_core::circuit::{Qubit, QubitError}; @@ -74,6 +74,11 @@ assert!(matches!( Qubit::try_from(-1i32), Err(QubitError::NegativeIndex(-1)) )); + +assert!(matches!( + Qubit::try_from(usize::MAX), + Err(QubitError::IndexTooLarge(_)) +)); ``` 常见转换错误如下: @@ -113,7 +118,7 @@ assert_eq!(ordered, vec![Qubit::new(0), Qubit::new(1), Qubit::new(2)]); ## 逻辑编号与存储位置 -`Qubit` 的编号是逻辑标识。在线路中,量子比特的实际顺序由 `Circuit` 保存的量子比特列表决定;在矩阵或状态向量中,轴顺序也由相关转换接口的量子比特顺序约定决定。 +`Qubit` 的编号是逻辑标识。在线路中,量子比特的实际顺序由 `Circuit` 保存的量子比特列表决定;在矩阵或态矢量中,轴顺序也由相关转换接口的量子比特顺序约定决定。 例如,下面的线路只包含两个量子比特,但它们的逻辑编号分别是 `10` 和 `20`: diff --git a/docs/api/rust/0_circuit/3_parameter.md b/docs/api/cn/rust/0_circuit/3_parameter.md similarity index 77% rename from docs/api/rust/0_circuit/3_parameter.md rename to docs/api/cn/rust/0_circuit/3_parameter.md index 335060ad..910abbf4 100644 --- a/docs/api/rust/0_circuit/3_parameter.md +++ b/docs/api/cn/rust/0_circuit/3_parameter.md @@ -20,8 +20,13 @@ use cqlib_core::circuit::Parameter; | `Parameter::pi()` | 创建数学常量 `π`。 | | `Parameter::e()` | 创建自然常数 `e`。 | | `Parameter::new(expr)` | 从底层表达式对象创建参数,通常用于内部或高级场景。 | +| `as_expr()` | 读取底层表达式对象的引用。 | +| `into_expr()` | 消耗参数并取出底层表达式对象。 | | `str::parse::()` | 从字符串解析参数表达式。 | | `Parameter::from(f64)` | 从浮点数创建数值常量。 | +| `Parameter::default()` | 创建表示整数 `0` 的默认参数。 | + +数值常量还支持从 `f32`、`u32`、`i32` 转换。`f64` 与 `f32` 的 `NaN`、正无穷和负无穷不是有限值,传入时构造会直接失败,因此应在调用前完成过滤。 ```rust use cqlib_core::circuit::Parameter; @@ -61,6 +66,8 @@ let neg = -phi; 每次运算都会生成新的参数表达式,不会修改原有 `Parameter`。因此,同一个符号参数可以安全地复用于多个表达式中。 +上述运算支持四种所有权组合:`Parameter` 与 `Parameter`、`&Parameter` 与 `&Parameter`、`Parameter` 与 `&Parameter`、`&Parameter` 与 `Parameter`,也支持 `Parameter` 与 `f64`、`f32`、`i32`、`u32` 的双向混合。一元取负对 `Parameter` 和 `&Parameter` 均可用。 + --- ## 数学函数 @@ -118,7 +125,7 @@ pub fn evaluate( use cqlib_core::circuit::Parameter; let x = Parameter::symbol("x"); -let expr = x.clone() + 0.0; +let expr: Parameter = x.clone() + 0.0; let simplified = expr.simplify()?; let canonical = simplified.canonicalized()?; @@ -143,7 +150,7 @@ use cqlib_core::circuit::Parameter; let x = Parameter::symbol("x"); let y = Parameter::symbol("y"); -let expr = x + 2.0; +let expr: Parameter = x + 2.0; let replaced = expr.replace("x", y.clone() * 3.0); ``` @@ -172,21 +179,24 @@ let dtheta = expr.derivative("theta")?; ## 等价判断 +`Parameter` 实现了 `PartialEq`、`Eq` 与 `Hash`,三者都基于底层表达式 AST 的结构:`x + y` 与 `y + x` 结构不同,因此不相等。需要按数学语义判断时,使用下面的保守等价接口。 + | 方法 | 说明 | | --- | --- | | `provably_equal(other, tolerance)` | 保守判断两个表达式是否相等。 | | `provably_equal_modulo(other, modulus, tolerance)` | 在给定周期模意义下进行保守相等判断。 | +`provably_equal_modulo()` 是保守判定:只有当两个表达式之差能够求值为具体数值,且该差值是模数的整数倍(在容差范围内)时才返回 `true`。如果差值中仍含有未绑定符号,即使数学上确实相差整数倍周期,也会返回 `false`。例如 `theta + pi` 与 `theta - pi` 相差 `2 * pi`,但差值中含自由符号 `theta`,该接口无法证明二者相等。 + ```rust use cqlib_core::circuit::Parameter; -let theta = Parameter::symbol("theta"); -let expr1 = theta.clone() + Parameter::pi(); -let expr2 = theta - Parameter::pi(); +let angle = Parameter::from(0.5); +let equivalent = Parameter::from(0.5 + 2.0 * std::f64::consts::PI); -assert!(expr1.provably_equal_modulo( - &expr2, - &(2.0 * Parameter::pi()), +assert!(angle.provably_equal_modulo( + &equivalent, + &Parameter::from(2.0 * std::f64::consts::PI), 1e-12, )); diff --git a/docs/api/rust/0_circuit/4_operation_instruction.md b/docs/api/cn/rust/0_circuit/4_operation_instruction.md similarity index 81% rename from docs/api/rust/0_circuit/4_operation_instruction.md rename to docs/api/cn/rust/0_circuit/4_operation_instruction.md index 9db2cc73..33a86171 100644 --- a/docs/api/rust/0_circuit/4_operation_instruction.md +++ b/docs/api/cn/rust/0_circuit/4_operation_instruction.md @@ -73,7 +73,36 @@ pub enum Instruction { 在普通手写线路中,用户通常不需要直接构造 `Instruction`。但在编写编译器 pass、IR 导入器、序列化工具或底层测试时,显式操作 `Instruction` 可以更精确地控制线路结构。 -### 2. 常用方法 +### 2. 稳定元数据 + +`Instruction` 提供一组不依赖 `Debug` 输出、也不随展示格式变化的稳定标识,适合在编译 pass、序列化和缓存键中使用。 + +| 方法 | 返回 | 说明 | +| --- | --- | --- | +| `name()` | `String` | 具体操作名或用户自定义门标签。与 `instruction_type()` 不同,它能区分同类别下的不同门。 | +| `instruction_type()` | `&'static str` | 指令类别名:`"standard"`、`"mcgate"`、`"unitary"`、`"circuit"`、`"directive"`、`"classical_data"`、`"classical_control"`、`"delay"`。 | + +`name()` 对各变体的取值来源:标准门取门名,多控制门取多控制门名,酉门取标签,复合门取门名,指令与经典操作取各自的操作名,`Delay` 固定为 `"delay"`。 + +类别判定用一组常量谓词,全部在编译期求值: + +| 谓词 | 说明 | +| --- | --- | +| `is_standard()` | 是否为标准门指令。 | +| `is_mcgate()` | 是否为多控制门指令。 | +| `is_unitary()` | 是否为用户自定义酉门指令。 | +| `is_circuit_gate()` | 是否为复合门指令。 | +| `is_directive()` | 是否为非酉指令。 | +| `is_classical_data()` | 是否为经典数据操作。 | +| `is_classical_control()` | 是否为结构化控制流。 | +| `is_delay()` | 是否为延迟指令。 | +| `is_quantum_gate()` | 是否为酉量子门,即前四类之一。 | + +### 3. 结构相等 + +`Instruction` 实现了 `PartialEq`。两个指令相等当且仅当变体与内部定义在结构上一致,比较过程不涉及量子比特绑定与参数取值。 + +### 4. 常用方法 | 方法 | 说明 | | --- | --- | @@ -84,7 +113,7 @@ pub enum Instruction { | `inverse(params)` | 尝试返回反指令及反参数;不可逆指令返回 `None`。 | | `control(num_new_ctrls)` | 尝试将当前指令提升为受控指令。 | -### 3. `From` 转换 +### 5. `From` 转换 `Instruction` 支持从多种底层类型转换而来,便于在构造 IR 或编译 pass 中快速创建指令。 @@ -119,7 +148,15 @@ pub enum Directive { | --- | --- | | `Barrier` | 编译或调度屏障,用于阻止相关量子比特上的操作跨越该边界重排。 | | `Measure` | 计算基测量,会产生经典结果,是非酉操作。 | -| `Reset` | 将量子比特复位到 `|0>`,通常破坏原有量子态相干性。 | +| `Reset` | 将量子比特复位到 `\|0>`,通常破坏原有量子态相干性。 | + +`Directive` 还提供稳定的操作名接口: + +```rust +pub const fn name(self) -> &'static str +``` + +分别返回 `"barrier"`、`"measure"`、`"reset"`。 `Directive` 的公开反演接口如下: @@ -199,6 +236,14 @@ pub struct Operation { | `params` | 当前操作携带的参数,使用 `CircuitParam` 表示。 | | `label` | 可选元数据标签,不改变操作的数学语义。 | +### `Operation::standard_gate` + +```rust +pub fn standard_gate(&self) -> Option +``` + +`Operation::standard_gate()` 在操作为标准门时返回对应标准门定义,否则返回 `None`。 + ### `Operation::matrix` ```rust @@ -227,6 +272,12 @@ pub enum ValueInstruction { | `from_instruction(inst)` | 将普通 `Instruction` 包装为 `ValueInstruction`。 | | `is_classical_control()` | 判断是否为构造层控制流。 | | `is_instruction()` | 判断是否为普通指令。 | +| `name()` | 返回具体操作名或用户自定义门标签。 | +| `instruction_type()` | 返回稳定类别名;构造层控制流固定为 `"classical_control"`,其余委托给内部 `Instruction`。 | +| `is_standard()` / `is_mcgate()` / `is_unitary()` / `is_circuit_gate()` | 判断是否分别为标准门、多控制门、自定义酉门、复合门指令。 | +| `is_directive()` / `is_classical_data()` / `is_delay()` | 判断是否分别为非酉指令、经典数据操作、延迟指令。 | +| `standard_gate()` | 当指令为标准门时返回对应 `StandardGate`。 | +| `directive()` | 当指令为非酉指令时返回对应 `Directive`。 | | `as_instruction()` | 读取内部 `Instruction` 引用。 | | `into_instruction()` | 消耗对象并取出内部 `Instruction`。 | diff --git a/docs/api/rust/0_circuit/5_gate_standard.md b/docs/api/cn/rust/0_circuit/5_gate_standard.md similarity index 87% rename from docs/api/rust/0_circuit/5_gate_standard.md rename to docs/api/cn/rust/0_circuit/5_gate_standard.md index d7c3b4b1..0b35e961 100644 --- a/docs/api/rust/0_circuit/5_gate_standard.md +++ b/docs/api/cn/rust/0_circuit/5_gate_standard.md @@ -61,6 +61,7 @@ pub enum StandardGate { | 方法 | 返回 | 说明 | | --- | --- | --- | +| `name()` | `&'static str` | 返回门的稳定操作名,例如 `"H"`、`"RX"`、`"CX"`。 | | `all()` | `&'static [StandardGate]` | 返回所有标准门枚举值,常用于生成门表、测试或目标 basis 检查。 | | `num_qubits()` | `usize` | 返回该门作用的总量子比特数量。 | | `num_ctrl_qubits()` | `usize` | 返回门内置控制位数量。 | @@ -105,7 +106,7 @@ assert!(StandardGate::RZ.is_diagonal()); | `RZ` | 1 | Z 轴旋转,通常采用 `exp(-i θ Z / 2)` 约定。 | | `RXY` | 2 | XY 平面任意轴旋转。 | | `U` | 3 | 通用单量子比特门。 | -| `Phase` | 1 | 相位门,对 `|1>` 分量施加相位。 | +| `Phase` | 1 | 相位门,对 `\|1>` 分量施加相位。 | | `XY` | 1 | XY 交互族单量子比特参数门。 | | `XY2P` / `XY2M` | 1 | 正/负半角 XY 门。 | @@ -139,7 +140,7 @@ pub fn matrix( ) -> Result>>, CircuitError> ``` -`matrix()` 返回该标准门的局部数值矩阵,调用时传入的 `params` 数量须等于 `self.num_params()`。 +`matrix()` 返回该标准门的局部数值矩阵。`params` 需按 `self.num_params()` 提供参数,其中任意一项为 `NaN` 或无穷大时返回 `InvalidParameterValue`;参数个数少于 `num_params()` 属于调用错误。 ```rust use cqlib_core::circuit::StandardGate; @@ -187,7 +188,7 @@ pub fn inverse( ) -> Option<(StandardGate, SmallVec<[Parameter; 3]>)> ``` -`inverse()` 返回当前标准门的反门枚举及变换后的参数列表。如果当前门不可逆,或传入参数数量与 `num_params()` 不匹配,则返回 `None`。 +`inverse()` 返回当前标准门的反门枚举及变换后的参数列表。每个标准门都有可表示的反门,因此该接口对标准门总是返回 `Some`。`params` 需按 `num_params()` 提供足量参数,参数个数不足属于调用错误。 常见反门关系如下: @@ -205,6 +206,15 @@ pub fn inverse( | `CX` / `CY` / `CZ` | 自身 | | `SWAP` | 自身 | | `CCX` | 自身 | +| `X2P` / `X2M` | 互为反门 | +| `Y2P` / `Y2M` | 互为反门 | +| `XY2P` / `XY2M` | 互为反门,参数保持不变 | +| `Phase(λ)` / `GPhase(λ)` | 参数取负 | +| `RXX` / `RYY` / `RZZ` / `RZX` | 参数取负 | +| `RXY(θ, φ)` | `RXY(-θ, φ)`,`φ` 保持不变 | +| `FSIM(θ, φ)` | `FSIM(-θ, -φ)` | +| `XY(θ)` | `XY(π + θ)` | +| `U(θ, φ, λ)` | `U(-θ, -λ, -φ)`,后两个参数互换 | ```rust use cqlib_core::circuit::{Parameter, StandardGate}; diff --git a/docs/api/rust/0_circuit/6_gate_unitary.md b/docs/api/cn/rust/0_circuit/6_gate_unitary.md similarity index 85% rename from docs/api/rust/0_circuit/6_gate_unitary.md rename to docs/api/cn/rust/0_circuit/6_gate_unitary.md index 02e37d16..685e40cf 100644 --- a/docs/api/rust/0_circuit/6_gate_unitary.md +++ b/docs/api/cn/rust/0_circuit/6_gate_unitary.md @@ -56,6 +56,24 @@ assert_eq!(gate.num_params(), 0); --- +## `UnitaryMatrix` + +`UnitaryMatrix` 是 `UnitaryGate` 的原始矩阵表示枚举,可通过 `matrix_repr()` 读取。 + +```rust +pub enum UnitaryMatrix { + Numeric(Array2>), + Symbolic(SymbolicMatrix), +} +``` + +| 变体 | 说明 | +| --- | --- | +| `Numeric(Array2>)` | 数值矩阵,仅适用于非参数化的酉门定义。 | +| `Symbolic(SymbolicMatrix)` | 符号矩阵,其自由符号按门的 `matrix_params` 顺序进行位置映射。 | + +--- + ## 数值矩阵定义 ```rust @@ -68,7 +86,7 @@ pub fn with_matrix(self, mat: Array2>) -> Result-<基础门名>`。 | ```rust use cqlib_core::circuit::{MCGate, StandardGate}; diff --git a/docs/api/rust/0_circuit/8_gate_circuit_gate.md b/docs/api/cn/rust/0_circuit/8_gate_circuit_gate.md similarity index 96% rename from docs/api/rust/0_circuit/8_gate_circuit_gate.md rename to docs/api/cn/rust/0_circuit/8_gate_circuit_gate.md index 7a7bef08..b9e541ed 100644 --- a/docs/api/rust/0_circuit/8_gate_circuit_gate.md +++ b/docs/api/cn/rust/0_circuit/8_gate_circuit_gate.md @@ -19,6 +19,7 @@ use cqlib_core::circuit::gate::FrozenCircuit; ```rust pub fn new(circuit: Circuit) -> Self pub fn circuit(&self) -> &Circuit +pub fn used_symbols(&self) -> &IndexSet pub fn symbolic_matrix(&self) -> Result, CircuitError> ``` @@ -205,7 +206,13 @@ circuit.circuit_gate(gate, qubits, params)?; - `params` 是外层调用时提供的位置参数列表。 ```rust -use cqlib_core::circuit::{Circuit, ParameterValue, Qubit}; +use cqlib_core::circuit::{Circuit, CircuitGate, ParameterValue, Qubit}; +use cqlib_core::circuit::gate::FrozenCircuit; + +let mut inner = Circuit::new(1); +inner.h(Qubit::new(0))?; + +let gate = CircuitGate::new("HBlock", FrozenCircuit::new(inner))?; let mut outer = Circuit::new(1); diff --git a/docs/api/rust/0_circuit/9_classical_control_flow.md b/docs/api/cn/rust/0_circuit/9_classical_control_flow.md similarity index 79% rename from docs/api/rust/0_circuit/9_classical_control_flow.md rename to docs/api/cn/rust/0_circuit/9_classical_control_flow.md index afde1c63..fb22b33f 100644 --- a/docs/api/rust/0_circuit/9_classical_control_flow.md +++ b/docs/api/cn/rust/0_circuit/9_classical_control_flow.md @@ -215,7 +215,62 @@ let bits = ClassicalExpr::bit_vec_literal(4, 0b1010)?; | `extract_bits(value, offset, width)` | 提取连续 bit 区间。 | | `concat(parts)` | 拼接多个 `BitVec` 表达式。 | | `pack_bits(bits)` | 将多个 `Bit` 表达式打包为 `BitVec`。 | -| `simplified()` | 返回化简后的表达式。 | + +### 内省与化简 + +| 方法 | 说明 | +| --- | --- | +| `ty()` | 返回表达式的静态类型。 | +| `kind()` | 返回底层 AST 节点。 | +| `vars()` | 返回表达式读取的全部可变经典变量集合。 | +| `values()` | 返回表达式读取的全部不可变经典值集合。 | +| `collect_vars(out)` | 将读取的经典变量写入给定的集合。 | +| `collect_values(out)` | 将读取的经典值写入给定的集合。 | +| `remap_classical_ids(var_map, value_map)` | 按映射表重映射表达式引用的经典句柄。 | +| `simplified()` | 返回结构化简后的表达式。 | +| `is_bool_true()` / `is_bool_false()` | 判断是否为 `Bool` 字面量。 | +| `is_bit_true()` / `is_bit_false()` | 判断是否为 `Bit` 字面量。 | + +--- + +## 表达式 AST 类型 + +`ClassicalExpr` 的底层节点由下列类型描述,它们都在 `cqlib_core::circuit` 模块根可见。 + +```rust +pub enum ClassicalUnaryOp { Not } +pub enum ClassicalBinaryOp { And, Or, Xor } +pub enum ClassicalCompareOp { Eq, Ne, Lt, Le, Gt, Ge } +pub enum ClassicalCast { BitToBool, BitVecToUInt } +``` + +```rust +pub enum ClassicalExprKind { + Var(ClassicalVar), + Value(ClassicalValue), + BoolLiteral(bool), + BitLiteral(bool), + UIntLiteral { width: NonZeroU32, value: u128 }, + BitVecLiteral { width: NonZeroU32, value: u128 }, + Unary { op: ClassicalUnaryOp, expr: ClassicalExpr }, + Binary { op: ClassicalBinaryOp, lhs: ClassicalExpr, rhs: ClassicalExpr }, + Compare { op: ClassicalCompareOp, lhs: ClassicalExpr, rhs: ClassicalExpr }, + Cast { cast: ClassicalCast, expr: ClassicalExpr }, + Select { condition: ClassicalExpr, then_expr: ClassicalExpr, else_expr: ClassicalExpr }, + ExtractBit { value: ClassicalExpr, index: u32 }, + ExtractBits { value: ClassicalExpr, offset: u32, width: NonZeroU32 }, + Concat { parts: Box<[ClassicalExpr]> }, + PackBits { bits: Box<[ClassicalExpr]> }, +} +``` + +| 类型 | 说明 | +| --- | --- | +| `ClassicalExprKind` | 表达式 AST 节点枚举,覆盖变量读取、字面量、运算、比较、类型转换、位操作。 | +| `ClassicalExprNode` | AST 节点,携带表达式的静态类型与节点内容。 | +| `ClassicalUnaryOp` / `ClassicalBinaryOp` | 一元与二元逻辑运算算子。 | +| `ClassicalCompareOp` | 比较算子。 | +| `ClassicalCast` | 显式类型转换算子。 | --- @@ -283,6 +338,40 @@ circuit.if_else( `for_uint` 要求循环变量、起点、终点和步长都是相同位宽的 `UInt` 表达式;`switch` 的 target 必须为 `UInt`,case 值必须能由该位宽表示。 +### 4. `switch` 与 `SwitchBuilder` + +`switch(target, build)` 的回调参数是 `SwitchBuilder`,用于按顺序登记各 case 与可选的 default 分支。 + +| 方法 | 说明 | +| --- | --- | +| `value(value, body)` | 登记一个精确匹配的 case 分支。 | +| `default(body)` | 登记默认分支;重复登记会返回错误。 | + +```rust +use cqlib_core::circuit::{Circuit, ClassicalType, Qubit}; + +let mut circuit = Circuit::new(2); +let state = circuit.var(ClassicalType::uint(2).expect("valid uint width")); + +circuit.switch(state.expr(), |case| { + case.value(0, |body| { + body.x(Qubit::new(0))?; + Ok(()) + })?; + case.value(1, |body| { + body.h(Qubit::new(1))?; + Ok(()) + })?; + case.default(|body| { + body.z(Qubit::new(0))?; + Ok(()) + })?; + Ok(()) +})?; + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + --- ## 低层控制流 IR diff --git a/docs/api/cn/rust/0_overview.md b/docs/api/cn/rust/0_overview.md new file mode 100644 index 00000000..5f5275e2 --- /dev/null +++ b/docs/api/cn/rust/0_overview.md @@ -0,0 +1,83 @@ +# Rust API 概览 + +## 欢迎查阅 Cqlib Rust API 参考手册! + +## 文档导航 + +### 量子电路(`cqlib_core::circuit`) + +- [Overview](0_circuit/0_overview.md) +- [Circuit](0_circuit/1_circuit.md) +- [Qubit](0_circuit/2_qubit.md) +- [Parameters](0_circuit/3_parameter.md) +- [Operations / Instructions](0_circuit/4_operation_instruction.md) +- [Standard Gates](0_circuit/5_gate_standard.md) +- [Unitary Gates](0_circuit/6_gate_unitary.md) +- [Multi-Controlled Gates](0_circuit/7_gate_mc_gate.md) +- [Circuit Gates](0_circuit/8_gate_circuit_gate.md) +- [Classical / Control Flow](0_circuit/9_classical_control_flow.md) +- [Symbolic Matrix](0_circuit/10_symbolic_matrix.md) +- [Ansatz](0_circuit/11_ansatz.md) +- [CFG](0_circuit/12_cfg.md) +- [Circuit to Matrix](0_circuit/13_circuit_to_matrix.md) +- [CircuitDag](0_circuit/14_circuit_dag.md) + +### 中间表达(`cqlib_core::ir`) + +- [Overview](1_ir/0_overview.md) +- [QCIS](1_ir/1_qcis.md) +- [OpenQASM 2.0](1_ir/2_qasm2.md) +- [OpenQASM 3.0](1_ir/3_qasm3.md) + +### 设备模块(`cqlib_core::device`) + +- [Overview](2_device/0_overview.md) +- [Topology](2_device/1_topology.md) +- [Device 与噪声属性](2_device/2_properties_device.md) +- [Layout](2_device/3_layout.md) +- [Noise](2_device/4_noise.md) +- [Result](2_device/5_result.md) +- [量子比特标识](2_device/6_qubits.md) + +### 量子信息(`cqlib_core::qis`) + +- [Overview](3_qis/0_overview.md) +- [Statevector](3_qis/1_statevector.md) +- [DensityMatrix](3_qis/2_density_matrix.md) +- [DensityMatrixNoise](3_qis/3_density_matrix_noise.md) +- [Stabilizer](3_qis/4_stabilizer.md) +- [ClassicalState](3_qis/5_classical_state.md) +- [Pauli](3_qis/6_pauli.md) +- [Hamiltonian](3_qis/7_hamiltonian.md) +- [Evolution](3_qis/8_evolution.md) +- [Metrics / Entropy](3_qis/9_metrics_entropy.md) + +### 编译优化(`cqlib_core::compile`) + +- [Overview](4_compile/0_overview.md) +- [Compiler](4_compile/1_compiler.md) +- [Transform](4_compile/2_transform.md) +- [Layout](4_compile/3_layout.md) +- [Routing](4_compile/4_routing.md) +- [SABRE](4_compile/5_sabre.md) +- [Decompose / Resynthesis](4_compile/6_decompose_resynthesis.md) +- [Knowledge](4_compile/7_knowledge.md) +- [Resource](4_compile/8_resource.md) +- [Commutation](4_compile/9_commutation.md) + +### 可视化(`cqlib_core::visualization`) + +- [Overview](5_visualization/0_overview.md) +- [文本线路图](5_visualization/1_draw_text.md) +- [SVG 线路图](5_visualization/2_draw_figure.md) +- [量子态可视化](5_visualization/3_state_plots.md) +- [测量结果可视化](5_visualization/4_result_plots.md) +- [落盘与输出](5_visualization/5_render_to_file.md) +- [视觉 IR](5_visualization/6_visual_ir.md) + +### 错误缓解(`cqlib_core::error_mitigation`) + +- [Overview](6_error_mitigation/0_overview.md) +- [零噪声外推](6_error_mitigation/1_zne.md) +- [虚拟蒸馏](6_error_mitigation/2_virtual_distillation.md) +- [统一流水线](6_error_mitigation/3_unified.md) diff --git a/docs/api/cn/rust/1_ir/0_overview.md b/docs/api/cn/rust/1_ir/0_overview.md new file mode 100644 index 00000000..2470aabd --- /dev/null +++ b/docs/api/cn/rust/1_ir/0_overview.md @@ -0,0 +1,187 @@ +# 中间表达 + +`cqlib_core::ir` + +`cqlib_core::ir` 是 Cqlib 的中间表达模块,提供 `Circuit` 与三种线路文本格式之间的双向转换:QCIS、OpenQASM 2.0 与 OpenQASM 3.0。crate 根为每种格式导出四个函数别名,格式模块内部再按解析与导出分文件组织,并各自持有独立的错误类型。 + +## Overview + +中间表达解决的是线路的文本化问题:线路在文件、工具与服务之间传递时是文本,而 Cqlib 内部统一用 `Circuit` 表示。`cqlib_core::ir` 的每个格式模块都提供「文本进、`Circuit` 出」与「`Circuit` 进、文本出」两个方向,转换过程不修改传入的 `Circuit`。 + +### crate 根别名 + +crate 根把每个格式的四个入口按 `<格式>_<动作>` 重新导出,它们与格式模块内的同名函数是同一个条目: + +| 动作 | QCIS | OpenQASM 2.0 | OpenQASM 3.0 | +| --- | --- | --- | --- | +| 字符串 → `Circuit` | `qcis_loads` | `qasm2_loads` | `qasm3_loads` | +| 文件 → `Circuit` | `qcis_load` | `qasm2_load` | `qasm3_load` | +| `Circuit` → 字符串 | `qcis_dumps` | `qasm2_dumps` | `qasm3_dumps` | +| `Circuit` → 文件 | `qcis_dump` | `qasm2_dump` | `qasm3_dump` | + +### 格式模块 + +每个格式模块(`cqlib_core::ir::qcis`、`cqlib_core::ir::qasm2`、`cqlib_core::ir::qasm3`)由 `load` 与 `dump` 两个子模块组成,转发同名函数并额外提供 `from_str` / `from_path` 与 `to_string` / `to_path`。`qasm2::ast` 是公开的抽象语法树模块,解析产物可以在这里单独查看。 + +### 错误类型 + +每个格式的解析与导出各有一个独立的错误枚举,crate 不做统一封装: + +| 格式 | 解析错误 | 导出错误 | +| --- | --- | --- | +| QCIS | `qcis::load::QcisParseError` | `qcis::dump::QcisDumpError` | +| OpenQASM 2.0 | `qasm2::load::QasmParseError` | `qasm2::dump::QasmDumpError` | +| OpenQASM 3.0 | `qasm3::load::Qasm3ParseError` | `qasm3::dump::Qasm3DumpError` | + +文件入口在 I/O 失败时返回带原始 `std::io::Error` 的 `IoError` 变体,原始错误可以用 `Error::source` 逐层取回。 + +### 双向转换与边界 + +`loads(dumps(circuit))` 得到的线路与输入线路在操作序列上等价,这就是双向转换。但文本格式的表达能力有限: + +- QCIS 只接受原生门集内的门,线路中若含其他门,需先分解到该门集再导出;经典存储(`store`)与经典控制不在 QCIS 的表达范围内,测量与屏障可以表达,含符号的门参数会被原样写成符号文本。 +- 导出失败时枚举会指出具体原因,例如不受支持的门、无法表达的经典操作,或不合法的延时刻度。 + +### 与编译的关系 + +导出是编译产物的可选落盘形式:线路降到目标门集之后即可用对应格式写出。格式差异只体现在门集与文本头上,`Circuit` 一侧的接口保持不变,因此也可以由一种格式读入、另一种格式写出,完成格式转换。 + +--- + +## 常用入口 + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::ir::{qcis_dumps, qcis_loads}; + +let mut circuit = Circuit::new(2); +circuit.h(Qubit::new(0)).unwrap(); +circuit.cz(Qubit::new(0), Qubit::new(1)).unwrap(); + +let qcis = qcis_dumps(&circuit).unwrap(); +let parsed = qcis_loads(&qcis).unwrap(); + +assert_eq!(parsed.num_qubits(), 2); +``` + +--- + +## 核心概念与术语 + +| 术语 | 说明 | +| --- | --- | +| **中间表达** | `Circuit` 与线路文本格式之间的表示层,由 `cqlib_core::ir` 下的格式模块提供。 | +| **解析** | 文本到 `Circuit` 的方向,对应 `loads`(字符串)与 `load`(文件)。 | +| **导出** | `Circuit` 到文本的方向,对应 `dumps`(字符串)与 `dump`(文件)。 | +| **双向转换** | 先导出再解析回到等价线路,即 `loads(dumps(circuit))`。 | +| **格式转换** | 一种格式读入、另一种格式写出,例如把 OpenQASM 2.0 文本写成 QCIS 文本。 | +| **原生门集** | 文本格式自身定义的门集合;导出时线路中的门必须都能落在该集合内。 | + +--- + +## `cqlib_core::ir` API 概览 + +### crate 根别名 + +| 名字 | 简介 | +| --- | --- | +| [`qcis_loads`](1_qcis.md) / [`qcis_load`](1_qcis.md) | 从 QCIS 字符串或文件解析 `Circuit`。 | +| [`qcis_dumps`](1_qcis.md) / [`qcis_dump`](1_qcis.md) | 把 `Circuit` 导出为 QCIS 字符串或文件。 | +| [`qasm2_loads`](2_qasm2.md) / [`qasm2_load`](2_qasm2.md) | 从 OpenQASM 2.0 字符串或文件解析 `Circuit`。 | +| [`qasm2_dumps`](2_qasm2.md) / [`qasm2_dump`](2_qasm2.md) | 把 `Circuit` 导出为 OpenQASM 2.0 字符串或文件。 | +| [`qasm3_loads`](3_qasm3.md) / [`qasm3_load`](3_qasm3.md) | 从 OpenQASM 3.0 字符串或文件解析 `Circuit`。 | +| [`qasm3_dumps`](3_qasm3.md) / [`qasm3_dump`](3_qasm3.md) | 把 `Circuit` 导出为 OpenQASM 3.0 字符串或文件。 | + +### 格式模块 + +| 名字 | 简介 | +| --- | --- | +| [`cqlib_core::ir::qcis`](1_qcis.md) | QCIS 格式,含 `load` / `dump` 子模块与 `from_str`、`to_string` 等辅助入口。 | +| [`cqlib_core::ir::qasm2`](2_qasm2.md) | OpenQASM 2.0 格式,另含公开的 `ast` 子模块。 | +| [`cqlib_core::ir::qasm3`](3_qasm3.md) | OpenQASM 3.0 格式。 | + +### 错误类型 + +| 名字 | 简介 | +| --- | --- | +| [`QcisParseError`](1_qcis.md) / [`QcisDumpError`](1_qcis.md) | QCIS 解析与导出错误。 | +| [`QasmParseError`](2_qasm2.md) / [`QasmDumpError`](2_qasm2.md) | OpenQASM 2.0 解析与导出错误。 | +| [`Qasm3ParseError`](3_qasm3.md) / [`Qasm3DumpError`](3_qasm3.md) | OpenQASM 3.0 解析与导出错误。 | + +--- + +## 快速示例 + +### 1. 解析 QCIS 文本并回写 + +```rust +use cqlib_core::ir::{qcis_dumps, qcis_loads}; + +let qcis = r#" + RX Q0 1.0 + RY Q1 pi/2 + X Q0 + CZ Q0 Q1 + "#; + +let circuit = qcis_loads(qcis).unwrap(); +assert_eq!(circuit.num_qubits(), 2); +``` + +### 2. 导出结果的精确文本 + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::ir::qcis_dumps; + +let mut c = Circuit::new(2); +let q0 = Qubit::new(0); +let q1 = Qubit::new(1); + +c.x(q0).unwrap(); +c.y(q1).unwrap(); +c.z(q0).unwrap(); +c.h(q1).unwrap(); + +let qcis = qcis_dumps(&c).unwrap(); +assert_eq!(qcis, "X Q0\nY Q1\nZ Q0\nH Q1\n"); +``` + +### 3. 写入文件并检查错误来源 + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::ir::qcis::dump::QcisDumpError; +use cqlib_core::ir::qcis_dump; +use std::error::Error; + +let mut c = Circuit::new(1); +c.h(Qubit::new(0)).unwrap(); + +let path = std::env::temp_dir().join("cqlib_ir_overview.qcis"); +qcis_dump(&c, &path).unwrap(); + +let missing = path.join("out.qcis"); +let error = qcis_dump(&c, missing).unwrap_err(); +assert!(matches!(error, QcisDumpError::IoError(_))); +assert!(error.source().is_some()); +``` + +--- + +## 校验与错误处理 + +解析错误与导出错误分开定义,文件失败用 `IoError` 变体表示,其余变体是内容层面的原因。 + +| 错误 | 触发场景 | +| --- | --- | +| `QcisParseError::IoError` | `qcis_load` 读取文件失败。 | +| `QcisParseError::{InvalidQubitFormat, InvalidQubitId, QubitCountMismatch, ParameterCountMismatch, MissingParameter, InvalidParameter, UnknownGate, EmptyLine}` | QCIS 文本中的比特写法、未知门、门元数或参数个数不匹配,以及空行或没有有效内容。 | +| `QcisDumpError::{UnsupportedGate, UnsupportedClassicalData, UnsupportedClassicalControl, SymbolicParameter, InvalidDelayParameter}` | 导出 QCIS 时遇到无法表达的门、经典数据、经典控制、符号参数或非法延时参数。 | +| `QasmParseError::{ParseError, ConversionError, UndefinedQubit, UndefinedRegister, UndefinedGate, ReservedGateName, InvalidArgument, MismatchedQubitCount, MismatchedParameterCount, RecursionLimitExceeded, EvaluationError, CircularGateDependency, UnsupportedVersion, DuplicateDeclaration, IncludeCycle, UnsupportedOpaqueGate}` | OpenQASM 2.0 语法、语义、门定义与版本声明问题。 | +| `QasmDumpError::{FormatError, MeasureInGateNotAllowed, ResetInGateNotAllowed, InvalidQubitIndex, UnsupportedClassicalType, UnsupportedClassicalData, UnsupportedClassicalControl, ConflictingGateDefinition}` | 导出 OpenQASM 2.0 时遇到非法生成内容、门定义中的测量或复位、非法比特下标、无法表达的经典类型与重名自定义门。 | +| `Qasm3ParseError::{ParseError, SemanticError, ConversionError, UnsupportedFeature, UndefinedSymbol, UndefinedGate, TypeError, InvalidArgument, MismatchedQubitCount, MismatchedParameterCount, RecursionLimitExceeded, CircularGateDependency}` | OpenQASM 3.0 语法、语义、符号解析、类型检查与降级失败。 | +| `Qasm3DumpError::{FormatError, UnsupportedInstruction, UnsupportedClassicalData, UnsupportedClassicalControl, MeasureInGateNotAllowed, ConflictingGateDefinition}` | 导出 OpenQASM 3.0 时遇到无法表达的指令、经典数据、经典控制与重名自定义门。 | +| `*DumpError::IoError` / `*ParseError::IoError` | 文件写入或读取失败;原始 I/O 错误可用 `Error::source` 取回。 | + +导出前线路必须已经落在目标格式的门集内,否则先做门分解再导出。 diff --git a/docs/api/cn/rust/1_ir/1_qcis.md b/docs/api/cn/rust/1_ir/1_qcis.md new file mode 100644 index 00000000..c9a8ca57 --- /dev/null +++ b/docs/api/cn/rust/1_ir/1_qcis.md @@ -0,0 +1,97 @@ +# QCIS API + +`cqlib_core::ir` 提供 QCIS 的导入与导出接口,用于在 `Circuit` 与 QCIS 文本/文件之间转换。 + +## 导入 + +```rust +use cqlib_core::ir::{qcis_load, qcis_loads, qcis_dump, qcis_dumps}; +``` + +## 接口一览 + +- `qcis_load(path) -> Result` +- `qcis_loads(qcis: &str) -> Result` +- `qcis_dump(circuit: &Circuit, path) -> Result<(), QcisDumpError>` +- `qcis_dumps(circuit: &Circuit) -> Result` + +文件入口的路径参数是 `P: AsRef` 泛型,`&str`、`PathBuf`、`&Path` 都可以直接传入。 + +### 格式模块内的入口 + +`cqlib_core::ir::qcis` 下的 `load` 与 `dump` 子模块转发同名函数,并在模块级别重导出一组 Rust 风格的等价入口: + +- `cqlib_core::ir::qcis::loads` / `cqlib_core::ir::qcis::from_str` +- `cqlib_core::ir::qcis::load` / `cqlib_core::ir::qcis::from_path` +- `cqlib_core::ir::qcis::dumps` / `cqlib_core::ir::qcis::to_string` +- `cqlib_core::ir::qcis::dump` / `cqlib_core::ir::qcis::to_path` + +## 解析(load / loads) + +### `qcis_loads` + +从 QCIS 字符串解析 `Circuit`,失败时返回 `QcisParseError`。 + +错误类型: + +- `IoError`:文件系统或 I/O 失败,字符串入口不会产生。 +- `InvalidQubitFormat`:比特写法不是 `Q`。 +- `InvalidQubitId`:`Q` 后的编号无法解析。 +- `QubitCountMismatch`:门的比特数与规格不符。 +- `ParameterCountMismatch`:门的参数个数与规格不符。 +- `MissingParameter`:参数表达式为空或无法解析。 +- `InvalidParameter`:延迟刻度不是固定的非负整数。 +- `UnknownGate`:门名不在 QCIS 指令集内。 +- `EmptyLine`:空行或没有有效内容。 + +### `qcis_load` + +从文件读取并解析,返回 `Result`:文件读取失败为 `IoError`,内容解析失败为对应变体。 + +## 导出(dump / dumps) + +### `qcis_dumps` + +将电路导出为 QCIS 字符串,失败返回 `QcisDumpError`。 + +错误类型: + +- `UnsupportedGate`:线路中含 QCIS 原生门集之外的门,例如标准恒等门、全局相位、多控制门、自定义门与酉门。 +- `UnsupportedClassicalData`:经典存储(`store`)无法表达。 +- `UnsupportedClassicalControl`:经典控制流无法表达。 +- `SymbolicParameter`:参数下标取不到对应参数,或延迟刻度无法求值。 +- `InvalidDelayParameter`:延迟指令的刻度不是非负整数。 +- `IoError`:写入缓冲失败。 + +`M`(测量)与 `B`(屏障)可以导出,`Reset` 不能。 + +### `qcis_dump` + +导出并写入文件,返回 `Result<(), QcisDumpError>`;文件写入失败为 `IoError`。 + +## QCIS 原生门限制 + +QCIS 解析与导出仅支持原生门集: + +- 单比特门:`H`, `S`, `SD`, `T`, `TD`, `X`, `X2P`, `X2M`, `Y`, `Y2P`, `Y2M`, `Z` +- 含参单比特门:`RX`, `RXY`, `RY`, `RZ`, `U`, `XY`, `XY2P`, `XY2M`, `PHASE` +- 多比特门:`RXX`, `RYY`, `RZX`, `RZZ`, `SWAP`, `CX`, `CCX`, `CY`, `CZ`, `CRX`, `CRY`, `CRZ`, `FSIM` + +除门之外,QCIS 文本还表示测量(`M`)、屏障(`B`)与延迟(`I Qn t`,`t` 为延迟刻度数,单位 0.5 ns,必须是非负整数)。 + +解析时 `SDG`、`TDG`、`Barrier` 分别作为 `SD`、`TD`、`B` 的等价写法接受;导出时统一写成 `SD`、`TD`、`B`。标准恒等门与全局相位不在原生门集内,QCIS 的 `I` 一律表示延迟。遇到无法表达的门时,应先分解到 QCIS 门集后再导出。 + +## 最小示例 + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::ir::{qcis_dumps, qcis_loads}; + +let mut c = Circuit::new(2); +c.h(Qubit::new(0)).unwrap(); +c.cz(Qubit::new(0), Qubit::new(1)).unwrap(); + +let text = qcis_dumps(&c).unwrap(); +let c2 = qcis_loads(&text).unwrap(); +assert_eq!(c2.num_qubits(), 2); +``` diff --git a/docs/api/cn/rust/1_ir/2_qasm2.md b/docs/api/cn/rust/1_ir/2_qasm2.md new file mode 100644 index 00000000..78aa68c2 --- /dev/null +++ b/docs/api/cn/rust/1_ir/2_qasm2.md @@ -0,0 +1,117 @@ +# OpenQASM 2.0 API + +`cqlib_core::ir` 提供 OpenQASM 2.0 的导入与导出接口。 + +## 导入 + +```rust +use cqlib_core::ir::{qasm2_load, qasm2_loads, qasm2_dump, qasm2_dumps}; +``` + +## 接口一览 + +- `qasm2_load(path) -> Result` +- `qasm2_loads(source: &str) -> Result` +- `qasm2_dump(circuit: &Circuit, path) -> Result<(), QasmDumpError>` +- `qasm2_dumps(circuit: &Circuit) -> Result` + +文件入口的路径参数是 `P: AsRef` 泛型,`&str`、`PathBuf`、`&Path` 都可以直接传入。 + +### 格式模块内的入口 + +`cqlib_core::ir::qasm2` 下的 `load` 与 `dump` 子模块转发同名函数,并在模块级别重导出一组 Rust 风格的等价入口: + +- `cqlib_core::ir::qasm2::loads` / `cqlib_core::ir::qasm2::from_str` +- `cqlib_core::ir::qasm2::load` / `cqlib_core::ir::qasm2::from_path` +- `cqlib_core::ir::qasm2::dumps` / `cqlib_core::ir::qasm2::to_string` +- `cqlib_core::ir::qasm2::dump` / `cqlib_core::ir::qasm2::to_path` + +`cqlib_core::ir::qasm2::ast` 是公开的抽象语法树模块,解析产物的语句、表达式与参数引用类型定义在其中。 + +## 解析(load / loads) + +### `qasm2_loads` + +从 QASM 字符串解析电路。 + +### `qasm2_load` + +从文件读取并解析电路。 + +两者失败时返回 `QasmParseError`,变体包括: + +- `IoError`:文件系统或 I/O 失败。 +- `ParseError`:语法错误。 +- `ConversionError`:AST 到 `Circuit` 的转换失败。 +- `UndefinedQubit`:引用了未定义的量子寄存器或比特。 +- `UndefinedRegister`:引用了未定义的经典寄存器。 +- `UndefinedGate`:引用了未定义的门。 +- `ReservedGateName`:试图重定义 qelib1 声明的门名。 +- `InvalidArgument`:参数格式或用法不合法。 +- `MismatchedQubitCount` / `MismatchedParameterCount`:门调用的比特数或参数个数不符。 +- `RecursionLimitExceeded` / `CircularGateDependency`:门展开超深或相互依赖成环。 +- `EvaluationError`:参数表达式求值失败。 +- `UnsupportedVersion`:版本声明不是 2.0。 +- `DuplicateDeclaration`:重复声明或命名冲突。 +- `IncludeCycle`:`include` 成环。 +- `UnsupportedOpaqueGate`:`opaque` 门被调用,无法在 `Circuit` 中表示。 + +## 导出(dump / dumps) + +### `qasm2_dumps` + +将电路导出为 QASM 字符串,失败返回 `QasmDumpError`。输出含自动生成注释、`OPENQASM 2.0;`、`include "qelib1.inc";` 头部与 `qreg/creg` 声明。 + +错误类型: + +- `FormatError`:内容生成失败。 +- `MeasureInGateNotAllowed` / `ResetInGateNotAllowed`:`gate` 定义体内出现测量或复位。 +- `InvalidQubitIndex`:比特下标非法。 +- `UnsupportedClassicalType`:经典类型无法表示为 `creg`。 +- `UnsupportedClassicalData` / `UnsupportedClassicalControl`:经典数据操作或经典控制无法表达。 +- `ConflictingGateDefinition`:不同的自定义门使用了同一个 OpenQASM 门名。 +- `IoError`:写入失败。 + +### `qasm2_dump` + +将导出结果写入文件,返回 `Result<(), QasmDumpError>`;文件写入失败为 `IoError`。 + +## 支持能力(概览) + +根据当前实现,QASM2 导入导出支持: + +- 标准门与参数门,含 `ch/cu1/cu3` 与 `sx/sxdg/crx/cry/crz/rxx/ryy/rzz/fsim` +- 测量、屏障、复位等指令 +- `qreg`、`creg`、`opaque` 声明,以及 `if (creg == 整数) qop;` 条件语句 +- `CircuitGate` 导出定义;只带矩阵的酉门导出为 `opaque` 声明 +- 未直接覆盖的扩展门以 `gate` 定义形式输出(如 `crx/cry/rzz/rxx/ryy/rzx`) +- `include "qelib1.inc";` 按内建引入处理,不要求本地存在该文件 + +## 限制 + +导出会按 OpenQASM 2.0 的表达能力取舍,下列结构会报错: + +- 经典控制中的 `else` 分支、循环、`switch` 与嵌套控制流 +- 无法表示为 `creg` 的经典类型,例如 `bool` 与无符号整数 +- `gate` 定义体内的测量与复位 +- 逐位条件(如 `if (c[0] == 1)`)与条件屏障 + +加载侧的 `loads` 不解析 `include` 引入的外部文件,`load` 才按文件所在目录解析。`rzx` 只出现在导出侧的扩展门定义中,加载不接受;全局相位不写成语句,只作为注释输出,重新加载时不会恢复。 + +## 最小示例 + +```rust +use cqlib_core::ir::{qasm2_dumps, qasm2_loads}; + +let qasm = r#" +OPENQASM 2.0; +include "qelib1.inc"; +qreg q[2]; +h q[0]; +cx q[0],q[1]; +"#; + +let c = qasm2_loads(qasm).unwrap(); +let out = qasm2_dumps(&c).unwrap(); +assert!(out.contains("OPENQASM 2.0;")); +``` diff --git a/docs/api/cn/rust/1_ir/3_qasm3.md b/docs/api/cn/rust/1_ir/3_qasm3.md new file mode 100644 index 00000000..4e5d5779 --- /dev/null +++ b/docs/api/cn/rust/1_ir/3_qasm3.md @@ -0,0 +1,163 @@ +# OpenQASM 3.0 API + +`cqlib_core::ir::qasm3` 提供 OpenQASM 3.0 的导入与导出接口,用于在 `Circuit` 与 OpenQASM 3.0 文本/文件之间转换。 + +## 导入 + +```rust +use cqlib_core::ir::{qasm3_load, qasm3_loads, qasm3_dump, qasm3_dumps}; +``` + +## 接口一览 + +- `qasm3_load(path: impl AsRef) -> Result` +- `qasm3_loads(source: &str) -> Result` +- `qasm3_dump(circuit: &Circuit, path: impl AsRef) -> Result<(), Qasm3DumpError>` +- `qasm3_dumps(circuit: &Circuit) -> Result` + +`qasm3` 模块内另提供行为一致的别名:`from_path`、`from_str`、`to_path`、`to_string`。 + +## 解析(load / loads) + +### `qasm3_loads` + +从 OpenQASM 3.0 字符串解析电路。 + +### `qasm3_load` + +从文件读取并解析电路。相对路径的 `include` 从输入文件所在目录解析。 + +两者失败时返回 `Qasm3ParseError`,常见变体: + +- `IoError` +- `ParseError` +- `SemanticError` +- `ConversionError` +- `UnsupportedFeature` +- `UndefinedSymbol` +- `UndefinedGate` +- `TypeError` +- `InvalidArgument` +- `MismatchedQubitCount` +- `MismatchedParameterCount` +- `RecursionLimitExceeded` +- `CircularGateDependency` + +其中 `IoError` 保留底层 I/O 错误,可通过 `source()` 读取。 + +## 导出(dump / dumps) + +### `qasm3_dumps` + +将电路导出为 OpenQASM 3.0 字符串。输出依次为 `OPENQASM 3.0;`、`include "stdgates.inc";`、扩展门定义、自定义门定义、量子与经典声明、主线路操作。 + +### `qasm3_dump` + +将导出结果写入文件,返回 `Result<(), Qasm3DumpError>`。 + +两者失败时返回 `Qasm3DumpError`,常见变体: + +- `IoError`:文件读写失败 +- `FormatError`:格式化失败 +- `UnsupportedInstruction`:无法表示的指令,如 `delay`、`XY`、`RXY` +- `UnsupportedClassicalData`:无法表示的经典存储 +- `UnsupportedClassicalControl`:无法表示的控制流,如 `while`、`for`、`break` / `continue` +- `MeasureInGateNotAllowed`:门定义体内包含测量 +- `ConflictingGateDefinition`:同名门定义的签名冲突 + +## 支持能力(概览) + +根据当前实现,OpenQASM 3.0 导入导出支持: + +- 头部 `OPENQASM 3;` 与 `OPENQASM 3.0;`,以及 `include "stdgates.inc";`(使用内置标准库,不要求磁盘文件) +- 标量量子比特与一维量子寄存器声明 +- 经典声明 `bit`、`bit[n]`、`bool`、`uint[n]`,以及角度与浮点输入声明 +- 映射到 `StandardGate` 的标准门与 Cqlib 扩展门 +- 自定义门定义与调用 +- 测量、`reset`、`barrier`、全局相位 +- 控制流 `if` / `else`、静态 `for`、常量分支 `switch` + +## 与 OpenQASM 2.0 接口的差异 + +在同一 `Circuit` 表示之上,本接口与 OpenQASM 2.0 接口的主要差异: + +| 方面 | OpenQASM 2.0 接口 | OpenQASM 3.0 接口 | +|---|---|---| +| 头部与标准库 | `OPENQASM 2.0;`、`include "qelib1.inc";` | `OPENQASM 3;` 或 `OPENQASM 3.0;`、`include "stdgates.inc";` | +| 量子声明 | `qreg q[2];` | `qubit[2] q;`,并支持标量 `qubit q;` | +| 经典声明 | `creg c[2];` | `bit`、`bit[n]`、`bool`、`uint[n]` | +| 测量 | `measure q[0] -> c[0];` | `c[0] = measure q[0];`、`c = measure q;` | +| 控制流 | `if (c == 1) ...` 后接单条语句 | `if` / `else` 块、静态 `for`、常量分支 `switch` | +| 全局相位 | 导出时仅以注释形式给出,不可加载 | `gphase(theta);`,可加载、可导出 | +| 输入声明 | 无 | 角度与浮点 `input` 声明 | + +## 限制 + +以下 OpenQASM 3.0 特性在当前实现中不支持,遇到时给出明确错误,不做部分降级: + +- 定时与脉冲:`delay`、`box`、`cal`、`defcal` +- 子程序与外部调用:`def` 子程序、`extern` +- 硬件相关:硬件量子比特、`output` 声明,以及除角度与浮点以外的 `input` 声明 +- 其它语句:`alias`、`pragma`、注解、旧式声明 +- 门修饰符:`negctrl`、`pow`;`ctrl` 与 `inv` 仅在目标门存在对应实现时可用 +- 部分标准门:`ch`、`cp`、`cu`、`cswap` +- 控制流:`while` 在实现中存在下降路径,但运行时条件的循环当前会被语义检查拒绝;`for` 只支持范围为常量的静态展开形式 +- 复杂经典算术、复杂左值切片、多维索引 +- 作用域内的量子比特声明 + +导出侧另有约束: + +- 不导出 `delay`、仅矩阵形式的 `UnitaryGate`、写入标量 `bit` 变量的普通赋值,以及门定义体内的测量 +- 不导出 `while`、`for`、`break`、`continue` 等控制流 +- 不导出 `XY`、`RXY` 两种标准门 + +## 往返行为 + +导出结果可被本接口重新加载。规范化规则: + +- 头部固定为 `OPENQASM 3.0;` 与 `include "stdgates.inc";`,与输入中的头部写法无关。 +- 顶层量子比特统一导出为 `qubit[n] q`,单比特情形同样写作 `qubit[1] q`。 +- 用户经典变量按 `c0`、`c1`… 命名,不可变的测量值按 `v0`、`v1`… 命名。 +- 测量紧接兼容的存储时折叠为赋值形式,如 `c0 = measure q;`。 +- 独立测量写入自动生成的 `bit[k] meas` 寄存器,如 `meas[0] = measure q[0];`。 +- 非连续或重排的寄存器测量拆分为逐位赋值,如 `c0[0] = measure q[2];`。 +- `stdgates.inc` 已提供的门直接调用;扩展门在主线路前生成一次 `gate` 定义,调用处仍使用其名称。 +- 全局相位以 `gphase(...);` 语句导出,重新加载后进入电路的全局相位。 +- 自动生成的名称会避开电路中已有的门名与寄存器名。 + +## 最小示例 + +```rust +use cqlib_core::ir::{qasm3_dumps, qasm3_loads}; + +let qasm = r#" +OPENQASM 3; +include "stdgates.inc"; +qubit[2] q; +h q[0]; +cx q[0], q[1]; +"#; + +let c = qasm3_loads(qasm).unwrap(); +let out = qasm3_dumps(&c).unwrap(); +assert!(out.contains("OPENQASM 3.0;")); +``` + +扩展门定义与往返: + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::ir::{qasm3_dumps, qasm3_loads}; + +let q0 = Qubit::new(0); +let q1 = Qubit::new(1); +let mut circuit = Circuit::new(2); +circuit.x2p(q0).unwrap(); +circuit.rzz(q0, q1, 0.25).unwrap(); + +let qasm = qasm3_dumps(&circuit).unwrap(); +assert!(qasm.contains("gate x2p q { rx(pi/2) q; }")); + +let round_trip = qasm3_loads(&qasm).unwrap(); +assert_eq!(round_trip.operations().len(), 2); +``` diff --git a/docs/api/cn/rust/2_device/0_overview.md b/docs/api/cn/rust/2_device/0_overview.md new file mode 100644 index 00000000..7f0532eb --- /dev/null +++ b/docs/api/cn/rust/2_device/0_overview.md @@ -0,0 +1,248 @@ +# 设备 + +`cqlib_core::device` + +`cqlib_core::device` 描述量子设备的静态信息与任务执行状态:比特与耦合构成的拓扑、比特与边的校准属性、逻辑比特到物理比特的布局、噪声模型,以及执行结果与状态。 + +## Overview + +设备模块把硬件相关的数据拆成几组互相独立的对象:拓扑只描述连接关系,属性只描述标定数据,布局只描述映射,噪声模型只描述信道。编译与执行各自按需取用,互不耦合。 + +### 拓扑 + +`Topology` 由物理比特列表与耦合列表构成,内部维护一张有向图,`graph()` 可以取出底层图引用。耦合是有向的:`(control, target)` 上有耦合,不代表反方向也有,因此连接性查询分成 `successors` / `predecessors`(有向)与 `neighbors_undirected`(无向)两类,度数查询也分成 `out_degree` / `in_degree`。耦合可以带一个名字标签,仅作标识之用。 + +### 设备与属性 + +`Device` 在拓扑之上叠加设备级默认值与局部覆盖,并记录设备名、已注册比特、不可用比特、原生门集与校准时间。设备级配置项大多有两套写法:`with_*` 消费并返回自身,用于链式构造;`set_*` 就地修改。比特级属性由 `QubitProp` 描述(`t1`、`t2`、`frequency`、读出误差与读出混淆概率),边级属性由 `EdgeProp` 描述,两者都持有一组 `InstructionProp`(指令、误差率与可选时长)。 + +查询某个比特的属性时,局部值优先,未设置则回退到设备级默认值:`get_t1` 先看该比特的 `t1`,取不到再返回 `default_t1`。设备还提供 `line`、`line_from_qubits`、`bidirectional_line`、`ring`、`star`、`grid`、`from_edges` 等工厂方法,按常见形态直接生成拓扑与比特集合。 + +### 原生指令能力 + +设备级 `native_gates` 是全局默认能力;某个比特或某条有向边若通过 `with_native_instruction` / `set_native_instruction` 配置了自己的指令列表,该列表就是此处能力的完整覆盖,不会回写默认值。校准数据只让受支持的指令带上误差率与时长,不会让不受支持的指令变为受支持。写入原生能力前会先完成校验,被拒绝的更新不会改变原有能力。`validate_operation`、`validate_value_operation` 与 `validate_circuit` 用同一套规则检查线路是否能在该设备上执行。 + +### 布局 + +`Layout` 维护逻辑比特到物理比特的映射,未承载逻辑比特的物理比特是空闲物理比特。映射可以按 `l2p_map` / `p2l_map` 整体读取,也可以按单个比特查询;`swap_physical` 交换两个物理位置上承载的逻辑比特,这是路由过程中移动逻辑比特的基本操作。 + +### 噪声模型 + +`NoiseModel` 按比特与操作登记噪声信道:读出误差用 `ReadoutError` 表示,单比特信道由 `SingleQubitNoise` 的变体给出(比特翻转、相位翻转、Pauli、去极化、振幅阻尼、相位阻尼),双比特信道由 `TwoQubitNoise` 给出(去极化、独立信道组合、关联 Pauli)。信道可以用 `to_kraus` 转成 Kraus 算符,供密度矩阵模拟使用。查询时用 `OperationKey` 指定门与比特组合。 + +### 执行结果 + +`ExecutionResult` 记录一次任务的生命周期:`new` 构造后处于 `Status::Queued`,`start` 后进入运行态,`finish` / `fail` / `cancel` 结束任务,`from_counts` 可以直接由测量计数构造出已完成的结果。测量结果以 `Outcome` 表示,它把比特串压缩成 64 位分块,同时支持按位置取位与还原比特串;`counts` 与 `probabilities` 分别是各结果的测量次数与归一化概率。 + +--- + +## 常用入口 + +```rust +use std::collections::HashSet; + +use cqlib_core::device::{Device, PhysicalQubit, QubitProp, Topology}; + +let q0 = PhysicalQubit::new(0); +let q1 = PhysicalQubit::new(1); +let topology = Topology::new(vec![q0, q1], vec![(q0, q1, "CX".to_string())]).unwrap(); + +let mut device = Device::new("demo".to_string(), HashSet::from_iter([q0, q1]), topology) + .unwrap() + .with_default_t1(40.0); + +device.add_qubit_properties(q0, QubitProp::new(0.02).with_t1(60.0)).unwrap(); + +assert_eq!(device.get_t1(q0), Some(60.0)); // 局部属性 +assert_eq!(device.get_t1(q1), Some(40.0)); // 回退到设备级默认值 +``` + +--- + +## 核心概念与术语 + +| 术语 | 说明 | +| --- | --- | +| **物理比特** | 设备上的一个物理位置,用 `PhysicalQubit` 表示。 | +| **逻辑比特** | 线路中的一根线,用 `LogicalQubit` 表示;即使编号相同,也与物理比特互相区分。 | +| **拓扑** | 物理比特与它们之间耦合构成的有向图,由 `Topology` 表示。 | +| **耦合** | 两个物理比特之间可以执行双比特门的连接关系。 | +| **有向耦合** | 带方向的耦合:`(control, target)` 上存在耦合不代表反方向也存在。 | +| **原生指令** | 设备上可以直接执行的指令;设备级集合是默认能力,比特与边可以各自覆盖。 | +| **局部覆盖** | 比特或边的原生指令列表,一旦非空就完整取代设备级默认能力,不回写默认值。 | +| **默认值回退** | 查询比特属性时,该比特没有局部值就返回设备级默认值。 | +| **初始布局** | 逻辑比特到物理比特的初始映射,由 `Layout` 表示。 | +| **空闲物理比特** | 当前没有承载逻辑比特的物理比特。 | +| **噪声信道** | 描述一种噪声作用的算符集合,可用 `to_kraus` 转为 Kraus 算符。 | +| **读出误差** | 测量时把 `\|0>` 读成 1 或把 `\|1>` 读成 0 的概率,由 `ReadoutError` 的 `p_0_given_1` 与 `p_1_given_0` 两个字段给出。 | +| **测量结果** | 一次采样得到的比特串,用 `Outcome` 表示。 | + +--- + +## `cqlib_core::device` API 概览 + +### 拓扑 + +| 名字 | 简介 | +| --- | --- | +| [`Topology`](1_topology.md) | 物理比特与耦合列表构成的有向图,含增删与连接性查询。 | +| [`Topology::line`](1_topology.md) | 按直线形态构造拓扑的构造函数。 | + +### 设备与属性 + +| 名字 | 简介 | +| --- | --- | +| [`Device`](2_properties_device.md) | 设备对象:拓扑、比特集合、原生门集、校准时间与默认属性。 | +| [`Device::line`](2_properties_device.md) / [`Device::ring`](2_properties_device.md) / [`Device::star`](2_properties_device.md) / [`Device::grid`](2_properties_device.md) | 按常见形态构建设备的工厂方法。 | +| [`QubitProp`](2_properties_device.md) | 比特属性:读取误差、混淆概率、`t1` / `t2`、频率与原生指令属性。 | +| [`EdgeProp`](2_properties_device.md) | 边属性:该有向边上的原生指令属性。 | +| [`InstructionProp`](2_properties_device.md) | 指令属性:指令对象、误差率与可选时长。 | +| [`Device::validate_circuit`](2_properties_device.md) / [`Device::validate_operation`](2_properties_device.md) | 按原生能力与拓扑校验线路或单个操作。 | + +### 布局 + +| 名字 | 简介 | +| --- | --- | +| [`Layout`](3_layout.md) | 逻辑比特到物理比特的映射,含绑定、解绑与物理比特交换。 | +| [`Layout::from_pairs`](3_layout.md) | 由 `(逻辑编号, 物理编号)` 对与物理比特总数构造布局。 | +| [`LogicalQubit`](3_layout.md) / [`PhysicalQubit`](3_layout.md) | 逻辑比特与物理比特的类型,`Display` 形式分别为 `L0` 与 `P11`。 | + +### 噪声模型 + +| 名字 | 简介 | +| --- | --- | +| [`NoiseModel`](4_noise.md) | 按比特与操作登记噪声信道的容器。 | +| [`SingleQubitNoise`](4_noise.md) | 单比特噪声信道枚举,含比特翻转、相位翻转、Pauli、去极化与两类阻尼。 | +| [`TwoQubitNoise`](4_noise.md) | 双比特噪声信道枚举,含去极化、独立组合与关联 Pauli。 | +| [`ReadoutError`](4_noise.md) | 读出误差,分别记录 `P(0\|1)` 与 `P(1\|0)`。 | +| [`OperationKey`](4_noise.md) | 噪声查询的键,由门与比特下标组合而成。 | + +### 执行结果 + +| 名字 | 简介 | +| --- | --- | +| [`ExecutionResult`](5_result.md) | 任务执行结果,含计数、概率与时间戳。 | +| [`Status`](5_result.md) | 任务状态:`Queued`、`Running`、`Completed`、`Failed` 与 `Cancelled`。 | +| [`Outcome`](5_result.md) | 紧凑的测量结果比特串,支持按位访问与还原。 | + +--- + +## 快速示例 + +### 1. 构建设备并配置属性 + +```rust +use std::collections::HashSet; + +use cqlib_core::device::{Device, PhysicalQubit, QubitProp, Topology}; + +let device = Device::line("line", 3).unwrap(); +assert_eq!(device.name(), "line"); +assert_eq!(device.qubits().count(), 3); +assert_eq!(device.num_usable_qubits(), 3); +assert!(device + .topology() + .supports_directed_coupling(PhysicalQubit::new(0), PhysicalQubit::new(1))); +assert!(!device + .topology() + .supports_directed_coupling(PhysicalQubit::new(1), PhysicalQubit::new(0))); + +let q0 = PhysicalQubit::new(0); +let q1 = PhysicalQubit::new(1); +let topology = Topology::new(vec![q0, q1], vec![(q0, q1, "CX".to_string())]).unwrap(); +let mut device = Device::new("demo".to_string(), HashSet::from_iter([q0, q1]), topology) + .unwrap() + .with_default_t1(40.0) + .with_default_t2(20.0); + +device + .add_qubit_properties(q0, QubitProp::new(0.02).with_t1(60.0).with_t2(30.0)) + .unwrap(); + +assert_eq!(device.get_t1(q0), Some(60.0)); +assert_eq!(device.get_readout_error(q0), Some(0.02)); +``` + +### 2. 建立布局 + +```rust +use cqlib_core::device::{Layout, LogicalQubit, PhysicalQubit}; + +let logical = vec![LogicalQubit::new(0), LogicalQubit::new(1)]; +let physical = vec![ + PhysicalQubit::new(100), + PhysicalQubit::new(101), + PhysicalQubit::new(102), +]; + +let layout = Layout::new(logical, physical, None).unwrap(); + +assert_eq!(layout.num_logical(), 2); +assert_eq!(layout.num_physical(), 3); +assert_eq!(layout.num_vacant_physical(), 1); +assert_eq!( + layout.vacant_physical_qubits().collect::>(), + vec![PhysicalQubit::new(102)] +); +``` + +也可以直接由比特对构造: + +```rust +use cqlib_core::device::{Layout, LogicalQubit, PhysicalQubit}; + +let layout = Layout::from_pairs(&[(2, 3), (0, 1)], 5).unwrap(); + +assert_eq!(layout.num_vacant_physical(), 3); +assert_eq!(layout.get_physical(LogicalQubit::new(2)), Some(PhysicalQubit::new(3))); +assert_eq!(layout.get_physical(LogicalQubit::new(1)), None); +``` + +### 3. 由测量计数构造执行结果 + +```rust +use std::collections::HashMap; + +use cqlib_core::circuit::Qubit; +use cqlib_core::device::{ExecutionResult, Outcome, Status}; + +let zero = Outcome::from_indices(1, []); +let one = Outcome::from_indices(1, [0]); + +let mut counts = HashMap::new(); +counts.insert(zero.clone(), 3); +counts.insert(one.clone(), 1); + +let result = ExecutionResult::from_counts( + "task".to_string(), + vec![Qubit::new(0)], + 4, + 1, + Some("backend".to_string()), + counts, +); + +assert_eq!(result.status(), &Status::Completed); +assert_eq!(result.counts().get(&zero), Some(&3)); + +let probabilities = result.probabilities().as_ref().unwrap(); +assert_eq!(probabilities.get(&zero), Some(&0.75)); +``` + +--- + +## 校验与错误处理 + +设备模块的错误按领域分成几个枚举,模块根导出 `DeviceError`、`DeviceValidationError`、`TopologyError` 与 `LayoutError`;`NoiseError` 与 `OutcomeError` 位于各自的子模块中。 + +| 错误 | 触发场景 | +| --- | --- | +| `TopologyError::{QubitNotFound, CouplingNotFound, QubitAlreadyExists, CouplingAlreadyExists, SelfCoupling, DuplicateQubitRemoval, DuplicateCouplingRemoval}` | 拓扑构造与修改:操作未知或重复的比特与耦合、自耦合、重复删除。 | +| `DeviceError::{InvalidOnlineQubit, QubitNotInDevice, QubitNotInTopology, EdgeNotInTopology, InvalidTopology}` | 设备构造与属性写入时,比特或边不在设备或拓扑中,或底层拓扑本身非法。 | +| `DeviceError::{NonStandardNativeInstruction, InvalidNativeInstructionArity, InvalidNativeInstructionErrorRate, InvalidNativeInstructionDuration}` | 原生指令不合法:非标准门、元数超出允许范围(设备级至多两比特、比特级恰好一比特、边级恰好两比特),或误差率与时长取值非法。 | +| `DeviceValidationError::{UnusablePhysicalQubit, MissingDirectedCoupling, UnsupportedInstruction, UndecomposedInstruction}` | `validate_operation`、`validate_value_operation` 与 `validate_circuit` 校验失败:比特不可用、缺少有向耦合、指令不在原生门集内或指令尚未分解。 | +| `LayoutError::{TooManyLogicalQubits, DuplicateLogicalQubit, DuplicatePhysicalQubit, InvalidLogicalQubit, InvalidPhysicalQubit, LogicalQubitAlreadyBound, PhysicalQubitAlreadyOccupied, LogicalQubitNotBound}` | 布局构造与绑定操作:逻辑比特多于物理比特、重复或非法编号、目标位置已被占用、解绑未绑定的逻辑比特。 | +| `NoiseError::{InvalidProbability, QubitCollision, InconsistentArity, Internal}` | 噪声信道参数超出 `[0, 1]`、同一个门内比特重复、元数与门不符;路径为 `cqlib_core::device::noise::NoiseError`。 | +| `OutcomeError::InvalidCharacter` | `Outcome::from_bitstring` 的比特串中含非 `0` / `1` 字符;路径为 `cqlib_core::device::result::OutcomeError`。 | + +设备级与比特/边级的原生能力写入都会先校验再落盘,被拒绝的更新不会改变对象原有状态。 diff --git a/docs/api/cn/rust/2_device/1_topology.md b/docs/api/cn/rust/2_device/1_topology.md new file mode 100644 index 00000000..feeefa29 --- /dev/null +++ b/docs/api/cn/rust/2_device/1_topology.md @@ -0,0 +1,107 @@ +# Topology + +`Topology` 是 `cqlib_core::device` 中的硬件连接图结构,使用有向图表示比特耦合关系。 + +## 导入 + +```rust +use cqlib_core::device::{PhysicalQubit, Topology, TopologyError}; +``` + +## 构造 + +### `Topology::new(qubits, coupling_map) -> Result` + +参数: + +- `qubits: Vec` +- `coupling_map: Vec<(PhysicalQubit, PhysicalQubit, String)>` + +说明: + +- 边按有向语义处理,`(q0, q1)` 与 `(q1, q0)` 不等价。 +- 同一有序比特对只允许一条耦合,名称不同也算重复;重复比特与自耦合返回错误。 + +### `Topology::line(qubits) -> Result` + +参数: + +- `qubits: Vec` + +说明: + +- 按给定顺序构造有向直线 `qubits[0] -> qubits[1] -> ...`,耦合数为 `len(qubits) - 1`,耦合名为空串。 + +## 只读接口 + +- `graph(&self) -> &StableGraph` +- `num_qubits(&self) -> usize` +- `num_couplings(&self) -> usize` +- `qubits(&self) -> impl Iterator` +- `supports_directed_coupling(&self, control: PhysicalQubit, target: PhysicalQubit) -> bool` +- `supports_coupling_either_direction(&self, a: PhysicalQubit, b: PhysicalQubit) -> bool` +- `successors(&self, qubit: PhysicalQubit) -> impl Iterator` +- `predecessors(&self, qubit: PhysicalQubit) -> impl Iterator` +- `neighbors_undirected(&self, qubit: PhysicalQubit) -> impl Iterator` +- `undirected_edges(&self) -> impl Iterator` +- `get_coupling_name(&self, control: PhysicalQubit, target: PhysicalQubit) -> Option` +- `contains_qubit(&self, qubit: &PhysicalQubit) -> bool` +- `out_degree(&self, qubit: &PhysicalQubit) -> usize` +- `in_degree(&self, qubit: &PhysicalQubit) -> usize` + +说明: + +- `successors` / `out_degree` 基于出边统计,`predecessors` / `in_degree` 基于入边统计。 +- `undirected_edges` 的每对按键值排序,方向相反的两条耦合折叠为一条。 +- 比特不在拓扑中时,邻居类查询返回空迭代,度数查询返回 `0`。 + +## 修改接口 + +- `add_qubits(&mut self, qubits: impl IntoIterator) -> Result<(), TopologyError>` +- `add_couplings(&mut self, couplings: impl IntoIterator) -> Result<(), TopologyError>` +- `remove_qubits(&mut self, qubits: impl IntoIterator) -> Result<(), TopologyError>` +- `remove_couplings(&mut self, couplings: impl IntoIterator) -> Result<(), TopologyError>` + +常见错误: + +- `TopologyError::QubitNotFound` +- `TopologyError::QubitAlreadyExists` +- `TopologyError::CouplingNotFound` +- `TopologyError::CouplingAlreadyExists` +- `TopologyError::SelfCoupling` +- `TopologyError::DuplicateQubitRemoval` +- `TopologyError::DuplicateCouplingRemoval` + +## 示例 + +```rust +use cqlib_core::device::{PhysicalQubit, Topology}; + +let q0 = PhysicalQubit::new(0); +let q1 = PhysicalQubit::new(1); +let q2 = PhysicalQubit::new(2); + +let mut topo = Topology::new( + vec![q0, q1, q2], + vec![(q0, q1, "CX".to_string()), (q1, q2, "CZ".to_string())], +) +.unwrap(); + +assert_eq!(topo.num_qubits(), 3); +assert_eq!(topo.num_couplings(), 2); +assert!(topo.supports_directed_coupling(q1, q2)); +assert!(!topo.supports_directed_coupling(q2, q1)); +assert!(topo.supports_coupling_either_direction(q2, q1)); +assert_eq!(topo.get_coupling_name(q1, q2).as_deref(), Some("CZ")); +assert_eq!(topo.successors(q1).collect::>(), vec![q2]); +assert_eq!(topo.neighbors_undirected(q1).collect::>(), vec![q0, q2]); +assert_eq!(topo.out_degree(&q1), 1); +assert_eq!(topo.in_degree(&q1), 1); + +topo.add_qubits([PhysicalQubit::new(3)]).unwrap(); +topo.add_couplings([(q2, PhysicalQubit::new(3), "CX".to_string())]) + .unwrap(); +topo.remove_couplings([(q2, PhysicalQubit::new(3))]).unwrap(); +topo.remove_qubits([PhysicalQubit::new(3)]).unwrap(); +assert_eq!(topo.num_qubits(), 3); +``` diff --git a/docs/api/cn/rust/2_device/2_properties_device.md b/docs/api/cn/rust/2_device/2_properties_device.md new file mode 100644 index 00000000..4821a121 --- /dev/null +++ b/docs/api/cn/rust/2_device/2_properties_device.md @@ -0,0 +1,246 @@ +# Device / Properties + +本页覆盖 `cqlib_core::device` 中用于设备属性建模的类型: + +- `InstructionProp` +- `QubitProp` +- `EdgeProp` +- `Device` + +## 导入 + +```rust +use std::collections::HashSet; + +use cqlib_core::circuit::{Instruction, StandardGate}; +use cqlib_core::device::{ + Device, DeviceError, EdgeProp, InstructionProp, PhysicalQubit, QubitProp, Topology, +}; +use time::OffsetDateTime; +``` + +## InstructionProp + +### 构造与方法 + +- `InstructionProp::new(instruction: Instruction, error_rate: f64) -> InstructionProp` +- `with_length(self, length: f64) -> InstructionProp` +- `set_length(&mut self, length: f64)` +- `with_instruction(self, instruction: Instruction) -> InstructionProp` +- `set_instruction(&mut self, instruction: Instruction)` +- `with_error_rate(self, error_rate: f64) -> InstructionProp` +- `set_error_rate(&mut self, error_rate: f64)` + +### 只读接口 + +- `instruction(&self) -> &Instruction` +- `error_rate(&self) -> f64` +- `length(&self) -> Option` + +## QubitProp + +### 构造与 Builder + +- `QubitProp::new(readout_error: f64) -> QubitProp` +- `with_prob_meas0_prep1(self, prob: f64) -> QubitProp` +- `set_prob_meas0_prep1(&mut self, prob: f64)` +- `with_prob_meas1_prep0(self, prob: f64) -> QubitProp` +- `set_prob_meas1_prep0(&mut self, prob: f64)` +- `with_t1(self, t1: f64) -> QubitProp` +- `set_t1(&mut self, t1: f64)` +- `with_t2(self, t2: f64) -> QubitProp` +- `set_t2(&mut self, t2: f64)` +- `with_frequency(self, frequency: f64) -> QubitProp` +- `set_frequency(&mut self, frequency: f64)` +- `with_native_instruction(self, prop: InstructionProp) -> Result` +- `set_native_instruction(&mut self, prop: InstructionProp) -> Result<(), DeviceError>` + +### 只读接口 + +- `readout_error(&self) -> f64` +- `prob_meas0_prep1(&self) -> Option` +- `prob_meas1_prep0(&self) -> Option` +- `t1(&self) -> Option` +- `t2(&self) -> Option` +- `frequency(&self) -> Option` +- `native_instructions(&self) -> &[InstructionProp]` + +说明: + +- 比特属性的原生指令只接受单比特标准门,非单比特指令返回 `DeviceError::InvalidNativeInstructionArity`。 + +## EdgeProp + +### 构造与方法 + +- `EdgeProp::new() -> EdgeProp` +- `with_native_instruction(self, prop: InstructionProp) -> Result` +- `set_native_instruction(&mut self, prop: InstructionProp) -> Result<(), DeviceError>` +- `native_instructions(&self) -> &[InstructionProp]` + +说明: + +- 边属性的原生指令只接受双比特标准门。 + +## Device + +### 构造 + +- `Device::new(name: impl Into, qubits: HashSet, topology: Topology) -> Result` + +说明: + +- 拓扑中的每个比特都必须登记在 `qubits` 中,否则返回 `DeviceError::InvalidOnlineQubit`。 + +### 静态构造方法 + +- `line(name: impl Into, num_qubits: u32) -> Result` +- `line_from_qubits(name: impl Into, physical_qubits: Vec) -> Result` +- `bidirectional_line(name: impl Into, num_qubits: u32) -> Result` +- `ring(name: impl Into, num_qubits: u32) -> Result` +- `star(name: impl Into, num_qubits: u32, center: u32) -> Result` +- `grid(name: impl Into, rows: u32, cols: u32) -> Result` +- `from_edges(name: impl Into, num_qubits: u32, edges: &[(u32, u32)]) -> Result` + +说明: + +- 静态构造方法生成的比特全部在线;直线类形态的耦合名为空串。 + +### 配置接口(builder) + +- `with_invalid_qubits(self, invalid_qubits: HashSet) -> Result` +- `set_invalid_qubits(&mut self, invalid_qubits: HashSet) -> Result<(), DeviceError>` +- `with_native_gates(self, gates: Vec) -> Result` +- `set_native_gates(&mut self, gates: Vec) -> Result<(), DeviceError>` +- `with_calibration_time(self, time: OffsetDateTime) -> Device` +- `set_calibration_time(&mut self, time: OffsetDateTime)` +- `with_default_t1(self, t1: f64) -> Device` +- `set_default_t1(&mut self, t1: f64)` +- `with_default_t2(self, t2: f64) -> Device` +- `set_default_t2(&mut self, t2: f64)` +- `with_default_readout_error(self, error: f64) -> Device` +- `set_default_readout_error(&mut self, error: f64)` +- `with_default_single_qubit_error(self, error: f64) -> Device` +- `set_default_single_qubit_error(&mut self, error: f64)` +- `with_default_two_qubit_error(self, error: f64) -> Device` +- `set_default_two_qubit_error(&mut self, error: f64)` + +说明: + +- `with_native_gates` 与 `set_native_gates` 只接受至多两比特的标准门,非法项使整次写入被拒绝,原有门集保持不变。 + +### 写入接口 + +- `add_qubit_properties(&mut self, qubit: PhysicalQubit, props: QubitProp) -> Result<(), DeviceError>` +- `add_edge_properties(&mut self, control: PhysicalQubit, target: PhysicalQubit, props: EdgeProp) -> Result<(), DeviceError>` + +常见错误: + +- `DeviceError::InvalidOnlineQubit` +- `DeviceError::QubitNotInDevice` +- `DeviceError::QubitNotInTopology` +- `DeviceError::EdgeNotInTopology` +- `DeviceError::NonStandardNativeInstruction` +- `DeviceError::InvalidNativeInstructionArity` +- `DeviceError::InvalidNativeInstructionErrorRate` +- `DeviceError::InvalidNativeInstructionDuration` + +### 查询接口 + +- `name(&self) -> &str` +- `qubits(&self) -> impl Iterator` +- `invalid_qubits(&self) -> impl Iterator` +- `is_usable_qubit(&self, qubit: PhysicalQubit) -> bool` +- `usable_qubits(&self) -> impl Iterator` +- `num_usable_qubits(&self) -> usize` +- `topology(&self) -> &Topology` +- `native_gates(&self) -> &[Instruction]` +- `qubit_properties(&self, qubit: PhysicalQubit) -> Option<&QubitProp>` +- `edge_properties(&self, control: PhysicalQubit, target: PhysicalQubit) -> Option<&EdgeProp>` +- `supports_native_instruction(&self, instruction: &Instruction, qargs: &[PhysicalQubit]) -> bool` +- `validate_operation(&self, operation: &Operation) -> Result<(), DeviceValidationError>` +- `validate_value_operation(&self, operation: &ValueOperation) -> Result<(), DeviceValidationError>` +- `validate_circuit(&self, circuit: &Circuit) -> Result<(), DeviceValidationError>` +- `single_qubit_error(&self, qubit: PhysicalQubit, instruction: &Instruction) -> Option` +- `two_qubit_error(&self, control: PhysicalQubit, target: PhysicalQubit, instruction: &Instruction) -> Option` +- `edge_error(&self, control: PhysicalQubit, target: PhysicalQubit) -> Option` +- `get_t1(&self, qubit: PhysicalQubit) -> Option` +- `get_t2(&self, qubit: PhysicalQubit) -> Option` +- `get_readout_error(&self, qubit: PhysicalQubit) -> Option` +- `default_single_qubit_error(&self) -> Option` +- `default_two_qubit_error(&self) -> Option` +- `default_t1(&self) -> Option` +- `default_t2(&self) -> Option` +- `default_readout_error(&self) -> Option` +- `calibration_time(&self) -> Option` + +说明: + +- `get_t1` / `get_t2` / `get_readout_error` 优先读取局部属性,缺失时回退默认值。 +- `single_qubit_error` / `two_qubit_error` 优先取局部原生指令属性,缺失时回退默认的单比特 / 双比特误差率;比特不可用或该方向不存在时返回 `None`。 +- `edge_error` 只读标定数据,取该有向边上原生指令的最小误差率。 + +## 示例 + +```rust +use std::collections::HashSet; + +use cqlib_core::circuit::{Instruction, StandardGate}; +use cqlib_core::device::{Device, EdgeProp, InstructionProp, PhysicalQubit, QubitProp, Topology}; + +let q0 = PhysicalQubit::new(0); +let q1 = PhysicalQubit::new(1); +let q2 = PhysicalQubit::new(2); + +let topology = Topology::new( + vec![q0, q1, q2], + vec![(q0, q1, "CX".to_string()), (q1, q2, "CX".to_string())], +) +.unwrap(); + +let mut device = Device::new("mock_backend", HashSet::from([q0, q1, q2]), topology) + .unwrap() + .with_default_t1(50.0) + .with_default_t2(35.0) + .with_default_readout_error(0.05) + .with_native_gates(vec![ + Instruction::Standard(StandardGate::H), + Instruction::Standard(StandardGate::CX), + ]) + .unwrap(); + +let qp0 = QubitProp::new(0.02) + .with_t1(80.0) + .with_t2(70.0) + .with_native_instruction(InstructionProp::new( + Instruction::Standard(StandardGate::H), + 0.001, + )) + .unwrap(); +device.add_qubit_properties(q0, qp0).unwrap(); + +let ep01 = EdgeProp::new() + .with_native_instruction(InstructionProp::new( + Instruction::Standard(StandardGate::CX), + 0.02, + )) + .unwrap(); +device.add_edge_properties(q0, q1, ep01).unwrap(); + +assert_eq!(device.get_t1(q0), Some(80.0)); +assert_eq!(device.get_t1(q2), Some(50.0)); // 回退默认值 +assert_eq!( + device.single_qubit_error(q0, &Instruction::Standard(StandardGate::H)), + Some(0.001) +); +assert_eq!( + device.two_qubit_error(q0, q1, &Instruction::Standard(StandardGate::CX)), + Some(0.02) +); +assert_eq!(device.edge_error(q0, q1), Some(0.02)); +assert_eq!(device.num_usable_qubits(), 3); + +device.set_invalid_qubits(HashSet::from([q2])).unwrap(); +assert!(!device.is_usable_qubit(q2)); +assert_eq!(device.num_usable_qubits(), 2); +``` diff --git a/docs/api/cn/rust/2_device/3_layout.md b/docs/api/cn/rust/2_device/3_layout.md new file mode 100644 index 00000000..7fff6114 --- /dev/null +++ b/docs/api/cn/rust/2_device/3_layout.md @@ -0,0 +1,118 @@ +# Layout + +`Layout` 用于维护路由过程中的逻辑比特与物理比特映射关系。 + +## 导入 + +```rust +use std::collections::BTreeMap; + +use cqlib_core::device::{Layout, LayoutError, LogicalQubit, PhysicalQubit}; +``` + +## 构造 + +### `Layout::new(logical, physical, init_map) -> Result` + +参数: + +- `logical: Vec` +- `physical: Vec` +- `init_map: Option>`(逻辑 -> 物理) + +说明: + +- 未出现在 `init_map` 中的逻辑比特按输入顺序映射到剩余的空闲物理比特。 + +常见错误: + +- `LayoutError::TooManyLogicalQubits` +- `LayoutError::DuplicateLogicalQubit` +- `LayoutError::DuplicatePhysicalQubit` +- `LayoutError::InvalidLogicalQubit` +- `LayoutError::InvalidPhysicalQubit` + +### `Layout::from_pairs(logical_physical, physical_count) -> Result` + +参数: + +- `logical_physical: &[(u32, u32)]`:`(逻辑编号, 物理编号)` 对。 +- `physical_count: u32`:物理比特总数,物理比特为 `0..physical_count`。 + +说明: + +- 逻辑比特即 `logical_physical` 中出现的逻辑编号;未被引用的物理比特是空闲物理比特。 + +## 只读接口 + +- `num_logical(&self) -> usize` +- `num_physical(&self) -> usize` +- `num_vacant_physical(&self) -> usize` +- `get_physical(&self, logical: LogicalQubit) -> Option` +- `get_logical(&self, physical: PhysicalQubit) -> Option` +- `logical_qubits(&self) -> impl Iterator` +- `physical_qubits(&self) -> impl Iterator` +- `vacant_physical_qubits(&self) -> impl Iterator` +- `is_physical_vacant(&self, physical: PhysicalQubit) -> bool` +- `l2p_map(&self) -> &BTreeMap` +- `p2l_map(&self) -> &BTreeMap` + +## 更新接口 + +- `bind(&mut self, logical: LogicalQubit, physical: PhysicalQubit) -> Result<(), LayoutError>` +- `unbind(&mut self, logical: LogicalQubit) -> Result` +- `swap_physical(&mut self, phys_a: PhysicalQubit, phys_b: PhysicalQubit) -> Result<(), LayoutError>` + +交换两个物理比特上承载的逻辑比特,这是路由过程中移动逻辑比特的基本操作。任一物理比特可以空闲:与空闲物理比特交换会把逻辑比特搬到空闲位置。 + +常见错误: + +- `LayoutError::LogicalQubitAlreadyBound` +- `LayoutError::PhysicalQubitAlreadyOccupied` +- `LayoutError::LogicalQubitNotBound` + +注意: + +- 物理比特不在布局集合内时返回 `LayoutError::InvalidPhysicalQubit`,不触发 panic。 + +## 示例 + +```rust +use std::collections::BTreeMap; + +use cqlib_core::device::{Layout, LogicalQubit, PhysicalQubit}; + +let logical = vec![LogicalQubit::new(0), LogicalQubit::new(1)]; +let physical = vec![ + PhysicalQubit::new(10), + PhysicalQubit::new(11), + PhysicalQubit::new(12), +]; +let init_map = [(LogicalQubit::new(0), PhysicalQubit::new(11))] + .into_iter() + .collect::>(); + +let mut layout = Layout::new(logical, physical, Some(init_map)).unwrap(); +assert_eq!(layout.num_logical(), 2); +assert_eq!(layout.num_physical(), 3); +assert_eq!(layout.num_vacant_physical(), 1); +assert_eq!( + layout.get_physical(LogicalQubit::new(0)), + Some(PhysicalQubit::new(11)) +); +assert!(layout.is_physical_vacant(PhysicalQubit::new(12))); + +let before_11 = layout.get_logical(PhysicalQubit::new(11)); +let before_12 = layout.get_logical(PhysicalQubit::new(12)); +layout + .swap_physical(PhysicalQubit::new(11), PhysicalQubit::new(12)) + .unwrap(); +assert_eq!(layout.get_logical(PhysicalQubit::new(12)), before_11); +assert_eq!(layout.get_logical(PhysicalQubit::new(11)), before_12); + +assert_eq!( + layout.unbind(LogicalQubit::new(0)).unwrap(), + PhysicalQubit::new(12) +); +assert!(layout.is_physical_vacant(PhysicalQubit::new(12))); +``` diff --git a/docs/api/rust/2_device/4_noise.md b/docs/api/cn/rust/2_device/4_noise.md similarity index 58% rename from docs/api/rust/2_device/4_noise.md rename to docs/api/cn/rust/2_device/4_noise.md index 259f4bf7..73fa572e 100644 --- a/docs/api/rust/2_device/4_noise.md +++ b/docs/api/cn/rust/2_device/4_noise.md @@ -13,13 +13,17 @@ ```rust use cqlib_core::circuit::{Qubit, StandardGate}; +use cqlib_core::device::noise::NoiseError; use cqlib_core::device::{ - NoiseError, NoiseModel, OperationKey, ReadoutError, SingleQubitNoise, TwoQubitNoise, + NoiseModel, OperationKey, ReadoutError, SingleQubitNoise, TwoQubitNoise, }; +use cqlib_core::qis::Pauli; ``` ## NoiseError +定义在 `cqlib_core::device::noise`,不在 `cqlib_core::device` 模块根重导出。 + 常见错误变体: - `InvalidProbability { value, context }` @@ -71,9 +75,9 @@ use cqlib_core::device::{ 构造: -- `OperationKey::new_single(gate, q0) -> OperationKey` -- `OperationKey::new_double(gate, q0, q1) -> Result` -- `OperationKey::new_triple(gate, q0, q1, q2) -> Result` +- `OperationKey::new_single(gate: StandardGate, q0: Qubit) -> OperationKey` +- `OperationKey::new_double(gate: StandardGate, q0: Qubit, q1: Qubit) -> Result` +- `OperationKey::new_triple(gate: StandardGate, q0: Qubit, q1: Qubit, q2: Qubit) -> Result` 查询: @@ -92,9 +96,9 @@ use cqlib_core::device::{ 写入: -- `add_readout_error(&mut self, qubit, error) -> Result<(), String>` -- `add_single_qubit_error(&mut self, gate, qubit, noise) -> Result<(), NoiseError>` -- `add_two_qubit_error(&mut self, gate, q0, q1, noise) -> Result<(), NoiseError>` +- `add_readout_error(&mut self, qubit: Qubit, error: ReadoutError) -> Result<(), NoiseError>` +- `add_single_qubit_error(&mut self, gate: StandardGate, qubit: Qubit, noise: SingleQubitNoise) -> Result<(), NoiseError>` +- `add_two_qubit_error(&mut self, gate: StandardGate, q0: Qubit, q1: Qubit, noise: TwoQubitNoise) -> Result<(), NoiseError>` 查询: @@ -102,11 +106,17 @@ use cqlib_core::device::{ - `get_single_qubit_errors(&self, key: &OperationKey) -> Option<&Vec>` - `get_two_qubit_errors(&self, key: &OperationKey) -> Option<&Vec>` +说明: + +- 同一个门与比特组合可以登记多条信道,查询返回列表;未登记的组合返回 `None`。 +- 双比特门的两个比特相同会让 `add_two_qubit_error` 返回 `NoiseError::QubitCollision`。 + ## 示例 ```rust use cqlib_core::circuit::{Qubit, StandardGate}; use cqlib_core::device::{NoiseModel, OperationKey, ReadoutError, SingleQubitNoise, TwoQubitNoise}; +use cqlib_core::qis::Pauli; let mut nm = NoiseModel::new(); @@ -125,6 +135,12 @@ nm.add_single_qubit_error( SingleQubitNoise::BitFlip(0.005), ) .unwrap(); +nm.add_single_qubit_error( + StandardGate::X, + Qubit::new(0), + SingleQubitNoise::PhaseFlip(0.004), +) +.unwrap(); nm.add_two_qubit_error( StandardGate::CX, @@ -133,11 +149,23 @@ nm.add_two_qubit_error( TwoQubitNoise::Depolarizing(0.02), ) .unwrap(); +nm.add_two_qubit_error( + StandardGate::CX, + Qubit::new(0), + Qubit::new(1), + TwoQubitNoise::CorrelatedPauli { + op_q0: Pauli::X, + op_q1: Pauli::Z, + p: 0.05, + }, +) +.unwrap(); let skey = OperationKey::new_single(StandardGate::X, Qubit::new(0)); let tkey = OperationKey::new_double(StandardGate::CX, Qubit::new(0), Qubit::new(1)).unwrap(); -assert!(nm.get_single_qubit_errors(&skey).is_some()); -assert!(nm.get_two_qubit_errors(&tkey).is_some()); +assert_eq!(nm.get_single_qubit_errors(&skey).unwrap().len(), 2); +assert_eq!(nm.get_two_qubit_errors(&tkey).unwrap().len(), 2); +assert_eq!(nm.get_readout_error(&Qubit::new(0)).unwrap().p_0_given_1, 0.02); +assert!(nm.get_readout_error(&Qubit::new(9)).is_none()); ``` - diff --git a/docs/api/cn/rust/2_device/5_result.md b/docs/api/cn/rust/2_device/5_result.md new file mode 100644 index 00000000..95757629 --- /dev/null +++ b/docs/api/cn/rust/2_device/5_result.md @@ -0,0 +1,171 @@ +# Outcome / Status / ExecutionResult + +本页覆盖量子任务结果相关类型: + +- `Outcome` +- `Status` +- `ExecutionResult` +- `OutcomeError` + +## 导入 + +```rust +use cqlib_core::circuit::Qubit; +use cqlib_core::device::result::OutcomeError; +use cqlib_core::device::{ExecutionResult, Outcome, Status}; +use std::collections::HashMap; +use time::OffsetDateTime; +``` + +## OutcomeError + +定义在 `cqlib_core::device::result`,不在 `cqlib_core::device` 模块根重导出。 + +当前包含: + +- `OutcomeError::InvalidCharacter(index, ch)`:比特串含非法字符。 + +## Outcome + +### 构造与转换 + +- `Outcome::new(chunks: SmallVec<[u64; 4]>) -> Outcome` +- `Outcome::from_indices(width: usize, indices: impl IntoIterator) -> Outcome`:构造宽度为 `width`、仅在 `indices` 列出的位置取值为 `1` 的结果,超出 `width` 的下标被忽略。 +- `Outcome::from_bitstring(s: &str) -> Result` +- 实现 `FromStr`,`s.parse::()` 等价于 `Outcome::from_bitstring(s)`,错误类型同为 `OutcomeError`。 +- `to_bitstring(&self, num_qubits: usize) -> String`:按给定宽度输出比特串。 + +说明: + +- 比特串一律按标准二进制形式书写(最高位在左),而内部按小端分块存储。 +- 位串宽度不由 `Outcome` 保存:解析 `"001"` 与解析 `"1"` 得到同一个 `Outcome`。因此输出宽度必须由调用方通过 `to_bitstring` 指定;请求宽度小于已存储值时,高位被截断。 +- `Outcome` 实现了 `Clone + Eq + Hash`,可直接作为计数字典 `HashMap` 的 key。 + +### 查询 + +- `is_one(&self, index: usize) -> bool` + +## Status + +枚举变体: + +- `Queued` +- `Running` +- `Completed` +- `Failed { error_msg: String, error_code: i32 }` +- `Cancelled` + +方法: + +- `is_terminal(&self) -> bool` +- `is_success(&self) -> bool` + +## ExecutionResult + +### 构造 + +```rust +ExecutionResult::new( + task_id: String, + qubits: Vec, + shots: usize, + num_qubits: usize, + backend: Option, + created_at: Option, +) -> ExecutionResult +``` + +```rust +ExecutionResult::from_counts( + task_id: String, + qubits: Vec, + shots: usize, + num_qubits: usize, + backend: Option, + counts: HashMap, +) -> ExecutionResult +``` + +说明: + +- `from_counts` 依次应用构造、`start`、`finish`、`calc_probabilities`,直接得到已完成的结果。 + +### 生命周期方法 + +- `start(&mut self, t: Option) -> &mut Self` +- `finish(&mut self, counts: HashMap, t: Option) -> &mut Self` +- `fail(&mut self, msg: String, code: i32)` +- `cancel(&mut self)` +- `calc_probabilities(&mut self) -> &mut Self` + +说明: + +- `finish` 写入计数时清除已计算的概率;`calc_probabilities` 按已记录计数的总和归一化,计数总和为 0 时不产生概率。 + +### 读取方法 + +- `task_id(&self) -> &str` +- `shots(&self) -> usize` +- `num_qubits(&self) -> usize` +- `qubits(&self) -> &Vec` +- `status(&self) -> &Status` +- `created_at(&self) -> &OffsetDateTime` +- `started_at(&self) -> &Option` +- `finished_at(&self) -> &Option` +- `backend(&self) -> Option<&String>` +- `counts(&self) -> &HashMap` +- `probabilities(&self) -> &Option>` + +说明: + +- `qubits[i]` 对应比特串第 `i` 位(权重 `2^i`);比特串按最高位在左书写,字符串顺序与 `qubits` 顺序相反。 + +## 示例 + +```rust +use cqlib_core::circuit::Qubit; +use cqlib_core::device::{ExecutionResult, Outcome, Status}; +use std::collections::HashMap; + +// 比特串按最高位在左书写:位 2 在左,位 0 在右。 +let outcome = Outcome::from_indices(3, [0, 2]); +assert!(outcome.is_one(0)); +assert!(!outcome.is_one(1)); +assert_eq!(outcome.to_bitstring(3), "101"); +assert_eq!(Outcome::from_bitstring("101").unwrap(), outcome); +assert!(Outcome::from_bitstring("10a").is_err()); + +let mut result = ExecutionResult::new( + "task-1".to_string(), + vec![Qubit::new(0), Qubit::new(1)], + 1000, + 2, + Some("sim".to_string()), + None, +); + +assert_eq!(result.status(), &Status::Queued); +result.start(None); +assert_eq!(result.status(), &Status::Running); + +let mut counts = HashMap::new(); +counts.insert(Outcome::from_bitstring("00").unwrap(), 750); +counts.insert(Outcome::from_bitstring("11").unwrap(), 250); +result.finish(counts, None).calc_probabilities(); + +assert_eq!(result.status(), &Status::Completed); +assert_eq!(result.counts().len(), 2); +assert_eq!(result.probabilities().as_ref().unwrap().len(), 2); + +let done = ExecutionResult::from_counts( + "task-2".to_string(), + vec![Qubit::new(0), Qubit::new(1)], + 1000, + 2, + Some("sim".to_string()), + HashMap::from([(Outcome::from_bitstring("01").unwrap(), 400)]), +); +assert_eq!(done.status(), &Status::Completed); +assert!(done.counts().contains_key(&Outcome::from_bitstring("01").unwrap())); +assert!(done.probabilities().is_some()); +``` diff --git a/docs/api/cn/rust/2_device/6_qubits.md b/docs/api/cn/rust/2_device/6_qubits.md new file mode 100644 index 00000000..dbb09d9d --- /dev/null +++ b/docs/api/cn/rust/2_device/6_qubits.md @@ -0,0 +1,156 @@ +# 量子比特标识 + +`cqlib_core::device` + +`LogicalQubit` 与 `PhysicalQubit` 是设备侧接口使用的强类型比特标识:前者标识线路中的逻辑比特,后者标识设备上的物理比特。两个类型都是 `Qubit` 的新类型包装,内部表示相同,但在类型层面互相区分。 + +## 导入 + +```rust +use cqlib_core::circuit::Qubit; +use cqlib_core::device::{Layout, LogicalQubit, PhysicalQubit}; +``` + +--- + +## LogicalQubit + +逻辑比特标识,用于线路中的逻辑比特。 + +```rust +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)] +#[repr(transparent)] +pub struct LogicalQubit(Qubit); +``` + +字段: + +- 内部的 `Qubit` 字段为私有,不通过公开接口直接访问。 + +### 构造 + +- `LogicalQubit::new(id: u32) -> LogicalQubit`:从数值标识构造,`const fn`。 +- `LogicalQubit::from_qubit(qubit: Qubit) -> LogicalQubit`:包装既有的线路比特,`const fn`。 +- 实现 `From`,等价于 `from_qubit`。 + +### 方法 + +- `fn qubit(self) -> Qubit`:返回底层线路比特,`const fn`。 +- `fn id(self) -> u32`:返回数值标识,`const fn`。 + +### trait 实现 + +- `Debug`、`Clone`、`Copy`、`PartialEq`、`Eq`、`Hash`、`Ord`、`PartialOrd`:派生实现,顺序与哈希基于数值标识。 +- `Display`:输出 `L{id}`,例如 `L0`。 +- `From for LogicalQubit`:等价于 `LogicalQubit::from_qubit`。 +- `From for Qubit`:等价于 `LogicalQubit::qubit`。 + +示例: + +```rust +use cqlib_core::circuit::Qubit; +use cqlib_core::device::LogicalQubit; + +let qubit = Qubit::new(3); +let logical = LogicalQubit::from_qubit(qubit); + +assert_eq!(logical.id(), 3); +assert_eq!(logical.qubit(), qubit); +assert_eq!(Qubit::from(logical), qubit); +assert_eq!(logical.to_string(), "L3"); + +let ordered = [LogicalQubit::new(2), LogicalQubit::new(0)]; +assert!(ordered[1] < ordered[0]); +``` + +--- + +## PhysicalQubit + +物理比特标识,用于设备上的比特位置。 + +```rust +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)] +#[repr(transparent)] +pub struct PhysicalQubit(Qubit); +``` + +字段: + +- 内部的 `Qubit` 字段为私有,不通过公开接口直接访问。 + +### 构造 + +- `PhysicalQubit::new(id: u32) -> PhysicalQubit`:从数值标识构造,`const fn`。 +- `PhysicalQubit::from_qubit(qubit: Qubit) -> PhysicalQubit`:包装比特形式的标识,`const fn`。 +- 实现 `From`,等价于 `from_qubit`。 + +### 方法 + +- `fn qubit(self) -> Qubit`:返回底层的比特标识,`const fn`。 +- `fn id(self) -> u32`:返回数值标识,`const fn`。 + +### trait 实现 + +- `Debug`、`Clone`、`Copy`、`PartialEq`、`Eq`、`Hash`、`Ord`、`PartialOrd`:派生实现,顺序与哈希基于数值标识。 +- `Display`:输出 `P{id}`,例如 `P100`。 +- `From for PhysicalQubit`:等价于 `PhysicalQubit::from_qubit`。 +- `From for Qubit`:等价于 `PhysicalQubit::qubit`。 + +示例: + +```rust +use cqlib_core::circuit::Qubit; +use cqlib_core::device::PhysicalQubit; + +let qubit = Qubit::new(11); +let physical = PhysicalQubit::new(qubit.id()); + +assert_eq!(physical.id(), 11); +assert_eq!(physical.qubit(), qubit); +assert_eq!(physical.to_string(), "P11"); + +assert!(PhysicalQubit::new(0) < PhysicalQubit::new(2)); +``` + +--- + +## 在布局中的角色 + +布局维护逻辑比特到物理比特的映射,是两个类型配合使用的主要场景:逻辑比特来自线路,物理比特来自设备,映射结果以强类型标识记录。两个类型之间没有相互转换,也不能直接比较,数值标识相同并不表示两者是同一个标识。 + +示例: + +```rust +use cqlib_core::device::{Layout, LogicalQubit, PhysicalQubit}; +use std::collections::BTreeMap; + +let logical = vec![LogicalQubit::new(0), LogicalQubit::new(1)]; +let physical = vec![ + PhysicalQubit::new(100), + PhysicalQubit::new(101), + PhysicalQubit::new(102), +]; + +let init_map = [(LogicalQubit::new(1), PhysicalQubit::new(102))] + .into_iter() + .collect::>(); + +let layout = Layout::new(logical, physical, Some(init_map)).unwrap(); + +assert_eq!(layout.num_logical(), 2); +assert_eq!(layout.num_physical(), 3); +assert_eq!(layout.num_vacant_physical(), 1); +assert_eq!( + layout.get_physical(LogicalQubit::new(0)), + Some(PhysicalQubit::new(100)) +); +assert_eq!( + layout.get_physical(LogicalQubit::new(1)), + Some(PhysicalQubit::new(102)) +); +assert_eq!( + layout.vacant_physical_qubits().collect::>(), + vec![PhysicalQubit::new(101)] +); +``` diff --git a/docs/api/cn/rust/3_qis/0_overview.md b/docs/api/cn/rust/3_qis/0_overview.md new file mode 100644 index 00000000..df8e001c --- /dev/null +++ b/docs/api/cn/rust/3_qis/0_overview.md @@ -0,0 +1,195 @@ +# QIS 量子信息 + +`cqlib_core::qis` + +`cqlib_core::qis` 提供量子信息科学相关的基础对象与本地模拟能力:量子态的多种表示、Pauli 算子与 Hamilton 量、Pauli 演化线路的构造,以及保真度、熵和纠缠度量。本页是该模块的概览与导航。 + +## Overview + +量子线路的仿真结果落在一个量子态上。不同的任务需要不同的态表示:只包含标准门与旋转门的线路可以用纯态表示描述;包含噪声或需要描述量子信道的场景必须使用混合态表示;只包含 Clifford 门的线路则可以使用稳定子表示,用多项式规模的数据描述量子态。 + +`cqlib_core::qis` 把「态的表示」与「作用于态的操作」组织在一起:每个模拟器都提供一组作用于态的常用操作,并通过与线路 IR 对接的入口接收整条线路。可观测量、演化与度量则建立在态表示之上。 + +### 四种态表示 + +| 表示 | 适用场景 | 噪声支持 | 可描述的规模 | +| --- | --- | --- | --- | +| `Statevector` | 理想通用线路,纯态 | 无 | 小规模系统 | +| `DensityMatrix` | 混合态与量子信道 | Kraus 算符 | 小规模系统 | +| `DensityMatrixNoise` | 带噪声的设备级仿真 | 完整噪声模型(门噪声与读出噪声) | 小规模系统 | +| `StabilizerState` | 仅含 Clifford 门的线路 | 无 | 大规模系统 | + +四种表示共享一致的接口命名:门操作按名字对应(`apply_h`、`apply_cx` 等),测量与采样接口一致(`measure`、`measure_all`、`sample_shots`、`sample`、`probs`)。 + +### 与线路的对接 + +三种稠密表示都提供两个与线路对接的入口: + +- `from_circuit`:执行一条线路并返回演化后的态,不修改输入线路。 +- `apply_circuit`:把线路原地作用到已有的态上,要求线路的比特数与态一致。 + +`StabilizerState` 额外提供 `run_circuit`,它在执行 Clifford 线路的同时返回运行期经典数据,返回类型为 `CircuitExecutionResult`。 + +### 算子、演化与度量 + +Pauli 算子与 Pauli 串是描述可观测量与稳定子生成元的基本对象。`Hamiltonian` 由带系数的 Pauli 串构成,实现 `Observable` trait 后可以作为观测量交给态的 `expectation` 方法求期望值。`PauliEvolution` 把 Pauli 串转换为可执行的演化线路。`metrics` 与 `entropy` 两个模块提供纯度、保真度、迹距离、熵与纠缠度量。 + +--- + +## 常用入口 + +```rust +use cqlib_core::qis::{DensityMatrix, Statevector}; + +// 纯态模拟 +let mut sv = Statevector::new(3); +sv.apply_h(0).unwrap(); +sv.apply_cx(0, 1).unwrap(); +let probs_sv = sv.probabilities(); + +// 混合态模拟 +let mut dm = DensityMatrix::new(3); +dm.apply_h(0).unwrap(); +dm.apply_cx(0, 1).unwrap(); +let probs_dm = dm.probabilities(); + +// 纯态下两种表示给出相同的结果 +assert!((probs_sv[0] - probs_dm[0]).abs() < 1e-10); +``` + +--- + +## 核心概念与术语 + +| 术语 | 说明 | +| --- | --- | +| **态矢量** | 纯态在计算基下的复振幅向量,第 `i` 个分量对应基态 `\|i>`。 | +| **混合态** | 不能用单一态矢量描述的量子态,由密度矩阵表示。 | +| **密度矩阵** | 描述量子态的矩阵,对角元给出计算基下的测量概率。 | +| **量子信道** | 态的保迹映射,用一组 Kraus 算符 `K_k` 表示。 | +| **稳定子态** | 由一组相互对易的 Pauli 算子(稳定子生成元)共同确定的本征态。 | +| **Clifford 门** | 把 Pauli 群映射到自身的门;稳定子表示只接受这类门。 | +| **读出噪声** | 测量环节引入的经典误差,作用于概率分布而非量子态本身。 | +| **坍缩** | 测量导致态投影到与测量结果一致的子空间。 | +| **采样** | 按态的概率分布生成多次独立测量结果。 | +| **可观测量** | 可获得期望值的物理量;由 `Observable` trait 描述。 | + +--- + +## `cqlib_core::qis` API 概览 + +### 模块与态表示 + +| 名字 | 简介 | +| --- | --- | +| [`cqlib_core::qis`](0_overview.md) | 模块概览、术语与错误处理。 | +| [`Statevector`](1_statevector.md) | 纯态模拟器,保存 `2^n` 个复振幅。 | +| [`DensityMatrix`](2_density_matrix.md) | 混合态模拟器,支持 Kraus 算符与偏迹。 | +| [`DensityMatrixNoise`](3_density_matrix_noise.md) | 带噪声模型的密度矩阵模拟器。 | +| [`StabilizerState`](4_stabilizer.md) | 稳定子态模拟器,支持大规模 Clifford 线路。 | + +### 经典数据 + +| 名字 | 简介 | +| --- | --- | +| [`ClassicalState`](5_classical_state.md) / [`RuntimeValue`](5_classical_state.md) | Clifford 线路运行期产生的经典值与变量。 | + +### 算子与可观测量 + +| 名字 | 简介 | +| --- | --- | +| [`Pauli`](6_pauli.md) / [`Phase`](6_pauli.md) | 单比特 Pauli 算子与相位。 | +| [`PauliString`](6_pauli.md) / [`PauliIter`](6_pauli.md) | Pauli 串及其按比特迭代器。 | +| [`Hamiltonian`](7_hamiltonian.md) / [`Observable`](7_hamiltonian.md) | 带系数的 Pauli 串之和,以及可观测量的统一接口。 | + +### 演化与度量 + +| 名字 | 简介 | +| --- | --- | +| [`PauliEvolution`](8_evolution.md) / [`TrotterMode`](8_evolution.md) | Pauli 串演化线路的构造与 Trotter 分解模式。 | +| [`metrics`](9_metrics_entropy.md) / [`entropy`](9_metrics_entropy.md) | 保真度、纯度、迹距离与熵、纠缠度量。 | + +--- + +## 快速示例 + +### 1. 纯态与混合态给出相同概率分布 + +```rust +use cqlib_core::qis::{DensityMatrix, Statevector}; + +let mut sv = Statevector::new(2); +sv.apply_h(0).unwrap(); +sv.apply_cx(0, 1).unwrap(); + +let mut dm = DensityMatrix::new(2); +dm.apply_h(0).unwrap(); +dm.apply_cx(0, 1).unwrap(); + +let probs_sv = sv.probabilities(); +let probs_dm = dm.probabilities(); +for (a, b) in probs_sv.iter().zip(probs_dm.iter()) { + assert!((a - b).abs() < 1e-10); +} +``` + +### 2. 稳定子态采样 + +```rust +use cqlib_core::qis::StabilizerState; +use std::collections::HashMap; + +let mut state = StabilizerState::new(2); +state.apply_h(0).unwrap(); +state.apply_cx(0, 1).unwrap(); + +let mut counts = HashMap::new(); +for outcome in state.sample_shots(1000) { + *counts.entry(outcome.to_bitstring(2)).or_insert(0usize) += 1; +} + +// Bell 态的采样结果只会出现关联结果 +assert!(counts.keys().all(|bits| bits == "00" || bits == "11")); +``` + +### 3. 求可观测量的期望值 + +```rust +use cqlib_core::qis::{Hamiltonian, PauliString, Statevector}; + +let mut sv = Statevector::new(2); +sv.apply_h(0).unwrap(); +sv.apply_cx(0, 1).unwrap(); + +let mut h = Hamiltonian::new(2); +h.add_term("ZZ".parse::().unwrap(), 0.5.into()).unwrap(); + +let exp = sv.expectation(&h).unwrap(); +assert!(exp.is_finite()); +``` + +--- + +## 校验与错误处理 + +`QisError` 是 `cqlib_core::qis` 的统一错误类型,态构造、门作用、测量与期望值计算失败都通过它返回。 + +| 错误 | 触发场景 | +| --- | --- | +| `QisError::QubitMismatch` | 算子或线路的比特数与态不一致。 | +| `QisError::DimensionMismatch` | 矩阵或向量的维度不匹配。 | +| `QisError::InvalidStateDimension` | 态矢量的长度不是 2 的幂,或与声明的比特数不符。 | +| `QisError::InvalidProbability` | 概率取值超出 `[0, 1]`。 | +| `QisError::IndexOutOfBounds` | 访问的比特或振幅下标越界。 | +| `QisError::UnsupportedOperation` | 当前模拟器不支持该操作。 | +| `QisError::PauliStringParseError` | Pauli 串解析失败。 | +| `QisError::NotNormalized` | 显式提供的态未归一化。 | +| `QisError::NotHermitian` | 要求为 Hermitian 的算子不满足自伴性。 | +| `QisError::NotPositiveSemidefinite` | 密度矩阵不是半正定。 | +| `QisError::InvalidParameterValue` | 参数取值非法。 | +| `QisError::InvalidSubsystem` | 纠缠度量中的子系统描述非法。 | +| `QisError::UnsupportedDimension` | 操作要求的维度不满足。 | +| `QisError::NonCliffordGate` | 向稳定子态作用了非 Clifford 门。 | +| `QisError::CircuitError` | 线路执行失败,例如门没有矩阵表示或存在未解析的符号参数。 | + +各页的具体触发条件见 [Statevector](1_statevector.md)、[DensityMatrix](2_density_matrix.md)、[DensityMatrixNoise](3_density_matrix_noise.md) 与 [StabilizerState](4_stabilizer.md)。 diff --git a/docs/api/cn/rust/3_qis/1_statevector.md b/docs/api/cn/rust/3_qis/1_statevector.md new file mode 100644 index 00000000..33f14470 --- /dev/null +++ b/docs/api/cn/rust/3_qis/1_statevector.md @@ -0,0 +1,269 @@ +# Statevector + +`cqlib_core::qis::state` + +本页覆盖纯态模拟器 `Statevector`:构造方式、振幅访问、与线路对接的入口、各类门操作、测量与采样接口,以及期望值计算。 + +## 导入 + +```rust +use cqlib_core::qis::Statevector; +``` + +--- + +## Statevector + +纯态模拟器,把量子态表示为 `2^num_qubits` 个复振幅。 + +```rust +pub struct Statevector { + pub num_qubits: usize, +} +``` + +字段: + +- `num_qubits` (`usize`):量子比特数。 + +振幅存放在私有的对齐缓冲区中,长度为 `2^num_qubits`;下标 `i` 的分量是计算基态 `|i>` 的系数,比特 0 对应最低位。振幅通过 `data()` 与 `data_mut()` 访问。实现 `Debug`、`Clone`。 + +方法: + +### 构造与振幅访问 + +- `fn new(num_qubits: usize) -> Self`:构造 `|0...0>` 态,首分量置 1,其余为 0。 +- `fn from_state(num_qubits: usize, initial_state: Vec) -> Result`:用给定振幅构造,要求长度等于 `2^num_qubits` 且已经归一化。 +- `fn data(&self) -> &[Complex64]`:按共享切片读取全部振幅。 +- `fn data_mut(&mut self) -> &mut [Complex64]`:按可变切片读取全部振幅;改写可能破坏归一化。 +- `fn probabilities(&self) -> Vec`:返回全部计算基态上的测量概率分布,即各振幅模的平方。 + +### 与线路对接 + +- `fn from_circuit(circuit: &Circuit) -> Result`:执行线路并返回演化后的态,输入线路不被修改。 +- `fn apply_circuit(&mut self, circuit: &Circuit) -> Result<(), QisError>`:把线路原地作用到当前态上,线路的比特数必须与态一致。 + +两条入口都会先对线路做分解,再逐条执行其中的指令。支持标准门、受控门、多控制门、带矩阵表示的酉门与 `Reset`,屏障和延迟指令被忽略,`Circuit::measure*` 产生的测量声明不参与状态演化。 + +### 通用门入口 + +- `fn apply_standard_gate(&mut self, gate: StandardGate, qubits: &[usize], params: &[f64]) -> Result<(), QisError>`:按标准门枚举分派到对应的专用实现;`qubits` 的个数与 `params` 的个数必须分别等于 `gate` 要求的值。 +- `fn apply_single_qubit_gate(&mut self, qubit: usize, matrix: [[Complex64; 2]; 2]) -> Result<(), QisError>`:作用任意 2×2 酉矩阵。 +- `fn apply_two_qubit_gate(&mut self, q0: usize, q1: usize, matrix: [[Complex64; 4]; 4]) -> Result<(), QisError>`:作用任意 4×4 酉矩阵,基序为 `|00>`、`|01>`、`|10>`、`|11>`,其中 `q0` 为高位。 +- `fn apply_unitary_gate(&mut self, qubits: &[usize], matrix: &ndarray::Array2) -> Result<(), QisError>`:作用任意 `2^n × 2^n` 酉矩阵,`qubits` 给出该矩阵作用的比特。 +- `fn apply_pauli_rotation(&mut self, pauli: &PauliString, theta: f64) -> Result<(), QisError>`:原地作用 `exp(-i·θ/2·P)`,其中 `P` 为 Hermitian 的 Pauli 串;该方法不把指数分解为基础门序列。 + +### 单比特门 + +- `fn apply_x(&mut self, qubit: usize) -> Result<(), QisError>`:Pauli-X。 +- `fn apply_y(&mut self, qubit: usize) -> Result<(), QisError>`:Pauli-Y。 +- `fn apply_z(&mut self, qubit: usize) -> Result<(), QisError>`:Pauli-Z。 +- `fn apply_h(&mut self, qubit: usize) -> Result<(), QisError>`:Hadamard 门。 +- `fn apply_s(&mut self, qubit: usize) -> Result<(), QisError>`:S 门。 +- `fn apply_sdg(&mut self, qubit: usize) -> Result<(), QisError>`:S† 门。 +- `fn apply_t(&mut self, qubit: usize) -> Result<(), QisError>`:T 门。 +- `fn apply_tdg(&mut self, qubit: usize) -> Result<(), QisError>`:T† 门。 +- `fn apply_u(&mut self, qubit: usize, theta: f64, phi: f64, lambda: f64) -> Result<(), QisError>`:通用单比特门 `U(θ, φ, λ)`。 +- `fn apply_phase(&mut self, qubit: usize, theta: f64) -> Result<(), QisError>`:相位门 `P(θ)`。 +- `fn apply_rx(&mut self, qubit: usize, theta: f64) -> Result<(), QisError>`:绕 X 轴旋转 `θ`。 +- `fn apply_ry(&mut self, qubit: usize, theta: f64) -> Result<(), QisError>`:绕 Y 轴旋转 `θ`。 +- `fn apply_rz(&mut self, qubit: usize, theta: f64) -> Result<(), QisError>`:绕 Z 轴旋转 `θ`。 +- `fn apply_x2p(&mut self, qubit: usize) -> Result<(), QisError>`:`Rx(π/2)`。 +- `fn apply_x2m(&mut self, qubit: usize) -> Result<(), QisError>`:`Rx(-π/2)`。 +- `fn apply_y2p(&mut self, qubit: usize) -> Result<(), QisError>`:`Ry(π/2)`。 +- `fn apply_y2m(&mut self, qubit: usize) -> Result<(), QisError>`:`Ry(-π/2)`。 +- `fn apply_xy2p(&mut self, qubit: usize, theta: f64) -> Result<(), QisError>`:`XY2P(θ)`。 +- `fn apply_xy2m(&mut self, qubit: usize, theta: f64) -> Result<(), QisError>`:`XY2M(θ)`。 +- `fn apply_xy(&mut self, qubit: usize, theta: f64) -> Result<(), QisError>`:`XY(θ)`,与 `XY2P`、`XY2M` 是不同的门。 +- `fn apply_rxy(&mut self, qubit: usize, theta: f64, phi: f64) -> Result<(), QisError>`:绕 XY 平面内方位角为 `phi` 的轴旋转 `theta`。 +- `fn apply_gphase(&mut self, phi: f64) -> Result<(), QisError>`:给整个态乘上全局相位 `e^(iφ)`。 + +### 双比特门 + +- `fn apply_swap(&mut self, q0: usize, q1: usize) -> Result<(), QisError>`:交换两个比特。 +- `fn apply_cx(&mut self, control: usize, target: usize) -> Result<(), QisError>`:受控 X 门。 +- `fn apply_cy(&mut self, control: usize, target: usize) -> Result<(), QisError>`:受控 Y 门。 +- `fn apply_cz(&mut self, q0: usize, q1: usize) -> Result<(), QisError>`:受控 Z 门,两个参数地位对称。 +- `fn apply_crx(&mut self, control: usize, target: usize, theta: f64) -> Result<(), QisError>`:受控 `Rx(θ)`。 +- `fn apply_cry(&mut self, control: usize, target: usize, theta: f64) -> Result<(), QisError>`:受控 `Ry(θ)`。 +- `fn apply_crz(&mut self, control: usize, target: usize, theta: f64) -> Result<(), QisError>`:受控 `Rz(θ)`。 +- `fn apply_rxx(&mut self, q0: usize, q1: usize, theta: f64) -> Result<(), QisError>`:`exp(-i·θ/2·X⊗X)`。 +- `fn apply_ryy(&mut self, q0: usize, q1: usize, theta: f64) -> Result<(), QisError>`:`exp(-i·θ/2·Y⊗Y)`。 +- `fn apply_rzz(&mut self, q0: usize, q1: usize, theta: f64) -> Result<(), QisError>`:`exp(-i·θ/2·Z⊗Z)`。 +- `fn apply_rzx(&mut self, q0: usize, q1: usize, theta: f64) -> Result<(), QisError>`:`exp(-i·θ/2·Z⊗X)`。 +- `fn apply_fsim(&mut self, q0: usize, q1: usize, theta: f64, phi: f64) -> Result<(), QisError>`:费米子模拟门,`theta` 为 iSWAP 角,`phi` 为受控相位角。 + +### 三比特门 + +- `fn apply_ccx(&mut self, c0: usize, c1: usize, target: usize) -> Result<(), QisError>`:Toffoli 门,两个控制位同时为 `|1>` 时翻转目标位。 + +### 测量、重置与采样 + +- `fn measure(&mut self, qubit: usize) -> Result`:在 Z 基下测量指定比特并使态坍缩,返回 `false` 表示结果 `|0>`、`true` 表示结果 `|1>`;测量后态在对应子空间内重新归一化。该操作是破坏性的。 +- `fn measure_with_rng(&mut self, qubit: usize, rng: &mut impl Rng) -> Result`:使用调用方提供的随机数发生器测量,便于复现。 +- `fn measure_all(&mut self) -> Outcome`:按 `0..num_qubits` 的顺序逐个测量,返回位打包的 `Outcome`。 +- `fn reset(&mut self, qubit: usize) -> Result<(), QisError>`:把指定比特重置为 `|0>`,实现为一次 Z 基测量并在结果为 `|1>` 时补一个 X 修正。 +- `fn sample_shots(&self, shots: usize) -> Vec`:并行采样若干次独立测量结果,不修改自身。 +- `fn sample(&self, measurement: &Measurement, shots: usize) -> Result`:按线路 `Measurement` 指定的输出约定采样;`measurement.qubits()[i]` 对应每个 `Outcome` 的第 `i` 位。 +- `fn probs(&self, measurement: &Measurement) -> Result, QisError>`:返回 `Measurement` 选定的边缘概率分布;该方法不执行线路,只把 `Measurement` 当作输出约定使用。 + +### 期望值 + +- `fn expectation(&self, h: &dyn Observable) -> Result`:计算 `⟨ψ|H|ψ⟩`。 + +--- + +## 示例 + +### 1. 制备 Bell 态 + +```rust +use cqlib_core::qis::Statevector; +use num_complex::Complex64; +use std::f64::consts::FRAC_1_SQRT_2; + +let mut sv = Statevector::new(2); +sv.apply_h(0).unwrap(); +sv.apply_cx(0, 1).unwrap(); + +// (|00⟩ + |11⟩)/√2 +assert_eq!(sv.num_qubits, 2); +assert!((sv.data()[0] - Complex64::new(FRAC_1_SQRT_2, 0.0)).norm() < 1e-10); +assert!((sv.data()[3] - Complex64::new(FRAC_1_SQRT_2, 0.0)).norm() < 1e-10); + +let probs = sv.probabilities(); +assert!((probs[0] - 0.5).abs() < 1e-10); +assert!((probs[3] - 0.5).abs() < 1e-10); +``` + +### 2. 从线路构造与原地作用线路 + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::qis::Statevector; + +let mut circuit = Circuit::new(2); +circuit.h(Qubit::new(0)).unwrap(); +circuit.cx(Qubit::new(0), Qubit::new(1)).unwrap(); + +let sv = Statevector::from_circuit(&circuit).unwrap(); +assert_eq!(sv.num_qubits, 2); + +let mut sv2 = Statevector::new(2); +sv2.apply_circuit(&circuit).unwrap(); +assert!(sv2.data().iter().zip(sv.data()).all(|(a, b)| (a - b).norm() < 1e-10)); +``` + +### 3. 作用自定义酉门 + +```rust +use cqlib_core::qis::Statevector; +use num_complex::Complex64; +use std::f64::consts::FRAC_1_SQRT_2; + +// H = [[1/√2, 1/√2], [1/√2, -1/√2]] +let h_matrix = [ + [Complex64::new(FRAC_1_SQRT_2, 0.0), Complex64::new(FRAC_1_SQRT_2, 0.0)], + [Complex64::new(FRAC_1_SQRT_2, 0.0), Complex64::new(-FRAC_1_SQRT_2, 0.0)], +]; + +let mut sv = Statevector::new(1); +sv.apply_single_qubit_gate(0, h_matrix).unwrap(); + +assert!((sv.data()[0] - Complex64::new(FRAC_1_SQRT_2, 0.0)).norm() < 1e-10); +assert!((sv.data()[1] - Complex64::new(FRAC_1_SQRT_2, 0.0)).norm() < 1e-10); +``` + +### 4. 作用多比特酉门 + +```rust +use cqlib_core::qis::Statevector; +use ndarray::Array2; + +let mut sv = Statevector::new(2); +sv.apply_h(0).unwrap(); +let before: Vec<_> = sv.data().to_vec(); + +// 4×4 单位矩阵作用在比特 [0, 1] 上 +let identity = Array2::eye(4); +sv.apply_unitary_gate(&[0, 1], &identity).unwrap(); + +assert!(sv.data().iter().zip(before.iter()).all(|(a, b)| (a - b).norm() < 1e-10)); +``` + +### 5. Pauli 串旋转 + +```rust +use cqlib_core::qis::{Pauli, PauliString, Statevector}; + +let mut sv = Statevector::new(2); +sv.apply_h(0).unwrap(); + +let mut pauli = PauliString::new(2); +pauli.set_pauli(0, Pauli::X); +pauli.set_pauli(1, Pauli::Z); + +sv.apply_pauli_rotation(&pauli, 0.413).unwrap(); + +let norm: f64 = sv.data().iter().map(|amp| amp.norm_sqr()).sum(); +assert!((norm - 1.0).abs() < 1e-10); +``` + +### 6. 测量与采样 + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::qis::Statevector; + +let mut sv = Statevector::new(4); +sv.apply_x(0).unwrap(); +sv.apply_x(2).unwrap(); + +let outcome = sv.measure_all(); +assert!(outcome.is_one(0)); +assert!(!outcome.is_one(1)); +assert!(outcome.is_one(2)); +assert!(!outcome.is_one(3)); + +// 按 Measurement 指定的比特顺序采样 +let mut circuit = Circuit::new(2); +circuit.x(Qubit::new(0)).unwrap(); +let out = circuit.measure_bits([Qubit::new(1), Qubit::new(0)]).unwrap(); + +let sv = Statevector::from_circuit(&circuit).unwrap(); +let result = sv.sample(&out, 16).unwrap(); +assert_eq!(result.shots(), 16); +``` + +### 7. 计算期望值 + +```rust +use cqlib_core::qis::{Hamiltonian, Pauli, PauliString, Statevector}; + +let sv = Statevector::new(1); + +let mut ps = PauliString::new(1); +ps.set_pauli(0, Pauli::Z); +let h = Hamiltonian::from_pauli(ps); + +let exp = sv.expectation(&h).unwrap(); +assert!((exp - 1.0).abs() < 1e-10); +``` + +--- + +## 校验与错误处理 + +构造、门作用、测量与期望值计算失败都通过 `QisError` 返回,不使用恐慌。 + +| 错误 | 触发场景 | +| --- | --- | +| `QisError::InvalidStateDimension` | `from_state` 的振幅数量不等于 `2^num_qubits`;`apply_circuit` 的线路比特数与态不一致。 | +| `QisError::NotNormalized` | `from_state` 传入的振幅未归一化。 | +| `QisError::IndexOutOfBounds` | 比特下标超出范围。 | +| `QisError::InvalidParameterValue` | 在零比特系统上作用门;双比特门给出了相同比特;`apply_unitary_gate` 的比特列表含重复项;`apply_standard_gate` 的比特数或参数个数与门不符。 | +| `QisError::UnsupportedOperation` | 线路包含当前入口不支持的操作,例如经典控制流。 | +| `QisError::CircuitError` | 线路中的门没有矩阵表示、含未解析的符号参数,或引用了不存在的比特。 | +| `QisError::QubitMismatch` | `expectation` 的观测量作用比特数与态不一致。 | + +未归一化的振幅、零比特系统上的门操作与相同比特构成的双比特门都属于调用方的输入错误,通常在运行前即可避免。 diff --git a/docs/api/cn/rust/3_qis/2_density_matrix.md b/docs/api/cn/rust/3_qis/2_density_matrix.md new file mode 100644 index 00000000..a28a52dc --- /dev/null +++ b/docs/api/cn/rust/3_qis/2_density_matrix.md @@ -0,0 +1,274 @@ +# DensityMatrix + +`cqlib_core::qis::state` + +本页覆盖混合态模拟器 `DensityMatrix`:构造方式、矩阵数据访问、物理性校验、与线路对接的入口、各类门操作、量子信道与偏迹、测量与采样接口,以及期望值计算。 + +## 导入 + +```rust +use cqlib_core::qis::DensityMatrix; +``` + +--- + +## DensityMatrix + +混合态模拟器,把量子态表示为 `2^N × 2^N` 的密度矩阵。相对只用一组复振幅描述的纯态,密度矩阵可以表示纯态、混合态以及量子信道作用后的结果。 + +```rust +pub struct DensityMatrix { + pub num_qubits: usize, +} +``` + +字段: + +- `num_qubits` (`usize`):量子比特数。 + +矩阵元素按行主序存放于私有缓冲区,长度为 `4^N`,通过 `data()` 访问。实现 `Debug`、`Clone`,以及 `AddAssign`(`+=`)。 + +方法: + +### 构造与数据访问 + +- `fn new(num_qubits: usize) -> Self`:构造纯态 `|0...0><0...0|`。 +- `fn maximally_mixed(num_qubits: usize) -> Self`:构造最大混合态 `I / 2^N`,各计算基态等概率且无相干项。 +- `fn zeros(num_qubits: usize) -> Self`:构造全零矩阵;它不是合法物理态(迹为 0),用于作为量子信道等操作的累加起点。 +- `fn from_state(num_qubits: usize, initial_state: Vec) -> Result`:由纯态振幅做外积 `ρ = |ψ><ψ|` 构造。 +- `fn from_density_matrix_state(num_qubits: usize, dm_state: Vec) -> Result`:直接由展平的 `2^N × 2^N` 矩阵构造,并校验 Hermitian 性、半正定性与单位迹。 +- `fn data(&self) -> &[Complex64]`:按共享切片读取展平的矩阵元素。 +- `fn probabilities(&self) -> Vec`:返回密度矩阵对角元,即各计算基态上的测量概率。 + +### 物理性校验 + +- `fn trace(&self) -> Complex64`:返回迹;合法物理态的迹等于 `1.0`。 +- `fn is_hermitian(&self, tol: f64) -> bool`:判断是否满足 `ρ = ρ†`。 +- `fn is_positive_semidefinite_approx(&self, tol: f64) -> bool`:判断所有本征值是否不小于 `-tol`;含 NaN 或 Inf 时返回 `false` 而不恐慌。 +- `fn validate_physical(&self, tol: f64) -> Result<(), QisError>`:依次校验 Hermitian 性、半正定性与单位迹。 + +### 与线路对接 + +- `fn from_circuit(circuit: &Circuit) -> Result`:执行线路并返回演化后的密度矩阵。 +- `fn apply_circuit(&mut self, circuit: &Circuit) -> Result<(), QisError>`:把线路原地作用到当前密度矩阵上,线路的比特数必须与态一致。 + +两条入口支持的指令与 `Statevector` 相同:标准门、受控门、多控制门、带矩阵表示的酉门与 `Reset`,屏障和延迟被忽略,`Circuit::measure*` 产生的测量声明不参与状态演化。 + +### 通用门入口 + +- `fn apply_standard_gate(&mut self, gate: StandardGate, qubits: &[usize], params: &[f64]) -> Result<(), QisError>`:按标准门枚举分派;`qubits` 与 `params` 的个数必须分别等于 `gate` 要求的值。 +- `fn apply_single_qubit_gate(&mut self, qubit: usize, matrix: [[Complex64; 2]; 2]) -> Result<(), QisError>`:作用任意 2×2 酉矩阵。 +- `fn apply_two_qubit_gate(&mut self, q0: usize, q1: usize, matrix: [[Complex64; 4]; 4]) -> Result<(), QisError>`:作用任意 4×4 酉矩阵。 +- `fn apply_unitary_gate(&mut self, qubits: &[usize], matrix: &ndarray::Array2) -> Result<(), QisError>`:按 `ρ → U ρ U†` 作用任意 `2^n × 2^n` 酉矩阵。 + +### 单比特门 + +- `fn apply_x(&mut self, qubit: usize) -> Result<(), QisError>`:Pauli-X。 +- `fn apply_y(&mut self, qubit: usize) -> Result<(), QisError>`:Pauli-Y。 +- `fn apply_z(&mut self, qubit: usize) -> Result<(), QisError>`:Pauli-Z。 +- `fn apply_h(&mut self, qubit: usize) -> Result<(), QisError>`:Hadamard 门。 +- `fn apply_s(&mut self, qubit: usize) -> Result<(), QisError>`:S 门。 +- `fn apply_sdg(&mut self, qubit: usize) -> Result<(), QisError>`:S† 门。 +- `fn apply_t(&mut self, qubit: usize) -> Result<(), QisError>`:T 门。 +- `fn apply_tdg(&mut self, qubit: usize) -> Result<(), QisError>`:T† 门。 +- `fn apply_u(&mut self, qubit: usize, theta: f64, phi: f64, lambda: f64) -> Result<(), QisError>`:通用单比特门 `U(θ, φ, λ)`。 +- `fn apply_phase(&mut self, qubit: usize, theta: f64) -> Result<(), QisError>`:相位门 `P(θ)`。 +- `fn apply_rx(&mut self, qubit: usize, theta: f64) -> Result<(), QisError>`:绕 X 轴旋转 `θ`。 +- `fn apply_ry(&mut self, qubit: usize, theta: f64) -> Result<(), QisError>`:绕 Y 轴旋转 `θ`。 +- `fn apply_rz(&mut self, qubit: usize, theta: f64) -> Result<(), QisError>`:绕 Z 轴旋转 `θ`。 +- `fn apply_x2p(&mut self, qubit: usize) -> Result<(), QisError>`:`Rx(π/2)`。 +- `fn apply_x2m(&mut self, qubit: usize) -> Result<(), QisError>`:`Rx(-π/2)`。 +- `fn apply_y2p(&mut self, qubit: usize) -> Result<(), QisError>`:`Ry(π/2)`。 +- `fn apply_y2m(&mut self, qubit: usize) -> Result<(), QisError>`:`Ry(-π/2)`。 +- `fn apply_xy2p(&mut self, qubit: usize, theta: f64) -> Result<(), QisError>`:`XY2P(θ)`。 +- `fn apply_xy2m(&mut self, qubit: usize, theta: f64) -> Result<(), QisError>`:`XY2M(θ)`。 +- `fn apply_xy(&mut self, qubit: usize, theta: f64) -> Result<(), QisError>`:`XY(θ)`。 +- `fn apply_rxy(&mut self, qubit: usize, theta: f64, phi: f64) -> Result<(), QisError>`:绕 XY 平面内指定轴旋转。 +- `fn apply_gphase(&mut self, phi: f64)`:全局相位对密度矩阵没有可观测影响,该方法不改变状态,也不返回结果。 + +### 双比特门与三比特门 + +- `fn apply_swap(&mut self, q0: usize, q1: usize) -> Result<(), QisError>`:交换两个比特。 +- `fn apply_cx(&mut self, control: usize, target: usize) -> Result<(), QisError>`:受控 X 门。 +- `fn apply_cy(&mut self, control: usize, target: usize) -> Result<(), QisError>`:受控 Y 门。 +- `fn apply_cz(&mut self, q0: usize, q1: usize) -> Result<(), QisError>`:受控 Z 门。 +- `fn apply_crx(&mut self, control: usize, target: usize, theta: f64) -> Result<(), QisError>`:受控 `Rx(θ)`。 +- `fn apply_cry(&mut self, control: usize, target: usize, theta: f64) -> Result<(), QisError>`:受控 `Ry(θ)`。 +- `fn apply_crz(&mut self, control: usize, target: usize, theta: f64) -> Result<(), QisError>`:受控 `Rz(θ)`。 +- `fn apply_rxx(&mut self, q0: usize, q1: usize, theta: f64) -> Result<(), QisError>`:`exp(-i·θ/2·X⊗X)`。 +- `fn apply_ryy(&mut self, q0: usize, q1: usize, theta: f64) -> Result<(), QisError>`:`exp(-i·θ/2·Y⊗Y)`。 +- `fn apply_rzz(&mut self, q0: usize, q1: usize, theta: f64) -> Result<(), QisError>`:`exp(-i·θ/2·Z⊗Z)`。 +- `fn apply_rzx(&mut self, q0: usize, q1: usize, theta: f64) -> Result<(), QisError>`:`exp(-i·θ/2·Z⊗X)`。 +- `fn apply_fsim(&mut self, q0: usize, q1: usize, theta: f64, phi: f64) -> Result<(), QisError>`:费米子模拟门。 +- `fn apply_ccx(&mut self, c0: usize, c1: usize, target: usize) -> Result<(), QisError>`:Toffoli 门。 + +### 量子信道与约化 + +- `fn apply_kraus(&mut self, ops: &[Vec], qs: &[usize]) -> Result<(), QisError>`:作用由 Kraus 算符给出的量子信道,演化为 `ρ → Σ_k K_k ρ K_k†`;每个算符按展平向量给出,比特数由 `qs.len()` 决定。 +- `fn partial_trace(&self, keep: &[usize]) -> Result`:对 `keep` 之外的比特求偏迹,返回比特数为 `keep.len()` 的新密度矩阵。 + +### 测量、重置与采样 + +- `fn measure(&mut self, qubit: usize) -> Result`:在 Z 基下测量并使密度矩阵坍缩,返回 `true` 表示结果 `|1>`、`false` 表示结果 `|0>`;坍缩后按下式重新归一化:`ρ' = Π_b ρ Π_b / Tr(Π_b ρ)`。 +- `fn measure_all(&mut self) -> Outcome`:按 `0..num_qubits` 的顺序逐个测量,返回位打包的 `Outcome`。 +- `fn reset(&mut self, qubit: usize) -> Result<(), QisError>`:把指定比特重置为 `|0>`。 +- `fn sample_shots(&self, shots: usize) -> Vec`:并行采样若干次独立测量结果,不修改自身。 +- `fn sample(&self, measurement: &Measurement, shots: usize) -> Result`:按线路 `Measurement` 指定的输出约定采样。 +- `fn probs(&self, measurement: &Measurement) -> Result, QisError>`:返回 `Measurement` 选定的边缘概率分布。 + +### 期望值 + +- `fn expectation(&self, h: &dyn Observable) -> Result`:计算 `Tr(ρ·H)`。 + +--- + +## 示例 + +### 1. 制备并读取概率 + +```rust +use cqlib_core::qis::DensityMatrix; + +let mut dm = DensityMatrix::new(1); +dm.apply_h(0).unwrap(); + +let probs = dm.probabilities(); +assert_eq!(probs.len(), 2); +assert!((probs[0] - 0.5).abs() < 1e-10); +assert!((probs[1] - 0.5).abs() < 1e-10); +``` + +### 2. 最大混合态与迹 + +```rust +use cqlib_core::qis::DensityMatrix; + +let dm = DensityMatrix::maximally_mixed(2); +assert_eq!(dm.num_qubits, 2); +assert_eq!(dm.data().len(), 16); +assert!((dm.trace().re - 1.0).abs() < 1e-10); +``` + +### 3. 由纯态与由矩阵构造 + +```rust +use cqlib_core::qis::DensityMatrix; +use num_complex::Complex64; + +// 由纯态振幅构造 +let state = vec![Complex64::new(0.0, 0.0), Complex64::new(1.0, 0.0)]; +let dm = DensityMatrix::from_state(1, state).unwrap(); +assert!((dm.data()[0].re - 0.0).abs() < 1e-10); +assert!((dm.data()[3].re - 1.0).abs() < 1e-10); + +// 由展平的密度矩阵构造 +let mut flat = vec![Complex64::new(0.0, 0.0); 4]; +flat[0] = Complex64::new(0.5, 0.0); +flat[3] = Complex64::new(0.5, 0.0); +let mixed = DensityMatrix::from_density_matrix_state(1, flat).unwrap(); +assert!((mixed.probabilities()[0] - 0.5).abs() < 1e-10); +assert!((mixed.probabilities()[1] - 0.5).abs() < 1e-10); +``` + +### 4. 校验物理性 + +```rust +use cqlib_core::qis::DensityMatrix; + +let mut dm = DensityMatrix::new(1); +dm.apply_h(0).unwrap(); + +assert!(dm.is_hermitian(1e-10)); +assert!(dm.is_positive_semidefinite_approx(1e-10)); +assert!(dm.validate_physical(1e-10).is_ok()); +``` + +### 5. 作用量子信道 + +```rust +use cqlib_core::qis::DensityMatrix; +use num_complex::Complex64; + +let p: f64 = 0.3; +let k0 = vec![ + Complex64::new((1.0 - p).sqrt(), 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new((1.0 - p).sqrt(), 0.0), +]; +let k1 = vec![ + Complex64::new(0.0, 0.0), + Complex64::new(p.sqrt(), 0.0), + Complex64::new(p.sqrt(), 0.0), + Complex64::new(0.0, 0.0), +]; + +let mut dm = DensityMatrix::new(1); +dm.apply_kraus(&[k0, k1], &[0]).unwrap(); + +assert!((dm.trace().re - 1.0).abs() < 1e-10); +``` + +### 6. 求偏迹 + +```rust +use cqlib_core::qis::DensityMatrix; + +let mut dm = DensityMatrix::new(2); +dm.apply_h(0).unwrap(); +dm.apply_cx(0, 1).unwrap(); + +// 追踪掉比特 1,比特 0 变成最大混合态 +let reduced = dm.partial_trace(&[0]).unwrap(); +assert_eq!(reduced.num_qubits, 1); +assert!((reduced.probabilities()[0] - 0.5).abs() < 1e-10); +assert!((reduced.probabilities()[1] - 0.5).abs() < 1e-10); +``` + +### 7. 累加密度矩阵 + +```rust +use cqlib_core::qis::DensityMatrix; + +// 以全零矩阵作为累加起点 +let mut acc = DensityMatrix::zeros(1); +acc += DensityMatrix::new(1); + +assert!((acc.trace().re - 1.0).abs() < 1e-10); +assert!((acc.probabilities()[0] - 1.0).abs() < 1e-10); +``` + +### 8. 期望值 + +```rust +use cqlib_core::qis::{DensityMatrix, Hamiltonian, Pauli, PauliString}; + +let mut dm = DensityMatrix::new(2); +dm.apply_h(0).unwrap(); +dm.apply_cx(0, 1).unwrap(); + +let mut ps = PauliString::new(2); +ps.set_pauli(0, Pauli::Z); +ps.set_pauli(1, Pauli::Z); +let h = Hamiltonian::from_pauli(ps); + +let exp = dm.expectation(&h).unwrap(); +assert!((exp - 1.0).abs() < 1e-10); +``` + +--- + +## 校验与错误处理 + +| 错误 | 触发场景 | +| --- | --- | +| `QisError::InvalidStateDimension` | `from_state` 的振幅数量不等于 `2^num_qubits`;`from_density_matrix_state` 的矩阵长度不等于 `4^num_qubits`;`apply_circuit` 的线路比特数与态不一致。 | +| `QisError::NotNormalized` | `from_state`、`from_density_matrix_state` 或 `validate_physical` 检出的迹不为 1。 | +| `QisError::NotHermitian` | `from_density_matrix_state` 或 `validate_physical` 检出的非自伴矩阵。 | +| `QisError::NotPositiveSemidefinite` | `from_density_matrix_state` 或 `validate_physical` 检出的负本征值。 | +| `QisError::IndexOutOfBounds` | 比特下标超出范围。 | +| `QisError::InvalidParameterValue` | 零比特系统上的门操作;双比特门给出相同比特;`apply_kraus` 的算符列表为空、比特列表为空或算符尺寸与比特数不符;`partial_trace` 的保留比特列表含重复项;`apply_standard_gate` 的比特数或参数个数与门不符。 | +| `QisError::UnsupportedOperation` | 线路包含当前入口不支持的操作。 | +| `QisError::CircuitError` | 线路中的门没有矩阵表示、含未解析的符号参数,或引用了不存在的比特;`expectation` 的观测量比特数不匹配。 | + +`+=` 要求两个密度矩阵的比特数相同,比特数不一致时直接恐慌,调用前应自行确认。 diff --git a/docs/api/cn/rust/3_qis/3_density_matrix_noise.md b/docs/api/cn/rust/3_qis/3_density_matrix_noise.md new file mode 100644 index 00000000..05af1f38 --- /dev/null +++ b/docs/api/cn/rust/3_qis/3_density_matrix_noise.md @@ -0,0 +1,242 @@ +# DensityMatrixNoise + +`cqlib_core::qis::state` + +本页覆盖带噪声模型的密度矩阵模拟器 `DensityMatrixNoise`:噪声模型的接入方式、与线路对接的入口、门操作、读出噪声接口,以及测量、采样与期望值计算。 + +## 导入 + +```rust +use cqlib_core::qis::DensityMatrixNoise; +use cqlib_core::device::{NoiseModel, ReadoutError, SingleQubitNoise, TwoQubitNoise}; +``` + +--- + +## DensityMatrixNoise + +在密度矩阵模拟器之上叠加噪声模型的模拟器。它在每个门之后按噪声模型查询同一门、同一比特上的噪声条目并施加相应信道;没有配置噪声模型或没有匹配条目时,只施加理想门。 + +```rust +pub struct DensityMatrixNoise { + pub state: DensityMatrix, + pub noise_model: Option, +} +``` + +字段: + +- `state` (`DensityMatrix`):底层的密度矩阵状态。 +- `noise_model` (`Option`):作用于门操作与读出的噪声模型;`None` 表示理想仿真。 + +实现 `Debug`、`Clone`。 + +方法: + +### 构造与线路对接 + +- `fn new(num_qubits: usize, noise_model: Option) -> Self`:按比特数与噪声模型构造,初始态为 `|0...0><0...0|`。 +- `fn from_circuit(circuit: &Circuit, noise_model: Option) -> Result`:执行线路并返回模拟器;线路会先被分解为基础门,噪声在每个门之后施加。 +- `fn apply_circuit(&mut self, circuit: &Circuit) -> Result<(), QisError>`:把线路原地作用到当前模拟器上,噪声按已配置的模型施加。 + +### 门操作 + +- `fn apply_standard_gate_noise(&mut self, gate: StandardGate, qs: &[usize], params: &[f64]) -> Result<(), QisError>`:标准门的统一入口,先校验并施加理想门,再按噪声模型施加匹配的门噪声。 +- `fn apply_x(&mut self, q: usize) -> Result<(), QisError>`:Pauli-X。 +- `fn apply_y(&mut self, q: usize) -> Result<(), QisError>`:Pauli-Y。 +- `fn apply_z(&mut self, q: usize) -> Result<(), QisError>`:Pauli-Z。 +- `fn apply_h(&mut self, q: usize) -> Result<(), QisError>`:Hadamard 门。 +- `fn apply_s(&mut self, q: usize) -> Result<(), QisError>`:S 门。 +- `fn apply_sdg(&mut self, q: usize) -> Result<(), QisError>`:S† 门。 +- `fn apply_t(&mut self, q: usize) -> Result<(), QisError>`:T 门。 +- `fn apply_tdg(&mut self, q: usize) -> Result<(), QisError>`:T† 门。 +- `fn apply_u(&mut self, q: usize, theta: f64, phi: f64, lam: f64) -> Result<(), QisError>`:通用单比特门。 +- `fn apply_phase(&mut self, q: usize, theta: f64) -> Result<(), QisError>`:相位门 `P(θ)`。 +- `fn apply_rx(&mut self, q: usize, theta: f64) -> Result<(), QisError>`:绕 X 轴旋转 `θ`。 +- `fn apply_ry(&mut self, q: usize, theta: f64) -> Result<(), QisError>`:绕 Y 轴旋转 `θ`。 +- `fn apply_rz(&mut self, q: usize, theta: f64) -> Result<(), QisError>`:绕 Z 轴旋转 `θ`。 +- `fn apply_gphase(&mut self, theta: f64) -> Result<(), QisError>`:全局相位。 +- `fn apply_x2p(&mut self, q: usize) -> Result<(), QisError>`:`Rx(π/2)`。 +- `fn apply_x2m(&mut self, q: usize) -> Result<(), QisError>`:`Rx(-π/2)`。 +- `fn apply_y2p(&mut self, q: usize) -> Result<(), QisError>`:`Ry(π/2)`。 +- `fn apply_y2m(&mut self, q: usize) -> Result<(), QisError>`:`Ry(-π/2)`。 +- `fn apply_rxy(&mut self, q: usize, theta: f64, phi: f64) -> Result<(), QisError>`:绕 XY 平面内指定轴旋转。 +- `fn apply_xy2p(&mut self, q: usize, theta: f64) -> Result<(), QisError>`:`XY2P(θ)`。 +- `fn apply_xy2m(&mut self, q: usize, theta: f64) -> Result<(), QisError>`:`XY2M(θ)`。 +- `fn apply_xy(&mut self, q: usize, theta: f64) -> Result<(), QisError>`:`XY(θ)`。 +- `fn apply_cx(&mut self, control: usize, target: usize) -> Result<(), QisError>`:受控 X 门。 +- `fn apply_cy(&mut self, control: usize, target: usize) -> Result<(), QisError>`:受控 Y 门。 +- `fn apply_cz(&mut self, q0: usize, q1: usize) -> Result<(), QisError>`:受控 Z 门。 +- `fn apply_ccx(&mut self, c1: usize, c2: usize, t: usize) -> Result<(), QisError>`:Toffoli 门。 +- `fn apply_swap(&mut self, q0: usize, q1: usize) -> Result<(), QisError>`:交换两个比特。 +- `fn apply_crx(&mut self, control: usize, target: usize, theta: f64) -> Result<(), QisError>`:受控 `Rx(θ)`。 +- `fn apply_cry(&mut self, control: usize, target: usize, theta: f64) -> Result<(), QisError>`:受控 `Ry(θ)`。 +- `fn apply_crz(&mut self, control: usize, target: usize, theta: f64) -> Result<(), QisError>`:受控 `Rz(θ)`。 +- `fn apply_rxx(&mut self, q0: usize, q1: usize, theta: f64) -> Result<(), QisError>`:`exp(-i·θ/2·X⊗X)`。 +- `fn apply_ryy(&mut self, q0: usize, q1: usize, theta: f64) -> Result<(), QisError>`:`exp(-i·θ/2·Y⊗Y)`。 +- `fn apply_rzz(&mut self, q0: usize, q1: usize, theta: f64) -> Result<(), QisError>`:`exp(-i·θ/2·Z⊗Z)`。 +- `fn apply_rzx(&mut self, q0: usize, q1: usize, theta: f64) -> Result<(), QisError>`:`exp(-i·θ/2·Z⊗X)`。 +- `fn apply_fsim(&mut self, q0: usize, q1: usize, theta: f64, phi: f64) -> Result<(), QisError>`:费米子模拟门。 +- `fn apply_unitary_gate(&mut self, qs: &[usize], mat: &ndarray::Array2) -> Result<(), QisError>`:作用任意酉矩阵。该入口没有对应的标准门类型,因此不施加门噪声;需要噪声建模时应使用具体的门方法。 + +### 概率与读出噪声 + +模拟器把「量子态的噪声」与「读出的噪声」分开表达:前者作用于密度矩阵本身,后者只改变报告出来的概率分布。 + +- `fn probabilities(&self) -> Vec`:返回密度矩阵对角元,即各计算基态的测量概率。包含已经施加的门噪声,不含读出噪声。 +- `fn probabilities_with_readout(&self, qubits: &[usize]) -> Result, QisError>`:在 `probabilities` 的基础上,对 `qubits` 中列出的比特施加读出噪声。 +- `fn probs(&self, measurement: &Measurement) -> Result, QisError>`:返回 `Measurement` 选定的边缘概率分布,不含读出噪声。 +- `fn probs_with_readout(&self, measurement: &Measurement) -> Result, QisError>`:在 `probs` 的基础上,对 `measurement.qubits()` 施加读出噪声。 + +### 测量、采样与期望值 + +- `fn measure(&mut self, qubit: usize) -> Result`:在 Z 基下测量并使底层密度矩阵坍缩;测量操作不施加噪声。 +- `fn measure_all(&mut self) -> Outcome`:按 `0..num_qubits` 的顺序逐个测量,返回位打包的 `Outcome`。 +- `fn reset(&mut self, qubit: usize) -> Result<(), QisError>`:把指定比特重置为 `|0>`;重置不施加读出噪声。 +- `fn sample_shots(&self, shots: usize) -> Vec`:并行采样若干次独立测量结果;采样的是量子态本身,不施加读出噪声。 +- `fn sample(&self, measurement: &Measurement, shots: usize) -> Result`:按线路 `Measurement` 指定的输出约定采样,同样不施加读出噪声。 +- `fn expectation(&self, h: &dyn Observable) -> Result`:计算 `Tr(ρ·O)`;结果包含门噪声,不含读出噪声。 + +### 噪声模型的配置 + +噪声模型由设备模块提供,配置方法包括: + +- `NoiseModel::new()`:构造空模型。 +- `add_single_qubit_error(StandardGate, Qubit, SingleQubitNoise) -> Result<(), NoiseError>`:为单比特门配置噪声。 +- `add_two_qubit_error(StandardGate, Qubit, Qubit, TwoQubitNoise) -> Result<(), NoiseError>`:为双比特门配置噪声。 +- `add_readout_error(Qubit, ReadoutError) -> Result<(), NoiseError>`:为指定比特配置读出噪声。 + +噪声的离散类型细节见设备模块文档。 + +--- + +## 示例 + +### 1. 带比特翻转噪声的单比特仿真 + +```rust +use cqlib_core::circuit::{Qubit, StandardGate}; +use cqlib_core::device::{NoiseModel, SingleQubitNoise}; +use cqlib_core::qis::DensityMatrixNoise; + +let mut noise_model = NoiseModel::new(); +noise_model + .add_single_qubit_error(StandardGate::X, Qubit::new(0), SingleQubitNoise::BitFlip(0.1)) + .unwrap(); + +let mut sim = DensityMatrixNoise::new(1, Some(noise_model)); +sim.apply_x(0).unwrap(); + +// 10% 比特翻转使 P(|1>) 为 0.9 +let probs = sim.probabilities_with_readout(&[0]).unwrap(); +assert!((probs[1] - 0.9).abs() < 1e-6); +assert!((probs[0] - 0.1).abs() < 1e-6); +``` + +### 2. 由线路构造模拟器 + +```rust +use cqlib_core::circuit::{Circuit, Qubit, StandardGate}; +use cqlib_core::device::{NoiseModel, SingleQubitNoise}; +use cqlib_core::qis::DensityMatrixNoise; + +let mut circuit = Circuit::new(1); +circuit.x(Qubit::new(0)).unwrap(); + +let mut noise_model = NoiseModel::new(); +noise_model + .add_single_qubit_error(StandardGate::X, Qubit::new(0), SingleQubitNoise::BitFlip(0.1)) + .unwrap(); + +let sim = DensityMatrixNoise::from_circuit(&circuit, Some(noise_model)).unwrap(); +assert_eq!(sim.state.num_qubits, 1); +assert!((sim.probabilities_with_readout(&[0]).unwrap()[1] - 0.9).abs() < 1e-6); +``` + +### 3. 读出噪声 + +```rust +use cqlib_core::circuit::Qubit; +use cqlib_core::device::{NoiseModel, ReadoutError}; +use cqlib_core::qis::DensityMatrixNoise; + +let mut noise_model = NoiseModel::new(); +noise_model + .add_readout_error( + Qubit::new(0), + ReadoutError { + p_0_given_1: 0.0, + p_1_given_0: 0.1, + }, + ) + .unwrap(); + +let sim = DensityMatrixNoise::new(1, Some(noise_model)); +let probs = sim.probabilities_with_readout(&[0]).unwrap(); + +// 真实态仍为 |0>,读出误差使 P(|1>) 为 0.1 +assert!((probs[1] - 0.1).abs() < 1e-6); +assert!((probs[0] - 0.9).abs() < 1e-6); +``` + +### 4. 区分理想分布与读出后的分布 + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::device::{NoiseModel, Outcome, ReadoutError}; +use cqlib_core::qis::DensityMatrixNoise; + +let mut circuit = Circuit::new(1); +circuit.x(Qubit::new(0)).unwrap(); +let out = circuit.measure(Qubit::new(0)).unwrap(); + +let mut noise_model = NoiseModel::new(); +noise_model + .add_readout_error( + Qubit::new(0), + ReadoutError { + p_0_given_1: 1.0, + p_1_given_0: 0.0, + }, + ) + .unwrap(); + +let sim = DensityMatrixNoise::from_circuit(&circuit, Some(noise_model)).unwrap(); +let ideal = sim.probs(&out).unwrap(); +let with_readout = sim.probs_with_readout(&out).unwrap(); + +assert!((ideal[&Outcome::from_bitstring("1").unwrap()] - 1.0).abs() < 1e-10); +assert!((with_readout[&Outcome::from_bitstring("0").unwrap()] - 1.0).abs() < 1e-10); +``` + +### 5. 采样不施加读出噪声 + +```rust +use cqlib_core::qis::DensityMatrixNoise; + +let mut sim = DensityMatrixNoise::new(2, None); +sim.apply_h(0).unwrap(); +sim.apply_cx(0, 1).unwrap(); + +let shots = sim.sample_shots(200); +assert_eq!(shots.len(), 200); +for outcome in &shots { + assert_eq!(outcome.is_one(0), outcome.is_one(1)); +} +``` + +--- + +## 校验与错误处理 + +| 错误 | 触发场景 | +| --- | --- | +| `QisError::InvalidParameterValue` | `apply_standard_gate_noise` 的比特数或参数个数与门不符;噪声操作本身失败。 | +| `QisError::InvalidStateDimension` | `apply_circuit` 的线路比特数与模拟器不一致。 | +| `QisError::UnsupportedOperation` | 线路包含当前入口不支持的操作,例如经典控制流。 | +| `QisError::CircuitError` | 线路中的门没有矩阵表示、含未解析的符号参数,或引用了不存在的比特。 | +| `QisError::IndexOutOfBounds` | 比特下标超出范围。 | +| `QisError::QubitMismatch` | `expectation` 的观测量比特数不匹配。 | +| `cqlib_core::device::NoiseError` | 噪声模型的配置方法失败,例如概率取值非法或门与比特的元数不匹配。 | + +读出噪声只在 `probabilities_with_readout` 与 `probs_with_readout` 中生效;`probabilities`、`probs`、`sample_shots` 与 `sample` 报告的都是施加读出噪声之前的分布。 diff --git a/docs/api/cn/rust/3_qis/4_stabilizer.md b/docs/api/cn/rust/3_qis/4_stabilizer.md new file mode 100644 index 00000000..60172027 --- /dev/null +++ b/docs/api/cn/rust/3_qis/4_stabilizer.md @@ -0,0 +1,242 @@ +# StabilizerState + +`cqlib_core::qis::state` + +本页覆盖稳定子态模拟器 `StabilizerState` 与线路执行结果 `CircuitExecutionResult`:构造方式、Clifford 门操作、生成元读取、Pauli 期望值、测量与采样接口。 + +## 导入 + +```rust +use cqlib_core::qis::StabilizerState; +use cqlib_core::qis::state::stabilizer::CircuitExecutionResult; +``` + +--- + +## StabilizerState + +稳定子态模拟器,用一组相互对易的 Pauli 算子(稳定子生成元)描述量子态。它只能表示稳定子态,因而只接受把 Pauli 群映射到自身的 Clifford 门;遇到任意角旋转、`T` 门或 `fSim` 等非 Clifford 操作时返回错误。可以描述的比特规模远大于稠密表示。 + +```rust +pub struct StabilizerState { + pub num_qubits: usize, +} +``` + +字段: + +- `num_qubits` (`usize`):量子比特数。 + +生成元表存放在私有字段中。实现 `Debug` 与 `Clone`;没有实现 `PartialEq`,需要比较两个态时比较 `get_stabilizers()` 的返回值。 + +方法: + +### 构造 + +- `fn new(n: usize) -> Self`:构造 `|0...0>` 态;初始生成元为第 `i` 行的 destabilizer `X_i` 与第 `n+i` 行的稳定子 `Z_i`,相位均为 `+1`。 +- `fn num_qubits(&self) -> usize`:返回量子比特数,与同名字段取值一致。 + +### 与线路对接 + +- `fn from_circuit(circuit: &Circuit) -> Result`:执行 Clifford 线路并返回最终的稳定子态。 +- `fn apply_circuit(&mut self, input_circuit: &Circuit) -> Result<(), QisError>`:把 Clifford 线路原地作用到当前态上,线路的比特数必须与态一致。 +- `fn run_circuit(circuit: &Circuit) -> Result`:执行 Clifford 线路,同时返回最终态与运行期经典数据。 + +`from_circuit` 与 `apply_circuit` 属于态层面的入口,它们把 `Circuit::measure*` 产生的测量与存储操作当作输出声明忽略,不使态坍缩,也不写入经典数据;需要执行语义(测量立即坍缩并写入经典数据)时应使用 `run_circuit`。 + +支持的指令为 Clifford 门 `I`、`H`、`X`、`Y`、`Z`、`S`、`SDG`、`X2P`、`X2M`、`Y2P`、`Y2M`、`CX`、`CY`、`CZ`、`SWAP`,以及 `Reset` 指令;屏障与延迟无副作用。`run_circuit` 额外执行 `MeasureBit`、`MeasureBits` 与 `Store`,但不支持控制流门。 + +### Clifford 门 + +- `fn apply_h(&mut self, qubit: usize) -> Result<(), QisError>`:Hadamard 门。 +- `fn apply_x(&mut self, qubit: usize) -> Result<(), QisError>`:Pauli-X。 +- `fn apply_y(&mut self, qubit: usize) -> Result<(), QisError>`:Pauli-Y。 +- `fn apply_z(&mut self, qubit: usize) -> Result<(), QisError>`:Pauli-Z。 +- `fn apply_s(&mut self, qubit: usize) -> Result<(), QisError>`:S 门。 +- `fn apply_sdg(&mut self, qubit: usize) -> Result<(), QisError>`:S† 门。 +- `fn apply_x2p(&mut self, qubit: usize) -> Result<(), QisError>`:`√X`,即 `Rx(π/2)`。 +- `fn apply_x2m(&mut self, qubit: usize) -> Result<(), QisError>`:`√X†`,即 `Rx(-π/2)`。 +- `fn apply_y2p(&mut self, qubit: usize) -> Result<(), QisError>`:`√Y`,即 `Ry(π/2)`。 +- `fn apply_y2m(&mut self, qubit: usize) -> Result<(), QisError>`:`√Y†`,即 `Ry(-π/2)`。 +- `fn apply_cx(&mut self, control: usize, target: usize) -> Result<(), QisError>`:受控 X 门。 +- `fn apply_cy(&mut self, control: usize, target: usize) -> Result<(), QisError>`:受控 Y 门。 +- `fn apply_cz(&mut self, q0: usize, q1: usize) -> Result<(), QisError>`:受控 Z 门。 +- `fn apply_swap(&mut self, q0: usize, q1: usize) -> Result<(), QisError>`:交换两个比特。 +- `fn apply_standard_gate(&mut self, gate: StandardGate, qubits: &[usize], params: &[f64]) -> Result<(), QisError>`:按标准门枚举分派;`gate` 不属于 Clifford 集合时报错。 + +### 生成元与 Pauli 期望值 + +- `fn get_stabilizers(&self) -> Vec`:返回 `n` 个稳定子生成元。 +- `fn get_destabilizers(&self) -> Vec`:返回 `n` 个 destabilizer 生成元。 +- `fn pauli_expectation(&self, pauli: &PauliString) -> Result`:返回 `⟨ψ|P|ψ⟩`,取值 `1` 表示 `P` 属于稳定子群、`-1` 表示属于取负的稳定子群、`0` 表示两者皆非。该判定覆盖生成元之积,不只针对单个生成元。 +- `fn to_stim_format(&self) -> String`:以兼容格式导出生成元表。 + +### 概率、测量与采样 + +- `fn probability_of(&self, bits: &[bool]) -> Result`:返回指定计算基态的概率;与稳定子态相容的位串返回 `Ok(1.0 / 2.0^k)`,其中 `k` 是测量结果随机的比特数;不相容时返回 `Ok(0.0)`。该调用不修改自身。 +- `fn probabilities(&self) -> Result, QisError>`:返回全部计算基态上的概率分布,长度为 `2^n`,下标 `i` 对应二进制表示为 `i` 的基态,比特 0 为最低位。该接口只适用于小规模系统,`n > 20` 时返回错误。 +- `fn measure(&mut self, qubit: usize) -> Result`:在 Z 基下测量并使态坍缩,返回 `false` 表示结果 `0`、`true` 表示结果 `1`。 +- `fn measure_all(&mut self) -> Outcome`:按顺序测量所有比特,返回位打包的 `Outcome`。 +- `fn reset(&mut self, qubit: usize) -> Result<(), QisError>`:把指定比特重置为 `|0>`,实现为一次 Z 基测量并在结果为 `|1>` 时补一个 X 修正。 +- `fn sample_shots(&self, shots: usize) -> Vec`:并行采样若干次独立测量结果;同一初始态总是产生相同的样本集合。 +- `fn sample(&self, measurement: &Measurement, shots: usize) -> Result`:按线路 `Measurement` 指定的输出约定采样,不读取也不执行线路本身;`measurement.qubits()[i]` 对应每个 `Outcome` 的第 `i` 位。 +- `fn probs(&self, measurement: &Measurement) -> Result, QisError>`:返回 `Measurement` 选定的边缘概率分布;其比特顺序与 `sample` 一致。该接口复用 `probabilities`,因此同样受 `n <= 20` 限制。 + +--- + +## CircuitExecutionResult + +`run_circuit` 的返回对象,同时包含最终量子态与执行期间产生的运行期经典数据。 + +```rust +pub struct CircuitExecutionResult { + pub state: StabilizerState, + pub classical: ClassicalState, +} +``` + +字段: + +- `state`:全部操作执行完毕后的稳定子态。 +- `classical`:测量结果与可变变量,按线路内的句柄索引。 + +实现 `Debug`。运行期经典值的具体形态见 [ClassicalState](5_classical_state.md)。 + +--- + +## 示例 + +### 1. Bell 态的稳定子采样 + +```rust +use cqlib_core::qis::StabilizerState; + +let mut s = StabilizerState::new(2); +s.apply_h(0).unwrap(); +s.apply_cx(0, 1).unwrap(); + +let shots = s.sample_shots(500); +assert_eq!(shots.len(), 500); +for shot in &shots { + assert_eq!(shot.is_one(0), shot.is_one(1)); +} +``` + +### 2. 读取稳定子生成元 + +```rust +use cqlib_core::qis::StabilizerState; + +let s = StabilizerState::new(2); +let stabs = s.get_stabilizers(); +assert_eq!(stabs.len(), 2); + +// 初态 |00⟩ 的稳定子为 +ZI 与 +IZ +let stabs: Vec = stabs.iter().map(|p| p.to_string()).collect(); +assert_eq!(stabs[0], "+IZ"); +assert_eq!(stabs[1], "+ZI"); +``` + +### 3. 计算指定基态的概率 + +```rust +use cqlib_core::qis::StabilizerState; + +let mut s = StabilizerState::new(2); +s.apply_h(0).unwrap(); +s.apply_cx(0, 1).unwrap(); + +// 只有 |00⟩ 与 |11⟩ 可能出现,各占一半 +assert!((s.probability_of(&[false, false]).unwrap() - 0.5).abs() < 1e-12); +assert!((s.probability_of(&[true, true]).unwrap() - 0.5).abs() < 1e-12); +assert_eq!(s.probability_of(&[false, true]).unwrap(), 0.0); +assert_eq!(s.probability_of(&[true, false]).unwrap(), 0.0); +``` + +### 4. Pauli 期望值 + +```rust +use cqlib_core::qis::{Pauli, PauliString, Phase, StabilizerState}; + +// |+⟩ 是 +X 的本征态 +let mut s = StabilizerState::new(1); +s.apply_h(0).unwrap(); + +let mut x = PauliString::new(1); +x.set_pauli(0, Pauli::X); +assert_eq!(s.pauli_expectation(&x).unwrap(), 1); + +// Bell 态下 ⟨ZI⟩ 为 0 +let mut bell = StabilizerState::new(2); +bell.apply_h(0).unwrap(); +bell.apply_cx(0, 1).unwrap(); + +let mut zi = PauliString::new(2); +zi.set_pauli(0, Pauli::Z); +zi.set_pauli(1, Pauli::I); +zi.phase = Phase::Plus; +assert_eq!(bell.pauli_expectation(&zi).unwrap(), 0); +``` + +### 5. 由线路构造并获取边缘分布 + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::qis::StabilizerState; + +let mut c = Circuit::new(2); +c.h(Qubit::new(0)).unwrap(); +c.cx(Qubit::new(0), Qubit::new(1)).unwrap(); +let out = c.measure_bits([Qubit::new(1), Qubit::new(0)]).unwrap(); + +// 该测量声明不使 Bell 态坍缩 +let stab = StabilizerState::from_circuit(&c).unwrap(); +let probs = stab.probs(&out).unwrap(); +assert!((probs.values().sum::() - 1.0).abs() < 1e-10); +``` + +### 6. 执行线路并读取运行期经典数据 + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::qis::{RuntimeValue, StabilizerState}; + +let mut c = Circuit::new(1); +c.x(Qubit::new(0)).unwrap(); +let measured = c.measure(Qubit::new(0)).unwrap(); + +let result = StabilizerState::run_circuit(&c).unwrap(); +assert_eq!( + result.classical.value(measured.value()), + Some(&RuntimeValue::Bit(true)) +); +``` + +### 7. 非 Clifford 门被拒绝 + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::qis::StabilizerState; + +let mut c = Circuit::new(1); +c.t(Qubit::new(0)).unwrap(); + +let result = StabilizerState::from_circuit(&c); +assert!(result.is_err()); +``` + +--- + +## 校验与错误处理 + +| 错误 | 触发场景 | +| --- | --- | +| `QisError::NonCliffordGate` | 向稳定子态作用非 Clifford 门,例如 `T`、`T†`、任意角度的 `Rx`、`Ry`、`Rz`、`Phase`、通用酉门。 | +| `QisError::QubitMismatch` | `probability_of` 的位串长度与比特数不符;`pauli_expectation` 的 Pauli 串比特数与态不一致。 | +| `QisError::IndexOutOfBounds` | 比特下标超出范围。 | +| `QisError::InvalidParameterValue` | 在零比特系统上作用门;双比特门给出了相同比特;`probabilities` 或 `probs` 在比特数超过上限时被调用。 | +| `QisError::InvalidStateDimension` | `apply_circuit` 的线路比特数与态不一致。 | +| `QisError::UnsupportedOperation` | 线路包含控制流等当前入口不支持的操作。 | +| `QisError::CircuitError` | 线路中的门无法执行或含未解析的符号参数。 | + +对同一初始态重复采样会得到相同的样本集合,因此采样结果可用于复现对比;需要不同的随机结果时应重新构造线路或改变门序列。 diff --git a/docs/api/cn/rust/3_qis/5_classical_state.md b/docs/api/cn/rust/3_qis/5_classical_state.md new file mode 100644 index 00000000..8aa5c07e --- /dev/null +++ b/docs/api/cn/rust/3_qis/5_classical_state.md @@ -0,0 +1,177 @@ +# 经典状态 + +`cqlib_core::qis::state` + +本页介绍 `cqlib_core::qis` 中的运行期经典数据:`RuntimeValue` 是带类型的运行时值,`ClassicalState` 按线路句柄保存并索引这些值。 + +线路 IR 保存的是经典句柄而不是具体数值。执行一条包含测量或经典存储的线路时,运行期经典数据把这些句柄填充为实际值:不可变经典值由测量产生,可变经典变量由存储语句写入。句柄的身份由线路 `CircuitId` 与表内索引共同确定,因此不同线路的句柄互不通用。 + +## 导入 + +```rust +use cqlib_core::qis::{ClassicalState, RuntimeValue}; +``` + +两个类型也位于 `cqlib_core::qis::state`,两种路径等价。 + +--- + +## RuntimeValue + +```rust +pub enum RuntimeValue { + Bit(bool), + Bool(bool), + UInt { width: u32, value: u128 }, + BitVec { width: u32, bits: Outcome }, +} +``` + +变体: + +| 变体 | 载荷 | 说明 | +| --- | --- | --- | +| `RuntimeValue::Bit(bool)` | `bool` | 单个比特,通常表示单比特测量结果。 | +| `RuntimeValue::Bool(bool)` | `bool` | 逻辑布尔值,用于控制流条件。 | +| `RuntimeValue::UInt { width, value }` | `width: u32`、`value: u128` | 指定位宽的无符号整数,值保存在 `u128` 中。 | +| `RuntimeValue::BitVec { width, bits }` | `width: u32`、`bits: Outcome` | 指定位宽的比特向量,比特承载在设备结果类型 `Outcome` 中。 | + +`BitVec` 的位序与设备结果层一致:位索引 `0` 是最低有效位,格式化为字符串时最高位显示在最左。 + +### 方法 + +- `fn ty(&self) -> ClassicalType`:返回该运行时值对应的静态经典类型。 +- `fn to_bitstring(&self) -> Option`:按最高位在左的顺序返回比特串;`Bit` 与 `BitVec` 返回 `Some`,`Bool` 与 `UInt` 返回 `None`。 + +### 其他行为 + +- 支持 `Debug`、`Clone`。 +- 按值比较与哈希:类型、位宽与载荷都相同才相等,因此 `RuntimeValue::Bit(true)` 与 `RuntimeValue::Bool(true)` 不相等。 + +示例: + +```rust +use cqlib_core::device::Outcome; +use cqlib_core::qis::RuntimeValue; + +let bit = RuntimeValue::Bit(true); +assert_eq!(bit.to_bitstring().as_deref(), Some("1")); + +let uint = RuntimeValue::UInt { width: 8, value: 42 }; +assert_eq!(uint, RuntimeValue::UInt { width: 8, value: 42 }); +assert_eq!(uint.to_bitstring(), None); + +// 类型不同则值不相等,即使载荷相同 +assert_ne!(RuntimeValue::Bit(true), RuntimeValue::Bool(true)); + +let bit_vec = RuntimeValue::BitVec { + width: 3, + bits: Outcome::from_bitstring("101").unwrap(), +}; +assert_eq!(bit_vec.to_bitstring().as_deref(), Some("101")); +``` + +--- + +## ClassicalState + +```rust +pub struct ClassicalState { + // 字段不公开 +} +``` + +保存一条线路的运行期经典数据:不可变经典值表、可变经典变量表,以及各自的静态类型表。两者的下标与线路内的句柄索引一一对应。 + +### 方法 + +- `fn value(&self, value: ClassicalValue) -> Option<&RuntimeValue>`:返回某个不可变经典值的运行期结果。 +- `fn var(&self, var: ClassicalVar) -> Option<&RuntimeValue>`:返回某个可变经典变量的当前运行期值。 + +两个方法都返回 `None`,当句柄属于另一条线路、类型不匹配、下标超出对应的表,或者对应的值尚未产生(未测量)或尚未存储时。 + +### 获取方式 + +`ClassicalState` 不作为普通构造对象使用,而是由稳定子模拟器的线路执行入口返回: + +```rust +pub fn run_circuit(circuit: &Circuit) -> Result +``` + +`CircuitExecutionResult` 包含最终量子态与本次执行的经典数据: + +```rust +pub struct CircuitExecutionResult { + pub state: StabilizerState, + pub classical: ClassicalState, +} +``` + +### 其他行为 + +- 支持 `Debug`、`Clone`。 + +示例: + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::qis::{RuntimeValue, StabilizerState}; + +let mut c = Circuit::new(2); +c.h(Qubit::new(0)).unwrap(); +c.cx(Qubit::new(0), Qubit::new(1)).unwrap(); +let left = c.measure(Qubit::new(0)).unwrap(); +let right = c.measure(Qubit::new(1)).unwrap(); + +let result = StabilizerState::run_circuit(&c).unwrap(); +let Some(RuntimeValue::Bit(left_bit)) = result.classical.value(left.value()) else { + panic!("expected first Bell measurement to produce a bit"); +}; +let Some(RuntimeValue::Bit(right_bit)) = result.classical.value(right.value()) else { + panic!("expected second Bell measurement to produce a bit"); +}; + +// Bell 态上的两次测量结果必须保持关联 +assert_eq!(left_bit, right_bit); +``` + +多比特测量的结果按 `Circuit::measure_bits()` 的小端输入顺序打包为 `BitVec`: + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::qis::{RuntimeValue, StabilizerState}; + +let mut c = Circuit::new(3); +c.x(Qubit::new(0)).unwrap(); +c.x(Qubit::new(2)).unwrap(); +let measured = c + .measure_bits([Qubit::new(2), Qubit::new(1), Qubit::new(0)]) + .unwrap(); + +let result = StabilizerState::run_circuit(&c).unwrap(); +let Some(RuntimeValue::BitVec { width, bits }) = result.classical.value(measured.value()) else { + panic!("expected BitVec measurement result"); +}; +assert_eq!(*width, 3); +assert_eq!(bits.to_bitstring(*width as usize), "101"); +``` + +--- + +## 校验与错误处理 + +`value()` 与 `var()` 以 `Option` 表达查不到的情况,本身不返回错误。写入侧的校验发生在执行线路时,类型不匹配或句柄不属于当前线路都会作为 `QisError` 返回: + +| 错误 | 触发场景 | +| --- | --- | +| `QisError::CircuitError` | 执行线路时使用了不属于该线路的经典句柄,对应 `CircuitError::ForeignClassicalHandle`。 | +| `QisError::UnsupportedOperation` | 经典值或经典变量的类型与线路 IR 中登记的类型不一致,或运行时值的类型无法参与该表达式运算。 | + +由于查表使用句柄中携带的 `CircuitId` 与索引,跨线路读取只会得到 `None`,不会读到另一条线路的数据。 + +--- + +## 相关页面 + +- [QIS 概览](0_overview.md):模块总览与术语表。 +- [Pauli 算子](6_pauli.md):从测量概率计算 Pauli 串期望值时使用的比特序约定。 diff --git a/docs/api/cn/rust/3_qis/6_pauli.md b/docs/api/cn/rust/3_qis/6_pauli.md new file mode 100644 index 00000000..b2a1d7c9 --- /dev/null +++ b/docs/api/cn/rust/3_qis/6_pauli.md @@ -0,0 +1,346 @@ +# Pauli 算子与 Pauli 串 + +`cqlib_core::qis::pauli` + +本页介绍单比特 Pauli 算子 `Pauli`、Pauli 群相位 `Phase`、多比特 Pauli 串 `PauliString`,以及按比特遍历 Pauli 串的迭代器 `PauliIter`。 + +`PauliString` 采用辛表示(symplectic encoding):每个比特用一对二进制位描述,`x[i] = 1` 表示该比特含 X 分量,`z[i] = 1` 表示该比特含 Z 分量,`(0,0)`、`(1,0)`、`(1,1)`、`(0,1)` 分别对应 `I`、`X`、`Y`、`Z`。乘法的相位变化按位查表累加,比特向量按位异或,因此乘法的代价是每比特常数时间。 + +## 导入 + +```rust +use cqlib_core::qis::pauli::{Pauli, PauliIter, PauliString, Phase}; +``` + +`Pauli`、`PauliString`、`PauliIter` 与 `Phase` 也在 `cqlib_core::qis` 顶层重导出。 + +--- + +## Phase + +Pauli 群的相位因子,与 4 阶循环群同构,表示 $i^n$。 + +```rust +#[repr(u8)] +pub enum Phase { + Plus = 0, // i^0 = 1 + I = 1, // i^1 = i + Minus = 2, // i^2 = -1 + MinusI = 3, // i^3 = -i +} +``` + +变体: + +| 变体 | 复数值 | 指数 | +| --- | --- | --- | +| `Phase::Plus` | `1` | 0 | +| `Phase::I` | `i` | 1 | +| `Phase::Minus` | `-1` | 2 | +| `Phase::MinusI` | `-i` | 3 | + +### 方法 + +- `fn to_complex(&self) -> Complex64`:返回该相位对应的复数。 + +### 其他行为 + +- `From`:按模 4 取余转换,`Phase::from(5)` 等于 `Phase::I`。 +- `Add`、`AddAssign`:相位相加即相位相乘,$i^a \cdot i^b = i^{(a+b) \bmod 4}$。 +- `Add`、`AddAssign`:以无符号整数作为指数增量。 +- `Mul`:与相加等价。 +- `Display`:输出 `1`、`i`、`-1`、`-i`,不带引号与调试修饰。 +- 支持 `Debug`、`Clone`、`Copy`、`PartialEq`、`Eq`、`Hash`。 + +示例: + +```rust +use cqlib_core::qis::pauli::Phase; +use num_complex::Complex64; + +// i * i = -1 +assert_eq!(Phase::I + Phase::I, Phase::Minus); +assert_eq!(Phase::I * Phase::I, Phase::Minus); + +// 指数回绕 +assert_eq!(Phase::MinusI + Phase::I, Phase::Plus); + +// 按模 4 构造 +assert_eq!(Phase::from(5), Phase::I); +assert_eq!(Phase::from(100), Phase::Plus); + +assert_eq!(Phase::I.to_complex(), Complex64::new(0.0, 1.0)); +assert_eq!(Phase::MinusI.to_string(), "-i"); +``` + +--- + +## Pauli + +单比特 Pauli 算子。 + +```rust +pub enum Pauli { + X, + Y, + Z, + I, +} +``` + +矩阵表示: + +| 变体 | 矩阵 | 说明 | +| --- | --- | --- | +| `Pauli::I` | $\begin{pmatrix} 1 & 0 \\ 0 & 1 \end{pmatrix}$ | 恒等算子 | +| `Pauli::X` | $\begin{pmatrix} 0 & 1 \\ 1 & 0 \end{pmatrix}$ | 比特翻转 | +| `Pauli::Y` | $\begin{pmatrix} 0 & -i \\ i & 0 \end{pmatrix}$ | Y 算子 | +| `Pauli::Z` | $\begin{pmatrix} 1 & 0 \\ 0 & -1 \end{pmatrix}$ | 相位翻转 | + +### 方法 + +- `fn to_symplectic(&self) -> (u8, u8)`:返回 `(x, z)` 辛表示分量。 +- `fn to_matrix(&self) -> Array2`:返回 2×2 复矩阵。 +- `fn mul_with_phase(&self, other: Pauli) -> (Pauli, Phase)`:返回乘积算子与相位,例如 `X * Z = -iY`。 + +### 其他行为 + +- `TryFrom`:接受大写字符 `I`、`X`、`Y`、`Z`,其他字符返回 `QisError::InvalidParameterValue`。 +- `FromStr`:单字符解析,空串或多字符返回 `QisError::InvalidParameterValue`。 +- `Display`:输出算子字符。 +- 支持 `Debug`、`Clone`、`Copy`、`PartialEq`、`Eq`、`Hash`。 + +示例: + +```rust +use cqlib_core::qis::pauli::{Pauli, Phase}; + +// X * Y = iZ +let (product, phase) = Pauli::X.mul_with_phase(Pauli::Y); +assert_eq!(product, Pauli::Z); +assert_eq!(phase, Phase::I); + +// Y * X = -iZ,与上式相差符号 +let (product, phase) = Pauli::Y.mul_with_phase(Pauli::X); +assert_eq!(product, Pauli::Z); +assert_eq!(phase, Phase::MinusI); + +assert_eq!(Pauli::Y.to_symplectic(), (1, 1)); +assert_eq!("X".parse::().unwrap(), Pauli::X); +assert!("XY".parse::().is_err()); +``` + +--- + +## PauliString + +多比特 Pauli 串,$P = \bigotimes_{i=0}^{N-1} P_i$。 + +```rust +pub struct PauliString { + pub num_qubits: usize, + pub phase: Phase, + pub z: BitVec, + pub x: BitVec, +} +``` + +字段: + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| `num_qubits` | `usize` | 串包含的比特数 $N$。 | +| `phase` | `Phase` | 全局相位因子。 | +| `z` | `BitVec` | Z 分量比特向量,长度为 `num_qubits`。 | +| `x` | `BitVec` | X 分量比特向量,长度为 `num_qubits`。 | + +字符串与比特下标约定:字符串中最左边的字符对应最高比特下标,即按张量积顺序书写。因此 `"XZI"` 表示比特 2 为 `X`、比特 1 为 `Z`、比特 0 为 `I`。字段本身按比特下标升序存储,`x[0]` 与 `z[0]` 描述比特 0。 + +### 构造与查询 + +- `fn new(num_qubits: usize) -> Self`:创建全为恒等算子的 Pauli 串,相位为 `Phase::Plus`。 +- `fn set_pauli(&mut self, idx: usize, p: Pauli)`:设置指定比特上的算子;下标越界时 panic。 +- `fn get_pauli(&self, idx: usize) -> Pauli`:读取指定比特上的算子;下标越界时 panic。 +- `fn try_set_pauli(&mut self, idx: usize, pauli: Pauli) -> Result<(), QisError>`:越界时返回 `QisError::IndexOutOfBounds`,不修改原串。 +- `fn try_get_pauli(&self, idx: usize) -> Result`:越界时返回 `QisError::IndexOutOfBounds`。 +- `fn iter(&self) -> PauliIter<'_>`:按比特下标升序遍历。 +- `fn support(&self) -> Vec`:返回非恒等算子的比特下标,升序排列。 +- `fn x_mask(&self) -> usize`:把 X 分量比特向量压成无符号整数掩码,第 `i` 位对应比特 `i`。 +- `fn z_mask(&self) -> usize`:把 Z 分量比特向量压成无符号整数掩码。 + +### 运算 + +- `fn commutes_with(&self, other: &Self) -> bool`:按辛内积判断对易性,内积为 0 即对易;比特数不同时 panic。 +- 串间的乘法由 `Mul` 与 `MulAssign` 提供,相位随乘积累加,见「其他行为」。 + +### 矩阵、相位与期望值 + +- `fn to_matrix(&self) -> Array2`:返回 $2^N \times 2^N$ 稠密矩阵。比特 0 是最低有效张量因子,串按 $P_{N-1} \otimes \cdots \otimes P_0$ 展开,全局相位计入结果。 +- `fn y_phase(&self) -> Complex64`:返回 Y 分量贡献的相位因子。辛表示下 $Y = iXZ$,$n$ 个 Y 贡献 $i^n$。 +- `fn expectation(&self, probs: &HashMap) -> Result`:在计算基概率分布上计算期望值 $\langle P \rangle = \sum_s p(s)\langle s|P|s\rangle$。 + +`expectation` 的约定与错误: + +- 概率字典的键按小端书写,最右字符对应比特 0。例如 `"01"` 表示比特 0 为 1、比特 1 为 0。 +- 串中含 X 或 Y 时,计算基下的期望值恒为 0,直接返回 `Ok(0.0)`。 +- 只含 Z 与 I 时,期望值为 $\langle P \rangle = \text{phase} \times \sum_s p(s) \times (-1)^{\sum_i z[i] s[i]}$。 +- 键长度与 `num_qubits` 不一致时返回 `QisError::DimensionMismatch`;键中出现 `0`、`1` 以外的字符时返回 `QisError::PauliStringParseError`。 + +### 其他行为 + +- `Display`:输出形如 `+XYZ` 或 `-iZIX`,比特按最高下标在左的顺序打印。 +- `FromStr`:接受 `[+|-][i|j]<算子序列>`,例如 `"XZI"`、`"+XYZ"`、`"-iZII"`、`"+jX"`;解析失败返回 `PauliStringParseError`。 +- `Mul for &PauliString`:返回新的 Pauli 串;两边比特数不同时 panic。 +- `MulAssign<&PauliString>`:原地相乘,相位按位累加、比特向量按位异或。 +- `IntoIterator for &PauliString`:等价于 `iter()`。 +- 支持 `Debug`、`Clone`、`PartialEq`、`Eq`、`Hash`。 + +示例: + +```rust +use cqlib_core::qis::pauli::{Pauli, PauliString}; + +// 逐位设置 +let mut ps = PauliString::new(3); +ps.set_pauli(0, Pauli::X); +assert_eq!(ps.to_string(), "+IIX"); + +ps.set_pauli(2, Pauli::Z); +assert_eq!(ps.to_string(), "+ZIX"); + +// 字符串解析使用最高比特在左的顺序 +let parsed: PauliString = "XZI".parse().unwrap(); +assert_eq!(parsed.get_pauli(0), Pauli::I); +assert_eq!(parsed.get_pauli(1), Pauli::Z); +assert_eq!(parsed.get_pauli(2), Pauli::X); +assert_eq!(parsed.support(), vec![1, 2]); +``` + +```rust +use cqlib_core::qis::pauli::{Pauli, PauliString}; + +// X * Z = -iY,相位随乘积一并保存 +let mut x = PauliString::new(1); +x.set_pauli(0, Pauli::X); +let mut z = PauliString::new(1); +z.set_pauli(0, Pauli::Z); + +let product = &x * &z; +assert_eq!(product.to_string(), "-iY"); + +// 原地乘法:X * Y = iZ +let mut y = PauliString::new(1); +y.set_pauli(0, Pauli::Y); + +x *= &y; +assert_eq!(x.to_string(), "+iZ"); +``` + +对易性与掩码: + +```rust +use cqlib_core::qis::pauli::{Pauli, PauliString}; + +// XX 与 ZZ 在两个比特上都反对易,反对易次数为偶数,因此整体对易 +let mut xx = PauliString::new(2); +xx.set_pauli(0, Pauli::X); +xx.set_pauli(1, Pauli::X); + +let mut zz = PauliString::new(2); +zz.set_pauli(0, Pauli::Z); +zz.set_pauli(1, Pauli::Z); + +assert!(xx.commutes_with(&zz)); + +// 同一比特上的 X 与 Z 反对易 +let mut x = PauliString::new(1); +x.set_pauli(0, Pauli::X); +let mut z = PauliString::new(1); +z.set_pauli(0, Pauli::Z); +assert!(!x.commutes_with(&z)); + +// 掩码:Y 同时置起 X 与 Z 分量 +let mut ps = PauliString::new(3); +ps.set_pauli(0, Pauli::X); +ps.set_pauli(2, Pauli::Y); +assert_eq!(ps.x_mask(), 0b101); +``` + +从概率分布计算期望值: + +```rust +use cqlib_core::qis::pauli::{Pauli, PauliString}; +use std::collections::HashMap; + +let mut zz = PauliString::new(2); +zz.set_pauli(0, Pauli::Z); +zz.set_pauli(1, Pauli::Z); + +// Bell 态的概率分布,键按小端书写 +let mut probs = HashMap::new(); +probs.insert("00".to_string(), 0.5); +probs.insert("11".to_string(), 0.5); + +assert!((zz.expectation(&probs).unwrap() - 1.0).abs() < 1e-10); +``` + +--- + +## PauliIter + +按比特下标升序遍历 Pauli 串的迭代器。 + +```rust +pub struct PauliIter<'a> { + // 字段不公开 +} +``` + +- `Iterator`:逐项返回比特上的算子,`size_hint` 精确。 +- 实现 `ExactSizeIterator` 与 `FusedIterator`。 + +示例: + +```rust +use cqlib_core::qis::pauli::{Pauli, PauliString}; + +let pauli: PauliString = "XYZI".parse().unwrap(); +let collected: Vec = pauli.iter().collect(); +let expected: Vec = (0..pauli.num_qubits) + .map(|index| pauli.get_pauli(index)) + .collect(); + +assert_eq!(collected, expected); + +// 借用 PauliString 也可以直接进入 for 循环 +let mut count = 0; +for _ in &pauli { + count += 1; +} +assert_eq!(count, 4); +``` + +--- + +## 校验与错误处理 + +`QisError` 与 `PauliStringParseError` 是本节各接口的错误来源: + +| 错误 | 触发场景 | +| --- | --- | +| `QisError::InvalidParameterValue` | `Pauli` 由字符或字符串构造时遇到非 `I`、`X`、`Y`、`Z` 的输入,或输入为空串、多字符。 | +| `QisError::IndexOutOfBounds` | `try_get_pauli()` 与 `try_set_pauli()` 的比特下标不小于 `num_qubits`。 | +| `QisError::DimensionMismatch` | `expectation()` 的概率键长度与 `num_qubits` 不一致。 | +| `QisError::PauliStringParseError` | `expectation()` 的概率键中出现非 `0`、`1` 字符。 | +| `PauliStringParseError::EmptyString` | 解析 `PauliString` 时输入为空串。 | +| `PauliStringParseError::InvalidCharacter` | 解析 `PauliString` 时出现非 `I`、`X`、`Y`、`Z` 的字符。 | +| `PauliStringParseError::NoOperators` | 解析 `PauliString` 时只有相位前缀而没有算子,例如 `"-"`。 | + +以下情况以 panic 表达,而不是返回错误:`set_pauli()` 与 `get_pauli()` 的下标越界,`commutes_with()`、`Mul`、`MulAssign` 两侧比特数不一致。需要以错误形式处理越界时,改用 `try_set_pauli()` 与 `try_get_pauli()`。 + +--- + +## 相关页面 + +- [QIS 概览](0_overview.md):模块总览与术语表。 +- [Hamiltonian 与 Observable](7_hamiltonian.md):由 Pauli 串与系数构造可观测量并求期望值。 diff --git a/docs/api/cn/rust/3_qis/7_hamiltonian.md b/docs/api/cn/rust/3_qis/7_hamiltonian.md new file mode 100644 index 00000000..a8a5cf38 --- /dev/null +++ b/docs/api/cn/rust/3_qis/7_hamiltonian.md @@ -0,0 +1,284 @@ +# Hamiltonian 与 Observable + +`cqlib_core::qis::hamiltonian` +`cqlib_core::qis::observable` + +本页介绍由 Pauli 串与复系数构成的观测量 `Hamiltonian`,以及计算期望值的统一接口 `Observable` trait。 + +`Hamiltonian` 是 $2^N \times 2^N$ 矩阵的稀疏表示,形式为 $H = \sum_k c_k P_k$,其中 $c_k$ 是复系数,$P_k$ 是 $N$ 比特 Pauli 串。同一个结构既用于物理解释中的哈密顿量,也用于任意需要求期望值的可观测量。 + +## 导入 + +```rust +use cqlib_core::qis::hamiltonian::Hamiltonian; +use cqlib_core::qis::observable::Observable; +``` + +两个类型也在 `cqlib_core::qis` 顶层重导出。 + +--- + +## Hamiltonian + +```rust +pub struct Hamiltonian { + pub num_qubits: usize, + pub terms: Vec<(PauliString, Complex64)>, +} +``` + +字段: + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| `num_qubits` | `usize` | 观测量作用的比特数。 | +| `terms` | `Vec<(PauliString, Complex64)>` | 项列表,每项由 Pauli 串与复系数组成。 | + +### 构造 + +- `fn new(num_qubits: usize) -> Self`:创建指定比特数下的零算子,项列表为空。 +- `fn from_pauli(pauli: PauliString) -> Self`:用单个 Pauli 串构造,系数取 `1.0`。等价于 `PauliString` 的 `Into` 转换。 +- `fn from_list(ops: Vec<(PauliString, Complex64)>) -> Result`:从项列表构造。空列表返回 `num_qubits` 为 0 的零算子;各项比特数不一致时返回 `QisError::QubitMismatch`。等价于 `TryFrom>`。 +- `fn add_term(&mut self, op: PauliString, coeff: Complex64) -> Result<(), QisError>`:追加一项。`op` 的比特数与 `num_qubits` 不一致时返回 `QisError::QubitMismatch`。 + +### 化简与缩放 + +- `fn simplify(&mut self)`:化简项列表。该操作分两步:把每个 `PauliString` 的内部相位吸收进系数,使所有 Pauli 串归一到 `Phase::Plus`;再按 X、Z 分量向量分组,合并相同算子的系数。绝对值小于 $10^{-10}$ 的系数会被丢弃。 +- `fn scale(&mut self, factor: Complex64)`:把所有项的系数乘以同一个复因子。 + +### 查询与转换 + +- `fn all_terms_commute(&self) -> bool`:判断所有 Pauli 项是否两两对易。为真时 $e^{-iHt}$ 可以按项分解为精确演化。 +- `fn to_matrix(&self) -> Result, QisError>`:返回 $2^N \times 2^N$ 稠密矩阵。任一项的比特数与 `num_qubits` 不一致时返回 `QisError::QubitMismatch`。空算量返回对应维度的零矩阵。 +- `fn to_trotter_circuit(&self, time: f64, steps: usize, mode: TrotterMode) -> Result`:见 [Trotter 演化](8_evolution.md)。 +- `fn to_evolution_circuit(&self, time: f64, steps: usize, mode: TrotterMode) -> Result`:见 [Trotter 演化](8_evolution.md)。 + +### 其他行为 + +- `Display`:逐项输出 `(系数) * Pauli串`,系数用复数的显示形式,项之间以 ` + ` 连接。项列表为空时输出空串。 +- `Add`:把两条 Hamilton 量的项列表直接拼接,不做合并;两边比特数不同时 panic。需要合并相同项时,在相加后调用 `simplify()`。 +- `From`、`TryFrom>`:分别对应 `from_pauli()` 与 `from_list()`。 +- 实现 `Observable`,见下文。 +- 支持 `Debug`、`Clone`、`PartialEq`。 + +示例: + +```rust +use cqlib_core::qis::{Hamiltonian, PauliString}; + +let mut h = Hamiltonian::new(2); +h.add_term("ZZ".parse::().unwrap(), 0.5.into()).unwrap(); +h.add_term("XX".parse::().unwrap(), 0.3.into()).unwrap(); + +assert_eq!(h.terms.len(), 2); +// ZZ 与 XX 在每个量子比特上都同时反交换,整体相乘后相互抵消,因此两者对易 +assert!(h.all_terms_commute()); +``` + +换成在各量子比特上对易性不同的项,判定结果为假: + +```rust +use cqlib_core::qis::{Hamiltonian, PauliString}; + +let mut h = Hamiltonian::new(2); +h.add_term("XI".parse::().unwrap(), 0.5.into()).unwrap(); +h.add_term("ZI".parse::().unwrap(), 0.3.into()).unwrap(); + +// 同一量子比特上的 X 与 Z 反交换 +assert!(!h.all_terms_commute()); +``` + +```rust +use cqlib_core::qis::{Hamiltonian, PauliString}; +use num_complex::Complex64; + +// 同一 Pauli 串的两项会被合并,系数相加 +let mut h = Hamiltonian::from_list(vec![ + ("ZZ".parse::().unwrap(), 0.5.into()), + ("ZZ".parse::().unwrap(), 0.3.into()), +]) +.unwrap(); + +h.simplify(); + +assert_eq!(h.terms.len(), 1); +assert!((h.terms[0].1 - Complex64::new(0.8, 0.0)).norm() < 1e-10); +``` + +```rust +use cqlib_core::qis::{Hamiltonian, PauliString}; + +// ZZ 与 IZ 对易,演化可以按项精确分解 +let mut h = Hamiltonian::new(2); +h.add_term("ZZ".parse().unwrap(), 1.0.into()).unwrap(); +h.add_term("IZ".parse().unwrap(), 0.5.into()).unwrap(); + +assert!(h.all_terms_commute()); +``` + +稠密矩阵: + +```rust +use cqlib_core::qis::{Hamiltonian, PauliString, Phase}; +use num_complex::Complex64; + +let mut x: PauliString = "X".parse().unwrap(); +x.phase = Phase::Minus; +let z: PauliString = "Z".parse().unwrap(); + +let hamiltonian = Hamiltonian::from_list(vec![ + (x, Complex64::new(2.0, 0.0)), + (z, Complex64::new(0.0, 1.0)), +]) +.unwrap(); + +assert_eq!( + hamiltonian.to_matrix().unwrap(), + ndarray::arr2(&[ + [Complex64::new(0.0, 1.0), Complex64::new(-2.0, 0.0)], + [Complex64::new(-2.0, 0.0), Complex64::new(0.0, -1.0)], + ]) +); +``` + +--- + +## Observable + +计算可观测量期望值的统一接口,用于在不同的量子态表示上求同一个观测量。 + +```rust +pub trait Observable { + fn expectation_statevector(&self, sv: &Statevector) -> Result; + fn expectation_density_matrix(&self, dm: &DensityMatrix) -> Result; + fn expectation_probs( + &self, + measurements: &[(PauliString, HashMap)], + ) -> Result; + fn num_qubits(&self) -> usize; + fn variance_statevector(&self, _sv: &Statevector) -> Result { + Err(QisError::UnsupportedOperation( + "variance_statevector not implemented for this observable type".into(), + )) + } +} +``` + +### 必需方法 + +- `fn expectation_statevector(&self, sv: &Statevector) -> Result`:计算纯态期望值 $\langle \psi | O | \psi \rangle$。态的比特数与 `num_qubits()` 不一致时返回 `QisError::QubitMismatch`。 +- `fn expectation_density_matrix(&self, dm: &DensityMatrix) -> Result`:计算混态期望值 $\text{Tr}(\rho O)$。比特数不一致时返回 `QisError::QubitMismatch`。 +- `fn expectation_probs(&self, measurements: &[(PauliString, HashMap)]) -> Result`:从测量概率计算期望值。每个元素是一个测量基及其状态串到概率的映射。状态串按小端书写,最右字符对应比特 0。 +- `fn num_qubits(&self) -> usize`:返回观测量作用的比特数,用于求期望值前的维度校验。 + +### 默认方法 + +- `fn variance_statevector(&self, sv: &Statevector) -> Result`:计算纯态方差 $\text{Var}(O) = \langle O^2 \rangle - \langle O \rangle^2$。trait 中的默认实现返回 `QisError::UnsupportedOperation`,支持方差计算的观测量需要覆写该方法。 + +### 实现者 + +| 类型 | 说明 | +| --- | --- | +| `Hamiltonian` | 逐项计算后按系数累加,用于多项哈密顿量。 | +| `PauliString` | 单项 Pauli 串,用于单个可观测量的直接测量。 | + +### `expectation_probs` 的测量基匹配 + +对每一个非恒等项,接口在给定的测量列表中查找相容的测量基:若测量基在项的非恒等位置上的 X、Z 分量与该项一致,即视为相容。恒等项直接贡献系数与全局相位,不需要测量基。 + +找不到相容测量基时返回 `QisError::UnsupportedOperation`。`Hamiltonian` 与 `PauliString` 的实现都按此规则匹配,状态串相关的错误分支略有差异: + +| 情况 | `Hamiltonian` | `PauliString` | +| --- | --- | --- | +| 状态串长度与比特数不一致 | `QisError::DimensionMismatch` | `QisError::QubitMismatch` | +| 状态串出现非 `0`、`1` 字符 | `QisError::PauliStringParseError` | `QisError::UnsupportedOperation` | +| 找不到相容测量基 | `QisError::UnsupportedOperation` | `QisError::UnsupportedOperation` | + +`Hamiltonian` 的实现忽略项中 Y 分量引入的相位,只计入全局相位与系数。 + +### 示例 + +Bell 态上的纯态期望值: + +```rust +use cqlib_core::qis::{Hamiltonian, Observable, PauliString, Statevector}; + +let mut sv = Statevector::new(2); +sv.apply_h(0).unwrap(); +sv.apply_cx(0, 1).unwrap(); + +let ps: PauliString = "ZZ".parse().unwrap(); +let h = Hamiltonian::from_pauli(ps); + +assert!((h.expectation_statevector(&sv).unwrap() - 1.0).abs() < 1e-10); +assert_eq!(h.num_qubits(), 2); +``` + +从测量概率求期望值。三条 Z 项都对同一个 ZZ 测量基相容: + +```rust +use cqlib_core::qis::{Hamiltonian, Observable, PauliString}; +use std::collections::HashMap; + +let mut h = Hamiltonian::new(2); +let mut z1 = PauliString::new(2); +z1.set_pauli(1, cqlib_core::qis::pauli::Pauli::Z); +h.add_term(z1, 2.0.into()).unwrap(); + +let mut z0 = PauliString::new(2); +z0.set_pauli(0, cqlib_core::qis::pauli::Pauli::Z); +h.add_term(z0, 3.0.into()).unwrap(); + +let mut zz = PauliString::new(2); +zz.set_pauli(0, cqlib_core::qis::pauli::Pauli::Z); +zz.set_pauli(1, cqlib_core::qis::pauli::Pauli::Z); +h.add_term(zz.clone(), 4.0.into()).unwrap(); + +let mut probs = HashMap::new(); +probs.insert("10".to_string(), 1.0); + +let measurements = vec![(zz, probs)]; +assert!((h.expectation_probs(&measurements).unwrap() - (-3.0)).abs() < 1e-10); +``` + +方差: + +```rust +use cqlib_core::qis::{Hamiltonian, Observable, PauliString, Statevector}; + +// |+> 态上 Z 的方差为 1,系数 2.0 的平方给出 4.0 +let mut sv = Statevector::new(1); +sv.apply_h(0).unwrap(); + +let mut z = PauliString::new(1); +z.set_pauli(0, cqlib_core::qis::pauli::Pauli::Z); + +let mut h = Hamiltonian::new(1); +h.add_term(z, 2.0.into()).unwrap(); + +assert!((h.variance_statevector(&sv).unwrap() - 4.0).abs() < 1e-10); +``` + +--- + +## 校验与错误处理 + +| 错误 | 触发场景 | +| --- | --- | +| `QisError::QubitMismatch` | 构造或追加项时 Pauli 串的比特数与 `num_qubits` 不一致;求期望值或方差时态与观测量的比特数不一致。 | +| `QisError::NotHermitian` | 求方差时观测量不是 Hermitian,例如 `PauliString` 的相位为 `±i`,或化简后仍残留虚部超过容差的系数。 | +| `QisError::UnsupportedOperation` | 未覆写 `variance_statevector()` 的观测量调用该方法;`expectation_probs()` 找不到相容的测量基。 | +| `QisError::DimensionMismatch` | `Hamiltonian::expectation_probs()` 中状态串长度与比特数不一致。 | +| `QisError::PauliStringParseError` | `Hamiltonian::expectation_probs()` 中状态串出现非 `0`、`1` 字符。 | + +`Add` 在两侧比特数不一致时 panic,而不是返回错误。 + +方差计算对数值噪声有一定容忍:结果落在零附近的小幅负值会被截断为 0,超出容差的负值改为返回 `QisError::InvalidParameterValue`。 + +--- + +## 相关页面 + +- [QIS 概览](0_overview.md):模块总览与术语表。 +- [Pauli 算子与 Pauli 串](6_pauli.md):项的构造、对易性与辛表示。 +- [Trotter 演化](8_evolution.md):把 Hamilton 量转换为演化线路。 diff --git a/docs/api/cn/rust/3_qis/8_evolution.md b/docs/api/cn/rust/3_qis/8_evolution.md new file mode 100644 index 00000000..73e420db --- /dev/null +++ b/docs/api/cn/rust/3_qis/8_evolution.md @@ -0,0 +1,321 @@ +# Pauli 演化与 Trotter 分解 + +`cqlib_core::qis::evolution` + +本页介绍把 Pauli 串与 Hamilton 量转换为演化线路的接口:`PauliEvolution` trait 提供单条 Pauli 串的旋转门,`Hamiltonian` 上的两个入口把它扩展成整段时间演化线路,`TrotterMode` 决定多项非对易情形的近似方式。 + +## 导入 + +```rust +use cqlib_core::qis::evolution::{PauliEvolution, TrotterMode}; +``` + +`PauliEvolution` 与 `TrotterMode` 也在 `cqlib_core::qis` 顶层重导出。 + +--- + +## TrotterMode + +Hamilton 量时间演化 $U(t) = e^{-iHt}$ 的 Trotter-Suzuki 分解模式。 + +```rust +pub enum TrotterMode { + FirstOrder, + SecondOrder, + Randomized(u64), +} +``` + +变体: + +| 变体 | 载荷 | 近似形式 | 误差量级 | +| --- | --- | --- | --- | +| `TrotterMode::FirstOrder` | 无 | $U(t) \approx \left[\prod_k e^{-i c_k t/n \cdot P_k}\right]^n$ | $O(t^2/n)$ | +| `TrotterMode::SecondOrder` | 无 | 每步先按正序、再按逆序各作用半角演化 | $O(t^3/n^2)$ | +| `TrotterMode::Randomized(u64)` | 随机种子 | 与一阶相同,但每步内各项的顺序随机打乱 | 与一阶同阶 | + +`Randomized` 携带的种子决定每一项在每一步中的排列顺序,同一个种子给出同一条线路,便于重复实验。 + +### 其他行为 + +- 支持 `Debug`、`Clone`、`Copy`、`PartialEq`、`Eq`、`Hash`。 + +示例: + +```rust +use cqlib_core::qis::evolution::TrotterMode; + +// 同一种子的模式相等,可重复使用 +assert_eq!(TrotterMode::Randomized(42), TrotterMode::Randomized(42)); +assert_ne!(TrotterMode::Randomized(42), TrotterMode::Randomized(43)); +assert_ne!(TrotterMode::FirstOrder, TrotterMode::SecondOrder); +``` + +--- + +## PauliEvolution + +向线路追加 Pauli 旋转门的扩展 trait。 + +```rust +pub trait PauliEvolution { + fn pauli_evolution( + &mut self, + pauli: &PauliString, + angle: impl Into, + qubits: &[Qubit], + ) -> Result<(), CircuitError>; +} +``` + +### 方法 + +- `fn pauli_evolution(&mut self, pauli: &PauliString, angle: impl Into, qubits: &[Qubit]) -> Result<(), CircuitError>`:追加演化算符 $e^{-i\theta/2 \cdot P}$。 + +参数: + +- `pauli` (`&PauliString`):要指数化的 Pauli 串 $P$,相位必须为 $\pm 1$(Hermitian)。 +- `angle` (`impl Into`):旋转角 $\theta$,可以是固定浮点数,也可以是符号参数。 +- `qubits` (`&[Qubit]`):作用位置,长度必须与 `pauli.num_qubits` 一致。 + +注意这里的角度约定:$\theta$ 是旋转角的一半因子所对应的角,$e^{-i\theta/2 \cdot P}$ 中的 $\theta$ 取 $2ct$ 才等价于 $e^{-ictP}$。把 Hamilton 量交给演化入口时不需要手动换算,入口内部已按此约定处理。 + +### 实现者 + +| 类型 | 说明 | +| --- | --- | +| `Circuit` | 把旋转门直接追加到目标线路的末尾。 | + +### 实现步骤 + +一、相位校验。Pauli 串的内部相位必须是 $\pm 1$,能为酉演化生成元的算符只有 Hermitian 算符,相位为 $\pm i$ 时直接报错。 + +二、相位吸收。把 $\pm 1$ 吸收进旋转角,得到有效角 $\theta_{\text{eff}} = \theta \cdot \text{phase}$。 + +三、基变换。对每个非恒等比特施加把分量转到 Z 基的门:`X` 用 `H`,`Y` 先用 $S^\dagger$ 再用 `H`,`Z` 不需要变换。 + +四、CNOT 链。按非恒等比特下标递增的顺序施加 `CNOT`,逐级累积奇偶性。 + +五、核心旋转。在最后一个非恒等比特上施加 `RZ(θ_eff)`。 + +六、逆序 CNOT 链与逆基变换,把比特恢复到原来的基。 + +当 Pauli 串全为恒等算子时,演化退化为全局相位 $e^{-i\theta/2}$,不产生任何门,相位记在线路的全局相位上。 + +### 错误 + +- `CircuitError::QubitCountMismatch`:`qubits` 的长度与 `pauli.num_qubits` 不一致。 +- `CircuitError::InvalidOperation`:Pauli 串的相位为 $\pm i$,不能作为酉演化的生成元。 + +### 示例 + +单比特旋转: + +```rust +use cqlib_core::circuit::Circuit; +use cqlib_core::qis::evolution::PauliEvolution; +use cqlib_core::qis::pauli::PauliString; + +let mut circuit = Circuit::new(1); +let qubits = circuit.qubits(); + +// Z 分量不需要基变换,只产生一个 RZ 门 +let pauli: PauliString = "Z".parse().unwrap(); +circuit + .pauli_evolution(&pauli, std::f64::consts::PI, &qubits) + .unwrap(); + +assert_eq!(circuit.operations().len(), 1); +``` + +多比特旋转: + +```rust +use cqlib_core::circuit::Circuit; +use cqlib_core::qis::evolution::PauliEvolution; +use cqlib_core::qis::pauli::PauliString; + +let mut circuit = Circuit::new(2); +let qubits = circuit.qubits(); + +// ZZ 链:CNOT(0->1) - RZ(1) - CNOT(0->1) +let pauli: PauliString = "ZZ".parse().unwrap(); +circuit + .pauli_evolution(&pauli, std::f64::consts::PI / 2.0, &qubits) + .unwrap(); + +let ops = circuit.operations(); +assert_eq!(ops.len(), 3); +assert_eq!(ops[0].qubits[0], qubits[0]); +assert_eq!(ops[0].qubits[1], qubits[1]); +assert_eq!(ops[1].qubits[0], qubits[1]); +``` + +全恒等串只贡献全局相位: + +```rust +use cqlib_core::circuit::Circuit; +use cqlib_core::qis::evolution::PauliEvolution; +use cqlib_core::qis::pauli::PauliString; + +let mut circuit = Circuit::new(2); +let qubits = circuit.qubits(); + +let pauli: PauliString = "II".parse().unwrap(); +circuit + .pauli_evolution(&pauli, std::f64::consts::PI, &qubits) + .unwrap(); + +assert_eq!(circuit.operations().len(), 0); +``` + +比特数不一致: + +```rust +use cqlib_core::circuit::Circuit; +use cqlib_core::qis::evolution::PauliEvolution; +use cqlib_core::qis::pauli::PauliString; + +let mut circuit = Circuit::new(2); +let qubits = circuit.qubits(); + +let pauli: PauliString = "XXX".parse().unwrap(); +assert!(circuit.pauli_evolution(&pauli, 1.0, &qubits).is_err()); +``` + +--- + +## Hamiltonian 上的演化入口 + +两个入口都接受相同的参数: + +- `time` (`f64`):总演化时间 $t$。 +- `steps` (`usize`):Trotter 步数 $n$,必须大于 0。 +- `mode` (`TrotterMode`):分解模式。 + +两者都先复制一份 Hamilton 量并化简,再检查系数的虚部:虚部绝对值超过 $10^{-10}$ 时返回 `QisError::NotHermitian`,因为只有 Hermitian 算符才能生成酉演化。 + +### `fn to_trotter_circuit(&self, time: f64, steps: usize, mode: TrotterMode) -> Result` + +按指定模式生成近似演化线路,输出线路的比特数与 `num_qubits` 一致。 + +对于系数为 $c_k$ 的项 $P_k$,时间片 $\Delta t = t/n$,项内旋转角取 $2 c_k \Delta t$。一阶模式逐片按正序作用所有项;二阶模式每片先按正序作用半角 $\Delta t/2$ 的演化,再按逆序作用一遍;随机模式在一阶的基础上对每片内各项的顺序做随机排列,排列由 `TrotterMode::Randomized` 携带的种子决定。 + +示例: + +```rust +use cqlib_core::qis::{Hamiltonian, PauliString}; +use cqlib_core::qis::evolution::TrotterMode; + +// H = 0.5 * ZZ +let mut h = Hamiltonian::new(2); +h.add_term("ZZ".parse::().unwrap(), 0.5.into()).unwrap(); + +// 2 步,每步 3 个门 +let circuit = h.to_trotter_circuit(1.0, 2, TrotterMode::FirstOrder).unwrap(); +assert_eq!(circuit.operations().len(), 6); + +// 二阶模式每片再重复一遍半角演化,1 步共 6 个门 +let circuit = h.to_trotter_circuit(1.0, 1, TrotterMode::SecondOrder).unwrap(); +assert_eq!(circuit.operations().len(), 6); +``` + +### `fn to_evolution_circuit(&self, time: f64, steps: usize, mode: TrotterMode) -> Result` + +自动在精确分解与 Trotter 近似之间选择。先判断所有项是否两两对易: + +- 对易时走精确路径,按 $U(t) = \prod_k e^{-i c_k t P_k}$ 逐项各作用一次,此时 `steps` 与 `mode` 不起作用。 +- 不对易时退化为 `to_trotter_circuit` 的同一套逻辑,`steps` 与 `mode` 生效。 + +示例: + +```rust +use cqlib_core::qis::{Hamiltonian, PauliString}; +use cqlib_core::qis::evolution::TrotterMode; + +// ZZ 与 ZI 对易,直接走精确路径 +let mut h = Hamiltonian::new(2); +h.add_term("ZZ".parse::().unwrap(), 0.5.into()).unwrap(); +h.add_term("ZI".parse::().unwrap(), 0.3.into()).unwrap(); + +let circuit = h.to_evolution_circuit(1.0, 1, TrotterMode::FirstOrder).unwrap(); +assert_eq!(circuit.num_qubits(), 2); +``` + +对易情形下精确路径与一阶 Trotter 给出相同的线路矩阵: + +```rust +use cqlib_core::circuit::circuit_to_matrix; +use cqlib_core::qis::{Hamiltonian, PauliString}; +use cqlib_core::qis::evolution::TrotterMode; + +let mut h = Hamiltonian::new(1); +h.add_term("Z".parse::().unwrap(), 0.7.into()).unwrap(); + +let time = 0.4_f64; +let exact = h.to_evolution_circuit(time, 1, TrotterMode::FirstOrder).unwrap(); +let trotter = h.to_trotter_circuit(time, 1, TrotterMode::FirstOrder).unwrap(); + +let m_exact = circuit_to_matrix(&exact, None).unwrap(); +let m_trotter = circuit_to_matrix(&trotter, None).unwrap(); +for (a, b) in m_exact.iter().zip(m_trotter.iter()) { + assert!((a - b).norm() < 1e-12); +} +``` + +精确路径的矩阵形式: + +```rust +use cqlib_core::circuit::circuit_to_matrix; +use cqlib_core::qis::{Hamiltonian, PauliString}; +use cqlib_core::qis::evolution::TrotterMode; + +// H = 0.5*Z,t = π,U = e^{-i*0.5*π*Z} = diag(-i, i) +let mut h = Hamiltonian::new(1); +h.add_term("Z".parse::().unwrap(), 0.5.into()).unwrap(); + +let circuit = h + .to_evolution_circuit(std::f64::consts::PI, 1, TrotterMode::FirstOrder) + .unwrap(); +let matrix = circuit_to_matrix(&circuit, None).unwrap(); + +assert!((matrix[[0, 0]] - num_complex::Complex64::new(0.0, -1.0)).norm() < 1e-12); +assert!((matrix[[1, 1]] - num_complex::Complex64::new(0.0, 1.0)).norm() < 1e-12); +``` + +--- + +## 校验与错误处理 + +| 错误 | 触发场景 | +| --- | --- | +| `QisError::InvalidParameterValue` | `steps` 为 0,或 Hamilton 量的项列表为空。 | +| `QisError::NotHermitian` | 化简后仍存在虚部绝对值超过容差的系数。 | +| `QisError::UnsupportedOperation` | 线路构造过程中出错,例如项中存在相位为 $\pm i$ 的 Pauli 串。 | +| `CircuitError::QubitCountMismatch` | `pauli_evolution()` 的 `qubits` 长度与 Pauli 串比特数不一致。 | +| `CircuitError::InvalidOperation` | `pauli_evolution()` 的 Pauli 串相位为 $\pm i$。 | + +`steps` 与空 Hamilton 量的检查发生在最前面,因此 `to_evolution_circuit()` 即使会走精确路径,也仍然要求 `steps` 大于 0。 + +示例: + +```rust +use cqlib_core::qis::Hamiltonian; +use cqlib_core::qis::evolution::TrotterMode; + +let h = Hamiltonian::new(2); +assert!(h.to_trotter_circuit(1.0, 1, TrotterMode::FirstOrder).is_err()); + +let mut h = Hamiltonian::new(2); +h.add_term("ZZ".parse().unwrap(), 0.5.into()).unwrap(); +assert!(h.to_trotter_circuit(1.0, 0, TrotterMode::FirstOrder).is_err()); +assert!(h.to_evolution_circuit(1.0, 0, TrotterMode::FirstOrder).is_err()); +``` + +--- + +## 相关页面 + +- [QIS 概览](0_overview.md):模块总览与术语表。 +- [Hamiltonian 与 Observable](7_hamiltonian.md):项的构造、化简、对易性判断与期望值。 +- [Pauli 算子与 Pauli 串](6_pauli.md):相位取值与字符串解析约定。 diff --git a/docs/api/cn/rust/3_qis/9_metrics_entropy.md b/docs/api/cn/rust/3_qis/9_metrics_entropy.md new file mode 100644 index 00000000..26598d7c --- /dev/null +++ b/docs/api/cn/rust/3_qis/9_metrics_entropy.md @@ -0,0 +1,299 @@ +# 度量与熵 + +`cqlib_core::qis::metrics` +`cqlib_core::qis::entropy` + +本页介绍量子态之间的度量与信息量函数。`metrics` 集合纯度、保真度、迹距离、冯·诺依曼熵与偏转置相关的量;`entropy` 集合线性熵、Rényi 熵,以及针对纠缠的度量。两模块的函数都以态矢量或密度矩阵为输入,返回 `Result`(`density_matrix_to_faer` 除外)。 + +## 导入 + +```rust +use cqlib_core::qis::metrics::{ + density_matrix_to_faer, entropy, logarithmic_negativity, partial_transpose, purity_mixed, + purity_pure, state_fidelity_mixed, state_fidelity_pure, state_fidelity_pure_mixed, + trace_distance_mixed, trace_distance_pure, +}; +use cqlib_core::qis::entropy::{ + concurrence, entanglement_entropy_pure, entanglement_of_formation, linear_entropy, negativity, + renyi_entropy, +}; +``` + +两个模块都在 `cqlib_core::qis` 下公开,`metrics` 与 `entropy` 本身不在顶层重导出。 + +--- + +## 纯度 + +### `purity_pure(sv: &Statevector) -> Result` + +返回态矢量的模方 $\langle \psi | \psi \rangle$。归一化的态矢量取值为 `1.0`,因此该函数也可以作为归一化程度的数值校验。 + +### `purity_mixed(dm: &DensityMatrix) -> Result` + +返回密度矩阵的纯度 $\text{Tr}(\rho^2)$。$\rho$ 为 Hermitian 矩阵,实现上按全部元素模方求和。取值落在 $1/2^N$(完全混合态)与 `1.0`(纯态)之间。 + +示例: + +```rust +use cqlib_core::qis::state::{DensityMatrix, Statevector}; +use cqlib_core::qis::metrics::{purity_mixed, purity_pure}; + +let sv = Statevector::new(2); +assert!((purity_pure(&sv).unwrap() - 1.0).abs() < 1e-10); + +// 单比特完全混合态 Tr((I/2)^2) = 0.5 +let dm = DensityMatrix::maximally_mixed(1); +assert!((purity_mixed(&dm).unwrap() - 0.5).abs() < 1e-10); +``` + +--- + +## 保真度 + +### `state_fidelity_pure(sv1: &Statevector, sv2: &Statevector) -> Result` + +两个纯态之间的保真度 $F(\psi, \phi) = |\langle \psi | \phi \rangle|^2$,取值 `0.0`(正交)到 `1.0`(同一态)。比特数不一致时返回 `QisError::QubitMismatch`。 + +### `state_fidelity_pure_mixed(sv: &Statevector, dm: &DensityMatrix) -> Result` + +纯态与混合态之间的保真度 $F(\psi, \rho) = \langle \psi | \rho | \psi \rangle$。比特数不一致时返回 `QisError::QubitMismatch`。 + +### `state_fidelity_mixed(dm1: &DensityMatrix, dm2: &DensityMatrix) -> Result` + +两个混合态之间的保真度 $F(\rho, \sigma) = \left(\text{Tr}\sqrt{\sqrt{\rho}\,\sigma\sqrt{\rho}}\right)^2$。比特数不一致时返回 `QisError::QubitMismatch`,特征分解失败时返回 `QisError::UnsupportedOperation`。 + +示例: + +```rust +use cqlib_core::qis::state::{DensityMatrix, Statevector}; +use cqlib_core::qis::metrics::{state_fidelity_pure, state_fidelity_pure_mixed}; + +let sv0 = Statevector::new(1); + +// |1> 态 +let mut sv1 = Statevector::new(1); +sv1.data_mut()[0] = num_complex::Complex64::new(0.0, 0.0); +sv1.data_mut()[1] = num_complex::Complex64::new(1.0, 0.0); + +assert!((state_fidelity_pure(&sv0, &sv0).unwrap() - 1.0).abs() < 1e-10); +assert!((state_fidelity_pure(&sv0, &sv1).unwrap() - 0.0).abs() < 1e-10); + +// 纯态 |0> 与完全混合态的保真度为 0.5 +let dm_mixed = DensityMatrix::maximally_mixed(1); +assert!((state_fidelity_pure_mixed(&sv0, &dm_mixed).unwrap() - 0.5).abs() < 1e-10); +``` + +--- + +## 迹距离 + +### `trace_distance_pure(sv1: &Statevector, sv2: &Statevector) -> Result` + +两个纯态之间的迹距离 $D(\psi, \phi) = \sqrt{1 - |\langle \psi | \phi \rangle|^2}$,取值 `0.0`(同一态)到 `1.0`(正交)。内部先求保真度,因此比特数不一致时同样返回 `QisError::QubitMismatch`。 + +### `trace_distance_mixed(dm1: &DensityMatrix, dm2: &DensityMatrix) -> Result` + +两个混合态之间的迹距离 $D(\rho, \sigma) = \frac{1}{2}\text{Tr}|\rho - \sigma|$。两者迹都为 1 时,该值等于 $\rho - \sigma$ 的正特征值之和。比特数不一致时返回 `QisError::QubitMismatch`。 + +示例: + +```rust +use cqlib_core::qis::state::{DensityMatrix, Statevector}; +use cqlib_core::qis::metrics::{trace_distance_mixed, trace_distance_pure}; + +let sv0 = Statevector::new(1); + +let mut sv1 = Statevector::new(1); +sv1.data_mut()[0] = num_complex::Complex64::new(0.0, 0.0); +sv1.data_mut()[1] = num_complex::Complex64::new(1.0, 0.0); + +assert!((trace_distance_pure(&sv0, &sv0).unwrap() - 0.0).abs() < 1e-10); +assert!((trace_distance_pure(&sv0, &sv1).unwrap() - 1.0).abs() < 1e-10); + +// |0><0| 与 |1><1| 的迹距离为 1 +let dm0 = DensityMatrix::new(1); +let dm1 = DensityMatrix::from_density_matrix_state( + 1, + vec![ + num_complex::Complex64::new(0.0, 0.0), + num_complex::Complex64::new(0.0, 0.0), + num_complex::Complex64::new(0.0, 0.0), + num_complex::Complex64::new(1.0, 0.0), + ], +) +.unwrap(); + +assert!((trace_distance_mixed(&dm0, &dm1).unwrap() - 1.0).abs() < 1e-10); +``` + +--- + +## 熵 + +`entropy` 属于 `metrics`,`linear_entropy` 与 `renyi_entropy` 属于 `entropy`。 + +### `entropy(dm: &DensityMatrix) -> Result` + +冯·诺依曼熵 $S(\rho) = -\text{Tr}(\rho \log_2 \rho)$,以比特为单位。计算时忽略绝对值小于 $10^{-12}$ 的特征值,以滤除数值噪声。特征分解失败时返回 `QisError::UnsupportedOperation`。 + +### `linear_entropy(dm: &DensityMatrix) -> Result` + +线性熵 $S_L(\rho) = 1 - \text{Tr}(\rho^2)$。纯态为 `0.0`,$N$ 比特完全混合态为 $1 - 1/2^N$。 + +### `renyi_entropy(dm: &DensityMatrix, alpha: f64) -> Result` + +Rényi 熵 $S_\alpha(\rho) = \frac{1}{1-\alpha}\log_2\left(\sum_i \lambda_i^\alpha\right)$,$\lambda_i$ 为密度矩阵的特征值。$\alpha$ 不大于 0 时返回 `QisError::InvalidParameterValue`;$\alpha$ 与 1 的差小于 `f64::EPSILON` 时改用冯·诺依曼熵,避免除零。结果截断为非负值。 + +示例: + +```rust +use cqlib_core::qis::state::DensityMatrix; +use cqlib_core::qis::metrics::entropy; +use cqlib_core::qis::entropy::{linear_entropy, renyi_entropy}; + +// 纯态熵为 0 +let pure = DensityMatrix::new(1); +assert!(entropy(&pure).unwrap().abs() < 1e-10); +assert!(linear_entropy(&pure).unwrap().abs() < 1e-10); + +// 单比特完全混合态的冯·诺依曼熵为 1 比特,线性熵为 0.5 +let mixed = DensityMatrix::maximally_mixed(1); +assert!((entropy(&mixed).unwrap() - 1.0).abs() < 1e-10); +assert!((linear_entropy(&mixed).unwrap() - 0.5).abs() < 1e-10); + +// Rényi 熵在 α 趋向 1 时退化为冯·诺依曼熵 +assert!((renyi_entropy(&mixed, 1.0).unwrap() - 1.0).abs() < 1e-10); +assert!((renyi_entropy(&mixed, 2.0).unwrap() - 1.0).abs() < 1e-10); +assert!(renyi_entropy(&mixed, -1.0).is_err()); +``` + +--- + +## 纠缠度量 + +`entanglement_entropy_pure`、`negativity`、`concurrence` 与 `entanglement_of_formation` 属于 `entropy`,`partial_transpose` 与 `logarithmic_negativity` 属于 `metrics`。 + +### `entanglement_entropy_pure(sv: &Statevector, subsys_a: &[usize]) -> Result` + +二分纯态的纠缠熵,即子系统 A 的约化密度矩阵的冯·诺依曼熵 $E(|\psi\rangle) = S(\rho_A)$。`subsys_a` 给出子系统 A 的比特下标,其余比特组成子系统 B。 + +以下情况返回 `QisError::InvalidSubsystem`:`subsys_a` 为空、包含全部比特,或存在重复下标。下标越界时返回 `QisError::IndexOutOfBounds`。 + +### `partial_transpose(dm: &DensityMatrix, target_qubits: &[usize]) -> Result` + +对指定子系统做偏转置,只转置 `target_qubits` 对应的下标,其余比特保持不变。矩阵按行优先展平,下标约定为 `idx = (ket << n) | bra`。目标下标越界时返回 `QisError::IndexOutOfBounds`。 + +### `negativity(dm: &DensityMatrix, subsys_a: &[usize]) -> Result` + +负度,取偏转置后负特征值绝对值之和 $N(\rho) = \frac{1}{2}\left(\|\rho^{T_A}\|_1 - 1\right)$。在 $2 \otimes 2$ 与 $2 \otimes 3$ 系统上,取值为 0 是可分性的充要条件。与对数负度不同,该量在张量积下不满足可加性。 + +### `logarithmic_negativity(dm: &DensityMatrix, sys_a: &[usize]) -> Result` + +对数负度 $E_N(\rho) = \log_2 \|\rho^{T_A}\|_1$,即可分态下为 0、纠缠态下大于 0 的纠缠度量,也是对可提纯纠缠量的上界。实现上先做偏转置,再把特征值绝对值求和后取以 2 为底的对数,结果下限截断为 0。 + +### `concurrence(dm: &DensityMatrix) -> Result` + +双比特系统的 concurrence $C(\rho) = \max(0, \lambda_1 - \lambda_2 - \lambda_3 - \lambda_4)$,$\lambda_i$ 为 $\sqrt{\sqrt{\rho}\,\tilde{\rho}\sqrt{\rho}}$ 按降序排列的特征值,$\tilde{\rho}$ 为自旋翻转后的密度矩阵。取值 `0.0`(可分)到 `1.0`(最大纠缠)。`dm` 不是双比特时返回 `QisError::UnsupportedDimension`。 + +### `entanglement_of_formation(dm: &DensityMatrix) -> Result` + +纠缠生成度,由 concurrence 导出的二元熵。可分态为 `0.0`,最大纠缠态为 `1.0`。非双比特输入同样返回 `QisError::UnsupportedDimension`。 + +示例: + +```rust +use cqlib_core::qis::state::{DensityMatrix, Statevector}; +use cqlib_core::qis::entropy::{ + concurrence, entanglement_entropy_pure, entanglement_of_formation, negativity, +}; +use cqlib_core::qis::metrics::logarithmic_negativity; + +// Bell 态 |Φ+⟩ 的纠缠熵为 1 比特 +let mut sv = Statevector::new(2); +sv.apply_h(0).unwrap(); +sv.apply_cx(0, 1).unwrap(); +assert!((entanglement_entropy_pure(&sv, &[0]).unwrap() - 1.0).abs() < 1e-10); + +// 同一状态的多种纠缠度量 +let mut dm = DensityMatrix::new(2); +dm.apply_h(0).unwrap(); +dm.apply_cx(0, 1).unwrap(); + +assert!((negativity(&dm, &[0]).unwrap() - 0.5).abs() < 1e-10); +assert!((logarithmic_negativity(&dm, &[0]).unwrap() - 1.0).abs() < 1e-10); +assert!((concurrence(&dm).unwrap() - 1.0).abs() < 1e-10); +assert!((entanglement_of_formation(&dm).unwrap() - 1.0).abs() < 1e-10); +``` + +可分态上各度量都归零: + +```rust +use cqlib_core::qis::state::{DensityMatrix, Statevector}; +use cqlib_core::qis::entropy::{concurrence, entanglement_entropy_pure, negativity}; +use cqlib_core::qis::metrics::logarithmic_negativity; + +// |+0> = |+> ⊗ |0>,两个比特之间没有纠缠 +let mut sv = Statevector::new(2); +sv.apply_h(0).unwrap(); +assert!(entanglement_entropy_pure(&sv, &[0]).unwrap().abs() < 1e-10); + +let dm = DensityMatrix::new(2); +assert!(negativity(&dm, &[0]).unwrap().abs() < 1e-10); +assert!(logarithmic_negativity(&dm, &[0]).unwrap().abs() < 1e-10); +assert!(concurrence(&dm).unwrap().abs() < 1e-10); +``` + +偏转置: + +```rust +use cqlib_core::qis::state::DensityMatrix; +use cqlib_core::qis::metrics::partial_transpose; +use num_complex::Complex64; + +// Bell 态密度矩阵:|00><00| + |00><11| + |11><00| + |11><11|,各占 0.5 +let mut data = vec![Complex64::new(0.0, 0.0); 16]; +data[0] = Complex64::new(0.5, 0.0); +data[3] = Complex64::new(0.5, 0.0); +data[12] = Complex64::new(0.5, 0.0); +data[15] = Complex64::new(0.5, 0.0); +let dm = DensityMatrix::from_density_matrix_state(2, data).unwrap(); + +// 偏转置后 |00><11| 被换到 |01><10| 的位置 +let pt = partial_transpose(&dm, &[0]).unwrap(); +assert!((pt.data()[0] - Complex64::new(0.5, 0.0)).norm() < 1e-10); +assert!((pt.data()[15] - Complex64::new(0.5, 0.0)).norm() < 1e-10); + +// 下标越界 +assert!(partial_transpose(&dm, &[2]).is_err()); +``` + +--- + +## 辅助转换 + +### `density_matrix_to_faer(dm: &DensityMatrix) -> Mat` + +把密度矩阵转换为特征分解库的矩阵类型,供需要直接调用特征分解的场景使用。输入按行优先展平,维度取 $2^N$。该函数不返回 `Result`,是其余依赖特征分解的函数所共用的底层步骤。 + +--- + +## 校验与错误处理 + +| 错误 | 触发场景 | +| --- | --- | +| `QisError::QubitMismatch` | 参与运算的两个态比特数不一致。 | +| `QisError::InvalidParameterValue` | `renyi_entropy()` 的 `alpha` 不大于 0。 | +| `QisError::InvalidSubsystem` | `entanglement_entropy_pure()` 的子系统为空、包含全部比特或存在重复下标。 | +| `QisError::IndexOutOfBounds` | `partial_transpose()` 的目标下标越界,或 `entanglement_entropy_pure()` 的子系统下标越界。 | +| `QisError::UnsupportedDimension` | `concurrence()` 与 `entanglement_of_formation()` 的输入不是双比特。 | +| `QisError::UnsupportedOperation` | 特征分解失败。 | + +`trace_distance_pure()` 通过 `state_fidelity_pure()` 间接返回 `QisError::QubitMismatch`;`negativity()` 与 `logarithmic_negativity()` 通过 `partial_transpose()` 间接返回下标相关错误。 + +--- + +## 相关页面 + +- [QIS 概览](0_overview.md):模块总览与术语表。 +- [Hamiltonian 与 Observable](7_hamiltonian.md):从态求可观测量期望值,可与本页的度量配合使用。 diff --git a/docs/api/cn/rust/4_compile/0_overview.md b/docs/api/cn/rust/4_compile/0_overview.md new file mode 100644 index 00000000..5e02811e --- /dev/null +++ b/docs/api/cn/rust/4_compile/0_overview.md @@ -0,0 +1,218 @@ +# 编译优化 + +`cqlib_core::compile` + +`cqlib_core::compile` 是 Cqlib 的编译管线模块。它以端到端的 `compile()` 函数覆盖从逻辑线路到满足目标约束的线路(设备目标下即物理线路)的完整流程,同时把各编译阶段拆成可独立调用的 transform,供需要精细控制的调用方使用。 + +## Overview + +编译是把给定输入线路改写为匹配特定量子设备拓扑、并针对执行目标优化的过程。多数线路都必须经过一系列变换才能兼容目标设备,并降低噪声对结果的影响。Cqlib 的推荐入口是 `compile()` 函数:一次调用完成规范化、知识规则优化、门分解、可选布局与路由、目标门集翻译和最终校验。 + +### 编译管线 + +`compile()` 每执行一步都会在 `CompileResult.steps` 中留下一条记录,记录的 `stage` 字段是一个**粗粒度的阶段归类**,取值只有以下 7 个: + +| `stage` | 归类含义 | +| --- | --- | +| `pre_init` | 目标约束解析与资源预检,在改动线路之前完成。 | +| `init` | 建立稳定的高层 IR:输入规范化与自定义门定义展开。 | +| `optimization` | 化简:知识规则重写、基于对易性的自反门抵消、重综合与单比特优化。 | +| `translation` | 降低操作种类:酉门合成、多控门分解、路由门集降级、目标门集转换与设备原生降级。 | +| `routing` | 初始布局选择与 SWAP 插入。 | +| `output` | 产物出口规范化。 | +| `validation` | 设备校验。 | + +`stage` 与步骤名是两套东西:步骤名(如 `optimize.pre_decomposition`)刻画这一步做什么,`stage` 只说明它归入哪一类。因此下面对编译流程的叙述性划分**可以跨多个 `stage` 取值**,两者不是一一对应。 + +`compile()` 按固定顺序编排以下步骤: + +1. **目标解析与资源校验**(`pre_init`)— 解析目标约束(`resolve.target`),校验辅助比特资源策略(`validate.resources`)。 +2. **输入规范化**(`init`)— 对输入线路做生产级规范化(`canonicalize.input`)。 +3. **定义展开与预分解优化**(`init` → `optimization`)— 先展开用户自定义门定义(`decompose.definitions`),让知识规则看到复合门内部的操作;随后执行预分解知识规则重写(`optimize.pre_decomposition`)。 +4. **门分解**(`translation` → `optimization`)— 矩阵酉门合成(`decompose.unitary`)、多控制门分解(`decompose.mc_gates`,按资源策略申请 ancilla),随后是分解后规范化(`canonicalize.after_decomposition`)、基于对易性的自反门抵消(`optimize.commutative_cancellation`)、双比特块重综合(`resynthesize.two_qubit_blocks`)与单比特优化(`optimize.one_qubit.post_decomposition`)。 +5. **分解后优化**(`optimization`)— 知识规则重写(`optimize.post_decomposition`),随后对单比特优化与双比特块重综合迭代至不动点(`optimize.one_qubit_fixed_point`)。 +6. **路由门集降级**(`translation`)— 把超出 SABRE 门元数约束的门(如 `CCX`)降级为至多两比特的操作(`decompose.routing_basis`)。 +7. **布局与路由**(`routing` → `optimization`)— 在设备目标下选择初始布局,插入 SWAP 满足拓扑邻接约束(`route.sabre`);增强模式下路由后执行清理与重综合(`optimize.post_routing`)。 +8. **目标门集转换**(`translation`)— 把线路降级到目标门集(`translate.target_basis`),设备目标进一步降级到原生指令集(`lower.device_instructions`)。 +9. **原生优化与最终校验**(`optimization` / `validation`)— 对原生指令做不动点优化(`optimize.native_fixed_point`),设备目标最后执行设备校验(`validate.device`)。 + +没有设备目标时,布局、路由与设备校验步骤会被跳过,并在 `steps` 中记录跳过原因。每个阶段都可以跳过 `compile()` 单独调用,见 [Transform](2_transform.md) 及后续页面。 + +### 选择优化力度 + +`CompileMode::Normal` 是保守的逻辑优化,使用生产默认 pass 参数;`CompileMode::Enhanced` 使用更强的分阶段工作流、更大的 pass 预算,并在有目标约束时执行目标感知清理。对大多数用例,`Normal` 已足够;当编译时间预算充裕、希望换取更好的路由后清理效果时使用 `Enhanced`。 + +### 选择目标约束 + +- `CompileTarget::Logical`:只做逻辑优化,产物不绑定任何硬件。适合算法层面的线路化简。 +- `CompileTarget::Basis(vec![...])`:降级到显式标准门集,不涉及设备拓扑。 +- `CompileTarget::Device(DeviceCompileTarget { .. })`:针对具体设备的路由与降级,产物必须匹配设备原生能力,并执行最终设备校验。 +- `CompileTarget::TopologyBasis { .. }`:在设备拓扑上路由,同时降级到显式门集;设备只提供容量、布局与路由约束,不强制门集匹配设备原生能力。 + +### 可复现性 + +编译中的布局与路由包含随机启发式。在 `DeviceCompileTarget` 中传入 `seed` 可获得确定性结果:相同线路、相同配置与相同 seed 产生相同 cqlib 结果。不同 cqlib 版本之间的产物不保证一致;seed 只保证同版本内的可重复性。 + +--- + +## 常用入口 + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::compile::{CompileConfig, CompileMode, CompileTarget, compile}; +use cqlib_core::compile::resource::ResourcePolicy; + +let mut circuit = Circuit::new(2); +circuit.h(Qubit::new(0)).unwrap(); +circuit.cx(Qubit::new(0), Qubit::new(1)).unwrap(); + +let result = compile( + &circuit, + CompileConfig { + mode: CompileMode::Normal, + target: CompileTarget::Logical, + resource_policy: ResourcePolicy::default(), + }, +) +.unwrap(); + +for step in &result.steps { + if step.changed && !step.skipped { + println!("{} {}", step.stage, step.name); + } +} +``` + +--- + +## 核心概念与术语 + +| 术语 | 说明 | +| --- | --- | +| **编译管线** | 把抽象线路逐步转换为满足目标约束的线路的流程,包括逻辑优化、分解、布局、路由和目标门集翻译等阶段。 | +| **Pass** | 编译管线中的一个独立变换步骤。多数 pass 既可以由 `compile()` 自动编排,也可以单独调用。 | +| **Transformer** | transform 的统一 trait。实现 `transform(&self, circuit, analysis) -> Result`,输入线路保持不变。 | +| **目标约束** | 编译产物需要满足的条件:逻辑空间(`CompileTarget::Logical`)、标准门集(`Basis`)、物理设备(`Device`)以及设备拓扑加门集(`TopologyBasis`)。 | +| **初始布局** | 编译开始前逻辑比特到物理比特的映射。可由调用方提供,也可由布局算法自动选择。 | +| **路由** | 在受限拓扑上通过插入 SWAP 等移动操作使所有二体交互满足邻接约束的过程。 | +| **门分解** | 把复合门、自定义酉门或多控制门展开为更基础的门序列。 | +| **知识规则** | 描述相邻操作模式及其替换形式的规则,用于模式化简、抵消和硬件原生门匹配。 | +| **Ancilla** | 分解过程中按需申请的辅助量子比特。`AncillaRequirement::CleanZero` 要求进出变换时处于 `\|0>`,`Dirty` 要求完整还原其原状态。 | + +--- + +## `cqlib_core::compile` API 概览 + +### 端到端编译入口 + +| 名字 | 简介 | +| --- | --- | +| [`compile`](1_compiler.md) | 推荐入口,一次调用完成配置、执行和结果报告。 | +| [`CompileConfig`](1_compiler.md) | 编译配置,包含模式、目标约束和资源策略。 | +| [`CompileMode`](1_compiler.md) | 优化力度枚举,包括 `Normal` 与 `Enhanced`。 | +| [`CompileTarget`](1_compiler.md) | 目标约束枚举,包括逻辑、门集、设备与拓扑加门集。 | +| [`DeviceCompileTarget`](1_compiler.md) | 设备目标输入,包含设备、可选初始布局与随机种子。 | +| [`CompileResult`](1_compiler.md) | 编译结果,包含优化后线路、分步报告和设备布局元数据。 | +| [`WorkflowStepReport`](1_compiler.md) | 单个工作流步骤的执行记录。 | +| [`DeviceCompilationMetadata`](1_compiler.md) | 设备编译产生的初始与最终布局。 | +| [`CompilerWorkflow`](1_compiler.md) | 可复用的工作流对象。 | + +### 基础 transform pass + +| 名字 | 简介 | +| --- | --- | +| [`Transformer`](2_transform.md) / [`TransformOutcome`](2_transform.md) | transform 统一 trait 与结果对象。 | +| [`CircuitAnalysis`](2_transform.md) | 线路特征快照。 | +| [`Canonicalizer`](2_transform.md) / [`canonicalize_circuit`](2_transform.md) | 规范化 pass。 | +| [`KnowledgeRewriter`](2_transform.md) / [`rewrite_circuit`](2_transform.md) | 基于知识规则的模式重写 pass。 | +| [`OptimizeOneQubitRuns`](2_transform.md) | 单比特连续门串优化。 | +| [`CommutativeCancellation`](2_transform.md) | 基于对易性的自反门抵消。 | +| [`LowerToRoutingBasis`](2_transform.md) | 路由门集降级。 | + +### 布局与路由 + +| 名字 | 简介 | +| --- | --- | +| [`trivial_layout`](3_layout.md) / [`greedy_layout`](3_layout.md) / [`vf2_perfect_layout`](3_layout.md) / [`sabre_layout`](3_layout.md) | 四种初始布局算法。 | +| [`LayoutObjective`](3_layout.md) / [`LayoutScore`](3_layout.md) | 布局目标权重与打分结果。 | +| [`CircuitLayoutAnalysis`](3_layout.md) / [`InteractionGraph`](3_layout.md) | 线路交互图分析。 | +| [`PhysicalLayoutGraph`](3_layout.md) / [`DistanceTable`](3_layout.md) | 设备物理图与最短距离表。 | +| [`route_with_layout`](4_routing.md) / [`route_sabre`](4_routing.md) | 变换层路由入口。 | +| [`RoutedCircuit`](4_routing.md) / [`SabreRouteResult`](4_routing.md) | 路由结果对象。 | +| [`SabreConfig`](5_sabre.md) / [`SabreHeuristicConfig`](5_sabre.md) / [`SabreVf2PrepassConfig`](5_sabre.md) | SABRE 算法配置。 | +| [`sabre_route`](5_sabre.md) | 已知初始布局条件下的 SABRE 路由。 | + +### 分解、重综合与目标门集 + +| 名字 | 简介 | +| --- | --- | +| [`decompose` 子模块](6_decompose_resynthesis.md) | 定义展开、酉门合成与多控制门分解。 | +| [`ResynthesizeTwoQubitBlocks`](6_decompose_resynthesis.md) / [`resynthesize_two_qubit_blocks`](6_decompose_resynthesis.md) | 双比特块重综合。 | +| [`TargetBasisLowerer`](6_decompose_resynthesis.md) / [`TargetBasisCostModel`](6_decompose_resynthesis.md) | 目标门集转换与代价评估。 | +| [`DeviceLowerer`](6_decompose_resynthesis.md) | 设备原生指令降级。 | + +### 知识规则与资源管理 + +| 名字 | 简介 | +| --- | --- | +| [`Rule`](7_knowledge.md) / [`RuleItem`](7_knowledge.md) / [`Condition`](7_knowledge.md) | 知识规则构造。 | +| [`RuleLibrary`](7_knowledge.md) / [`RuleMetadata`](7_knowledge.md) / [`RuleKind`](7_knowledge.md) | 规则库验证、分类与查询。 | +| [`MatchBindings`](7_knowledge.md) / [`rule_matches_operations`](7_knowledge.md) | 规则结构匹配。 | +| [`ResourceManager`](8_resource.md) / [`ResourcePolicy`](8_resource.md) / [`ResourceLimits`](8_resource.md) | Ancilla 资源规划与租借。 | +| [`Commutation`](9_commutation.md) / [`CommutationChecker`](9_commutation.md) / [`check_commutation`](9_commutation.md) | 对易性保守证明。 | + +--- + +## 快速示例 + +### 1. 端到端编译 + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::compile::{CompileConfig, CompileMode, CompileTarget, compile}; +use cqlib_core::compile::resource::ResourcePolicy; + +let mut circuit = Circuit::new(2); +circuit.h(Qubit::new(0)).unwrap(); +circuit.cx(Qubit::new(0), Qubit::new(1)).unwrap(); + +let result = compile( + &circuit, + CompileConfig { + mode: CompileMode::Normal, + target: CompileTarget::Logical, + resource_policy: ResourcePolicy::default(), + }, +) +.unwrap(); + +assert!(!result.steps.is_empty()); +``` + +### 2. 单独执行一个 transform + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::compile::transform::{Canonicalizer, CanonicalizeConfig, Transformer}; + +let mut circuit = Circuit::new(1); +circuit.i(Qubit::new(0)).unwrap(); + +let outcome = Canonicalizer::new(CanonicalizeConfig::production()) + .transform(&circuit, None) + .unwrap(); +assert!(outcome.changed()); +``` + +--- + +## 校验与错误处理 + +编译过程在配置解析、变换执行和设备校验阶段都可能失败,统一通过 `CompilerError` 返回。常见场景: + +| 错误 | 触发场景 | +| --- | --- | +| `CompilerError::InvalidInput` | 配置非法,例如 `routing_trials` 为零、目标门集为空。 | +| `CompilerError` 其他变体 | 变换执行失败、路由不可达、设备容量不足等。 | + +设备相关错误请参考设备模块文档;知识规则解析错误请参考 [Knowledge](7_knowledge.md);资源不足错误请参考 [Resource](8_resource.md)。 diff --git a/docs/api/cn/rust/4_compile/1_compiler.md b/docs/api/cn/rust/4_compile/1_compiler.md new file mode 100644 index 00000000..d5b55a05 --- /dev/null +++ b/docs/api/cn/rust/4_compile/1_compiler.md @@ -0,0 +1,224 @@ +# Compiler + +`cqlib_core::compile` 的端到端编译入口,包括 `compile()` 函数、编译配置对象、编译结果对象与可复用工作流对象。 + +## 导入 + +```rust +use cqlib_core::compile::{ + CompileConfig, CompileMode, CompileResult, CompileTarget, CompilerWorkflow, + DeviceCompilationMetadata, DeviceCompileTarget, WorkflowStepReport, compile, +}; +``` + +--- + +## 函数 + +### `compile(circuit: &Circuit, config: CompileConfig) -> Result` + +运行配置好的编译工作流,返回编译结果。输入线路不会被修改。返回结果记录优化后的线路与按执行顺序排列的分步报告;设备目标额外返回初始与最终布局。配置的目标、原生实现或最终设备校验无法满足时报错。 + +示例: + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::compile::{CompileConfig, CompileMode, CompileTarget, compile}; +use cqlib_core::compile::resource::ResourcePolicy; + +let mut circuit = Circuit::new(2); +circuit.h(Qubit::new(0)).unwrap(); +circuit.cx(Qubit::new(0), Qubit::new(1)).unwrap(); + +let result = compile( + &circuit, + CompileConfig { + mode: CompileMode::Normal, + target: CompileTarget::Logical, + resource_policy: ResourcePolicy::default(), + }, +) +.unwrap(); + +assert_eq!(result.mode, CompileMode::Normal); +assert!(!result.steps.is_empty()); +assert_eq!(result.circuit.qubits().len(), 2); +``` + +--- + +## CompileMode + +优化力度枚举。 + +```rust +pub enum CompileMode { + Normal, + Enhanced, +} +``` + +- `CompileMode::Normal`:保守的逻辑优化,使用生产默认 pass 参数(默认值)。 +- `CompileMode::Enhanced`:更强的分阶段工作流,使用更大的 pass 预算,并在有目标约束时执行目标感知清理。 + +实现 `Default`(为 `Normal`)、`Copy`、`Eq`、`Hash`。 + +--- + +## CompileConfig + +编译配置,描述逻辑优化力度、可选目标约束与布局前可用的辅助比特资源权限。 + +```rust +pub struct CompileConfig { + pub mode: CompileMode, + pub target: CompileTarget, + pub resource_policy: ResourcePolicy, +} +``` + +字段: + +- `mode`:优化工作流模式。 +- `target`:互斥的逻辑、门集或物理设备目标。 +- `resource_policy`:预布局分解 pass 的辅助比特资源权限。控制是否允许分配逻辑干净 ancilla 或借用脏输入比特。设备目标会从可用物理比特推导硬容量,不受此策略限制。 + +--- + +## CompileTarget + +目标约束枚举。 + +```rust +pub enum CompileTarget { + Logical, + Basis(Vec), + Device(DeviceCompileTarget), + TopologyBasis { + device_target: DeviceCompileTarget, + basis: Vec, + }, +} +``` + +- `Logical`:在逻辑比特空间编译,不做目标相关降级。 +- `Basis(Vec)`:降级到显式标准门集。 +- `Device(DeviceCompileTarget)`:针对具体设备的路由与降级。 +- `TopologyBasis { device_target, basis }`:在设备拓扑上路由,同时降级到显式门集。设备只用于容量、布局与路由,不要求产物门集匹配设备原生能力,因此不执行精确设备原生降级或最终设备校验。 + +--- + +## DeviceCompileTarget + +设备编译输入。 + +```rust +pub struct DeviceCompileTarget { + pub device: Device, + pub initial_layout: Option, + pub seed: Option, +} +``` + +字段: + +- `device`:约束产物的有序原生能力设备。 +- `initial_layout`:调用方提供的逻辑到物理初始布局。 +- `seed`:设备布局与路由启发式的确定性随机种子。 + +--- + +## CompileResult + +`compile()` 的返回对象。 + +```rust +pub struct CompileResult { + pub circuit: Circuit, + pub changed: bool, + pub mode: CompileMode, + pub steps: Vec, + pub device_metadata: Option, +} +``` + +方法: + +- `step(&self, name: &str) -> Option<&WorkflowStepReport>`:返回第一个名字匹配的分步报告。 +- `step_changed(&self, name: &str) -> bool`:该名字的报告中是否有未被跳过且确实变更了线路的记录。 + +--- + +## WorkflowStepReport + +单个工作流步骤的执行记录。 + +```rust +pub struct WorkflowStepReport { + pub stage: &'static str, + pub name: &'static str, + pub changed: bool, + pub skipped: bool, + pub reason: Option, +} +``` + +字段: + +- `stage`:粗粒度阶段名,例如 `"pre_init"`、`"init"`、`"optimization"`、`"translation"`、`"routing"`、`"output"`、`"validation"`。 +- `name`:工作流内步骤名。 +- `changed`:该步骤是否改变了线路表示。 +- `skipped`:该步骤是否被有意跳过。 +- `reason`:跳过或配置说明。 + +--- + +## DeviceCompilationMetadata + +设备编译产生的物理布局信息。 + +```rust +pub struct DeviceCompilationMetadata { + pub initial_layout: Layout, + pub final_layout: Layout, +} +``` + +- `initial_layout`:路由开始前的逻辑到物理布局。 +- `final_layout`:所有路由 SWAP 之后的逻辑到物理布局。 + +--- + +## CompilerWorkflow + +可复用的编译工作流对象。 + +- `CompilerWorkflow::new(config: CompileConfig) -> Self` +- `config(&self) -> &CompileConfig` +- `run(&self, circuit: &Circuit) -> Result` + +示例: + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::compile::{CompileConfig, CompileMode, CompileTarget, CompilerWorkflow}; +use cqlib_core::compile::resource::ResourcePolicy; + +let workflow = CompilerWorkflow::new(CompileConfig { + mode: CompileMode::Enhanced, + target: CompileTarget::Logical, + resource_policy: ResourcePolicy::default(), +}); + +let mut circuit = Circuit::new(2); +circuit.h(Qubit::new(0)).unwrap(); +circuit.cx(Qubit::new(0), Qubit::new(1)).unwrap(); + +let result = workflow.run(&circuit).unwrap(); +``` + +--- + +## 错误 + +`CompilerError` 是编译模块的统一错误类型,配置非法(如 `routing_trials` 为零、空目标门集)、变换执行失败与最终设备校验失败均通过它返回。 diff --git a/docs/api/cn/rust/4_compile/2_transform.md b/docs/api/cn/rust/4_compile/2_transform.md new file mode 100644 index 00000000..573136b5 --- /dev/null +++ b/docs/api/cn/rust/4_compile/2_transform.md @@ -0,0 +1,214 @@ +# Transform + +`cqlib_core::compile::transform` 提供可复用的编译 pass。所有 pass 实现 `Transformer` trait,返回 `TransformOutcome`,不修改输入线路。端到端 `compile()` 自动编排这些 pass;需要单独观察或调试某个阶段时可以直接调用。 + +## 导入 + +```rust +use cqlib_core::compile::transform::{ + CircuitAnalysis, CanonicalizeConfig, CanonicalizeResult, Canonicalizer, + canonicalize_circuit, CommutativeCancellation, KnowledgeRewriteResult, KnowledgeRewriteStats, + KnowledgeRewriter, LowerToRoutingBasis, OptimizeOneQubitRuns, RewriteConfig, RewriteMode, + TransformOutcome, Transformer, rewrite_circuit, +}; +``` + +--- + +## Transformer / TransformOutcome + +transform 的统一 trait 与结果对象。 + +```rust +pub trait Transformer { + fn name(&self) -> &'static str; + fn transform( + &self, + circuit: &Circuit, + analysis: Option<&CircuitAnalysis>, + ) -> Result; +} +``` + +`transform` 的 `analysis` 参数为可选预计算线路分析:传入可避免跨工作流阶段重复的结构扫描,`None` 时 transform 自行推导所需事实。`TransformOutcome` 方法: + +- `changed(&self) -> bool`:该 pass 是否改变了线路。 +- `into_circuit(self, original: &Circuit) -> Circuit`:取出输出线路;`Unchanged` 时与输入等价(核心工作流应直接匹配枚举以保持零拷贝)。 + +--- + +## CircuitAnalysis + +线路特征快照,供工作流决定执行哪些 pass。字段公开: + +```rust +pub struct CircuitAnalysis { + pub has_classical_data: bool, + pub has_classical_control: bool, + pub has_measurement: bool, + pub has_classical_values: bool, + pub has_classical_vars: bool, + pub has_runtime_classical: bool, + pub needs_classical_handle_preservation: bool, + pub has_circuit_gate_definitions: bool, + pub has_unitary_circuit_definitions: bool, + pub has_unitary_gates: bool, + pub has_mc_gates: bool, +} +``` + +- `CircuitAnalysis::analyze(circuit: &Circuit) -> Self`:分析线路并返回快照。 + +--- + +## Canonicalizer / canonicalize_circuit + +规范化 pass,统一指令形式、折叠全局相位、消除空操作。 + +### CanonicalizeConfig + +字段私有,通过构建器方法配置: + +- `CanonicalizeConfig::new() -> Self`:生产默认配置。 +- `CanonicalizeConfig::production() -> Self`:生产默认配置(同 `new`)。 +- `with_round_limit(self, round_limit: u8) -> Self`:最大规范化轮数。 +- `recurse_control_flow(self, enabled: bool) -> Self`:是否递归进入控制流块。 +- 其余 `fold_gphase`、`canonicalize_instruction_form`、`drop_noops`、`canonicalize_barriers` 均有同名构建器方法。 + +### Canonicalizer + +- `Canonicalizer::new(config: CanonicalizeConfig) -> Self` +- `Canonicalizer::production() -> Self`:生产默认配置。 +- `config(&self) -> &CanonicalizeConfig` +- `run(&self, circuit: &Circuit) -> Result` +- 实现 `Transformer` 与 `Default`。 + +### CanonicalizeResult + +```rust +pub struct CanonicalizeResult { + pub circuit: Circuit, + pub changed: bool, + pub rounds: u8, +} +``` + +### canonicalize_circuit(circuit: &Circuit) -> Result + +函数式入口,使用生产默认配置。 + +--- + +## KnowledgeRewriter / rewrite_circuit + +基于知识规则的模式重写 pass,是编译管线逻辑优化的核心。 + +### RewriteMode + +```rust +pub enum RewriteMode { + Optimize, + Lowering, +} +``` + +- `Optimize`:保守优化,接受的替换必须严格改善局部代价。 +- `Lowering`:显式降级,分解与硬件原生规则可以局部扩张。 + +### RewriteConfig + +字段私有,通过构建器方法配置: + +- `RewriteConfig::new() -> Self`:生产默认配置。 +- `RewriteConfig::production() -> Self`:生产优化配置。 +- `RewriteConfig::lowering() -> Self`:降级配置。 +- `with_max_rounds(self, max_rounds: u8) -> Self` +- `with_max_window_ops(self, max_window_ops: usize) -> Self` +- `with_max_pattern_len(self, max_pattern_len: usize) -> Self` +- `recurse_control_flow(self, enabled: bool) -> Self` +- `skip_labeled_ops(self, enabled: bool) -> Self` +- `with_enabled_kinds(self, kinds: Vec) -> Self` +- `with_mode(self, mode: RewriteMode) -> Self` +- `with_target_instructions(self, instructions: Vec) -> Self` +- `try_with_target_instructions(self, instructions: Vec) -> Result` +- `enabled_kinds(&self) -> &[RuleKind]` +- `target_instruction_basis(&self) -> Option>` + +### KnowledgeRewriter + +- `KnowledgeRewriter::new(config: RewriteConfig) -> Self` +- `KnowledgeRewriter::production() -> Self` +- `KnowledgeRewriter::lowering() -> Self` +- `config(&self) -> &RewriteConfig` +- `run(&self, circuit: &Circuit) -> Result` +- 实现 `Transformer`。 + +### KnowledgeRewriteResult + +```rust +pub struct KnowledgeRewriteResult { + pub circuit: Circuit, + pub changed: bool, + pub stats: KnowledgeRewriteStats, +} +``` + +### KnowledgeRewriteStats + +```rust +pub struct KnowledgeRewriteStats { + pub rounds_executed: u8, + pub rules_applied: usize, + pub changed_sequences: usize, + pub reached_fixpoint: bool, +} +``` + +### rewrite_circuit(circuit: &Circuit, config: &RewriteConfig) -> Result + +函数式入口。 + +--- + +## OptimizeOneQubitRuns + +单比特连续门串优化。 + +- `OptimizeOneQubitRuns::logical() -> Self`:目标中立的优化器,使用严格逻辑代价。 +- `OptimizeOneQubitRuns::basis(target_basis: Vec) -> Result`:按精确降级到目标门集后的代价优化。 +- 实现 `Transformer`。 + +--- + +## CommutativeCancellation + +基于对易性的自反门抵消 pass。 + +- `CommutativeCancellation::new() -> Self` +- 实现 `Transformer`。 + +--- + +## LowerToRoutingBasis + +路由门集降级 pass,把线路转换为适合路由阶段处理的形式。 + +- `LowerToRoutingBasis::new(preferred_basis: Option>) -> Self` +- 实现 `Transformer`。 + +--- + +## 示例 + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::compile::transform::{CanonicalizeConfig, Canonicalizer, Transformer}; + +let mut circuit = Circuit::new(1); +circuit.i(Qubit::new(0)).unwrap(); + +let outcome = Canonicalizer::new(CanonicalizeConfig::production()) + .transform(&circuit, None) + .unwrap(); +assert!(outcome.changed()); +``` diff --git a/docs/api/cn/rust/4_compile/3_layout.md b/docs/api/cn/rust/4_compile/3_layout.md new file mode 100644 index 00000000..807eed27 --- /dev/null +++ b/docs/api/cn/rust/4_compile/3_layout.md @@ -0,0 +1,215 @@ +# Layout + +`cqlib_core::compile::transform::layout` 提供初始布局算法与布局分析工具。布局算法把逻辑比特映射到物理比特,为后续路由阶段提供起点。 + +## 导入 + +```rust +use cqlib_core::compile::transform::layout::{ + CircuitLayoutAnalysis, Interaction, InteractionGraph, LayoutDiagnostics, LayoutObjective, + LayoutResult, LayoutScore, PhysicalLayoutGraph, Vf2EdgeRequirement, Vf2LayoutConfig, + analyze_circuit_for_layout, greedy_layout, prepare_sabre_circuit, prepare_sabre_device_target, + sabre_layout, trivial_layout, vf2_perfect_layout, +}; +``` + +--- + +## 布局函数 + +- `trivial_layout(circuit: &Circuit, device: &Device, objective: &LayoutObjective) -> Result`:平凡布局,按编号顺序映射。 +- `greedy_layout(circuit: &Circuit, device: &Device, objective: &LayoutObjective) -> Result`:贪心布局,按交互强度顺序放置。 +- `vf2_perfect_layout(circuit: &Circuit, device: &Device, objective: &LayoutObjective, config: &Vf2LayoutConfig) -> Result`:VF2 精确布局,在线路交互图能精确嵌入设备拓扑时给出完美布局。 +- `sabre_layout(circuit: &Circuit, device: &Device, objective: &LayoutObjective, config: &SabreConfig) -> Result`:SABRE 布局,使用 SABRE 启发式细化候选布局。 + +预计算入口: + +- `analyze_circuit_for_layout(circuit: &Circuit) -> CircuitLayoutAnalysis` +- `prepare_sabre_circuit(circuit: &Circuit) -> PreparedSabreCircuit` +- `prepare_sabre_device_target(device: &Device) -> Result` +- `trivial_layout_prepared(analysis: &CircuitLayoutAnalysis, physical: &PhysicalLayoutGraph, objective: &LayoutObjective) -> Result` +- `greedy_layout_prepared(...)`:同上。 +- `vf2_perfect_layout_prepared(analysis, physical, objective, config) -> Result` +- `sabre_layout_prepared(prepared, prepared_target, objective, config) -> Result` + +--- + +## LayoutObjective + +布局目标权重,用于给候选布局打分。 + +```rust +pub struct LayoutObjective { + pub distance_weight: f64, + pub direction_weight: f64, + pub two_qubit_error_weight: f64, + pub readout_error_weight: f64, +} +``` + +- `LayoutObjective::topology_only() -> Self`:纯拓扑目标。 +- `LayoutObjective::fidelity_aware() -> Self`:默认保真度感知目标。 +- `LayoutObjective::auto_from_physical(physical: &PhysicalLayoutGraph) -> Self`:有校准数据时选择保真度感知目标,否则退回拓扑目标。 +- `LayoutObjective::fidelity_required(physical: &PhysicalLayoutGraph) -> Result`:保真度感知目标,无校准数据时报错。 +- `uses_fidelity(&self) -> bool` +- `score_layout(&self, analysis: &CircuitLayoutAnalysis, physical: &PhysicalLayoutGraph, layout: &Layout) -> Result` + +--- + +## LayoutScore + +布局打分结果。 + +```rust +pub struct LayoutScore { + pub total: f64, + pub distance: f64, + pub direction: f64, + pub two_qubit_error: f64, + pub readout_error: f64, + pub used_fidelity: bool, +} +``` + +--- + +## LayoutDiagnostics + +布局过程诊断信息。 + +```rust +pub struct LayoutDiagnostics { + pub is_perfect: bool, + pub candidates_evaluated: usize, + pub used_fidelity: bool, + pub notes: Vec, +} +``` + +- `LayoutDiagnostics::new() -> Self` +- `with_note(self, note: impl Into) -> Self`:追加诊断说明。 + +--- + +## LayoutResult + +布局函数返回对象。 + +```rust +pub struct LayoutResult { + pub layout: Layout, + pub score: Option, + pub diagnostics: LayoutDiagnostics, +} +``` + +--- + +## Vf2EdgeRequirement / Vf2LayoutConfig + +```rust +pub enum Vf2EdgeRequirement { ... } +``` + +- `Vf2EdgeRequirement::positive_interactions()`:只要求正交互边匹配。 +- `Vf2EdgeRequirement::all_interactions()`:要求所有交互边匹配。 + +```rust +pub struct Vf2LayoutConfig { + pub candidate_limit: usize, + pub call_limit: Option, + pub edge_requirement: Vf2EdgeRequirement, +} +``` + +实现 `Default`。 + +--- + +## 布局分析对象 + +### Interaction + +一条逻辑比特对交互。 + +```rust +pub struct Interaction { + pub left: LogicalQubit, + pub right: LogicalQubit, + pub weight: f64, + pub directed_weight_left_to_right: f64, + pub directed_weight_right_to_left: f64, + pub first_seen_order: usize, +} +``` + +### InteractionGraph + +线路交互图: + +- `interactions(&self) -> impl Iterator` +- `is_empty(&self) -> bool` +- `logical_activity(&self) -> impl Iterator` + +### CircuitLayoutAnalysis + +线路布局分析结果。 + +```rust +pub struct CircuitLayoutAnalysis { + pub logical_qubits: Vec, + pub interactions: InteractionGraph, +} +``` + +### DistanceTable + +物理图最短距离表: + +- `qubits(&self) -> &[PhysicalQubit]` +- `distance(&self, a: PhysicalQubit, b: PhysicalQubit) -> Option` + +### PhysicalLayoutGraph + +设备物理布局图: + +- `PhysicalLayoutGraph::from_device(device: &Device) -> Result` +- `physical_qubits(&self) -> &[PhysicalQubit]` +- `distances(&self) -> &DistanceTable` +- `distance(&self, a, b) -> Option` +- `is_adjacent_undirected(&self, a, b) -> bool` +- `readout_error(&self, qubit) -> Option` +- `supports_two_qubit_gate_directed(&self, a, b) -> bool` +- `two_qubit_gate_error_directed(&self, a, b) -> Option` +- `supports_directed_coupling(&self, a, b) -> bool` +- `has_fidelity_data(&self) -> bool`、`has_readout_error_data(&self) -> bool`、`has_two_qubit_error_data(&self) -> bool` + +### PreparedSabreCircuit / PreparedSabreTarget + +- `PreparedSabreCircuit`:由 `prepare_sabre_circuit()` 返回,携带线路分析(`analysis()`、`logical_qubits()`)。 +- `PreparedSabreTarget`:由 `prepare_sabre_device_target()` 返回,携带 `physical()` 物理图。 + +--- + +## 示例 + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::compile::sabre::SabreConfig; +use cqlib_core::compile::transform::layout::{LayoutObjective, sabre_layout}; +use cqlib_core::device::Device; + +let mut circuit = Circuit::new(3); +circuit.cx(Qubit::new(0), Qubit::new(2)).unwrap(); + +let device = Device::line("line-3", 3).unwrap(); +let result = sabre_layout( + &circuit, + &device, + &LayoutObjective::topology_only(), + &SabreConfig::default(), +) +.unwrap(); + +println!("layout: {:?}", result.layout); +``` diff --git a/docs/api/cn/rust/4_compile/4_routing.md b/docs/api/cn/rust/4_compile/4_routing.md new file mode 100644 index 00000000..6f4f0ef0 --- /dev/null +++ b/docs/api/cn/rust/4_compile/4_routing.md @@ -0,0 +1,82 @@ +# Routing + +`cqlib_core::compile::transform::routing` 提供变换层路由入口。路由在受限拓扑上插入 SWAP,使线路中所有二体交互满足设备邻接约束。 + +## 导入 + +```rust +use cqlib_core::compile::transform::routing::{RoutedCircuit, SabreRouteResult, route_sabre, route_with_layout}; +use cqlib_core::compile::transform::layout::LayoutObjective; +use cqlib_core::compile::sabre::SabreConfig; +``` + +--- + +## 函数 + +### `route_with_layout(circuit: &Circuit, device: &Device, initial_layout: &Layout, config: &SabreConfig) -> Result` + +已知初始布局条件下的确定性路由。 + +### `route_sabre(circuit: &Circuit, device: &Device, objective: &LayoutObjective, config: &SabreConfig) -> Result` + +带布局选择的 SABRE 路由:先按 `objective` 选出初始布局,再执行路由。 + +--- + +## RoutedCircuit + +已知初始布局条件下的路由结果,字段私有: + +- `circuit(&self) -> &Circuit`:路由后的线路。 +- `into_circuit(self) -> Circuit`:消费并取出线路。 +- `initial_layout(&self) -> &Layout`:路由开始前的布局。 +- `final_layout(&self) -> &Layout`:路由结束后的布局。 +- `swap_count(&self) -> usize`:插入的 SWAP 数量。 +- `diagnostics(&self) -> &SabreRoutingDiagnostics`:路由诊断信息,见 [SABRE](5_sabre.md)。 + +--- + +## SabreRouteResult + +带布局选择的路由结果,字段私有: + +- `routed(&self) -> &RoutedCircuit`:路由结果。 +- `into_routed(self) -> RoutedCircuit`:消费并取出路由结果。 +- `layout_score(&self) -> Option<&LayoutScore>`:选中布局的分数。SABRE 按预测原生路由质量选择胜者,该分数只作诊断,不是路由选择键。 +- `layout_diagnostics(&self) -> &LayoutDiagnostics`:布局过程诊断信息。 + +--- + +## 示例 + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::compile::sabre::SabreConfig; +use cqlib_core::compile::transform::layout::LayoutObjective; +use cqlib_core::compile::transform::routing::route_sabre; +use cqlib_core::device::Device; + +let mut circuit = Circuit::new(3); +circuit.cx(Qubit::new(0), Qubit::new(2)).unwrap(); + +let device = Device::line("line-3", 3).unwrap(); +let result = route_sabre( + &circuit, + &device, + &LayoutObjective::topology_only(), + &SabreConfig::default(), +) +.unwrap(); + +println!("swap_count: {}", result.routed().swap_count()); +``` + +--- + +## 错误 + +- `CompilerError::InvalidInput`:SABRE 配置非法,例如 `routing_trials` 为零。 +- 其他 `CompilerError` 变体:路由失败,例如初始布局缺失或不可达、设备容量不足。 + +低层 `sabre_route` 与 `SabreConfig` 的细节见 [SABRE](5_sabre.md)。 diff --git a/docs/api/cn/rust/4_compile/5_sabre.md b/docs/api/cn/rust/4_compile/5_sabre.md new file mode 100644 index 00000000..7e9a9d87 --- /dev/null +++ b/docs/api/cn/rust/4_compile/5_sabre.md @@ -0,0 +1,158 @@ +# SABRE + +`cqlib_core::compile::sabre` 提供 SWAP 路由的底层入口与配置。当初始布局已知、线路必须满足设备拓扑约束时使用 `sabre_route`。固定随机种子可复现多轮试探的平局裁决与选择。 + +## 导入 + +```rust +use cqlib_core::compile::sabre::{ + SabreConfig, SabreHeuristicConfig, SabreRoutingDiagnostics, SabreRoutingResult, + SabreVf2PrepassConfig, normalize_initial_layout, sabre_route, validate_reachable_interactions, +}; +``` + +--- + +## 函数 + +### `sabre_route(circuit: &Circuit, device: &Device, initial_layout: &Layout, config: &SabreConfig) -> Result` + +已知初始布局条件下的 SABRE 路由。 + +### `normalize_initial_layout(logical_qubits: &[LogicalQubit], device: &Device, initial_layout: &Layout) -> Result` + +把完整逻辑到物理布局对设备可用比特做规范化。 + +### `validate_reachable_interactions(circuit: &Circuit, device: &Device, initial_layout: &Layout) -> Result<(), CompilerError>` + +在不执行路由的前提下验证原生移动可达性。 + +--- + +## SabreConfig + +SABRE 布局细化与路由共用的配置。 + +```rust +pub struct SabreConfig { + pub layout_trials: usize, + pub layout_assignment_budget: usize, + pub vf2_prepass: Option, + pub refinement_iterations: usize, + pub routing_trials: usize, + pub seed: Option, + pub heuristic: SabreHeuristicConfig, +} +``` + +字段: + +- `layout_trials`:在确定性与交互感知候选之外增加的随机化初始布局数量。 +- `layout_assignment_budget`:布局组件分配状态的最大探索量;耗尽会报告预算耗尽,区别于已证明不可行。 +- `vf2_prepass`:有界 VF2 预检查,用于添加拓扑完美候选;`None` 关闭预检查。 +- `refinement_iterations`:每个布局试探的正向/反向细化轮数。 +- `routing_trials`:每个完整细化布局执行的完整路由试探数。搜索直接返回最佳路由,不为中间细化状态执行路由。 +- `seed`:确定性随机种子。相同种子产生相同 cqlib 结果。 +- `heuristic`:交换选择启发式配置。 + +方法: + +- `SabreConfig::deterministic_seeded(seed: u64) -> Self`:紧凑的确定性配置,适合测试与示例。 +- `validate(&self) -> Result<(), CompilerError>`:校验路由字段(有意忽略布局细化字段,路由从具体初始布局开始,不依赖布局专用旋钮)。 + +实现 `Default`:`layout_trials=10`、`layout_assignment_budget=1_000_000`、`vf2_prepass=Some(SabreVf2PrepassConfig { candidate_limit: 10, call_limit: 1_000_000 })`、`refinement_iterations=1`、`routing_trials=1`、`seed=None`、`heuristic=SabreHeuristicConfig::default()`。 + +--- + +## SabreHeuristicConfig + +SABRE 交换选择启发式配置。主分数组合当前前沿层和、按活跃层缩放的前瞻与乘法拥塞;有原生方案时在窄结构窗口内用精确原生双比特代价裁决候选。 + +```rust +pub struct SabreHeuristicConfig { + pub basic_weight: f64, + pub lookahead_weights: Vec, + pub decay_increment: Option, + pub decay_reset: usize, + pub attempt_limit: usize, + pub best_epsilon: f64, +} +``` + +字段: + +- `basic_weight`:当前前沿层总距离的权重。 +- `lookahead_weights`:每个活跃层缩放前瞻层总和的权重;默认 `[0.5, 0.25, 0.125, 0.0625, 0.03125]`。 +- `decay_increment`:启发式 SWAP 使用某个物理比特后其拥塞乘数的增量;`None` 关闭拥塞控制。 +- `decay_reset`:衰减值重置前的启发式 SWAP 尝试次数。 +- `attempt_limit`:未路由前沿节点时允许的连续启发式 SWAP 次数,超过后回退到最短路径逃逸。 +- `best_epsilon`:候选 SWAP 分数视为平局的浮点容差。 + +实现 `Default`:`basic_weight=1.0`、`lookahead_weights=[0.5, 0.25, 0.125, 0.0625, 0.03125]`、`decay_increment=Some(0.002)`、`decay_reset=10`、`attempt_limit=1000`、`best_epsilon=1e-10`。 + +--- + +## SabreVf2PrepassConfig + +用于给 SABRE 布局候选播种的有界 VF2 预检查。 + +```rust +pub struct SabreVf2PrepassConfig { + pub candidate_limit: usize, + pub call_limit: usize, +} +``` + +- `candidate_limit`:VF2 打分的完整完美映射数量上限。 +- `call_limit`:VF2 尝试的部分映射扩展数量上限。 + +--- + +## SabreRoutingResult + +`sabre_route` 的返回对象。 + +```rust +pub struct SabreRoutingResult { + pub circuit: Circuit, + pub initial_layout: Layout, + pub final_layout: Layout, + pub swap_count: usize, + pub diagnostics: SabreRoutingDiagnostics, +} +``` + +- `circuit`:插入 SWAP 后的物理线路。 +- `initial_layout`:选中试探使用的初始逻辑到物理布局。 +- `final_layout`:所有路由操作之后的最终布局。 +- `swap_count`:插入的 SWAP 数量(含控制流尾声)。 +- `diagnostics`:路由搜索行为诊断。 + +--- + +## SabreRoutingDiagnostics + +路由过程诊断信息,字段公开: + +```rust +pub struct SabreRoutingDiagnostics { + pub trials_evaluated: usize, + pub selected_trial_index: usize, + pub fallback_count: usize, + pub control_flow_blocks_routed: usize, + pub two_qubit_depth: usize, + pub operation_count: usize, + pub native_two_qubit_count: usize, + pub native_two_qubit_depth: usize, + pub native_total_depth: usize, + pub native_operation_count: usize, + pub unknown_loop_count: usize, + pub requirement_signature_count: usize, + pub eager_pair_state_count: usize, + pub lazy_pair_l1_lookup_count: usize, + pub lazy_pair_l1_hit_count: usize, + pub lazy_pair_l1_cached_count: usize, +} +``` + +实现 `Default` 与 `PartialEq`。 diff --git a/docs/api/cn/rust/4_compile/6_decompose_resynthesis.md b/docs/api/cn/rust/4_compile/6_decompose_resynthesis.md new file mode 100644 index 00000000..bda086fe --- /dev/null +++ b/docs/api/cn/rust/4_compile/6_decompose_resynthesis.md @@ -0,0 +1,478 @@ +# Decompose / Resynthesis + +`cqlib_core::compile::transform::decompose` + +门分解、双比特块重综合、目标门集转换与设备降级,位于 `cqlib_core::compile::transform` 的 `decompose`、`resynthesis`、`target_basis` 与 `device_lowering` 子模块。 + +门分解按操作可用的表示分层:定义展开处理自带实现线路的门,数值合成处理带固定矩阵的酉门,多控制门分解处理受控操作。定义展开先于数值合成执行,使带实现线路的酉门在进入矩阵合成之前已经展开。 + +## 导入 + +```rust +use cqlib_core::compile::transform::decompose::{ + DecompositionRuleStats, DecomposeDefinitions, DecomposeMcGates, DecomposeUnitaries, + McGateDecomposeConfig, UnitaryDecomposeConfig, decompose_mc_gates, + decompose_mc_gates_with_rule_stats, decompose_unitaries, + decompose_unitaries_with_rule_stats, expand_definitions, mc_gate, unitary, +}; +use cqlib_core::compile::transform::decompose::unitary::{ + KakDecomposition, OneQubitUnitaryDecomposition, TwoQubitSynthesisTarget, + TwoQubitUnitaryDecomposeBasis, TwoQubitUnitarySynthesisResult, kak_decompose, + synthesize_numeric_1q_unitary, synthesize_numeric_2q_unitary, +}; +use cqlib_core::compile::transform::resynthesis::{ + ResynthesizeTwoQubitBlocks, TwoQubitBlockResynthesisConfig, + resynthesize_two_qubit_blocks, +}; +use cqlib_core::compile::transform::target_basis::{ + TargetBasisCost, TargetBasisCostModel, TargetBasisLowerer, TargetBasisSignature, +}; +use cqlib_core::compile::transform::DeviceLowerer; +``` + +--- + +## 定义展开 + +- `expand_definitions(circuit: &Circuit) -> Result`:展开电路门定义并返回新线路。 +- `DecomposeDefinitions`:`Transformer` 适配器,实现 `decompose_definitions` 步骤。 + +--- + +## 酉门合成 + +线路级入口位于 `cqlib_core::compile::transform::decompose`,对应的数值合成原语位于 `cqlib_core::compile::transform::decompose::unitary`。 + +- `decompose_unitaries(circuit: &Circuit, config: UnitaryDecomposeConfig) -> Result` +- `decompose_unitaries_with_rule_stats(circuit: &Circuit, config: UnitaryDecomposeConfig) -> Result<(TransformOutcome, DecompositionRuleStats), CompilerError>` +- `DecomposeUnitaries`:`Transformer` 适配器,配置在构造时绑定,`DecomposeUnitaries::new(config)` 构造,实现 `Default`。 + +### UnitaryDecomposeConfig + +```rust +pub struct UnitaryDecomposeConfig { + pub two_qubit_target: TwoQubitSynthesisTarget, + pub recurse_control_flow: bool, +} +``` + +字段: + +- `two_qubit_target`:精确双比特数值合成使用的目标能力,由 `TwoQubitSynthesisTarget::from_instructions(target_basis: Option<&[Instruction]>)` 构造;`None` 表示无目标约束,启用中立的 Pauli 旋转回退。 +- `recurse_control_flow`:是否递归进入控制流块。 + +实现 `Default`,等价于无目标约束且递归进入控制流块。 + +### DecompositionRuleStats + +```rust +pub struct DecompositionRuleStats { + pub hits: usize, + pub misses: usize, + pub inserts: usize, +} +``` + +pass 内规则缓存统计:命中、未命中与写入次数。 + +--- + +## 数值酉门合成 + +`cqlib_core::compile::transform::decompose::unitary` 提供矩阵到门序列的直接合成,不经过线路遍历。 + +- `synthesize_numeric_1q_unitary(matrix: &Array2) -> Result` +- `synthesize_numeric_2q_unitary(matrix: &Array2, qubits: [Qubit; 2], basis: TwoQubitUnitaryDecomposeBasis) -> Result` +- `kak_decompose(matrix: &Array2) -> Result` + +`Array2` 与 `Complex64` 分别来自 `ndarray` 与 `num_complex`。 + +针对目标门集的合成规划与代价评估由该子模块内部完成,不作为常规接口使用。 + +### OneQubitUnitaryDecomposition + +```rust +pub struct OneQubitUnitaryDecomposition { + pub theta: f64, + pub phi: f64, + pub lambda: f64, + pub global_phase: f64, +} +``` + +所表示的矩阵为 `exp(i * global_phase) * U(theta, phi, lambda)`。 + +### TwoQubitUnitarySynthesisResult + +```rust +pub struct TwoQubitUnitarySynthesisResult { + pub operations: Vec, + pub global_phase: f64, +} +``` + +- `operations`:实现该酉门的标准门操作序列,精确到 `global_phase`。 +- `global_phase`:乘在操作序列上的标量相位。 + +### KakDecomposition + +```rust +pub struct KakDecomposition { + pub global_phase: f64, + pub k1l: Array2, + pub k1r: Array2, + pub k2l: Array2, + pub k2r: Array2, + pub a: f64, + pub b: f64, + pub c: f64, +} +``` + +字段: + +- `global_phase`:乘在完整分解上的标量相位。 +- `k1l`、`k1r`:作用在 Cartan 相互作用之后的左、右局部因子。 +- `k2l`、`k2r`:作用在 Cartan 相互作用之前的左、右局部因子。 +- `a`、`b`、`c`:规范 Pauli-XX、Pauli-YY、Pauli-ZZ 相互作用坐标。 + +### TwoQubitUnitaryDecomposeBasis + +```rust +pub enum TwoQubitUnitaryDecomposeBasis { + PauliRotations, + Cx, + Cy, + Cz, + Rzz, +} +``` + +变体: + +- `PauliRotations`:局部 `U` 门加 `RXX`、`RYY`、`RZZ`。 +- `Cx`、`Cy`、`Cz`:局部 `U` 门加对应的受控门模板。 +- `Rzz`:局部 `U` 门加 `RZZ`。 + +### TwoQubitSynthesisTarget + +目标门集能力描述,供数值酉门合成与双比特块重综合指明后端支持的原生门集。 + +```rust +pub struct TwoQubitSynthesisTarget { + // 字段私有 +} +``` + +方法: + +- `from_instructions(target_basis: Option<&[Instruction]>) -> Result`:从目标门集构造能力描述;`None` 表示无目标约束。 +- `from_standard_gates(native_1q: Vec, native_2q: Vec, fallback_pauli: bool) -> Result`:直接给定原生单比特门与双比特门。 +- `unconstrained() -> Self`:无目标约束,启用中立的 Pauli 旋转回退。 +- `native_1q(&self) -> &[StandardGate]` / `native_2q(&self) -> &[StandardGate]`:读取已登记的原生门。 +- `fallback_pauli(&self) -> bool`:是否启用 Pauli 旋转回退。 + +实现 `Default`,等价于 `unconstrained()`。 + +--- + +## 多控制门分解 + +- `DecomposeMcGates`:`Transformer` 适配器,配置在构造时绑定,用 `DecomposeMcGates::new(config)` 构造,实现 `Default`。 +- `decompose_mc_gates(circuit: &Circuit, config: McGateDecomposeConfig) -> Result`:重写多控制门。 +- `decompose_mc_gates_with_rule_stats(circuit: &Circuit, config: McGateDecomposeConfig) -> Result<(TransformOutcome, DecompositionRuleStats), CompilerError>`:诊断形式,额外返回本次运行的分解规则统计。 +- `decompose_mc_gates_for_device(circuit: &Circuit, device: &Device, resource_policy: ResourcePolicy) -> Result`:在设备可用比特数上限内重写多控制门;这是布局前的逻辑变换,不检查耦合拓扑。 + +### McGateDecomposeConfig + +```rust +pub struct McGateDecomposeConfig { + pub resource_policy: ResourcePolicy, + pub resource_limits: ResourceLimits, +} +``` + +- `resource_policy`:本分解 pass 的辅助比特资源权限。 +- `resource_limits`:硬逻辑比特上限。 + +--- + +## 多控制门原语 + +`cqlib_core::compile::transform::decompose::mc_gate` 提供按门族划分的精确合成原语,直接给出操作序列,不做线路遍历、不选择算法、不申请辅助比特。**正常编译流程应使用 `DecomposeMcGates` 走转换器接口**;只有在需要自行控制辅助比特分配或实现自定义降级策略时,才直接调用这些原语。 + +原语按门族分组,每组通常提供「无辅助比特」「若干干净辅助比特」「若干脏辅助比特」以及针对单辅助比特的固定深度变体: + +| 门族 | 覆盖范围 | +| --- | --- | +| MCX | 多控制 X 门。 | +| 多控制 SU(2) | 多控制特殊酉旋转,旋转轴由 `Su2RotationAxis` 给出。 | +| Pauli | 多控制 Pauli 门族。 | +| RZZ | 多控制 RZZ。 | +| Pauli 旋转 | 多控制 Pauli 旋转。 | +| 旋转 | 多控制 `RX` / `RY` / `RZ` 及其固有受控形态。 | +| 相位 | 多控制 `S` / `SDG` / `T` / `TDG` / `Phase`。 | +| QCIS | 面向 QCIS 门集的合成原语。 | +| Hadamard | 多控制 Hadamard。 | +| SWAP | 多控制 SWAP。 | +| FSIM | 多控制 FSIM。 | +| 酉门 | 多控制通用酉门。 | + +共同参数: + +- `controls` (`&[Qubit]`):控制比特,须为展开后的全部控制,含门本身固有的控制。 +- `target`,或双比特原语的 `first`、`second`:目标比特。 +- `clean_ancillas` / `clean_ancilla`:干净辅助比特,须以 `|0>` 进入并恢复为 `|0>`。 +- `dirty_ancillas` / `dirty_ancilla`:脏辅助比特,可以任意未知态进入,但须精确恢复。 + +原语返回操作序列,可组装为线路。超出消耗前缀的额外辅助比特被忽略;辅助比特必须与所有控制比特和目标比特互不相同。完整清单见 `mc_gate` 模块的公开面。 + +--- + +## 双比特块重综合 + +- `resynthesize_two_qubit_blocks(circuit: &Circuit, config: TwoQubitBlockResynthesisConfig) -> Result` +- `ResynthesizeTwoQubitBlocks`:`Transformer` 适配器,配置在构造时绑定,`ResynthesizeTwoQubitBlocks::new(config)` 构造。 + +### TwoQubitBlockResynthesisConfig + +```rust +pub struct TwoQubitBlockResynthesisConfig { + pub two_qubit_target: TwoQubitSynthesisTarget, + pub max_block_ops: usize, + pub max_crossed_ops: usize, + pub max_scan_span: usize, + pub skip_labeled_ops: bool, + pub recurse_control_flow: bool, + pub commutation: CommutationConfig, +} +``` + +字段: + +- `two_qubit_target`:精确双比特数值合成使用的目标能力。 +- `max_block_ops`:单个合成块允许的最大源操作数。 +- `max_crossed_ops`:收集块时允许跨越的最大非块操作数。被跨越操作保持在原位置,且必须与合成替换对易。 +- `max_scan_span`:双比特锚点单侧的收集预算。 +- `skip_labeled_ops`:把带标签操作视为硬边界。 +- `recurse_control_flow`:是否递归进入结构化经典控制体。 +- `commutation`:本地收集器使用的语义对易引擎配置。 + +方法: + +- `TwoQubitBlockResynthesisConfig::normal(two_qubit_target: TwoQubitSynthesisTarget) -> Self`:默认预算配置。 +- `TwoQubitBlockResynthesisConfig::enhanced(two_qubit_target: TwoQubitSynthesisTarget) -> Self`:放宽预算的变体,以编译时间换更好的路由后清理。 + +--- + +## 目标门集转换 + +### TargetBasisLowerer + +- `TargetBasisLowerer::new(target_basis: Vec) -> Result` +- `target_basis(&self) -> &[Instruction]` +- `requires_lowering(&self, circuit: &Circuit) -> bool` +- 实现 `Transformer`。 + +### TargetBasisSignature + +- `TargetBasisSignature::from_standard_gates(gates: &[StandardGate]) -> Self` + +### TargetBasisCost + +```rust +pub struct TargetBasisCost { + pub two_qubit_ops: usize, + pub depth: usize, + pub total_ops: usize, + pub parameterized_ops: usize, +} +``` + +### TargetBasisCostModel + +- `TargetBasisCostModel::new(target_basis: Vec) -> Result` +- `TargetBasisCostModel::from_lowerer(lowerer: Arc) -> Result` +- `target_basis(&self) -> &[Instruction]` +- `cost_of_fixed_operations(&self, qubits: Vec, operations: Vec) -> Result` + +--- + +## 设备降级 + +### DeviceLowerer + +```rust +pub struct DeviceLowerer<'a> { + device: &'a Device, +} +``` + +- `DeviceLowerer::new(device: &'a Device) -> Self` +- `device(&self) -> &'a Device` +- 实现 `Transformer`,把线路降级到设备原生指令集。 + +--- + +## 示例 + +### 1. 定义展开与酉门合成 + +```rust +use cqlib_core::circuit::{Circuit, Qubit, UnitaryGate}; +use cqlib_core::compile::transform::decompose::{ + UnitaryDecomposeConfig, decompose_unitaries, expand_definitions, +}; +use ndarray::array; +use num_complex::Complex64; + +// 定义展开:把 CircuitGate 展开为其定义线路 +let mut definition = Circuit::new(1); +definition.h(Qubit::new(0)).unwrap(); +let gate = definition.to_gate("custom_h").unwrap(); + +let mut circuit = Circuit::new(1); +circuit.append(gate, [Qubit::new(0)], [], None).unwrap(); + +let expanded = expand_definitions(&circuit).unwrap(); +assert_eq!(expanded.num_qubits(), 1); + +// 自定义酉门合成 +let matrix = array![ + [Complex64::new(0.0, 0.0), Complex64::new(1.0, 0.0)], + [Complex64::new(1.0, 0.0), Complex64::new(0.0, 0.0)], +]; +let gate = UnitaryGate::new("x_matrix", 1, 0).with_matrix(matrix).unwrap(); +let mut circuit = Circuit::new(1); +circuit.unitary(gate, vec![Qubit::new(0)]).unwrap(); + +let synthesized = + decompose_unitaries(&circuit, UnitaryDecomposeConfig::default()).unwrap(); +assert_eq!(synthesized.num_qubits(), 1); +``` + +### 2. 数值酉门合成 + +```rust +use cqlib_core::circuit::Qubit; +use cqlib_core::compile::transform::decompose::unitary::{ + TwoQubitUnitaryDecomposeBasis, kak_decompose, synthesize_numeric_1q_unitary, + synthesize_numeric_2q_unitary, +}; +use ndarray::array; +use num_complex::Complex64; + +let source = array![ + [Complex64::new(0.0, 0.0), Complex64::new(1.0, 0.0)], + [Complex64::new(1.0, 0.0), Complex64::new(0.0, 0.0)], +]; +let decomposition = synthesize_numeric_1q_unitary(&source).unwrap(); +assert!(decomposition.theta.is_finite()); +assert!(decomposition.global_phase.is_finite()); + +let swap = array![ + [ + Complex64::new(1.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0) + ], + [ + Complex64::new(0.0, 0.0), + Complex64::new(1.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0) + ], + [ + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(1.0, 0.0) + ], + [ + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(1.0, 0.0), + Complex64::new(0.0, 0.0) + ], +]; +let synthesis = synthesize_numeric_2q_unitary( + &swap, + [Qubit::new(0), Qubit::new(1)], + TwoQubitUnitaryDecomposeBasis::Cx, +) +.unwrap(); +assert!(!synthesis.operations.is_empty()); + +let kak = kak_decompose(&swap).unwrap(); +assert_eq!(kak.k1l.dim(), (2, 2)); +assert!(kak.a.is_finite()); +``` + +### 3. 多控制门分解 + +```rust +use cqlib_core::circuit::{Circuit, Instruction, MCGate, Qubit, StandardGate}; +use cqlib_core::compile::resource::ResourcePolicy; +use cqlib_core::compile::transform::TransformOutcome; +use cqlib_core::compile::transform::decompose::{McGateDecomposeConfig, decompose_mc_gates}; + +let mut circuit = Circuit::new(3); +circuit + .append( + Instruction::McGate(Box::new(MCGate::new(2, StandardGate::X))), + [Qubit::new(0), Qubit::new(1), Qubit::new(2)], + [], + None, + ) + .unwrap(); + +let result = decompose_mc_gates( + &circuit, + McGateDecomposeConfig { + resource_policy: ResourcePolicy::default(), + ..McGateDecomposeConfig::default() + }, +) +.unwrap(); + +let TransformOutcome::Changed(decomposed) = result else { + panic!("the MC gate should be decomposed"); +}; +assert!(matches!( + decomposed.operations()[0].instruction, + Instruction::Standard(StandardGate::CCX), +)); +``` + +### 4. 双比特块重综合 + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::compile::transform::decompose::unitary::TwoQubitSynthesisTarget; +use cqlib_core::compile::transform::resynthesis::{ + TwoQubitBlockResynthesisConfig, resynthesize_two_qubit_blocks, +}; + +let mut circuit = Circuit::new(2); +circuit.cx(Qubit::new(0), Qubit::new(1)).unwrap(); +circuit.cx(Qubit::new(0), Qubit::new(1)).unwrap(); + +let config = + TwoQubitBlockResynthesisConfig::normal(TwoQubitSynthesisTarget::unconstrained()); +assert_eq!(config.max_block_ops, 16); + +let result = resynthesize_two_qubit_blocks(&circuit, config).unwrap(); +assert!(matches!( + result, + cqlib_core::compile::transform::TransformOutcome::Changed(_) +)); +``` + +--- + +## 错误 + +- `CompilerError`:分解或合成失败,例如矩阵不是酉矩阵、目标门集无法覆盖输入操作、资源不足。 +- `CompilerError::InvalidInput`:输入矩阵比特数不符、含非有限元素或非酉。 +- `CompilerError::TransformFailed`:比特重复、辅助比特数量不足、门族不受支持或底层合成失败。 diff --git a/docs/api/cn/rust/4_compile/7_knowledge.md b/docs/api/cn/rust/4_compile/7_knowledge.md new file mode 100644 index 00000000..d0ba6be2 --- /dev/null +++ b/docs/api/cn/rust/4_compile/7_knowledge.md @@ -0,0 +1,142 @@ +# Knowledge + +`cqlib_core::compile::knowledge` 提供编译知识规则、验证后的规则库与结构匹配能力。知识规则描述一个相邻操作模式、可选符号参数约束以及替换该模式的操作序列,是知识规则重写与门分解的规则来源。 + +## 导入 + +```rust +use cqlib_core::compile::knowledge::{ + ConcreteOperationView, MatchBindings, MatchError, MatchedReplacement, RuleId, RuleKind, + RuleLibrary, RuleLibraryError, RuleMetadata, conditions_hold, instantiate_target, + match_rule_item, rule_matches_operations, +}; +use cqlib_core::compile::knowledge::rule::{Condition, Rule, RuleItem, RuleValidationError}; +``` + +--- + +## 规则构造 + +规则内量子比特标签是非负整数且必须从零开始连续排列。它们是占位符:标签 `0` 在匹配时可以绑定任意具体 `Qubit`。 + +### RuleItem + +规则模式或替换目标中的单条操作。 + +- `RuleItem::standard(gate: StandardGate, qubits: &[u32], params: Vec) -> Self` +- `RuleItem::mc_gate(gate: MCGate, qubits: &[u32], params: Vec) -> Self` +- `symbols(&self) -> HashSet`:条目中出现的符号。 +- `validate(&self) -> Result<(), RuleValidationError>`:结构校验。 +- `equivalent_to(&self, other: &Self) -> bool`:条目等价判断。 + +### Condition + +规则匹配的符号参数约束: + +```rust +pub enum Condition { + Eq(Parameter, Parameter), + EqMod(Parameter, Parameter, Parameter), +} +``` + +- `symbols(&self) -> HashSet`:约束中出现的符号。 + +### Rule + +- `Rule::new(name: &str, ops: Vec, target: Vec) -> Rule`:构造规则;条件由后续 setter 或等价方法附加。 +- `validate(&self) -> Result<(), RuleValidationError>` +- `num_qubits(&self) -> usize`:规则涉及比特标签数。 +- `collect_free_symbols(&self) -> HashSet` +- `operation_qubits(&self) -> BTreeSet`、`target_qubits(&self) -> BTreeSet` +- `operation_symbols`、`target_symbols`、`condition_symbols`:各部分的符号集合。 + +--- + +## RuleKind + +规则类别: + +```rust +pub enum RuleKind { + Simplify, // 化简 + Cancel, // 抵消 + Merge, // 合并 + Commute, // 显式对易(A; B -> B; A) + Decompose, // 分解或降级 + Canonicalize, // 规范化 + HardwareNative,// 硬件原生 + Other, +} +``` + +--- + +## RuleLibrary + +规则库,负责规则的加载、验证、分类与查询。 + +- `RuleLibrary::new() -> Self` +- `RuleLibrary::builtin_rules() -> Result<&'static RuleLibrary, RuleLibraryError>` +- `RuleLibrary::from_rules(rules: Vec, kind: RuleKind) -> Result` +- `RuleLibrary::from_dsl_str(source: &str, kind: RuleKind) -> Result` +- `RuleLibrary::from_dsl_file(path: impl AsRef, kind: RuleKind) -> Result` +- `add_rule(&mut self, rule: Rule, kind: RuleKind) -> Result` +- `extend_rules(&mut self, rules: Vec, kind: RuleKind) -> Result, RuleLibraryError>` +- `len(&self) -> usize`、`is_empty(&self) -> bool` +- `rules(&self) -> &[Rule]` +- `get(&self, id: RuleId) -> Option<&Rule>`、`metadata(&self, id: RuleId) -> Option<&RuleMetadata>` +- `id_by_name(&self, name: &str) -> Option`、`get_by_name(&self, name: &str) -> Option<&Rule>`、`contains(&self, name: &str) -> bool` +- `candidates_for_first_instruction(&self, instruction: &Instruction) -> Result<&[RuleId], RuleLibraryError>` +- `rules_by_kind(&self, kind: RuleKind) -> &[RuleId]` +- `filter_rule_ids_by_instruction_keys(&self, op_instructions: &[Instruction], target_instructions: &[Instruction]) -> Result, RuleLibraryError>` + +### RuleId + +库内规则索引,`RuleId(index)` 元组结构,支持比较与哈希。 + +### RuleMetadata + +```rust +pub struct RuleMetadata { + pub id: RuleId, + pub kind: RuleKind, + pub pattern_len: usize, + pub rewrite_len: usize, + pub qubit_count: usize, + pub first_instruction: Instruction, + pub cost_delta: isize, + pub has_conditions: bool, +} +``` + +--- + +## 结构匹配 + +### MatchBindings + +匹配绑定结果: + +- `MatchBindings::new() -> Self` +- `qubits(&self) -> &HashMap`:规则比特标签到具体比特的绑定。 +- `qubit(&self, rule_qubit: u32) -> Option` +- `params(&self) -> &HashMap`:符号参数绑定。 +- `param(&self, symbol: &str) -> Option<&Parameter>` + +### 匹配函数 + +- `match_rule_item(item: &RuleItem, concrete: ConcreteOperationView<'_>, bindings: &mut MatchBindings) -> Result`:把单条规则条目匹配到一个具体操作视图,就地更新绑定。 +- `conditions_hold(conditions: Option<&[Condition]>, bindings: &MatchBindings) -> bool` +- `instantiate_target(target: &[RuleItem], bindings: &MatchBindings) -> Result, MatchError>` +- `rule_matches_operations(rule: &Rule, operations: &[ConcreteOperationView<'_>]) -> Result, MatchError>`:不匹配返回 `Ok(None)`。 + +匹配函数返回 `MatchError`,常见变体包括 `MatchError::UnsupportedRuleInstruction` 等。 + +--- + +## 异常情况 + +- `RuleLibraryError`:规则库构造或验证失败。 +- `RuleValidationError`:规则结构校验失败。 +- `MatchError`:匹配失败。 diff --git a/docs/api/cn/rust/4_compile/8_resource.md b/docs/api/cn/rust/4_compile/8_resource.md new file mode 100644 index 00000000..5dbe607c --- /dev/null +++ b/docs/api/cn/rust/4_compile/8_resource.md @@ -0,0 +1,141 @@ +# Resource + +`cqlib_core::compile::resource` 提供 ancilla 辅助比特的申请、规划、租借与回收管理。 + +干净资源与脏资源是恢复契约,不是态模拟结果。干净资源进出变换时必须处于 `|0>`;脏资源可能处于未知或纠缠态,变换释放租约前必须完整还原其输入状态。 + +## 导入 + +```rust +use cqlib_core::compile::resource::{ + AncillaRequirement, ResourceError, ResourceLease, ResourceLimits, ResourceManager, ResourcePlan, + ResourcePolicy, ResourceRequest, +}; +``` + +--- + +## 资源契约对象 + +### AncillaRequirement + +```rust +pub enum AncillaRequirement { + CleanZero, // 进出变换时处于 |0> + Dirty, // 可进入未知态,必须精确还原 +} +``` + +### ResourceRequest + +```rust +pub struct ResourceRequest { + pub requirement: AncillaRequirement, + pub count: usize, + pub excluded: BTreeSet, +} +``` + +- `requirement`:消耗算法要求的状态恢复契约。 +- `count`:算法要求的辅助比特数量。 +- `excluded`:算法不得消耗为辅助资源的比特,通常包含数据、控制与目标比特。每个排除比特必须已在管理器中注册。 + +申请不占用资源,仅供 `ResourceManager::preview` 规划。 + +### ResourcePlan + +由 `preview` 产生的无副作用规划快照。可检查、可比较,但不保留比特、不修改线路;必须把选中的规划传给 `commit` 才能使用其中的比特。规划是管理器专属快照,任何成功的资源池变更都会使旧规划失效。 + +字段私有,通过方法读取: + +- `qubits(&self) -> &[Qubit]`:规划到的比特。 +- `num_new_qubits(&self) -> usize`:规划引入的新比特数。 + +### ResourceLease + +由 `commit` 产生的已生效租约。 + +- `qubits(&self) -> &[Qubit]`:租借的比特。 + +--- + +## ResourcePolicy / ResourceLimits + +```rust +pub struct ResourcePolicy { + pub max_pre_layout_clean_ancillas: usize, + pub allow_dirty_borrowing: bool, +} +``` + +- `max_pre_layout_clean_ancillas`:布局前编译器可创建的逻辑干净 ancilla 总数。已释放比特仍计入总数,可复用。 +- `allow_dirty_borrowing`:是否允许在脏资源契约下借用输入比特。只允许消耗算法接受未知输入态并精确还原的情况;永远不会让输入比特满足 clean-zero 申请。 + +实现 `Default`(不创建干净 ancilla、不借用输入比特)。 + +```rust +pub struct ResourceLimits { + pub max_total_qubits: Option, +} +``` + +- `max_total_qubits`:完整逻辑线路的最大总比特数上限(含输入比特与编译器分配的干净 ancilla);`None` 表示不强制总比特上限。 + +实现 `Default`、`Copy`。 + +--- + +## ResourceManager + +资源管理器,把无副作用的规划与实际变更分离。 + +- `ResourceManager::from_circuit(circuit: &Circuit, policy: ResourcePolicy, limits: ResourceLimits) -> Result`:从线路构造管理器。管理器须与线路保持同步演进。 +- `preview(&self, request: &ResourceRequest) -> Result`:预览申请,不占用资源、不改变线路。 +- `commit(&mut self, circuit: &mut Circuit, plan: ResourcePlan) -> Result`:按规划实际占用资源并修改线路。`plan` 按值传入。 +- `release(&mut self, lease: &ResourceLease) -> Result<(), ResourceError>`:释放租约。 +- `enter_post_layout(&mut self, circuit: &Circuit) -> Result<(), ResourceError>`:进入布局后阶段。 +- `verify_consistency(&self, circuit: &Circuit) -> Result<(), ResourceError>`:校验管理器与线路一致。 +- `verify_idle(&self, circuit: &Circuit) -> Result<(), ResourceError>`:校验所有租约已释放。 + +### 用法约定 + +消耗资源的变换在租约生效期间使用 `lease.qubits()`,并须在释放前完成恢复(干净 ancilla 回到 `|0>`,脏 ancilla 还原原状态)。普通编译只需在 `CompileConfig` 里给出 `ResourcePolicy`,管理器生命周期由编译器内部维护。 + +--- + +## 示例 + +```rust +use cqlib_core::circuit::Circuit; +use cqlib_core::compile::resource::{ + AncillaRequirement, ResourceLimits, ResourceManager, ResourcePolicy, ResourceRequest, +}; + +let mut circuit = Circuit::new(2); +let mut manager = ResourceManager::from_circuit( + &circuit, + ResourcePolicy { max_pre_layout_clean_ancillas: 1, allow_dirty_borrowing: false }, + ResourceLimits::default(), +) +.unwrap(); + +let request = ResourceRequest { + requirement: AncillaRequirement::CleanZero, + count: 1, + excluded: Default::default(), +}; + +// 预览不占用资源、不改变线路 +let plan = manager.preview(&request).unwrap(); +let lease = manager.commit(&mut circuit, plan).unwrap(); + +// 消耗资源的变换在此使用 lease.qubits(),之后须恢复 |0> +manager.release(&lease).unwrap(); +manager.verify_idle(&circuit).unwrap(); +``` + +--- + +## 错误 + +`ResourceError` 是资源管理的统一错误类型,资源不足、租约状态非法、管理器与线路不一致等场景均通过它返回。 diff --git a/docs/api/cn/rust/4_compile/9_commutation.md b/docs/api/cn/rust/4_compile/9_commutation.md new file mode 100644 index 00000000..6cd53437 --- /dev/null +++ b/docs/api/cn/rust/4_compile/9_commutation.md @@ -0,0 +1,87 @@ +# Commutation + +`cqlib_core::compile::commutation` 提供量子操作交换的保守证明。返回 `None` 表示配置的证明源无法建立对易性,并不证明两个操作不对易。 + +## 导入 + +```rust +use cqlib_core::compile::commutation::{ + Commutation, CommutationChecker, CommutationConfig, CommutationResult, + algebraic_commutation, check_commutation, +}; +``` + +--- + +## 函数 + +### `check_commutation(lhs_inst, lhs_qubits, lhs_params, rhs_inst, rhs_qubits, rhs_params) -> CommutationResult` + +使用共享内置检查器判断两个操作是否对易。参数分别为左右两侧的 `&Instruction`、`&[Qubit]`、`&[Parameter]`。 + +### `algebraic_commutation(lhs_inst, lhs_qubits, lhs_params, rhs_inst, rhs_qubits, rhs_params) -> CommutationResult` + +只使用符号代数证明源(含元数校验、恒等、同操作、支撑不相交等事实)判断对易。返回 `None` 保持结果保守,允许规则或矩阵回退继续执行。 + +### CommutationResult + +`pub type CommutationResult = Option;` + +--- + +## Commutation + +对易证明结果: + +```rust +pub enum Commutation { + Exact, // 精确对易 + UpToGlobalPhase(Parameter), // 相差全局相位(lhs * rhs = exp(i*phase) * rhs * lhs) +} +``` + +方法: + +- `is_exact(&self) -> bool`:是否为精确对易,不引入全局相位。 +- `phase(&self) -> Parameter`:证明携带的相位,精确对易时为 `0.0`。 + +--- + +## CommutationConfig + +对易检查器配置。 + +```rust +pub struct CommutationConfig { + pub enable_rule_oracle: bool, + pub enable_matrix_fallback: bool, + pub max_matrix_qubits: usize, +} +``` + +- `enable_rule_oracle`:启用来自编译器知识库的显式 `A; B -> B; A` 规则匹配。 +- `enable_matrix_fallback`:启用小规模局部矩阵比较作为具体门的回退。 +- `max_matrix_qubits`:矩阵回退允许的最大支撑比特数,作用于两操作支撑的排序并集,而非每个操作单独计算。 + +实现 `Default`:`enable_rule_oracle=true`、`enable_matrix_fallback=true`、`max_matrix_qubits=4`。 + +--- + +## CommutationChecker + +可复用的对易检查器,证明顺序为:结构事实 → 支持门族的符号代数 → 显式知识库对易规则(启用时)→ 具体局部矩阵比较(启用且规模足够小时)。 + +- `CommutationChecker::builtin() -> Self`:内置规则与默认配置。 +- `CommutationChecker::with_config(config: CommutationConfig) -> Self`:内置规则与显式配置。 +- `CommutationChecker::from_library(library: &RuleLibrary, config: CommutationConfig) -> Self`:使用规则库中的对易规则参与证明。 +- `config(&self) -> &CommutationConfig` +- `check(&self, lhs_inst: &Instruction, lhs_qubits: &[Qubit], lhs_params: &[Parameter], rhs_inst: &Instruction, rhs_qubits: &[Qubit], rhs_params: &[Parameter]) -> CommutationResult` + +--- + +## 注意事项 + +- 证明结果保守:`None` 只代表无法证明,不代表不对易。 +- 参数比较是符号且容差的,可证明相等的表达式被视为同一应用,即使语法表示不同。 +- 经典控制流相关操作不在本检查器的证明范围内。 +- 带符号参数的操作参与证明时保持符号参数不变。 diff --git a/docs/api/cn/rust/5_visualization/0_overview.md b/docs/api/cn/rust/5_visualization/0_overview.md new file mode 100644 index 00000000..770029ca --- /dev/null +++ b/docs/api/cn/rust/5_visualization/0_overview.md @@ -0,0 +1,202 @@ +# 可视化 + +`cqlib_core::visualization` + +`cqlib_core::visualization` 是 Cqlib 的可视化模块,把线路、量子态与测量结果渲染为可查看、可保存的图形产物。模块以 SVG 为统一输出形态:每个绘制入口先返回 SVG 标记字符串,再由输出入口写入 `.svg` 文件,或栅格化为 `.png` 文件。 + +## Overview + +可视化回答三类问题:一条线路长什么样、一个量子态在布洛赫球与密度矩阵上如何分布、一次测量的计数如何分布。模块按这三类对象划分为 `circuit`、`state`、`result` 三个子模块,另有一个共享的错误类型 `VisualizationError`。 + +### 渲染管线 + +线路绘制分两步:先把线路转换为与后端无关的可视化 IR,再由具体后端绘制。 + +1. **IR 构建** — `build_visual_circuit` 按线路顺序把操作排入互不重叠的列,为每个操作解析样式、标签与参数文本,并对控制流块递归收集子线路。 +2. **后端绘制** — 文本后端把 IR 绘制成 Unicode 框线字符图,图形后端把 IR 绘制成 SVG 标记。两个后端消费同一份 IR,因此同一条线路在两种后端下的门顺序、列划分与标签一致。 +3. **输出** — SVG 字符串可直接嵌入使用,也可写入文件;扩展名为 `.png` 的路径会先栅格化。 + +量子态与测量结果绘制是单步的:`state` 与 `result` 的绘制入口内部完成数据校验、归一化与布局,直接返回 SVG 字符串。 + +### 两类线路后端 + +- **文本后端**(`circuit_to_text`):产出可按宽度折行的等宽字符图,适合在终端与日志中查看。 +- **图形后端**(`circuit_to_figure`):产出 SVG 标记,支持按门覆盖样式、按列折叠,以及 PNG 栅格化。 + +### 显示位序与折行折叠 + +`reverse_bits` 控制显示位序:置为 `true` 时量子比特按相反顺序显示,量子态的基标签与密度矩阵行列也按相反顺序排列,便于与外部约定的比特顺序对齐。 + +文本后端用 `line_width` 控制折行,图形后端用 `fold` 控制折叠:宽度或列数超过上限时,图按段拆分显示,段与段之间以方向标记衔接。 + +### 量子态与测量结果的输入 + +量子态绘制接受 `StateVisualizationSource` 的实现者,`Statevector` 与 `DensityMatrix` 均已实现该 trait;纯态在绘制前先展开为密度矩阵。测量结果绘制直接接受 `ExecutionResult`,按结果记录的量子比特数把测量结果格式化为比特串标签。 + +--- + +## 常用入口 + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::visualization::{FigureDrawerOptions, circuit_to_figure}; + +let mut circuit = Circuit::new(2); +circuit.h(Qubit::new(0)).unwrap(); +circuit.cx(Qubit::new(0), Qubit::new(1)).unwrap(); + +let svg = circuit_to_figure(&circuit, &FigureDrawerOptions::default()).unwrap(); +assert!(svg.contains(">(), + 7, + 2, + Some("simulator".to_string()), + None, +); +let counts = [("00", 2usize), ("11", 5)] + .into_iter() + .map(|(bits, count)| (Outcome::from_bitstring(bits).unwrap(), count)) + .collect::>(); +result.finish(counts, None); + +let svg = plot_histogram(&result, &ResultPlotOptions::default()).unwrap(); +assert!(svg.contains(" Result` + +把线路绘制为 Unicode 框线字符图。内部先构建可视化 IR,再交由文本后端绘制;控制流块会被展开为按列排布的块标记。 + +参数: + +- `circuit` (`&Circuit`):待绘制的线路。 +- `options` (`&TextDrawerOptions`):文本绘制选项。 + +返回: + +- `Result`:成功时为 UTF-8 文本图字符串;IR 中量子比特数为零时返回 `"empty circuit"`。 + +异常情况: + +- `VisualizationError::CircuitBuild`:`decompose_circuit_gates` 为 `true` 时展开线路门定义失败。 +- `VisualizationError::UnknownQubit`:操作引用了不在线路量子比特列表中的量子比特。 +- `VisualizationError::ParameterIndexOutOfBounds`:符号参数的索引超出线路参数表的长度。 + +示例: + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::visualization::{TextDrawerOptions, circuit_to_text}; + +let mut circuit = Circuit::new(3); +circuit.h(Qubit::new(0)).unwrap(); +circuit.cx(Qubit::new(0), Qubit::new(2)).unwrap(); +circuit.measure(Qubit::new(0)).unwrap(); +circuit.measure(Qubit::new(1)).unwrap(); +circuit.measure(Qubit::new(2)).unwrap(); + +let text = circuit_to_text(&circuit, &TextDrawerOptions::default()).unwrap(); +assert!(text.contains("Q0:")); +``` + +--- + +## TextDrawerOptions + +文本绘制选项。 + +```rust +pub struct TextDrawerOptions { + pub cell_width: usize, + pub show_params: bool, + pub decompose_circuit_gates: bool, + pub line_width: isize, + pub initial_state: bool, + pub reverse_bits: bool, +} +``` + +字段: + +- `cell_width` (`usize`):保留字段,当前渲染宽度按内容动态计算。默认 `9`。 +- `show_params` (`bool`):是否在门标签后附加参数文本。默认 `true`。 +- `decompose_circuit_gates` (`bool`):绘制前是否展开线路门定义。默认 `false`。 +- `line_width` (`isize`):折行的最大宽度。取值大于 `10` 时按该值折行;取负值时禁用折行;取值在 `0` 到 `10` 之间时回退为默认宽度。默认 `80`。 +- `initial_state` (`bool`):是否在每个量子比特线开头显示 `|0>`。默认 `false`。 +- `reverse_bits` (`bool`):是否反转量子比特的显示顺序。默认 `false`。 + +默认值由 `TextDrawerOptions::default()` 提供,也可用结构体更新语法只覆盖需要改动的字段: + +```rust +use cqlib_core::visualization::TextDrawerOptions; + +let options = TextDrawerOptions { + line_width: 12, + ..TextDrawerOptions::default() +}; +``` + +--- + +## `draw_text_from_visual(visual, options) -> Result` + +从已构建的可视化 IR 绘制文本图。已有缓存 IR 时使用本入口,可避免重复构建。 + +参数: + +- `visual` (`&VisualCircuit`):由 `build_visual_circuit` 构建的可视化 IR。 +- `options` (`&TextDrawerOptions`):文本绘制选项。 + +返回: + +- `Result`:成功时为 UTF-8 文本图字符串;IR 中量子比特数为零时返回 `"empty circuit"`。 + +异常情况: + +- 当前实现只返回 `Ok`;保留 `Result` 返回类型是为了与 `circuit_to_text` 保持对称。 + +示例: + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::visualization::{ + TextDrawerOptions, VisualBuildOptions, build_visual_circuit, draw_text_from_visual, +}; + +let mut circuit = Circuit::new(2); +circuit.h(Qubit::new(0)).unwrap(); +circuit.cx(Qubit::new(0), Qubit::new(1)).unwrap(); + +let options = TextDrawerOptions::default(); +let visual = build_visual_circuit(&circuit, &VisualBuildOptions::default()).unwrap(); +let text = draw_text_from_visual(&visual, &options).unwrap(); +assert!(text.contains("H")); +``` + +--- + +## 输出形态 + +### 基本线路 + +```rust +let mut circuit = Circuit::new(3); +circuit.h(Qubit::new(0)).unwrap(); +circuit.cx(Qubit::new(0), Qubit::new(2)).unwrap(); +circuit.measure(Qubit::new(0)).unwrap(); +circuit.measure(Qubit::new(1)).unwrap(); +circuit.measure(Qubit::new(2)).unwrap(); +``` + +在默认选项下的输出: + +```text + + Q0: ───H──■──M─ + │ + Q1: ──────┼──M─ + │ + Q2: ──────X──M─ + +``` + +每条线对应一个量子比特,两个量子比特线之间是连接列:受控门用 `■` 标记控制位、用 `│` 与 `X` 连接目标位。测量、复位与延时等指令以操作名或符号出现在对应列上。 + +### 折行 + +`line_width` 小于图的自然宽度时,图被拆成多段: + +```rust +let mut circuit = Circuit::new(1); +for _ in 0..10 { + circuit.h(Qubit::new(0)).unwrap(); +} +let options = TextDrawerOptions { + line_width: 12, + ..TextDrawerOptions::default() +}; +let text = circuit_to_text(&circuit, &options).unwrap(); +``` + +输出中每段之间以空行分隔,段末与段首分别以 `»` 与 `«` 标记接续方向: + +```text + » + Q0: ───H──H──H──H─» + » + +« » +« Q0: ───H──H──H──H─» +« » + +« +« Q0: ───H──H─ +« +``` + +`line_width` 取负值时不折行,输出中不出现 `«` 与 `»`。 + +### 显示初始态与反转位序 + +`initial_state` 为 `true` 时,每条量子比特线的开头显示 `|0>`: + +```text + + Q0: |0>───H─ + +``` + +`reverse_bits` 为 `true` 时按相反顺序显示量子比特: + +```rust +let mut circuit = Circuit::new(2); +circuit.x(Qubit::new(0)).unwrap(); +let options = TextDrawerOptions { + reverse_bits: true, + ..TextDrawerOptions::default() +}; +let text = circuit_to_text(&circuit, &options).unwrap(); +``` + +```text + + Q1: ───── + + Q0: ───X─ + +``` + +### 参数与线路门 + +`show_params` 为 `false` 时,门标签只显示门名,不附加参数文本: + +```text + + Q0: ───RX─ + +``` + +`decompose_circuit_gates` 为 `false` 时,线路门按整体显示为带边框的块;置为 `true` 时先展开门定义,再按展开后的操作绘制。 + +--- + +## 相关页面 + +- 图形后端的绘制入口与选项见 [SVG 线路图](2_draw_figure.md)。 +- 参数文本的格式化规则见 [SVG 线路图](2_draw_figure.md) 中的参数格式化一节。 diff --git a/docs/api/cn/rust/5_visualization/2_draw_figure.md b/docs/api/cn/rust/5_visualization/2_draw_figure.md new file mode 100644 index 00000000..f8e9cb33 --- /dev/null +++ b/docs/api/cn/rust/5_visualization/2_draw_figure.md @@ -0,0 +1,322 @@ +# SVG 线路图 + +`cqlib_core::visualization::circuit::figure` + +本页覆盖线路的图形后端:绘制入口 `circuit_to_figure`、基于可视化 IR 的入口 `draw_figure_from_visual`、绘制选项 `FigureDrawerOptions`,以及选项引用到的样式与参数格式化类型。图形后端把线路渲染为 SVG 标记字符串,画布底色为白色。 + +## 导入 + +```rust +use cqlib_core::visualization::{FigureDrawerOptions, circuit_to_figure}; +``` + +--- + +## `circuit_to_figure(circuit, options) -> Result` + +生成线路的 SVG 标记。内部先构建可视化 IR,再交由图形后端绘制。 + +参数: + +- `circuit` (`&Circuit`):待绘制的线路。 +- `options` (`&FigureDrawerOptions`):图形绘制选项。 + +返回: + +- `Result`:成功时为 SVG 标记字符串。 + +异常情况: + +- `VisualizationError::CircuitBuild`:`decompose_circuit_gates` 为 `true` 时展开线路门定义失败。 +- `VisualizationError::UnknownQubit`:操作引用了不在线路量子比特列表中的量子比特。 +- `VisualizationError::ParameterIndexOutOfBounds`:符号参数的索引超出线路参数表的长度。 + +示例: + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::visualization::{FigureDrawerOptions, circuit_to_figure}; + +let mut circuit = Circuit::new(1); +circuit.h(Qubit::new(0)).unwrap(); + +let svg = circuit_to_figure(&circuit, &FigureDrawerOptions::default()).unwrap(); +assert!(svg.contains(", + pub initial_state: bool, + pub reverse_bits: bool, +} +``` + +字段: + +- `show_params` (`bool`):是否在门标签后附加参数文本。默认 `true`。 +- `decompose_circuit_gates` (`bool`):绘制前是否展开线路门定义。默认 `false`。 +- `parameter_format` (`ParameterFormatOptions`):门参数文本的格式化选项。默认 `ParameterFormatOptions::default()`。 +- `width_per_column` (`f64`):每个逻辑列的图宽缩放系数。默认 `1.2`。 +- `height_per_qubit` (`f64`):每个量子比特的图高缩放系数。默认 `0.9`。 +- `dpi` (`u32`):导出 PNG 时的栅格化 DPI。默认 `160`,此时 PNG 缩放为 `1.0`,尺寸与 SVG 原生像素一致;更大的取值按比例放大 PNG 尺寸,SVG 输出不受影响。取值为 `0` 时绘制失败。 +- `gate_width` (`f64`):门的基础宽度(数据单位),同时作为列宽下限。默认 `1.1`。 +- `gate_height` (`f64`):门的基础高度(数据单位)。默认 `1.5`。 +- `moment_spacing` (`f64`):相邻列之间的水平间距。默认 `0.3`。 +- `connect_height` (`f64`):折叠后各行之间的垂直间距。默认 `2.0`。 +- `fold` (`i32`):每行最大列数。默认 `18`;取负值或 `0` 时不折叠。 +- `style` (`FigureDrawStyle`):样式预设。默认 `FigureDrawStyle::Cqlib`。 +- `gate_styles` (`HashMap`):按门名覆盖样式,覆盖项合并到基础样式表之上。默认空映射。 +- `initial_state` (`bool`):是否在量子比特标签中显示 `|0>`。默认 `false`。 +- `reverse_bits` (`bool`):是否反转量子比特的显示顺序。默认 `false`。 + +默认值由 `FigureDrawerOptions::default()` 提供: + +```rust +use cqlib_core::visualization::FigureDrawerOptions; + +let options = FigureDrawerOptions { + fold: 8, + ..FigureDrawerOptions::default() +}; +``` + +--- + +## FigureDrawStyle + +图形样式预设。 + +```rust +pub enum FigureDrawStyle { + /// Cqlib 默认样式。 + Cqlib, +} +``` + +`FigureDrawStyle::Cqlib` 决定线路走线、连线、门边框、门填充与标签的默认配色和线宽。 + +--- + +## GateStyle + +单个门的样式覆盖项。所有字段均为可选,为 `None` 时沿用基础样式。 + +```rust +pub struct GateStyle { + pub border_color: Option, + pub background_color: Option, + pub font_size: Option, + pub text_color: Option, + pub line_color: Option, + pub line_width: Option, +} +``` + +字段: + +- `border_color` (`Option`):门边框颜色。默认 `None`。 +- `background_color` (`Option`):门填充颜色。默认 `None`。 +- `font_size` (`Option`):门标签字号,同时参与门宽测量。默认 `None`。 +- `text_color` (`Option`):门标签颜色。默认 `None`。 +- `line_color` (`Option`):门的连线颜色。默认 `None`。 +- `line_width` (`Option`):门的线宽。默认 `None`。 + +以门名为键写入 `FigureDrawerOptions::gate_styles` 即可覆盖该门的样式: + +```rust +use cqlib_core::circuit::{Circuit, Parameter, Qubit}; +use cqlib_core::visualization::{FigureDrawerOptions, GateStyle, circuit_to_figure}; +use std::collections::HashMap; + +let mut circuit = Circuit::new(1); +let long_param = Parameter::symbol("styledlongparametername"); +circuit.rz(Qubit::new(0), long_param).unwrap(); + +let mut gate_styles = HashMap::new(); +gate_styles.insert( + "RZ".to_string(), + GateStyle { + font_size: Some(16.0), + ..GateStyle::default() + }, +); + +let svg = circuit_to_figure( + &circuit, + &FigureDrawerOptions { + gate_styles, + ..FigureDrawerOptions::default() + }, +) +.unwrap(); +assert!(svg.contains(" Self`:按给定选项构造格式化器。 +- `fn format_circuit_param(&self, circuit: &Circuit, param: &CircuitParam) -> Result`:格式化一个参数条目。`CircuitParam::Fixed` 直接使用其数值;`CircuitParam::Index` 通过线路的参数表解析为符号表达式。 + +异常情况: + +- `VisualizationError::ParameterIndexOutOfBounds`:`CircuitParam::Index` 的索引超出线路参数表的长度。 + +```rust +use cqlib_core::circuit::{Circuit, CircuitParam}; +use cqlib_core::visualization::{ParameterFormatOptions, ParameterFormatter}; + +let formatter = ParameterFormatter::new(ParameterFormatOptions::default()); +let circuit = Circuit::new(0); + +assert_eq!( + formatter + .format_circuit_param(&circuit, &CircuitParam::Fixed(1.2345)) + .unwrap(), + "1.23" +); +``` + +--- + +## `draw_figure_from_visual(visual, options, output_path) -> String` + +从已构建的可视化 IR 绘制 SVG。已有缓存 IR 时使用本入口,可避免重复构建。 + +参数: + +- `visual` (`&VisualCircuit`):由 `build_visual_circuit` 构建的可视化 IR。 +- `options` (`&FigureDrawerOptions`):图形绘制选项。 +- `output_path` (`Option<&str>`):保留参数,当前被忽略。 + +返回: + +- `String`:SVG 标记字符串。 + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::visualization::{ + FigureDrawerOptions, VisualBuildOptions, build_visual_circuit, draw_figure_from_visual, +}; + +let mut circuit = Circuit::new(1); +circuit.h(Qubit::new(0)).unwrap(); +let visual = build_visual_circuit(&circuit, &VisualBuildOptions::default()).unwrap(); +let svg = draw_figure_from_visual(&visual, &FigureDrawerOptions::default(), None); +assert!(svg.contains(" Result` + +绘制单个布洛赫向量。 + +参数: + +- `vector` (`[f64; 3]`):布洛赫坐标 `(x, y, z)`,分量通常落在 `[-1, 1]`。模长大于 `1` 的向量会被投影回单位球面,落在球内的向量保持不变。 +- `options` (`&StatePlotOptions`):绘图选项。 + +返回: + +- `Result`:SVG 标记字符串。向量标签固定为 `q0`。 + +示例: + +```rust +use cqlib_core::visualization::{StatePlotOptions, plot_bloch_vector}; + +let svg = plot_bloch_vector([0.0, 0.0, 1.0], &StatePlotOptions::default()).unwrap(); +assert!(svg.contains(" Result` + +为每个量子比特绘制一个约化布洛赫向量。每个向量由该比特的约化密度矩阵计算得到,标签按量子比特编号写成 `q0`、`q1` 等。 + +参数: + +- `state` (`&S`,`S: StateVisualizationSource + ?Sized`):输入态,`Statevector` 与 `DensityMatrix` 均可直接传入。 +- `options` (`&StatePlotOptions`):绘图选项。 + +返回: + +- `Result`:SVG 标记字符串。多个球排成网格,每行最多 4 个。 + +异常情况: + +- `VisualizationError::InvalidInput`:状态缓冲长度与量子比特数不符,或量子比特数过大导致矩阵维度溢出。 + +示例: + +```rust +use cqlib_core::qis::Statevector; +use cqlib_core::visualization::{StatePlotOptions, plot_bloch_multivector}; +use num_complex::Complex64; + +let state = Statevector::from_state( + 1, + vec![ + Complex64::new(1.0 / 2.0_f64.sqrt(), 0.0), + Complex64::new(1.0 / 2.0_f64.sqrt(), 0.0), + ], +) +.unwrap(); + +let svg = plot_bloch_multivector(&state, &StatePlotOptions::default()).unwrap(); +assert!(svg.contains(" Result` + +绘制密度矩阵的实部与虚部,每部分一个面板。只绘制虚部非零的输入:当所有元素的虚部均为零时,SVG 中不生成虚部面板。每个单元的面积随 `sqrt(|value| / max_abs)` 缩放,使小幅度元素仍可见;`max_abs` 取矩阵元素模长的最大值,下限为 `1e-12`。 + +参数: + +- `state` (`&S`,`S: StateVisualizationSource + ?Sized`):输入态。纯态先展开为密度矩阵。 +- `options` (`&StatePlotOptions`):绘图选项。填充不透明度由 `alpha` 控制。 + +返回: + +- `Result`:SVG 标记字符串,面板标题为 `Re[rho]` 与 `Im[rho]`。 + +异常情况: + +- `VisualizationError::InvalidInput`:状态缓冲长度与量子比特数不符,或量子比特数过大导致矩阵维度溢出。 + +示例: + +```rust +use cqlib_core::qis::Statevector; +use cqlib_core::visualization::{StatePlotOptions, plot_state_city}; +use num_complex::Complex64; + +let state = Statevector::from_state( + 1, + vec![Complex64::new(1.0, 0.0), Complex64::new(0.0, 0.0)], +) +.unwrap(); + +let svg = plot_state_city(&state, &StatePlotOptions::default()).unwrap(); +assert!(svg.contains("Re[rho]")); +``` + +--- + +## `plot_state_paulivec(state, options) -> Result` + +把泡利基期望值绘制为柱状图。标签为 `I`、`X`、`Y`、`Z` 的张量积,共 `4^n` 条,其中 `n` 为量子比特数。非负柱与负值柱使用不同颜色。 + +参数: + +- `state` (`&S`,`S: StateVisualizationSource + ?Sized`):输入态。 +- `options` (`&StatePlotOptions`):绘图选项。`color` 的第一个元素是非负柱颜色,第二个元素是负值柱颜色。 + +返回: + +- `Result`:SVG 标记字符串。柱数不超过 `64` 时才绘制旋转的横轴标签。 + +异常情况: + +- `VisualizationError::InvalidInput`:状态缓冲长度与量子比特数不符,或量子比特数过大导致矩阵维度溢出。 + +示例: + +```rust +use cqlib_core::qis::Statevector; +use cqlib_core::visualization::{StatePlotOptions, plot_state_paulivec}; +use num_complex::Complex64; + +let state = Statevector::from_state( + 1, + vec![ + Complex64::new(1.0 / 2.0_f64.sqrt(), 0.0), + Complex64::new(1.0 / 2.0_f64.sqrt(), 0.0), + ], +) +.unwrap(); + +let svg = plot_state_paulivec(&state, &StatePlotOptions::default()).unwrap(); +assert!(svg.contains(", + pub color: Vec, + pub alpha: f64, + pub reverse_bits: bool, + pub figsize: Option<(f64, f64)>, +} +``` + +字段: + +- `title` (`Option`):图标题。默认 `None`,不绘制标题。 +- `color` (`Vec`):部分绘图族使用的配色。`plot_state_paulivec` 取第一个元素作为非负柱颜色,缺省为 `#4569d4`;取第二个元素作为负值柱颜色,缺省为 `#d64b5f`。其余绘图族使用固定配色。默认空向量。 +- `alpha` (`f64`):`plot_state_city` 柱体的填充不透明度。默认 `1.0`。 +- `reverse_bits` (`bool`):是否反转计算基的显示位序。默认 `false`。置为 `true` 时,密度矩阵图的行列标签按相反顺序排列,矩阵元素按位序映射回存储位置;布洛赫图与泡利图的标签顺序随之反转。 +- `figsize` (`Option<(f64, f64)>)`:图尺寸,单位为类英寸单位,按每单位 `100` 像素换算为 SVG 像素。默认 `None`,各绘图族按自身布局计算画布尺寸。给定尺寸时,密度矩阵图与泡利图对宽、高的下限分别为 `4` 与 `3`,布洛赫图对宽、高的下限均为 `3`。 + +```rust +use cqlib_core::visualization::StatePlotOptions; + +let options = StatePlotOptions { + title: Some("State city".to_string()), + reverse_bits: true, + ..StatePlotOptions::default() +}; +``` + +--- + +## 辅助 API + +### StateVisualizationSource + +量子态绘图入口接受的输入 trait。它把核心态对象适配为密度矩阵数据,而不是另一套数据容器。 + +```rust +pub trait StateVisualizationSource { + fn num_qubits(&self) -> usize; + fn density_matrix_data(&self) -> Result, VisualizationError>; +} +``` + +实现者: + +- `Statevector`:按 `ρ = |ψ⟩⟨ψ|` 展开为密度矩阵。 +- `DensityMatrix`:直接返回其数据。 + +实现者需要返回行主序的密度矩阵数据,长度为 `4^num_qubits`;维度非法时以 `VisualizationError::InvalidInput` 报错。 + +### `state_to_density_matrix(state) -> Result<(usize, Vec), VisualizationError>` + +把核心态对象转换为行主序密度矩阵。返回量子比特数与长度为 `4^num_qubits` 的数据。 + +异常情况: + +- `VisualizationError::InvalidInput`:量子比特数与缓冲长度不一致,或量子比特数过大导致矩阵维度溢出。 + +```rust +use cqlib_core::qis::Statevector; +use cqlib_core::visualization::state_to_density_matrix; +use num_complex::Complex64; + +let state = Statevector::from_state(1, vec![Complex64::new(1.0, 0.0), Complex64::new(0.0, 0.0)]) + .unwrap(); +let (num_qubits, rho) = state_to_density_matrix(&state).unwrap(); +assert_eq!(num_qubits, 1); +assert_eq!(rho.len(), 4); +``` + +### `local_bloch_vectors(state) -> Result, VisualizationError>` + +计算每个量子比特的约化布洛赫向量,返回 `(比特编号, [x, y, z])` 列表,按比特编号升序排列。 + +异常情况: + +- `VisualizationError::InvalidInput`:状态缓冲长度与量子比特数不符。 + +```rust +use cqlib_core::qis::Statevector; +use cqlib_core::visualization::local_bloch_vectors; +use num_complex::Complex64; + +let state = Statevector::from_state(1, vec![Complex64::new(1.0, 0.0), Complex64::new(0.0, 0.0)]) + .unwrap(); +let vectors = local_bloch_vectors(&state).unwrap(); +assert_eq!(vectors.len(), 1); +assert!((vectors[0].1[2] - 1.0).abs() < 1e-10); +``` + +--- + +## 示例 + +### 纯态与混合态使用同一组入口 + +`Statevector` 与 `DensityMatrix` 都实现 `StateVisualizationSource`,同一组绘图入口对二者通用: + +```rust +use cqlib_core::qis::{DensityMatrix, Statevector}; +use cqlib_core::visualization::{StatePlotOptions, plot_state_city, plot_state_paulivec}; +use num_complex::Complex64; + +let statevector = Statevector::from_state( + 1, + vec![ + Complex64::new(1.0 / 2.0_f64.sqrt(), 0.0), + Complex64::new(1.0 / 2.0_f64.sqrt(), 0.0), + ], +) +.unwrap(); +let density_matrix = + DensityMatrix::from_state(statevector.num_qubits, statevector.data().to_vec()).unwrap(); + +let sv_svg = plot_state_paulivec(&statevector, &StatePlotOptions::default()).unwrap(); +let dm_svg = plot_state_paulivec(&density_matrix, &StatePlotOptions::default()).unwrap(); +assert_eq!(sv_svg, dm_svg); +``` + +### 多比特态的布洛赫图 + +多比特态为每个量子比特绘制一个约化布洛赫向量,可用于观察纠缠态中各比特的局部信息: + +```rust +use cqlib_core::qis::Statevector; +use cqlib_core::visualization::{StatePlotOptions, plot_bloch_multivector}; +use num_complex::Complex64; + +let bell = Statevector::from_state( + 2, + vec![ + Complex64::new(1.0 / 2.0_f64.sqrt(), 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(1.0 / 2.0_f64.sqrt(), 0.0), + ], +) +.unwrap(); + +let svg = plot_bloch_multivector(&bell, &StatePlotOptions::default()).unwrap(); +assert!(svg.contains("q0")); +assert!(svg.contains("q1")); +``` + +--- + +## 相关页面 + +- 把绘制结果写出为文件见 [落盘与输出](5_render_to_file.md)。 diff --git a/docs/api/cn/rust/5_visualization/4_result_plots.md b/docs/api/cn/rust/5_visualization/4_result_plots.md new file mode 100644 index 00000000..9ac8e018 --- /dev/null +++ b/docs/api/cn/rust/5_visualization/4_result_plots.md @@ -0,0 +1,186 @@ +# 测量结果可视化 + +`cqlib_core::visualization::result` + +本页覆盖测量结果的可视化:直方图 `plot_histogram`、概率分布图 `plot_distribution`,以及绘图选项 `ResultPlotOptions`。两个入口都接受执行结果对象,返回 SVG 标记字符串。 + +## 导入 + +```rust +use cqlib_core::visualization::{ResultPlotOptions, plot_distribution, plot_histogram}; +``` + +--- + +## `plot_histogram(result, options) -> Result` + +把执行结果的测量计数绘制为直方图。计数按结果的量子比特数格式化为比特串标签,作为横轴刻度;纵轴从零起算,不绘制负值区间。 + +参数: + +- `result` (`&ExecutionResult`):包含测量计数的执行结果。 +- `options` (`&ResultPlotOptions`):排序、配色与布局选项。 + +返回: + +- `Result`:SVG 标记字符串。 + +异常情况: + +- `VisualizationError::InvalidInput`:执行结果不含计数,或绘图选项不一致。 + +示例: + +```rust +use cqlib_core::circuit::Qubit; +use cqlib_core::device::{ExecutionResult, Outcome}; +use cqlib_core::visualization::{ResultPlotOptions, plot_histogram}; +use std::collections::HashMap; + +let mut result = ExecutionResult::new( + "task-visualization".to_string(), + (0..2).map(|idx| Qubit::new(idx as u32)).collect::>(), + 7, + 2, + Some("simulator".to_string()), + None, +); +let counts = [("00", 2usize), ("11", 5)] + .into_iter() + .map(|(bits, count)| (Outcome::from_bitstring(bits).unwrap(), count)) + .collect::>(); +result.finish(counts, None); + +let svg = plot_histogram(&result, &ResultPlotOptions::default()).unwrap(); +assert!(svg.contains(" Result` + +把执行结果的测量计数归一化为概率后绘制。每一条计数除以计数总和得到概率,柱高与纵轴刻度按概率标注。 + +参数: + +- `result` (`&ExecutionResult`):包含测量计数的执行结果。 +- `options` (`&ResultPlotOptions`):排序、配色与布局选项。 + +返回: + +- `Result`:SVG 标记字符串。 + +异常情况: + +- `VisualizationError::InvalidInput`:执行结果不含计数,计数求和为零,或绘图选项不一致。 + +示例: + +```rust +let svg = plot_distribution(&result, &ResultPlotOptions::default()).unwrap(); +assert!(svg.contains(", + pub color: Vec, + pub number_to_keep: Option, + pub sort: String, + pub target_string: Option, + pub legend: Option>, + pub bar_labels: bool, + pub title: Option, +} +``` + +字段: + +- `figsize` (`Option<(f64, f64)>`):图尺寸,单位为类英寸单位,按每单位 `100` 像素换算为 SVG 像素,宽、高的下限均为 `2`。默认 `None`,画布为 `760 × 480` 像素。 +- `color` (`Vec`):每个数据集的颜色。默认空向量,此时使用内置的定性调色板。 +- `number_to_keep` (`Option`):保留取值最大的 `k` 条,其余聚合成标签为 `rest` 的条目。默认 `None`,不聚合。`k` 为 `0` 或不小于条目数时不聚合;取值并列时按键的标签排序,保证输出跨平台一致。 +- `sort` (`String`):排序策略。默认 `"asc"`。取值见下表。 +- `target_string` (`Option`):`sort = "hamming"` 使用的目标比特串。默认 `None`。 +- `legend` (`Option>`):图例条目,每个数据集一条。默认 `None`,不绘制图例。结果绘制入口只产生一个数据集,因此给出图例时其长度必须为 `1`。 +- `bar_labels` (`bool`):是否在柱顶绘制数值标签。默认 `true`。 +- `title` (`Option`):图标题。默认 `None`,不绘制标题。 + +### 排序策略 + +| 取值 | 说明 | +| --- | --- | +| `asc` | 按比特串标签的字典序升序。 | +| `desc` | 按比特串标签的字典序降序。 | +| `value` | 按标签在各数据集中的峰值升序;峰值并列时按标签升序。 | +| `value_desc` | 按标签在各数据集中的峰值降序;峰值并列时按标签升序。 | +| `hamming` | 按与 `target_string` 的汉明距离升序;距离并列时按标签升序。标签 `rest` 始终排在末尾。 | + +```rust +use cqlib_core::visualization::ResultPlotOptions; + +let options = ResultPlotOptions { + sort: "value_desc".to_string(), + number_to_keep: Some(8), + title: Some("Measurement counts".to_string()), + ..ResultPlotOptions::default() +}; +``` + +--- + +## 示例 + +### 按汉明距离排序并聚合其余条目 + +沿用上例的执行结果,按与目标比特串的汉明距离排序,只保留计数最大的 `1` 条,其余聚合成 `rest`: + +```rust +use cqlib_core::visualization::{ResultPlotOptions, plot_histogram}; + +let options = ResultPlotOptions { + color: vec!["#123456".to_string()], + number_to_keep: Some(1), + sort: "hamming".to_string(), + target_string: Some("00".to_string()), + legend: Some(vec!["sim".to_string()]), + bar_labels: false, + title: Some("Hamming order".to_string()), + ..ResultPlotOptions::default() +}; + +let svg = plot_histogram(&result, &options).unwrap(); +assert!(svg.contains("rest")); +``` + +### 自定义画布尺寸与配色 + +沿用上例的执行结果,指定画布尺寸、配色、图例与标题: + +```rust +use cqlib_core::visualization::{ResultPlotOptions, plot_distribution}; + +let options = ResultPlotOptions { + figsize: Some((3.2, 2.4)), + color: vec!["#0f766e".to_string()], + legend: Some(vec!["probability".to_string()]), + bar_labels: false, + title: Some("Distribution options".to_string()), + ..ResultPlotOptions::default() +}; + +let svg = plot_distribution(&result, &options).unwrap(); +assert!(svg.contains("width=\"320\"")); +assert!(svg.contains("height=\"240\"")); +``` + +--- + +## 相关页面 + +- 把绘制结果写出为文件见 [落盘与输出](5_render_to_file.md)。 diff --git a/docs/api/cn/rust/5_visualization/5_render_to_file.md b/docs/api/cn/rust/5_visualization/5_render_to_file.md new file mode 100644 index 00000000..dbb08c42 --- /dev/null +++ b/docs/api/cn/rust/5_visualization/5_render_to_file.md @@ -0,0 +1,155 @@ +# 落盘与输出 + +`cqlib_core::visualization` + +本页覆盖可视化的文件输出入口:线路图的 `render_figure_to_file`,量子态图与结果图的 `render_state_plot_to_file`、`render_result_plot_to_file`。三个入口共用同一套输出契约:扩展名决定写出格式,内容为 SVG 标记。 + +## 导入 + +```rust +use cqlib_core::visualization::{ + render_figure_to_file, render_result_plot_to_file, render_state_plot_to_file, +}; +``` + +--- + +## `render_figure_to_file(circuit, output_path, options) -> Result<(), VisualizationError>` + +把线路直接渲染到输出文件。内部先构建可视化 IR 并生成 SVG 标记,再写出文件;不需要先调用 `circuit_to_figure`。 + +参数: + +- `circuit` (`&Circuit`):待渲染的线路。 +- `output_path` (`&str`):目标文件路径。扩展名为 `.png` 时写出栅格图,其余扩展名写出 SVG 向量图。 +- `options` (`&FigureDrawerOptions`):图形绘制选项。`dpi` 决定 PNG 的栅格化缩放:默认 `160` 对应缩放 `1.0`,更大的取值按比例放大 PNG 尺寸。 + +返回: + +- `Result<(), VisualizationError>`:成功时不返回数据。 + +异常情况: + +- `VisualizationError::CircuitBuild`:`decompose_circuit_gates` 为 `true` 时展开线路门定义失败。 +- `VisualizationError::UnknownQubit`:操作引用了不在线路量子比特列表中的量子比特。 +- `VisualizationError::ParameterIndexOutOfBounds`:符号参数的索引超出线路参数表的长度。 +- `VisualizationError::InvalidInput`:`dpi` 为 `0`,或缩放后的画布尺寸超出可表示范围。 +- `VisualizationError::SvgRenderFailed`:SVG 解析或栅格化失败。 +- `VisualizationError::Io`:文件写入失败。 + +示例: + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::visualization::{FigureDrawerOptions, render_figure_to_file}; + +let mut circuit = Circuit::new(1); +circuit.h(Qubit::new(0)).unwrap(); + +render_figure_to_file(&circuit, "circuit.svg", &FigureDrawerOptions::default()).unwrap(); +render_figure_to_file(&circuit, "circuit.png", &FigureDrawerOptions::default()).unwrap(); +``` + +--- + +## `render_state_plot_to_file(svg, output_path) -> Result<(), VisualizationError>` + +把量子态绘图入口产出的 SVG 标记写出为文件。 + +参数: + +- `svg` (`&str`):由量子态绘图入口返回的 SVG 标记。 +- `output_path` (`&str`):目标文件路径。扩展名为 `.png` 时写出栅格图,其余扩展名写出 SVG 向量图。 + +返回: + +- `Result<(), VisualizationError>`:成功时不返回数据。 + +异常情况: + +- `VisualizationError::InvalidInput`:PNG 缩放后的画布尺寸超出可表示范围。 +- `VisualizationError::SvgRenderFailed`:SVG 解析或栅格化失败。 +- `VisualizationError::Io`:文件写入失败。 + +示例: + +```rust +use cqlib_core::qis::Statevector; +use cqlib_core::visualization::{ + StatePlotOptions, plot_bloch_multivector, render_state_plot_to_file, +}; +use num_complex::Complex64; + +let state = Statevector::from_state(1, vec![Complex64::new(1.0, 0.0), Complex64::new(0.0, 0.0)]) + .unwrap(); +let svg = plot_bloch_multivector(&state, &StatePlotOptions::default()).unwrap(); +render_state_plot_to_file(&svg, "bloch.svg").unwrap(); +``` + +--- + +## `render_result_plot_to_file(svg, output_path) -> Result<(), VisualizationError>` + +把结果绘图入口产出的 SVG 标记写出为文件。 + +参数: + +- `svg` (`&str`):由 `plot_histogram` 或 `plot_distribution` 返回的 SVG 标记。 +- `output_path` (`&str`):目标文件路径。扩展名为 `.png` 时写出栅格图,其余扩展名写出 SVG 向量图。 + +返回: + +- `Result<(), VisualizationError>`:成功时不返回数据。 + +异常情况: + +- `VisualizationError::InvalidInput`:PNG 缩放后的画布尺寸超出可表示范围。 +- `VisualizationError::SvgRenderFailed`:SVG 解析或栅格化失败。 +- `VisualizationError::Io`:文件写入失败。 + +示例: + +```rust +use cqlib_core::circuit::Qubit; +use cqlib_core::device::{ExecutionResult, Outcome}; +use cqlib_core::visualization::{ + ResultPlotOptions, plot_histogram, render_result_plot_to_file, +}; +use std::collections::HashMap; + +let mut result = ExecutionResult::new( + "task-visualization".to_string(), + (0..2).map(|idx| Qubit::new(idx as u32)).collect::>(), + 7, + 2, + Some("simulator".to_string()), + None, +); +let counts = [("00", 2usize), ("11", 5)] + .into_iter() + .map(|(bits, count)| (Outcome::from_bitstring(bits).unwrap(), count)) + .collect::>(); +result.finish(counts, None); + +let svg = plot_histogram(&result, &ResultPlotOptions::default()).unwrap(); +render_result_plot_to_file(&svg, "histogram.svg").unwrap(); +``` + +--- + +## 输出格式 + +| 扩展名 | 写出内容 | +| --- | --- | +| `.png`(不区分大小写) | 栅格图。SVG 先按缩放系数确定位图尺寸,再编码为 PNG。 | +| 其他扩展名 | 原样写出 SVG 标记文本。 | + +栅格化缩放:线路图按 `dpi / 160` 缩放,默认 `dpi` 取值下缩放为 `1.0`;量子态图与结果图固定按 `1.0` 写出。缩放只改变位图尺寸,SVG 的坐标系统保持不变。 + +--- + +## 相关页面 + +- 线路绘制选项见 [SVG 线路图](2_draw_figure.md)。 +- 量子态绘图入口见 [量子态可视化](3_state_plots.md)。 +- 结果绘图入口见 [测量结果可视化](4_result_plots.md)。 diff --git a/docs/api/cn/rust/5_visualization/6_visual_ir.md b/docs/api/cn/rust/5_visualization/6_visual_ir.md new file mode 100644 index 00000000..a04c7ef0 --- /dev/null +++ b/docs/api/cn/rust/5_visualization/6_visual_ir.md @@ -0,0 +1,622 @@ +# 可视化 IR + +`cqlib_core::visualization` + +本页覆盖线路绘制的中间表示层:构建入口 `build_visual_circuit`、构建选项 `VisualBuildOptions`,以及 IR 数据类型 `VisualCircuit`、`VisualOperation`、`VisualOpStyle`、`VisualChildren`、`VisualControlFlowKind`、`VisualCondition`。 + +## 导入 + +```rust +use cqlib_core::visualization::{ + VisualBuildOptions, VisualChildren, VisualCircuit, VisualCondition, VisualControlFlowKind, + VisualOpStyle, VisualOperation, build_visual_circuit, +}; +``` + +--- + +## 在管线中的位置 + +线路绘制分两步:先把线路构建为与绘制后端无关的可视化 IR,再由具体后端绘制。 + +```text +Circuit ──build_visual_circuit──▶ VisualCircuit ──┬── circuit_to_text / draw_text_from_visual ──▶ 文本图 + └── circuit_to_figure / draw_figure_from_visual ──▶ SVG 图 +``` + +构建阶段承担与线路语义相关的工作:把量子比特映射为下标、把参数格式化为标签文本、把指令归类为绘制样式,并把操作排入互不重叠的列。绘制阶段只消费这些结果,不再读回线路。控制流块在构建阶段被递归收集为子线路,由绘制后端展开为逐条标记。 + +中间层把构建与绘制解耦,带来三点结果: + +- 文本后端与图形后端消费同一份 IR,因此同一条线路在两种后端下的门顺序、列划分与标签一致。 +- 自定义绘制只需消费 `VisualCircuit`,无需重复实现量子比特映射、参数格式化与列调度。 +- 构建选项可以独立于绘制选项调整。文本后端经 `circuit_to_text` 进入时使用默认的参数格式化选项,若要让文本图采用别的参数显示模式,先自行构建 IR 再交给 `draw_text_from_visual`。 + +--- + +## `build_visual_circuit(circuit, options) -> Result` + +把线路构建为可视化 IR。 + +参数: + +- `circuit` (`&Circuit`):待构建的线路。 +- `options` (`&VisualBuildOptions`):构建选项。 + +返回: + +- `Result`:成功时为可视化 IR,其中量子比特数等于线路的量子比特数。 + +异常情况: + +- `VisualizationError::CircuitBuild`:`decompose_circuit_gates` 为 `true` 时展开线路门定义失败。 +- `VisualizationError::UnknownQubit`:操作引用了不在线路量子比特列表中的量子比特。 +- `VisualizationError::ParameterIndexOutOfBounds`:符号参数的索引超出线路参数表的长度。 + +示例: + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::visualization::{VisualBuildOptions, build_visual_circuit}; + +let mut circuit = Circuit::new(2); +circuit.h(Qubit::new(0)).unwrap(); +circuit.cx(Qubit::new(0), Qubit::new(1)).unwrap(); + +let visual = build_visual_circuit(&circuit, &VisualBuildOptions::default()).unwrap(); +assert_eq!(visual.num_qubits(), 2); +assert_eq!(visual.operations.len(), 2); +assert_eq!(visual.operations[0].label, "H"); +assert_eq!(visual.operations[1].lanes, vec![0, 1]); +assert_eq!(visual.num_columns, 2); +``` + +--- + +## VisualBuildOptions + +可视化 IR 的构建选项。这些选项只影响构建阶段,由文本后端与图形后端共享。 + +```rust +pub struct VisualBuildOptions { + pub decompose_circuit_gates: bool, + pub reserve_full_span_for_multi_qubit: bool, + pub parameter_format: ParameterFormatOptions, +} +``` + +字段: + +- `decompose_circuit_gates` (`bool`):构建前是否展开线路门定义。默认 `false`。 +- `reserve_full_span_for_multi_qubit` (`bool`):为涉及多个量子比特的操作保留整段下标(`min..=max`)而非只保留操作自身的下标。默认 `true`。 +- `parameter_format` (`ParameterFormatOptions`):参数标签文本的格式化选项。默认 `ParameterFormatOptions::default()`,即 `ParameterDisplayMode::Numeric`。 + +默认值由 `VisualBuildOptions::default()` 提供,也可用结构体更新语法只覆盖需要改动的字段: + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::visualization::{VisualBuildOptions, build_visual_circuit}; + +let mut sub = Circuit::new(2); +sub.h(Qubit::new(0)).unwrap(); +sub.cx(Qubit::new(0), Qubit::new(1)).unwrap(); +let sub_gate = sub.to_gate("SUB_BELL").unwrap(); + +let mut circuit = Circuit::new(2); +circuit + .append(sub_gate, vec![Qubit::new(0), Qubit::new(1)], Vec::new(), None) + .unwrap(); + +let visual = build_visual_circuit( + &circuit, + &VisualBuildOptions { + decompose_circuit_gates: true, + ..VisualBuildOptions::default() + }, +) +.unwrap(); +assert_eq!(visual.operations[0].label, "H"); +``` + +`parameter_format` 是嵌套结构,按字段更新: + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::visualization::{ + ParameterDisplayMode, ParameterFormatOptions, VisualBuildOptions, build_visual_circuit, +}; +use std::f64::consts::PI; + +let mut circuit = Circuit::new(1); +circuit.rx(Qubit::new(0), PI / 2.0).unwrap(); + +let options = VisualBuildOptions { + parameter_format: ParameterFormatOptions { + mode: ParameterDisplayMode::PiFractionPreferred, + ..ParameterFormatOptions::default() + }, + ..VisualBuildOptions::default() +}; +let visual = build_visual_circuit(&circuit, &options).unwrap(); +assert_eq!(visual.operations[0].params, vec!["π/2".to_string()]); +``` + +--- + +## VisualCircuit + +布局完成后的、与绘制后端无关的可视化 IR。 + +```rust +pub struct VisualCircuit { + pub qubits: Vec, + pub operations: Vec, + pub num_columns: usize, +} +``` + +字段: + +- `qubits` (`Vec`):按显示顺序排列的量子比特,操作的 `lanes` 即该列表中的下标。 +- `operations` (`Vec`):按线路顺序排列、已排入列的操作。 +- `num_columns` (`usize`):被占用的列数。 + +方法: + +- `fn num_qubits(&self) -> usize`:返回量子比特数。 + +```rust +use cqlib_core::circuit::Qubit; +use cqlib_core::visualization::{VisualCircuit, VisualOpStyle, VisualOperation}; + +let visual = VisualCircuit { + qubits: vec![Qubit::new(0)], + operations: vec![VisualOperation { + column: 0, + lanes: vec![0], + covered_lanes: vec![0], + label: "H".to_string(), + params: vec![], + style: VisualOpStyle::Gate, + span_box: false, + children: None, + span_cols: 1, + }], + num_columns: 1, +}; +assert_eq!(visual.num_qubits(), 1); +``` + +--- + +## VisualOperation + +绘制后端消费的原子 IR 节点。一个操作对应一条列上的一段绘制内容。 + +```rust +pub struct VisualOperation { + pub column: usize, + pub lanes: Vec, + pub covered_lanes: Vec, + pub label: String, + pub params: Vec, + pub style: VisualOpStyle, + pub span_box: bool, + pub children: Option, + pub span_cols: usize, +} +``` + +字段: + +- `column` (`usize`):排入的列序号。 +- `lanes` (`Vec)`:操作涉及的量子比特在 `VisualCircuit::qubits` 中的下标,按操作的操作数顺序排列。 +- `covered_lanes` (`Vec`):为避免重叠而在本列上保留的量子比特下标。控制流块保留全部下标;屏障在 `lanes` 为空时保留全部下标;其余操作在 `lanes` 为空时保留 `[0]`。涉及多于一个下标时,若 `reserve_full_span_for_multi_qubit` 为 `true`,则保留 `lanes` 的 `min..=max` 整段,否则保留 `lanes` 本身。量子比特数为零时为空。 +- `label` (`String`):主显示标签。 +- `params` (`Vec`):已格式化的参数标签。 +- `style` (`VisualOpStyle`):绘制样式。 +- `span_box` (`bool`):为 `true` 时该操作按跨多条量子比特线的整体块绘制。自定义酉门、线路门与存储指令取 `true`。 +- `children` (`Option`):控制流操作的子线路,非控制流操作为 `None`。 +- `span_cols` (`usize`):该操作保留的逻辑列数。普通操作为 `1`;控制流块按子线路的列数加上分隔与边距计算。 + +```rust +use cqlib_core::visualization::{VisualOpStyle, VisualOperation}; + +let op = VisualOperation { + column: 1, + lanes: vec![0, 1], + covered_lanes: vec![0, 1], + label: "X".to_string(), + params: vec!["π/2".to_string()], + style: VisualOpStyle::Controlled { num_controls: 1 }, + span_box: false, + children: None, + span_cols: 1, +}; +assert_eq!(op.label, "X"); +``` + +--- + +## VisualOpStyle + +单个操作的绘制样式。每个变体决定该操作由后端如何绘制。 + +```rust +pub enum VisualOpStyle { + /// 普通门块。 + Gate, + /// 受控操作,操作数开头的 `num_controls` 个量子比特为控制位。 + Controlled { + num_controls: usize, + }, + /// 受控 Z 标记,按两个以竖线相连的圆点绘制。 + Cz, + /// 两个量子比特间的交换标记。 + Swap, + /// 屏障标记。 + Barrier, + /// 测量标记。 + Measure, + /// 复位标记。 + Reset, + /// 延时标记。 + Delay, + /// 控制流标记。 + ControlFlow { + kind: VisualControlFlowKind, + }, +} +``` + +### 样式与标签的归类规则 + +构建阶段按下表把指令归类为样式与标签: + +| 指令 | `style` | `label` | +| --- | --- | --- | +| 标准门 `SWAP` | `Swap` | `SWAP` | +| 标准门 `CZ` | `Cz` | `CZ` | +| 标准门且控制位数大于 0 | `Controlled { num_controls }` | 目标门名 | +| 其余标准门 | `Gate` | 门名 | +| 多控门,控制位为 `1` 且基门为 `Z` | `Cz` | `CZ` | +| 其余多控门 | `Controlled { num_controls }` | 基门门名 | +| 自定义酉门 | `Gate`,`span_box` 为 `true` | `UnitaryGate::label()`,为空时 `Unitary` | +| 线路门 | `Gate`,`span_box` 为 `true` | `CircuitGate::name()`,为空时 `Gate` | +| 屏障 | `Barrier` | `B` | +| 测量 | `Measure` | `M` | +| 复位 | `Reset` | `R` | +| 延时 | `Delay` | `D` | +| 经典控制流 | `ControlFlow { kind }` | `IF`、`WH`、`FOR`、`SW`、`Break` 或 `Continue` | +| 测量到位指令 | `Measure` | `M` | +| 存储指令 | `Gate`,`span_box` 为 `true` | `STORE` | + +门名按标准门口径缩写:`SDG` 显示为 `SD`,`TDG` 显示为 `TD`,`Phase` 与 `GPhase` 显示为 `P`。受控门的标签取其目标门名:`CX` 与 `CCX` 为 `X`,`CY` 为 `Y`,`CRX`、`CRY`、`CRZ` 分别为 `RX`、`RY`、`RZ`,其余情形去掉门名开头的控制前缀。 + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::visualization::{VisualBuildOptions, VisualOpStyle, build_visual_circuit}; + +let mut circuit = Circuit::new(3); +circuit.ccx(Qubit::new(0), Qubit::new(1), Qubit::new(2)).unwrap(); +circuit.swap(Qubit::new(0), Qubit::new(1)).unwrap(); + +let visual = build_visual_circuit(&circuit, &VisualBuildOptions::default()).unwrap(); +assert_eq!(visual.operations[0].label, "X"); +assert!(matches!( + visual.operations[0].style, + VisualOpStyle::Controlled { num_controls: 2 } +)); +assert!(matches!(visual.operations[1].style, VisualOpStyle::Swap)); +``` + +--- + +## VisualChildren + +附着在控制流操作上的子线路。子线路保留父级的量子比特列表,因此分支体与循环体只有触及部分量子比特时,绘制仍与外围线路对齐。 + +```rust +pub enum VisualChildren { + IfElse { + then_circuit: Box, + else_circuit: Option>, + }, + While { + body_circuit: Box, + }, + For { + body_circuit: Box, + }, + Switch { + case_circuits: Vec<(String, Box)>, + default_circuit: Option>, + }, +} +``` + +变体: + +- `IfElse`:条件分支。`then_circuit` 为条件成立时执行的子线路,`else_circuit` 为条件不成立时执行的子线路,无假分支时为 `None`。 +- `While`:`while` 循环体。`body_circuit` 为循环体内执行的子线路。 +- `For`:`for` 循环体。`body_circuit` 为循环体内执行的子线路。 +- `Switch`:多分支选择。`case_circuits` 按源码顺序给出各分支的标签与子线路,`default_circuit` 为默认分支的子线路,无默认分支时为 `None`。 + +```rust +use cqlib_core::circuit::{Circuit, ClassicalExpr, Qubit}; +use cqlib_core::visualization::{VisualBuildOptions, VisualChildren, build_visual_circuit}; + +let mut circuit = Circuit::new(2); +let condition = ClassicalExpr::bool_literal(true); +circuit + .if_else(condition, |body| body.x(Qubit::new(1)), |body| body.z(Qubit::new(1))) + .unwrap(); + +let visual = build_visual_circuit(&circuit, &VisualBuildOptions::default()).unwrap(); +let op = &visual.operations[0]; +assert_eq!(op.label, "IF true"); + +match op.children.as_ref() { + Some(VisualChildren::IfElse { + then_circuit, + else_circuit, + }) => { + assert_eq!(then_circuit.operations[0].label, "X"); + assert_eq!(else_circuit.as_ref().unwrap().operations[0].label, "Z"); + } + _ => panic!("expected IfElse children"), +} +``` + +--- + +## VisualControlFlowKind + +绘制后端使用的控制流标记族。 + +```rust +pub enum VisualControlFlowKind { + IfElseBlock { + has_false_branch: bool, + condition: VisualCondition, + }, + WhileBlock { + condition: VisualCondition, + }, + ForBlock { + range: VisualCondition, + }, + SwitchBlock { + target: VisualCondition, + }, + Break, + Continue, + IfStart, + ElseStart, + WhileStart, + ForStart, + SwitchStart, + CaseStart, + DefaultStart, + End, +} +``` + +变体分两组。 + +源块变体,由构建阶段产生: + +- `IfElseBlock`:条件分支块。`has_false_branch` 表示源块是否含假分支,`condition` 为分支条件的显示元数据。 +- `WhileBlock`:`while` 循环块。`condition` 为循环条件的显示元数据。 +- `ForBlock`:`for` 循环块。`range` 为循环区间表达式的显示元数据。 +- `SwitchBlock`:多分支选择块。`target` 为分支目标表达式的显示元数据。 +- `Break` 与 `Continue`:结构化 `break` 与 `continue` 标记。 + +展开后的标记变体,由绘制后端在绘制前展开源块时产生: + +- `IfStart`、`ElseStart`:条件分支的起始与假分支起始标记。 +- `WhileStart`、`ForStart`:循环起始标记。 +- `SwitchStart`、`CaseStart`、`DefaultStart`:多分支选择的起始、分支与默认分支标记。 +- `End`:块结束标记。 + +绘制前,文本后端与图形后端都会先把源块变体展开为逐条标记,再逐条绘制。标记的标签形如 `If-0 true`、`Else-0`、`End-0`,序号在同一张图内递增。因此展开后的标记变体不会出现在 `build_visual_circuit` 的返回值中,它描述的是后端绘制阶段的中间形态。 + +```rust +use cqlib_core::circuit::{Circuit, ClassicalExpr, Qubit}; +use cqlib_core::visualization::{ + VisualBuildOptions, VisualChildren, VisualControlFlowKind, VisualOpStyle, + build_visual_circuit, +}; + +let mut circuit = Circuit::new(2); +let condition = ClassicalExpr::bool_literal(false); +circuit + .while_(condition, |body| { + body.h(Qubit::new(0))?; + body.cx(Qubit::new(0), Qubit::new(1)) + }) + .unwrap(); + +let visual = build_visual_circuit(&circuit, &VisualBuildOptions::default()).unwrap(); +let op = &visual.operations[0]; +assert!(op.label.starts_with("WH false")); +assert!(matches!( + op.style, + VisualOpStyle::ControlFlow { + kind: VisualControlFlowKind::WhileBlock { .. } + } +)); + +match op.children.as_ref() { + Some(VisualChildren::While { body_circuit }) => { + assert_eq!(body_circuit.operations.len(), 2); + } + _ => panic!("expected While children"), +} +``` + +`for` 循环与多分支选择同样以子线路形式记录,子线路的操作序列与源码顺序一致: + +```rust +use cqlib_core::circuit::{Circuit, ClassicalExpr, ClassicalType, Qubit}; +use cqlib_core::visualization::{VisualBuildOptions, VisualChildren, build_visual_circuit}; + +let mut circuit = Circuit::new(2); +let loop_var = circuit.var(ClassicalType::uint(3).unwrap()); +circuit + .for_uint( + loop_var, + ClassicalExpr::uint_literal(3, 0).unwrap(), + ClassicalExpr::uint_literal(3, 4).unwrap(), + ClassicalExpr::uint_literal(3, 1).unwrap(), + |body, _| body.cx(Qubit::new(0), Qubit::new(1)), + ) + .unwrap(); + +let visual = build_visual_circuit(&circuit, &VisualBuildOptions::default()).unwrap(); +assert!(visual.operations[0].label.starts_with("FOR")); +match visual.operations[0].children.as_ref() { + Some(VisualChildren::For { body_circuit }) => { + assert_eq!(body_circuit.operations[0].label, "X"); + } + _ => panic!("expected For children"), +} +``` + +--- + +## VisualCondition + +控制流可视化使用的条件显示元数据。 + +```rust +pub struct VisualCondition { + pub label: String, +} +``` + +字段: + +- `label` (`String`):分支条件的显示标签。 + +标签文本由条件的经典表达式生成:布尔字面量显示为 `true` 或 `false`,位字面量显示为 `bit(v)`,无符号整数字面量显示其数值,位向量字面量显示为 `bits(v)`,其余表达式形态回退为其 `Debug` 文本。 + +```rust +use cqlib_core::visualization::VisualCondition; + +let condition = VisualCondition { + label: "true".to_string(), +}; +assert_eq!(condition.label, "true"); +``` + +--- + +## 典型用法 + +### 检查线路结构与列划分 + +共享同一量子比特的操作必须排在相邻列,落在不同量子比特上的操作可以共用一列: + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::visualization::{VisualBuildOptions, build_visual_circuit}; + +let mut circuit = Circuit::new(2); +circuit.h(Qubit::new(0)).unwrap(); +circuit.x(Qubit::new(1)).unwrap(); + +let visual = build_visual_circuit(&circuit, &VisualBuildOptions::default()).unwrap(); +let cols: Vec = visual.operations.iter().map(|op| op.column).collect(); +assert_eq!(cols, vec![0, 0]); +``` + +### 检查操作样式与占位 + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::visualization::{VisualBuildOptions, VisualOpStyle, build_visual_circuit}; + +let mut circuit = Circuit::new(3); +circuit.ccx(Qubit::new(0), Qubit::new(1), Qubit::new(2)).unwrap(); + +let visual = build_visual_circuit(&circuit, &VisualBuildOptions::default()).unwrap(); +let op = &visual.operations[0]; +assert_eq!(op.label, "X"); +assert!(matches!(op.style, VisualOpStyle::Controlled { num_controls: 2 })); +assert_eq!(op.covered_lanes, vec![0, 1, 2]); +``` + +### 手工构造 IR 并渲染 + +手工构造的 `VisualCircuit` 可以直接交给绘制入口使用。以下 IR 描述一条只含单个 `H` 门的线路: + +```rust +use cqlib_core::circuit::Qubit; +use cqlib_core::visualization::{ + TextDrawerOptions, VisualCircuit, VisualOpStyle, VisualOperation, draw_text_from_visual, +}; + +let visual = VisualCircuit { + qubits: vec![Qubit::new(0)], + operations: vec![VisualOperation { + column: 0, + lanes: vec![0], + covered_lanes: vec![0], + label: "H".to_string(), + params: vec![], + style: VisualOpStyle::Gate, + span_box: false, + children: None, + span_cols: 1, + }], + num_columns: 1, +}; + +let text = draw_text_from_visual(&visual, &TextDrawerOptions::default()).unwrap(); +assert!(text.contains("H")); +``` + +绘制结果为: + +```text + + Q0: ───H─ + +``` + +### 以自定义构建选项渲染 + +构建选项只作用在构建阶段,因此同一份线路的绘制文本可以随构建选项改变: + +```rust +use cqlib_core::circuit::{Circuit, Parameter, Qubit}; +use cqlib_core::visualization::{ + FigureDrawerOptions, ParameterDisplayMode, TextDrawerOptions, VisualBuildOptions, + build_visual_circuit, draw_figure_from_visual, draw_text_from_visual, +}; + +let mut circuit = Circuit::new(1); +let theta = Parameter::symbol("theta"); +circuit.rx(Qubit::new(0), theta + 1.0).unwrap(); + +let mut build_options = VisualBuildOptions::default(); +build_options.parameter_format.mode = ParameterDisplayMode::SymbolicWithValue; +let visual = build_visual_circuit(&circuit, &build_options).unwrap(); + +let text = draw_text_from_visual(&visual, &TextDrawerOptions::default()).unwrap(); +assert!(text.contains("1 + theta")); + +let svg = draw_figure_from_visual(&visual, &FigureDrawerOptions::default(), None); +assert!(svg.contains(" = dyn Fn(&Circuit, Option<&Hamiltonian>, Option) -> (f64, f64) + 'a; +``` + +参数依次为待执行线路、待估计的可观测量与 shot 数,返回 `(期望值, 方差)`。分母线路不携带可观测量,第二个参数为 `None`;`ZNEMitigation::run_em_sequence` 不指定 shot 数,第三个参数为 `None`。公开方法统一以 `&Estimator<'_>` 接收它,即 trait 对象引用 `&dyn Fn(&Circuit, Option<&Hamiltonian>, Option) -> (f64, f64)`,闭包与函数项都可以直接取引用传入。返回值中的方差只有虚拟蒸馏会使用,零噪声外推只取期望值。 + +### 缓解流水线状态 + +`ErrorMitigation` 的每个实例遵循固定生命周期:创建 → `run()` → `get_mitigated()`。`run` 与 `get_mitigated` 各只能成功调用一次,顺序颠倒或重复调用都返回错误;调用失败时实例状态保持不变,可以修正参数后重试。需要多次缓解时重新创建实例。 + +--- + +## 常用入口 + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::error_mitigation::{ + ErrorMitigation, ExtrapolateMethod, MitigationMethod, ProcessArgs, RunArgs, ZneConfig, +}; +use cqlib_core::qis::{Hamiltonian, Pauli, PauliString}; +use num_complex::Complex64; + +let q0 = Qubit::new(0); +let mut circuit = Circuit::new(1); +circuit.x(q0).unwrap(); + +let mut pauli = PauliString::new(1); +pauli.set_pauli(0, Pauli::Z); +let hamiltonian = Hamiltonian::from_list(vec![(pauli, Complex64::new(1.0, 0.0))]).unwrap(); + +let mut mitigation = ErrorMitigation::new( + circuit, + MitigationMethod::Zne(ZneConfig { + fold_levels: vec![0, 1, 2], + }), +) +.unwrap(); + +// 对接模拟器或真机后端:返回 (期望值, 方差) +let estimator = |circuit: &Circuit, hamiltonian: Option<&Hamiltonian>, shots: Option| { + assert!(hamiltonian.is_some()); + assert_eq!(shots, Some(256)); + (circuit.operations().len() as f64 + 0.5, 0.0) +}; + +mitigation + .run( + &hamiltonian, + RunArgs::Zne { + gate_set: None, + shots: Some(256), + }, + &estimator, + ) + .unwrap(); + +let mitigated = mitigation + .get_mitigated(ProcessArgs::Zne { + method: ExtrapolateMethod::Polynomial, + degree: None, + }) + .unwrap(); + +assert_eq!(mitigated.expectation, 0.5); +``` + +--- + +## 核心概念与术语 + +| 术语 | 说明 | +| --- | --- | +| **错误缓解** | 在期望值层面修正噪声导致的系统性偏差的方法集合,不是量子纠错。 | +| **估计器(Estimator)** | 由调用方提供的回调,形态为 `Estimator`;接收待执行线路、可选可观测量与可选 shot 数,返回 `(期望值, 方差)`。 | +| **零噪声外推(ZNE)** | 通过门折叠放大线路噪声,得到一组不同噪声强度下的期望值,再外推到零噪声极限的方法。 | +| **折叠等级** | 零噪声外推的噪声放大级别。等级 0 对应原始线路,等级 `l` 对应噪声因子 `2 * l + 1`。 | +| **噪声因子** | 折叠线路相对原始线路的噪声放大倍数,恒等于 `2 * 折叠等级 + 1`。 | +| **门折叠** | 把线路改写为 `U -> U (U† U)^level` 的构造方式;只对指定门折叠时称为选择性折叠。 | +| **虚拟蒸馏** | 通过副本与 copy-swap 线路估计 `Tr(O ρ^M) / Tr(ρ^M)` 的方法,`M` 为副本数。 | +| **副本(copy)** | 虚拟蒸馏中并行准备的基底线路份数,最少为 2。 | +| **copy-swap 线路** | 把多份基底线路并排放在互不重叠的比特区间、并在首份与其他每份之间插入逐比特 SWAP 的线路,宽度等于副本数乘以基底线路宽度。 | +| **分子线路 / 分母线路** | 虚拟蒸馏的两条统计线路。分子线路携带扩展后的可观测量;分母线路不携带可观测量,估计器收到 `None`。 | +| **缓解流水线** | `ErrorMitigation` 表示的顺序式流程:`new` 创建、`run` 收集原始数据、`get_mitigated` 做方法相关的后处理。 | + +--- + +## `cqlib_core::error_mitigation` API 概览 + +### 低层入口 + +| 名字 | 简介 | +| --- | --- | +| [`ZNEMitigation`](1_zne.md) | 零噪声外推的低层入口:门折叠、批量取值与外推。 | +| [`ExtrapolateMethod`](1_zne.md) | 外推方法枚举,包括多项式与指数。 | +| [`ZneConfig`](1_zne.md) | 零噪声外推的折叠等级配置,供统一流水线与方法枚举使用。 | +| [`VirtualDistillation`](2_virtual_distillation.md) | 虚拟蒸馏的低层入口:copy-swap 线路构造与分子、分母统计。 | +| [`VirtualDistillationConfig`](2_virtual_distillation.md) | 虚拟蒸馏的副本数配置,供统一流水线与方法枚举使用。 | + +### 统一流水线 + +| 名字 | 简介 | +| --- | --- | +| [`ErrorMitigation`](3_unified.md) | 顺序式统一入口,编排构造、执行与后处理。 | +| [`MitigationMethod`](3_unified.md) | 缓解方法枚举,携带对应方法的配置。 | +| [`RunArgs`](3_unified.md) | 执行阶段的参数枚举。 | +| [`ProcessArgs`](3_unified.md) | 后处理阶段的参数枚举。 | +| [`MitigatedResult`](3_unified.md) | 最终缓解结果,包含期望值与可选方差。 | + +### 回调与错误 + +| 名字 | 简介 | +| --- | --- | +| [`Estimator`](0_overview.md) | 估计器回调类型别名,全部缓解方法共用。 | +| [`ErrorMitigationError`](3_unified.md) | 模块统一错误类型。 | + +--- + +## 快速示例 + +### 1. 用统一流水线执行虚拟蒸馏 + +```rust +use cqlib_core::circuit::Circuit; +use cqlib_core::error_mitigation::{ + ErrorMitigation, MitigationMethod, ProcessArgs, RunArgs, VirtualDistillationConfig, +}; +use cqlib_core::qis::{Hamiltonian, Pauli, PauliString}; +use num_complex::Complex64; + +let mut pauli = PauliString::new(1); +pauli.set_pauli(0, Pauli::Z); +let hamiltonian = Hamiltonian::from_list(vec![(pauli, Complex64::new(1.0, 0.0))]).unwrap(); + +let mut mitigation = ErrorMitigation::new( + Circuit::new(1), + MitigationMethod::VirtualDistillation(VirtualDistillationConfig { copies: 2 }), +) +.unwrap(); + +// 分子线路带可观测量,分母线路不带 +mitigation + .run( + &hamiltonian, + RunArgs::VirtualDistillation { + shots_numerator: 3, + shots_denominator: 2, + }, + &|_circuit, hamiltonian_arg, shots| { + if hamiltonian_arg.is_some() { + assert_eq!(shots, Some(3)); + (1.5, 0.25) + } else { + assert_eq!(shots, Some(2)); + (2.0, 1.0) + } + }, + ) + .unwrap(); + +let mitigated = mitigation + .get_mitigated(ProcessArgs::VirtualDistillation) + .unwrap(); + +assert!((mitigated.expectation - 0.75).abs() < 1e-12); +assert!((mitigated.variance.unwrap() - 0.203125).abs() < 1e-12); +``` + +### 2. 全局折叠与选择性折叠 + +```rust +use cqlib_core::circuit::gate::{Instruction, StandardGate}; +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::error_mitigation::{ExtrapolateMethod, ZNEMitigation}; + +let q0 = Qubit::new(0); +let mut circuit = Circuit::new(1); +circuit.h(q0).unwrap(); +circuit.s(q0).unwrap(); + +let zne = ZNEMitigation::new(circuit, vec![0, 1, 2]); +assert_eq!(zne.noise_factors(), &[1, 3, 5]); + +// 全局折叠:三个等级各得到一条折叠线路 +let global = zne.fold_circuits(None).unwrap(); +assert_eq!(global.len(), 3); + +// 选择性折叠:只放大 S 门,H 门保持原样 +let gate_set = vec![Instruction::Standard(StandardGate::S)]; +let selective = zne.fold_circuits(Some(&gate_set)).unwrap(); +assert_eq!(selective[1].operations().len(), 4); + +// 以噪声因子为自变量外推到零噪声 +let extrapolated = zne + .extrapolate(&[1.5, 3.5, 5.5], ExtrapolateMethod::Polynomial, 1) + .unwrap(); +assert!((extrapolated - 0.5).abs() < 1e-10); +``` + +### 3. 低层虚拟蒸馏的分子与分母 + +```rust +use cqlib_core::circuit::Circuit; +use cqlib_core::error_mitigation::VirtualDistillation; +use cqlib_core::qis::{Hamiltonian, Pauli, PauliString}; +use num_complex::Complex64; + +let mut pauli = PauliString::new(1); +pauli.set_pauli(0, Pauli::Z); +let hamiltonian = Hamiltonian::from_list(vec![(pauli, Complex64::new(1.0, 0.0))]).unwrap(); + +let vd = VirtualDistillation::new(Circuit::new(1), 2).unwrap(); + +// 两份基底线路加一对逐比特 SWAP +let copy_swap = vd.build_copy_swap_circuit().unwrap(); +assert_eq!(copy_swap.width(), 2); + +let (mu_vd, var_vd) = vd + .run_vd(&hamiltonian, 3, 2, &|_circuit, hamiltonian_arg, shots| { + if hamiltonian_arg.is_some() { + assert_eq!(shots, Some(3)); + (1.5, 0.25) + } else { + assert_eq!(shots, Some(2)); + (2.0, 1.0) + } + }) + .unwrap(); + +assert!((mu_vd - 0.75).abs() < 1e-12); +assert!((var_vd - 0.203125).abs() < 1e-12); +``` + +--- + +## 校验与错误处理 + +缓解过程中的校验分两处:配置层面的校验发生在创建阶段,输入层面的校验发生在折叠、执行与外推阶段,两者都通过 `ErrorMitigationError` 返回。折叠等级为负时,低层折叠入口返回的是线路错误 `CircuitError::InvalidControlOperation`,而统一流水线的创建阶段直接拒绝该配置。 + +| 错误 | 触发场景 | +| --- | --- | +| `ErrorMitigationError::InvalidCopies` | 虚拟蒸馏的副本数小于 2。 | +| `ErrorMitigationError::InvalidFoldLevel` | 零噪声外推的折叠等级为负,在 `ErrorMitigation::new` 中被拒绝。 | +| `ErrorMitigationError::HamiltonianQubitCountMismatch` | 哈密顿量的比特数与基底线路宽度不一致,见 [ZNE](1_zne.md) 与 [虚拟蒸馏](2_virtual_distillation.md)。 | +| `ErrorMitigationError::ZeroDenominatorMean` | 虚拟蒸馏的分母线路均值为零,比值无法计算。 | +| `ErrorMitigationError::RunRequiredBeforeMitigation` / `AlreadyRun` / `AlreadyMitigated` | 缓解流水线的调用顺序错误或重复调用。 | +| `ErrorMitigationError::RunArgsMethodMismatch` / `ProcessArgsMethodMismatch` | 执行参数或后处理参数与 `MitigationMethod` 不一致。 | +| `ErrorMitigationError::EmptyNoisyResults` / `NoisyResultsLengthMismatch` / `InvalidPolynomialDegree` / `NonPositiveNoisyResults` / `SingularExponentialFit` / `SingularPolynomialFit` | 外推输入非法或拟合失败。 | +| `ErrorMitigationError::Circuit` / `Qis` | 由线路操作或量子信息计算透传的错误。 | + +全部变体的完整定义见 [统一流水线](3_unified.md)。 + +--- + +## 下一步 + +- [零噪声外推](1_zne.md):折叠语义、噪声因子与外推方法。 +- [虚拟蒸馏](2_virtual_distillation.md):copy-swap 线路、分子与分母统计。 +- [统一流水线](3_unified.md):`ErrorMitigation` 状态机、参数枚举与错误类型。 diff --git a/docs/api/cn/rust/6_error_mitigation/1_zne.md b/docs/api/cn/rust/6_error_mitigation/1_zne.md new file mode 100644 index 00000000..dcd7c768 --- /dev/null +++ b/docs/api/cn/rust/6_error_mitigation/1_zne.md @@ -0,0 +1,230 @@ +# 零噪声外推 + +`cqlib_core::error_mitigation::zne_mitigation` + +零噪声外推通过门折叠把基底线路改写成一组噪声强度递增的线路,在每条折叠线路上取出期望值,再用拟合外推到零噪声极限。本页覆盖折叠、批量取值与外推的公开 API。 + +## 导入 + +```rust +use cqlib_core::circuit::gate::Instruction; +use cqlib_core::error_mitigation::{ExtrapolateMethod, ZNEMitigation, ZneConfig}; +``` + +--- + +## ZNEMitigation + +零噪声外推的低层入口。它持有基底线路与折叠等级,把折叠、取值与外推分成三个可独立调用的阶段。 + +```rust +pub struct ZNEMitigation { + circuit: Circuit, + fold_levels: Vec, + noise_factors: Vec, +} +``` + +字段私有,通过方法读取: + +- `fn circuit(&self) -> &Circuit`:原始(未折叠)的基底线路。 +- `fn fold_levels(&self) -> &[i32]`:配置的折叠等级,顺序与构造时一致。 +- `fn noise_factors(&self) -> &[i32]`:与折叠等级一一对应的噪声因子,恒为 `2 * level + 1`。 + +其余方法: + +- `fn new(circuit: Circuit, fold_levels: Vec) -> Self`:创建入口,取得线路所有权并据等级换算出噪声因子。构造阶段不校验等级是否非负。 +- `fn fold_circuits(&self, gate_set: Option<&[Instruction]>) -> Result, CircuitError>`:按每个折叠等级各构造一条折叠线路。 +- `fn run_em_sequence(&self, gate_set: Option<&[Instruction]>, hamiltonian: &Hamiltonian, estimator: &Estimator<'_>) -> Result, ErrorMitigationError>`:逐等级折叠并调用估计器取值,估计器收到的 shot 数为 `None`。 +- `fn run_em_sequence_with_shots(&self, gate_set: Option<&[Instruction]>, hamiltonian: &Hamiltonian, shots: Option, estimator: &Estimator<'_>) -> Result, ErrorMitigationError>`:同上,并把 shot 数透传给估计器。 +- `fn extrapolate(&self, noisy_results: &[f64], method: ExtrapolateMethod, degree: usize) -> Result`:按方法分派到下面两个外推方法。 +- `fn poly_extrapolate(&self, noisy_results: &[f64], degree: usize) -> Result`:多项式外推。 +- `fn exp_extrapolate(&self, noisy_results: &[f64]) -> Result`:指数外推。 + +`ZNEMitigation` 没有 builder 方法,也没有 `Default` 实现:实例只能由 `new` 创建,折叠等级在读接口中不可修改,改变等级需要重新创建实例。它实现 `Debug` 与 `Clone`。 + +### `fold_circuits(&self, gate_set: Option<&[Instruction]>) -> Result, CircuitError>` + +按等级构造折叠线路,返回的向量长度等于折叠等级数,顺序与 `fold_levels` 一致。 + +参数: + +- `gate_set`:可选的门集合,决定折叠范围。传 `None` 为全局折叠,把整条线路改写为 `U -> U (U† U)^level`,其中逆线路由基底线路整体取逆得到;传 `Some` 为选择性折叠,逐条操作检查名称,命中集合中任一名称的操作才被放大,其余操作原样保留。 + +返回: + +- `Vec`:每个折叠等级对应的折叠线路。 + +异常情况: + +- `CircuitError::InvalidControlOperation`:某一折叠等级为负。 +- `CircuitError`:基底线路取逆失败等线路层面的错误。 + +等级为 0 时直接返回基底线路的副本,不做任何追加。选择性折叠按操作名称匹配,名称由 `Instruction::name()` 给出,与操作携带的参数或自身的线路定义无关。 + +### `run_em_sequence_with_shots(&self, gate_set: Option<&[Instruction]>, hamiltonian: &Hamiltonian, shots: Option, estimator: &Estimator<'_>) -> Result, ErrorMitigationError>` + +逐等级折叠并调用估计器,返回每条折叠线路的期望值。`run_em_sequence` 是它的无 shot 版本,固定把 `None` 传给估计器。 + +参数: + +- `gate_set`:同 `fold_circuits`,控制折叠范围。 +- `hamiltonian`:待估计的可观测量,其比特数必须等于基底线路的宽度。 +- `shots`:每次执行的 shot 数,原样透传给估计器。 +- `estimator`:估计器回调,每条折叠线路调用一次,第二个参数恒为 `Some(hamiltonian)`;回调形态见 [概览](0_overview.md)。 + +返回: + +- `Vec`:各折叠线路的期望值,顺序与 `fold_levels` 一致。估计器返回的方差在此阶段被丢弃。 + +异常情况: + +- `ErrorMitigationError::HamiltonianQubitCountMismatch`:哈密顿量的比特数与基底线路宽度不一致,错误中同时给出期望值与实际值。 +- `ErrorMitigationError::Circuit`:折叠失败时透传的线路错误。 + +### 外推 + +三个外推方法都以噪声因子为自变量、以 `noisy_results` 为因变量,返回零噪声(自变量为 0)处的估计值,并且都要求 `noisy_results` 非空且长度等于噪声因子个数。 + +- `fn poly_extrapolate(&self, noisy_results: &[f64], degree: usize) -> Result`:最小二乘多项式拟合,返回 `x = 0` 处的截距。`degree` 为多项式次数,必须小于数据点数。 +- `fn exp_extrapolate(&self, noisy_results: &[f64]) -> Result`:按 `y(x) = A * exp(-x / tau)` 建模,在 log 空间对 `ln(y) = ln(A) + m * x` 做线性回归,返回 `A`。 +- `fn extrapolate(&self, noisy_results: &[f64], method: ExtrapolateMethod, degree: usize) -> Result`:`Polynomial` 使用 `degree` 转发到 `poly_extrapolate`;`Exponential` 忽略 `degree` 转发到 `exp_extrapolate`。 + +### 示例 + +```rust +use cqlib_core::circuit::gate::{Instruction, StandardGate}; +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::error_mitigation::ZNEMitigation; + +let q0 = Qubit::new(0); +let mut circuit = Circuit::new(1); +circuit.h(q0).unwrap(); +circuit.s(q0).unwrap(); + +// 折叠等级 [0, 1, 2] 对应噪声因子 [1, 3, 5] +let zne = ZNEMitigation::new(circuit, vec![0, 1, 2]); +assert_eq!(zne.fold_levels(), &[0, 1, 2]); +assert_eq!(zne.noise_factors(), &[1, 3, 5]); + +// 全局折叠:等级 0 原样返回,等级 1 把整条线路折叠为 U -> U U† U +let global = zne.fold_circuits(None).unwrap(); +assert_eq!(global[0].operations().len(), 2); +assert_eq!(global[1].operations().len(), 6); + +// 选择性折叠:只放大 S 门,H 门保持原样 +let gate_set = vec![Instruction::Standard(StandardGate::S)]; +let selective = zne.fold_circuits(Some(&gate_set)).unwrap(); +assert_eq!(selective[1].operations().len(), 4); +``` + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::error_mitigation::{ExtrapolateMethod, ZNEMitigation}; +use cqlib_core::qis::{Hamiltonian, Pauli, PauliString}; +use num_complex::Complex64; + +let q0 = Qubit::new(0); +let mut circuit = Circuit::new(1); +circuit.x(q0).unwrap(); + +let zne = ZNEMitigation::new(circuit, vec![0, 1, 2]); + +let mut pauli = PauliString::new(1); +pauli.set_pauli(0, Pauli::Z); +let hamiltonian = Hamiltonian::from_list(vec![(pauli, Complex64::new(1.0, 0.0))]).unwrap(); + +// 估计器返回 (期望值, 方差),零噪声外推只使用期望值 +let noisy = zne + .run_em_sequence_with_shots( + None, + &hamiltonian, + Some(256), + &|circuit, hamiltonian_arg, shots| { + assert!(hamiltonian_arg.is_some()); + assert_eq!(shots, Some(256)); + (circuit.operations().len() as f64 + 0.5, 0.0) + }, + ) + .unwrap(); + +// 噪声因子为 [1, 3, 5],实测值满足 y = 0.5 + x +assert_eq!(noisy, vec![1.5, 3.5, 5.5]); + +// 多项式外推取 x = 0 处的截距 +let mitigated = zne + .extrapolate(&noisy, ExtrapolateMethod::Polynomial, 1) + .unwrap(); +assert!((mitigated - 0.5).abs() < 1e-10); + +// 指数外推:模型为 y = A * exp(-x / tau),返回 A,同时忽略 degree +let a = 2.5_f64; +let tau = 4.0_f64; +let noisy: Vec = zne + .noise_factors() + .iter() + .map(|&x| a * (-(x as f64) / tau).exp()) + .collect(); +let mitigated = zne + .extrapolate(&noisy, ExtrapolateMethod::Exponential, 99) + .unwrap(); +assert!((mitigated - a).abs() < 1e-10); +``` + +--- + +## ExtrapolateMethod + +外推方法枚举,决定 `extrapolate` 走哪条拟合路径。 + +```rust +pub enum ExtrapolateMethod { + Polynomial, + Exponential, +} +``` + +- `Polynomial`:多项式外推。`extrapolate` 的 `degree` 参数在该分支生效。 +- `Exponential`:指数外推。`extrapolate` 的 `degree` 参数在该分支被忽略。 + +实现 `Debug`、`Clone`、`Copy`、`PartialEq`、`Eq`、`Hash`。 + +--- + +## ZneConfig + +零噪声外推的方法配置,由统一流水线所在子模块 `cqlib_core::error_mitigation::unified` 定义并经模块根重导出,供 `MitigationMethod::Zne` 携带。 + +```rust +pub struct ZneConfig { + pub fold_levels: Vec, +} +``` + +字段: + +- `fold_levels`:折叠等级列表,决定折叠线路条数与噪声因子;等级 `l` 对应噪声因子 `2 * l + 1`。 + +没有 `Default` 实现,必须显式给出 `fold_levels`。实现 `Debug`、`Clone`、`PartialEq`、`Eq`。 + +`ErrorMitigation::new` 在创建阶段检查其中的折叠等级,任一等极为负即返回 `ErrorMitigationError::InvalidFoldLevel`;等级为空向量可以通过创建阶段的检查,但后续外推会因没有数据点而报错。 + +--- + +## 错误 + +折叠、取值与外推分属两类错误:折叠返回线路错误,取值与外推返回模块统一错误。 + +| 错误 | 触发场景 | +| --- | --- | +| `CircuitError::InvalidControlOperation` | `fold_circuits` 遇到负的折叠等级。 | +| `ErrorMitigationError::HamiltonianQubitCountMismatch` | `run_em_sequence` 或 `run_em_sequence_with_shots` 收到的哈密顿量比特数与基底线路宽度不一致。 | +| `ErrorMitigationError::EmptyNoisyResults` | 外推收到的结果序列为空,例如折叠等级为空向量。 | +| `ErrorMitigationError::NoisyResultsLengthMismatch` | 外推结果序列的长度不等于噪声因子个数。 | +| `ErrorMitigationError::InvalidPolynomialDegree` | 多项式次数不小于数据点数。 | +| `ErrorMitigationError::NonPositiveNoisyResults` | 指数外推收到的结果中存在非正值。 | +| `ErrorMitigationError::SingularExponentialFit` | 指数外推的线性回归退化。 | +| `ErrorMitigationError::SingularPolynomialFit` | 多项式外推的正规方程矩阵奇异。 | +| `ErrorMitigationError::Circuit` / `Qis` | 由折叠或线路操作透传的错误。 | + +错误类型的完整定义见 [统一流水线](3_unified.md)。 diff --git a/docs/api/cn/rust/6_error_mitigation/2_virtual_distillation.md b/docs/api/cn/rust/6_error_mitigation/2_virtual_distillation.md new file mode 100644 index 00000000..13f1ecfe --- /dev/null +++ b/docs/api/cn/rust/6_error_mitigation/2_virtual_distillation.md @@ -0,0 +1,180 @@ +# 虚拟蒸馏 + +`cqlib_core::error_mitigation::virtual_distillation` + +虚拟蒸馏通过多份密度矩阵拷贝与 copy-swap 线路,估计比值 `Tr(O ρ^M) / Tr(ρ^M)`,其中 `M` 为副本数,`O` 为由哈密顿量表示的可观测量。本页覆盖 copy-swap 线路构造与分子、分母统计的公开 API。 + +## 导入 + +```rust +use cqlib_core::error_mitigation::VirtualDistillation; +``` + +--- + +## VirtualDistillation + +虚拟蒸馏的低层入口。它持有基底线路与副本数,可以单独构造 copy-swap 线路、单独跑分母或分子线路,也可以一步得到比值与方差。 + +```rust +pub struct VirtualDistillation { + circuit: Circuit, + copies: usize, +} +``` + +字段私有,通过方法读取: + +- `fn copies(&self) -> usize`:当前配置的副本数。 + +其余方法: + +- `fn new(circuit: Circuit, copies: usize) -> Result`:创建入口,取得线路所有权。模板线路只能通过 `new` 传入,没有对应的读接口。 +- `fn set_copies(&mut self, copies: usize) -> Result<(), ErrorMitigationError>`:更新副本数。更新失败时保持原值不变。 +- `fn build_copy_swap_circuit(&self) -> Result`:由基底线路构造 copy-swap 线路。 +- `fn run_denominator_circuit(&self, shots: usize, estimator: &Estimator<'_>) -> Result<(f64, f64), CircuitError>`:跑分母线路,返回 `(均值, 方差)`。 +- `fn run_numerator_circuit(&self, hamiltonian: &Hamiltonian, shots: usize, estimator: &Estimator<'_>) -> Result<(f64, f64), ErrorMitigationError>`:跑分子线路,返回 `(均值, 方差)`。 +- `fn run_vd(&self, hamiltonian: &Hamiltonian, shots_numerator: usize, shots_denominator: usize, estimator: &Estimator<'_>) -> Result<(f64, f64), ErrorMitigationError>`:跑完整流程,返回缓解后的 `(期望值, 方差)`。 + +`VirtualDistillation` 没有 builder 方法,也没有 `Default` 实现:实例只能由 `new` 创建,副本数通过 `set_copies` 修改。它实现 `Debug` 与 `Clone`。 + +### `new(circuit: Circuit, copies: usize) -> Result` + +参数: + +- `circuit`:基底线路,后续所有派生线路都由它构造。 +- `copies`:副本数,必须不小于 2。 + +返回: + +- `VirtualDistillation`:可用于构造线路与取值。 + +异常情况: + +- `ErrorMitigationError::InvalidCopies`:副本数小于 2,错误中携带传入的副本数。 + +### `build_copy_swap_circuit(&self) -> Result` + +构造 copy-swap 线路,包含三个部分:先对基底线路做门分解,再把基底线路并排准备 `copies` 份——第 `i` 份整体平移到从 `i × 基底宽度` 开始的比特区间,各份之间互不重叠——最后在首份与其他每一份之间逐比特插入 `SWAP`。 + +- 线路宽度为 `copies × 基底宽度`,其中基底宽度取分解后线路的宽度。 +- 操作数为 `copies` 份基底操作加上 `(copies - 1) × 基底宽度` 个 `SWAP`。 + +返回: + +- `Circuit`:copy-swap 线路。 + +异常情况: + +- `CircuitError`:基底线路分解失败,或向新线路追加操作失败。 + +### 分子与分母 + +- `fn run_denominator_circuit(&self, shots: usize, estimator: &Estimator<'_>) -> Result<(f64, f64), CircuitError>`:在 copy-swap 线路上估计分母 `Tr(ρ^M)`。估计器收到的可观测量参数为 `None`,shot 数为 `Some(shots)`;回调形态见 [概览](0_overview.md)。 +- `fn run_numerator_circuit(&self, hamiltonian: &Hamiltonian, shots: usize, estimator: &Estimator<'_>) -> Result<(f64, f64), ErrorMitigationError>`:在 copy-swap 线路上估计分子 `Tr(O ρ^M)`。可观测量会在模块内部先扩展到 copy-swap 线路的宽度:原有 Pauli 项保持原比特索引与相位、系数不变,新增的高索引比特上补 `Z`;扩展结果经估计器的可观测量参数传入,shot 数为 `Some(shots)`。 +- `fn run_vd(&self, hamiltonian: &Hamiltonian, shots_numerator: usize, shots_denominator: usize, estimator: &Estimator<'_>) -> Result<(f64, f64), ErrorMitigationError>`:先校验哈密顿量的比特数等于基底线路宽度,再依次执行分子线路与分母线路,最后组合成缓解结果。分子与分母使用各自的 shot 数。 + +`run_vd` 的异常情况: + +- `ErrorMitigationError::HamiltonianQubitCountMismatch`:哈密顿量的比特数与基底线路宽度不一致,错误中同时给出期望值与实际值。 +- `ErrorMitigationError::ZeroDenominatorMean`:分母线路的均值为零,比值无法计算。 +- `ErrorMitigationError::Circuit`:扩展后的可观测量宽度与 copy-swap 线路宽度不一致时透传的线路错误。 + +### 比值与方差 + +`run_vd` 的返回值按 `mu_vd = mu_num / mu_den` 组合期望值;方差按一阶泰勒近似、并假设分子与分母相互独立,组合为 `var_num / mu_den^2 + mu_num^2 * var_den / mu_den^4`。这组公式只在模块内部使用,对外只通过 `run_vd` 取得结果,统一流水线在后处理阶段复用同一组公式。 + +### 示例 + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::error_mitigation::VirtualDistillation; + +let q0 = Qubit::new(0); +let mut circuit = Circuit::new(1); +circuit.x(q0).unwrap(); + +// 两份单比特副本:X 作用于比特 0、X 作用于比特 1,再加一对逐比特 SWAP +let vd = VirtualDistillation::new(circuit, 2).unwrap(); +let copy_swap = vd.build_copy_swap_circuit().unwrap(); +assert_eq!(copy_swap.width(), 2); +assert_eq!(copy_swap.operations().len(), 3); +``` + +```rust +use cqlib_core::circuit::Circuit; +use cqlib_core::error_mitigation::{ErrorMitigationError, VirtualDistillation}; + +// 副本数通过 set_copies 调整,非法值被拒绝且不改变原值 +let mut vd = VirtualDistillation::new(Circuit::new(1), 2).unwrap(); +assert_eq!(vd.copies(), 2); + +vd.set_copies(3).unwrap(); +assert_eq!(vd.copies(), 3); + +let err = vd.set_copies(1).unwrap_err(); +assert!(matches!(err, ErrorMitigationError::InvalidCopies(1))); +assert_eq!(vd.copies(), 3); +``` + +```rust +use cqlib_core::circuit::Circuit; +use cqlib_core::error_mitigation::VirtualDistillation; +use cqlib_core::qis::{Hamiltonian, Pauli, PauliString}; +use num_complex::Complex64; + +let mut pauli = PauliString::new(1); +pauli.set_pauli(0, Pauli::Z); +let hamiltonian = Hamiltonian::from_list(vec![(pauli, Complex64::new(1.0, 0.0))]).unwrap(); + +// 分子线路带可观测量,分母线路不带;两者使用各自的 shot 数 +let vd = VirtualDistillation::new(Circuit::new(1), 2).unwrap(); +let (mu_vd, var_vd) = vd + .run_vd(&hamiltonian, 3, 2, &|_circuit, hamiltonian_arg, shots| { + if hamiltonian_arg.is_some() { + assert_eq!(shots, Some(3)); + (1.5, 0.25) + } else { + assert_eq!(shots, Some(2)); + (2.0, 1.0) + } + }) + .unwrap(); + +assert!((mu_vd - 0.75).abs() < 1e-12); +assert!((var_vd - 0.203125).abs() < 1e-12); +``` + +--- + +## VirtualDistillationConfig + +虚拟蒸馏的方法配置,由统一流水线所在子模块 `cqlib_core::error_mitigation::unified` 定义并经模块根重导出,供 `MitigationMethod::VirtualDistillation` 携带。 + +```rust +pub struct VirtualDistillationConfig { + pub copies: usize, +} +``` + +字段: + +- `copies`:副本数,最少为 2。 + +没有 `Default` 实现,必须显式给出 `copies`。实现 `Debug`、`Clone`、`PartialEq`、`Eq`。 + +`ErrorMitigation::new` 在创建阶段检查该字段,小于 2 即返回 `ErrorMitigationError::InvalidCopies`。 + +--- + +## 错误 + +| 错误 | 触发场景 | +| --- | --- | +| `ErrorMitigationError::InvalidCopies` | 副本数小于 2,见 `new`、`set_copies` 与 `ErrorMitigation::new`。 | +| `ErrorMitigationError::HamiltonianQubitCountMismatch` | `run_vd` 收到的哈密顿量比特数与基底线路宽度不一致。 | +| `ErrorMitigationError::ZeroDenominatorMean` | 分母线路的均值为零。 | +| `ErrorMitigationError::Circuit` | 携带 `CircuitError::QubitCountMismatch`,表示扩展后的可观测量宽度与 copy-swap 线路宽度不一致。 | +| `CircuitError` | `build_copy_swap_circuit` 与 `run_denominator_circuit` 直接返回的线路错误。 | + +错误类型的完整定义见 [统一流水线](3_unified.md)。 diff --git a/docs/api/cn/rust/6_error_mitigation/3_unified.md b/docs/api/cn/rust/6_error_mitigation/3_unified.md new file mode 100644 index 00000000..92977a3b --- /dev/null +++ b/docs/api/cn/rust/6_error_mitigation/3_unified.md @@ -0,0 +1,384 @@ +# 统一流水线 + +`cqlib_core::error_mitigation::unified` + +统一流水线把零噪声外推与虚拟蒸馏收敛到同一条顺序式流程:创建实例、执行取值、后处理出结果。本页覆盖统一流水线本身、三个参数枚举、结果对象与模块统一错误类型。 + +## 导入 + +```rust +use cqlib_core::error_mitigation::{ + ErrorMitigation, ErrorMitigationError, ExtrapolateMethod, MitigatedResult, MitigationMethod, + ProcessArgs, RunArgs, VirtualDistillationConfig, ZneConfig, +}; +``` + +--- + +## ErrorMitigation + +默认推荐入口。它保存基底线路、选定的缓解方法与流水线状态,把「构造缓解线路并取值」与「由原始数据算出缓解结果」拆成 `run` 与 `get_mitigated` 两步。 + +```rust +pub struct ErrorMitigation { + // 字段私有:基底线路、缓解方法与流水线状态 +} +``` + +方法: + +- `fn new(circuit: Circuit, method: MitigationMethod) -> Result`:创建流水线并校验方法配置。 +- `fn run(&mut self, hamiltonian: &Hamiltonian, run_args: RunArgs, estimator: &Estimator<'_>) -> Result<(), ErrorMitigationError>`:构造并执行该方法所需的线路,留存原始数据,不直接返回缓解值。 +- `fn get_mitigated(&mut self, process_args: ProcessArgs) -> Result`:由留存的原始数据算出最终结果。 + +统一流水线不提供读取原始数据的接口,也不提供修改已选方法的接口,两者都只在内部留存。它实现 `Debug` 与 `Clone`。 + +### 状态机 + +每个实例的生命周期固定为创建 → `run()` → `get_mitigated()`,两步各只能成功执行一次: + +| 当前状态 | 调用 | 结果 | +| --- | --- | --- | +| 已创建 | `run` | 执行并转入已运行。 | +| 已创建 | `get_mitigated` | `ErrorMitigationError::RunRequiredBeforeMitigation`。 | +| 已运行 | `run` | `ErrorMitigationError::AlreadyRun`。 | +| 已运行 | `get_mitigated`,参数与方法一致 | 返回 `MitigatedResult`,转入已完成。 | +| 已运行 | `get_mitigated`,参数与方法不一致 | `ErrorMitigationError::ProcessArgsMethodMismatch`。 | +| 已完成 | `run` 或 `get_mitigated` | `ErrorMitigationError::AlreadyMitigated`。 | + +调用失败时状态保持原值(已完成状态例外,它不再接受任何调用),因此参数不匹配或输入校验失败后可以改正参数重试。需要跑第二条缓解流程时重新创建实例。 + +### `new(circuit: Circuit, method: MitigationMethod) -> Result` + +参数: + +- `circuit`:基底线路,`run` 阶段的所有派生线路都由它构造。 +- `method`:缓解方法与对应配置。 + +返回: + +- `ErrorMitigation`:状态为已创建。 + +异常情况: + +- `ErrorMitigationError::InvalidFoldLevel`:`MitigationMethod::Zne` 的折叠等级中存在负值。 +- `ErrorMitigationError::InvalidCopies`:`MitigationMethod::VirtualDistillation` 的副本数小于 2。 + +### `run(&mut self, hamiltonian: &Hamiltonian, run_args: RunArgs, estimator: &Estimator<'_>) -> Result<(), ErrorMitigationError>` + +按选定方法构造缓解线路并逐条取值,结果留在实例内部。 + +参数: + +- `hamiltonian`:待估计的可观测量,其比特数必须等于基底线路宽度。 +- `run_args`:执行参数,变体必须与 `MitigationMethod` 配套。 +- `estimator`:估计器回调,形态见 [概览](0_overview.md)。 + +执行细节按方法区分: + +- 零噪声外推:按配置的折叠等级构造折叠线路,逐条调用估计器,可观测量参数为 `Some(hamiltonian)`,shot 数为 `RunArgs::Zne::shots`;只取返回值中的期望值。折叠线路、期望值、噪声因子与执行参数一并留存。 +- 虚拟蒸馏:构造 copy-swap 线路并把可观测量扩展到该线路宽度,分子线路的估计器收到 `Some(扩展后的可观测量)` 与 `Some(shots_numerator)`,分母线路收到 `None` 与 `Some(shots_denominator)`;分子与分母的均值、方差一并留存。 + +返回: + +- `Result<(), ErrorMitigationError>`:成功仅表示原始数据已就位,缓解值由 `get_mitigated` 给出。 + +异常情况: + +- `ErrorMitigationError::RunArgsMethodMismatch`:`run_args` 的变体与配置的方法不一致。 +- `ErrorMitigationError::HamiltonianQubitCountMismatch`:哈密顿量的比特数与基底线路宽度不一致。 +- `ErrorMitigationError::AlreadyRun` / `ErrorMitigationError::AlreadyMitigated`:当前状态不允许再次执行。 +- `ErrorMitigationError::Circuit`:折叠、分解或线路构造失败时透传的线路错误。 + +### `get_mitigated(&mut self, process_args: ProcessArgs) -> Result` + +对留存的原始数据做方法相关的后处理。 + +参数: + +- `process_args`:后处理参数,变体必须与 `MitigationMethod` 配套。 + +后处理细节按方法区分: + +- 零噪声外推:按 `ProcessArgs::Zne::method` 选择外推方式。`Polynomial` 分支在 `degree` 为 `None` 时取 `min(数据点数 - 1, 1)`,其中数据点数等于折叠等级数,即默认按一次多项式外推;`Exponential` 分支不使用 `degree`。结果中的 `variance` 为 `None`。 +- 虚拟蒸馏:按分子与分母的均值、方差组合出比值与方差,结果中的 `variance` 为 `Some`。 + +返回: + +- `MitigatedResult`:缓解后的期望值与可选方差。 + +异常情况: + +- `ErrorMitigationError::RunRequiredBeforeMitigation`:尚未调用 `run`。 +- `ErrorMitigationError::ProcessArgsMethodMismatch`:`process_args` 的变体与配置的方法不一致。 +- `ErrorMitigationError::AlreadyMitigated`:已经取过一次缓解结果。 +- `ErrorMitigationError::ZeroDenominatorMean`、`EmptyNoisyResults`、`NoisyResultsLengthMismatch`、`InvalidPolynomialDegree`、`NonPositiveNoisyResults`、`SingularExponentialFit`、`SingularPolynomialFit`:后处理阶段透传的外推或比值错误,见 [零噪声外推](1_zne.md) 与 [虚拟蒸馏](2_virtual_distillation.md)。 + +### 示例 + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::error_mitigation::{ + ErrorMitigation, ErrorMitigationError, ExtrapolateMethod, MitigatedResult, MitigationMethod, + ProcessArgs, RunArgs, ZneConfig, +}; +use cqlib_core::qis::{Hamiltonian, Pauli, PauliString}; +use num_complex::Complex64; + +let q0 = Qubit::new(0); +let mut circuit = Circuit::new(1); +circuit.x(q0).unwrap(); + +let mut pauli = PauliString::new(1); +pauli.set_pauli(0, Pauli::Z); +let hamiltonian = Hamiltonian::from_list(vec![(pauli, Complex64::new(1.0, 0.0))]).unwrap(); + +let mut mitigation = ErrorMitigation::new( + circuit, + MitigationMethod::Zne(ZneConfig { + fold_levels: vec![0, 1, 2], + }), +) +.unwrap(); + +mitigation + .run( + &hamiltonian, + RunArgs::Zne { + gate_set: None, + shots: Some(256), + }, + &|circuit, hamiltonian_arg, shots| { + assert!(hamiltonian_arg.is_some()); + assert_eq!(shots, Some(256)); + (circuit.operations().len() as f64 + 0.5, 0.0) + }, + ) + .unwrap(); + +// run 只能成功调用一次 +let rerun_err = mitigation + .run( + &hamiltonian, + RunArgs::Zne { + gate_set: None, + shots: Some(256), + }, + &|_circuit, _hamiltonian, _shots| (0.0, 0.0), + ) + .unwrap_err(); +assert!(matches!(rerun_err, ErrorMitigationError::AlreadyRun)); + +// degree 为 None 时按 min(数据点数 - 1, 1) 取值,此处数据点数为 3 +let mitigated = mitigation + .get_mitigated(ProcessArgs::Zne { + method: ExtrapolateMethod::Polynomial, + degree: None, + }) + .unwrap(); + +assert_eq!( + mitigated, + MitigatedResult { + expectation: 0.5, + variance: None, + } +); + +// get_mitigated 也只能成功调用一次 +let second_err = mitigation + .get_mitigated(ProcessArgs::Zne { + method: ExtrapolateMethod::Polynomial, + degree: Some(1), + }) + .unwrap_err(); +assert!(matches!(second_err, ErrorMitigationError::AlreadyMitigated)); +``` + +```rust +use cqlib_core::circuit::Circuit; +use cqlib_core::error_mitigation::{ + ErrorMitigation, MitigationMethod, ProcessArgs, RunArgs, VirtualDistillationConfig, +}; +use cqlib_core::qis::{Hamiltonian, Pauli, PauliString}; +use num_complex::Complex64; + +let mut pauli = PauliString::new(1); +pauli.set_pauli(0, Pauli::Z); +let hamiltonian = Hamiltonian::from_list(vec![(pauli, Complex64::new(1.0, 0.0))]).unwrap(); + +let mut mitigation = ErrorMitigation::new( + Circuit::new(1), + MitigationMethod::VirtualDistillation(VirtualDistillationConfig { copies: 2 }), +) +.unwrap(); + +mitigation + .run( + &hamiltonian, + RunArgs::VirtualDistillation { + shots_numerator: 3, + shots_denominator: 2, + }, + &|_circuit, hamiltonian_arg, shots| { + if hamiltonian_arg.is_some() { + assert_eq!(shots, Some(3)); + (1.5, 0.25) + } else { + assert_eq!(shots, Some(2)); + (2.0, 1.0) + } + }, + ) + .unwrap(); + +let mitigated = mitigation + .get_mitigated(ProcessArgs::VirtualDistillation) + .unwrap(); + +assert!((mitigated.expectation - 0.75).abs() < 1e-12); +assert!((mitigated.variance.unwrap() - 0.203125).abs() < 1e-12); +``` + +--- + +## MitigationMethod + +缓解方法枚举,携带对应方法的配置。 + +```rust +pub enum MitigationMethod { + Zne(ZneConfig), + VirtualDistillation(VirtualDistillationConfig), +} +``` + +- `Zne(ZneConfig)`:选择零噪声外推,配置折叠等级。 +- `VirtualDistillation(VirtualDistillationConfig)`:选择虚拟蒸馏,配置副本数。 + +实现 `Debug`、`Clone`、`PartialEq`、`Eq`。方法与两类参数必须配套: + +| 方法 | 执行参数 | 后处理参数 | +| --- | --- | --- | +| `MitigationMethod::Zne` | `RunArgs::Zne` | `ProcessArgs::Zne` | +| `MitigationMethod::VirtualDistillation` | `RunArgs::VirtualDistillation` | `ProcessArgs::VirtualDistillation` | + +配置结构体见 [零噪声外推](1_zne.md) 与 [虚拟蒸馏](2_virtual_distillation.md)。 + +--- + +## RunArgs + +执行阶段的参数枚举。 + +```rust +pub enum RunArgs { + Zne { + gate_set: Option>, + shots: Option, + }, + VirtualDistillation { + shots_numerator: usize, + shots_denominator: usize, + }, +} +``` + +- `Zne::gate_set`:选择性折叠的门集合;`None` 表示全局折叠,匹配按操作名称进行。 +- `Zne::shots`:透传给估计器的 shot 数;`None` 表示不指定。 +- `VirtualDistillation::shots_numerator`:分子线路的 shot 数。 +- `VirtualDistillation::shots_denominator`:分母线路的 shot 数。 + +实现 `Debug`、`Clone`、`PartialEq`、`Eq`,其中 `PartialEq` 是自定义实现:`Zne` 分支比较 `shots` 与 `gate_set` 中每个操作的名称,要求元素个数一致且逐一名称相同,不比较操作携带的参数与线路定义,因此名称相同的不同操作实例判定为相等;`VirtualDistillation` 分支比较两个 shot 数。变体不同则判定为不相等。 + +--- + +## ProcessArgs + +后处理阶段的参数枚举。 + +```rust +pub enum ProcessArgs { + Zne { + method: ExtrapolateMethod, + degree: Option, + }, + VirtualDistillation, +} +``` + +- `Zne::method`:外推方法。 +- `Zne::degree`:多项式次数。`ExtrapolateMethod::Polynomial` 分支为 `None` 时取 `min(数据点数 - 1, 1)`,数据点数等于折叠等级数;`ExtrapolateMethod::Exponential` 分支忽略该字段。 +- `VirtualDistillation`:无附加字段。 + +实现 `Debug`、`Clone`、`Copy`、`PartialEq`、`Eq`。 + +--- + +## MitigatedResult + +最终缓解结果。 + +```rust +pub struct MitigatedResult { + pub expectation: f64, + pub variance: Option, +} +``` + +字段: + +- `expectation`:缓解后的期望值。 +- `variance`:缓解结果的方差,只有虚拟蒸馏给出;零噪声外推为 `None`。 + +实现 `Debug`、`Clone`、`PartialEq`。 + +--- + +## ErrorMitigationError + +模块统一错误类型,配置校验、输入校验、流水线状态与外推拟合的错误都通过它返回。 + +```rust +pub enum ErrorMitigationError { + Circuit(CircuitError), + Qis(QisError), + InvalidCopies(usize), + HamiltonianQubitCountMismatch { expected: usize, actual: usize }, + ZeroDenominatorMean, + InvalidFoldLevel(i32), + RunRequiredBeforeMitigation, + AlreadyRun, + AlreadyMitigated, + RunArgsMethodMismatch, + ProcessArgsMethodMismatch, + EmptyNoisyResults, + NoisyResultsLengthMismatch { expected: usize, actual: usize }, + InvalidPolynomialDegree { degree: usize, num_points: usize }, + NonPositiveNoisyResults, + SingularExponentialFit, + SingularPolynomialFit, +} +``` + +前两个变体透传来自线路模块与量子信息模块的错误,其余变体给出具体数值。实现 `Debug` 与 `Display`。 + +| 错误 | 触发场景 | +| --- | --- | +| `Circuit(CircuitError)` | 折叠、线路分解或线路构造失败。 | +| `Qis(QisError)` | 量子信息层面的计算失败。 | +| `InvalidCopies(usize)` | 虚拟蒸馏副本数小于 2:`VirtualDistillation::new`、`VirtualDistillation::set_copies` 与 `ErrorMitigation::new` 都会检查。 | +| `HamiltonianQubitCountMismatch { expected, actual }` | 哈密顿量比特数与基底线路宽度不一致,`expected` 为线路宽度,`actual` 为哈密顿量比特数。 | +| `ZeroDenominatorMean` | 虚拟蒸馏分母线路的均值为零。 | +| `InvalidFoldLevel(i32)` | `ErrorMitigation::new` 收到负的折叠等级。 | +| `RunRequiredBeforeMitigation` | 未调用 `run` 就调用 `get_mitigated`。 | +| `AlreadyRun` | 同一实例第二次调用 `run`。 | +| `AlreadyMitigated` | 已完成缓解的实例再次调用 `run` 或 `get_mitigated`。 | +| `RunArgsMethodMismatch` | `run` 的参数变体与配置的方法不一致。 | +| `ProcessArgsMethodMismatch` | `get_mitigated` 的参数变体与配置的方法不一致。 | +| `EmptyNoisyResults` | 外推收到的结果序列为空,例如折叠等级为空向量。 | +| `NoisyResultsLengthMismatch { expected, actual }` | 外推结果序列长度不等于噪声因子个数。 | +| `InvalidPolynomialDegree { degree, num_points }` | 多项式次数不小于数据点数。 | +| `NonPositiveNoisyResults` | 指数外推收到非正的结果值。 | +| `SingularExponentialFit` | 指数外推的线性回归退化。 | +| `SingularPolynomialFit` | 多项式外推的正规方程矩阵奇异。 | diff --git a/docs/api/en/c/0_circuit/0_overview.md b/docs/api/en/c/0_circuit/0_overview.md new file mode 100644 index 00000000..4b1c2259 --- /dev/null +++ b/docs/api/en/c/0_circuit/0_overview.md @@ -0,0 +1,71 @@ +# Quantum Circuit (circuit) + +The quantum circuit module of the C API centers on the opaque handle `CCircuit*` and provides circuit construction and inspection, symbolic parameters, gate operations, classical data and control flow, symbolic matrices, parameterized templates (ansatz), control flow graphs and circuit DAGs. + +See [Overview](../0_overview.md) for the global conventions on error codes, memory ownership and the two-step array output pattern. + +--- + +## Module navigation + +| Category | Page | Main objects | Description | +| --- | --- | --- | --- | +| Circuit container | [Circuit](1_circuit.md) | `CCircuit*` | Circuit construction, property queries, composition, inversion, decomposition, global phase and parameter binding. | +| Qubit | [Qubit](2_qubit.md) | `CQubit` | Logical qubit identifier: creation, ID access, checked conversions, comparison and string representation. | +| Parameters | [Parameters](3_parameter.md) | `CParameter*`, `CCircuitParam`, `CParameterValue` | Expression parsing, evaluation and interning into the circuit parameter table. | +| Operation IR | [Operations / Instructions](4_operation_instruction.md) | `COperation*`, `CValueOperation*` | Storage-layer operations, circuit operation snapshots and delay instructions. | +| Standard gates | [Standard Gates](5_gate_standard.md) | `circuit_h` and friends | Built-in standard gates with numeric-angle and symbolic-angle (`*_param`) variants. | +| Unitary gates | [Unitary Gates](6_gate_unitary.md) | `circuit_unitary` and friends | Numeric matrix gates and symbolic matrix gates. | +| Multi-controlled gates | [Multi-Controlled Gates](7_gate_mc_gate.md) | `circuit_multi_control` | Add control qubits in front of a standard gate. | +| Circuit gates | [Circuit Gates](8_gate_circuit_gate.md) | `CCircuitGate*`, `circuit_to_gate` | Freeze a circuit into a reusable composite gate. | +| Classical data and control flow | [Classical / Control Flow](9_classical_control_flow.md) | `CClassicalVar*`, `CClassicalExpr*`, `circuit_if`, ... | Measurement, classical expressions and structured control flow. | +| Symbolic matrix | [Symbolic Matrix](10_symbolic_matrix.md) | `CSymbolicMatrix*` | Dense symbolic matrices that retain parameters, and equivalence checking. | +| Ansatz | [Ansatz](11_ansatz.md) | `CTwoLocal`, `CQAOAAnsatz`, ... | Parameterized circuit templates. | +| Control flow graph | [CFG](12_cfg.md) | `CCircuitCFG*` | Circuit control flow graph view, queries and reconstruction. | +| Matrix conversion | [Circuit to Matrix](13_circuit_to_matrix.md) | `circuit_to_matrix` and friends | Numeric matrices of purely unitary circuits and global phase. | +| Circuit DAG | [Circuit DAG](14_circuit_dag.md) | `CCircuitDag*` | Dependency DAG view and circuit reconstruction. | + +--- + +## Core concepts + +| Concept | Description | +| --- | --- | +| **Opaque handle** | Types such as `struct CCircuit` are only forward-declared in the header with no exported fields; all access goes through functions, and handles are released with the matching `*_free`. | +| **Qubit numbering** | Logical qubits are `uint32_t` ids. `circuit_new(n)` creates consecutive ids `0..n-1`; `circuit_from_qubits` accepts an explicit (including sparse) id list. | +| **Gate function prefix** | Gate operations all start with `circuit_`: fixed gates such as `circuit_h`, `circuit_cx`; angle-carrying gates provide a numeric variant (`circuit_rx`) and a symbolic variant (`circuit_rx_param`, taking a `CParameter*`). | +| **Symbolic parameters** | `param_parse` parses an expression from a string; once interned into the circuit's parameter table, a parameter is represented as a `CCircuitParam` (fixed value or table index). | +| **Measurement and control flow** | Measurement produces immutable classical values owned by the circuit; classical expressions and `circuit_if`/`circuit_while`/`circuit_for_uint`/`circuit_switch` describe runtime control flow. | + +Controlled gates follow the order "control qubits first, target qubit last"; for example, in `circuit_cx(qc, control, target)` the `control` argument is the control qubit. + +--- + +## Minimal example + +```c +#include +#include +#include + +int main(void) { + CCircuit *qc = circuit_new(2); + circuit_h(qc, 0); /* 0 on success, negative on error */ + circuit_cx(qc, 0, 1); + + printf("qubits = %zu\n", circuit_num_qubits(qc)); + printf("depth = %zd\n", circuit_depth(qc, true)); + printf("operations= %zu\n", circuit_num_operations(qc)); + + /* Two-step read of the qubit list */ + uintptr_t n = circuit_qubits_len(qc); + uint32_t *ids = malloc(n * sizeof(uint32_t)); + circuit_qubits(qc, ids, n); /* ids = {0, 1} */ + + free(ids); + circuit_free(qc); + return 0; +} +``` + +After construction, a circuit is usually executed by the [Statevector](../3_qis/1_statevector.md) or [DensityMatrix](../3_qis/2_density_matrix.md) simulators; a circuit containing measurement or control flow no longer has a single fixed unitary matrix representation, and the matrix conversion interfaces apply only to purely quantum sub-circuits (see [Circuit to Matrix](13_circuit_to_matrix.md)). diff --git a/docs/api/en/c/0_circuit/10_symbolic_matrix.md b/docs/api/en/c/0_circuit/10_symbolic_matrix.md new file mode 100644 index 00000000..8c162dd5 --- /dev/null +++ b/docs/api/en/c/0_circuit/10_symbolic_matrix.md @@ -0,0 +1,527 @@ +# Symbolic Matrix + +This page covers the C API functions for symbolic matrices: symbolic complex values, matrix construction, shape and element queries, simplification, symbol substitution, evaluation under parameter bindings, in-place gate application (the `apply_*` family), and global-phase equivalence checks at both the matrix and the circuit level. See [Overview](../0_overview.md) for the error-code and memory-management conventions. + +--- + +## Core concepts + +A symbolic matrix (opaque `CSymbolicMatrix*` handle) has one symbolic complex number per element: its real and imaginary parts are parameter expressions that may contain unbound symbolic parameters. Unlike numeric matrix conversion, symbolic conversion preserves the parameter expressions of the circuit; concrete values can later be substituted through a bindings string. + +Numeric evaluation results are written as `Complex64` structs: + +```c +typedef struct Complex64 { + double re; /* real part */ + double im; /* imaginary part */ +} Complex64; +``` + +Every function returning a `CSymbolicMatrix*` allocates a new handle; free it with `symbolic_matrix_free`. Functions returning a `CSymbolicComplex*` allocate a new symbolic complex handle; free it with `symbolic_complex_free`. + +--- + +## Symbolic complex values + +Each matrix element is a symbolic complex value (opaque `CSymbolicComplex*` handle): its real and imaginary parts are parameter expressions. The functions in this section are the element-level API; they also supply elements to `symbolic_matrix_new` and the `apply_*` permutation/diagonal gates. + +### symbolic_complex_new(re, im) + +Builds a symbolic complex value from real and imaginary parameter expressions. + +- `re` (`const CParameter*`): real-part expression handle. +- `im` (`const CParameter*`): imaginary-part expression handle. + +Returns a newly allocated `CSymbolicComplex*`; NULL when either input is NULL. + +### symbolic_complex_zero() / symbolic_complex_one() / symbolic_complex_i() + +Return the additive identity `0 + 0i`, the multiplicative identity `1 + 0i`, and the imaginary unit `0 + 1i`. + +No parameters; each returns a newly allocated `CSymbolicComplex*`. + +### symbolic_complex_from_real(re) + +Builds a symbolic complex value from a real expression with zero imaginary part. + +- `re` (`const CParameter*`): real-part expression handle. + +Returns a newly allocated `CSymbolicComplex*`; NULL on NULL input. + +### symbolic_complex_from_complex(re, im) + +Builds a symbolic complex constant from a pair of floating-point values. + +- `re` (`double`): real part. +- `im` (`double`): imaginary part. + +Returns a newly allocated `CSymbolicComplex*`; NULL when either component is not finite (NaN/Inf). + +### symbolic_complex_exp_i(theta) + +Builds `exp(i*theta)`, i.e. `cos(theta) + i*sin(theta)`. + +- `theta` (`const CParameter*`): phase expression handle. + +Returns a newly allocated `CSymbolicComplex*`; NULL on NULL input. + +### symbolic_complex_re(ptr) / symbolic_complex_im(ptr) + +Return a clone of the real/imaginary part expression (free with `param_free`). + +- `ptr` (`const CSymbolicComplex*`): symbolic complex handle. + +Returns a newly allocated `CParameter*`; NULL on NULL input. + +### symbolic_complex_evaluate(ptr, bindings, out) + +Evaluates the symbolic complex value under parameter bindings. + +- `ptr` (`const CSymbolicComplex*`): symbolic complex handle. +- `bindings` (`const char*`): bindings string formatted as `"name:value,..."` (NULL means no bindings). +- `out` (`Complex64*`): receives the result. + +Returns 0 on success, -1 on NULL input, -3 on unevaluable symbols, -4 on a malformed bindings string. + +### symbolic_complex_simplify(ptr) + +Simplifies the real and imaginary parts and returns the simplified result. + +- `ptr` (`const CSymbolicComplex*`): source handle. + +Returns a newly allocated `CSymbolicComplex*`; NULL on NULL input or simplification failure. + +### symbolic_complex_replace(ptr, symbol, replacement) + +Replaces every occurrence of `symbol` in both parts with `replacement` and returns the substituted result. + +- `ptr` (`const CSymbolicComplex*`): source handle. +- `symbol` (`const char*`): symbol name. +- `replacement` (`const CParameter*`): replacement expression handle. + +Returns a newly allocated `CSymbolicComplex*`; NULL on NULL input or a non-UTF-8 symbol name. + +### symbolic_complex_is_zero_exact(ptr) / symbolic_complex_is_one_exact(ptr) + +Return whether both parts are **exactly** zero/one (no simplification; constant structure only). + +- `ptr` (`const CSymbolicComplex*`): symbolic complex handle. + +Returns 1/0, or -1 on NULL input. + +### symbolic_complex_simplifies_to_zero(ptr) + +Returns whether the value simplifies to exactly zero. + +- `ptr` (`const CSymbolicComplex*`): symbolic complex handle. + +Returns 1 when it simplifies to zero, 0 when not, -1 on NULL input, -3 on simplification failure. + +### symbolic_complex_free(ptr) + +Frees a symbolic complex handle. + +- `ptr` (`CSymbolicComplex*`): handle to free; NULL is allowed. + +No return value. + +--- + +## Matrix construction and release + +### symbolic_eye(dim) + +Creates a `dim x dim` symbolic identity matrix. + +- `dim` (`uintptr_t`): matrix dimension. + +Returns a newly allocated `CSymbolicMatrix*`; NULL only on allocation failure. + +### symbolic_matrix_new(rows, cols, elements, len) + +Builds a `rows x cols` symbolic matrix from an array of element handles in row-major order. + +- `rows` (`uintptr_t`): row count, must be greater than 0. +- `cols` (`uintptr_t`): column count, must be greater than 0. +- `elements` (`const CSymbolicComplex* const*`): `rows * cols` symbolic complex handles. +- `len` (`uintptr_t`): array length, must equal `rows * cols`. + +Returns a newly allocated `CSymbolicMatrix*`; NULL when `elements` is NULL, the length does not match, a dimension is zero, or any element handle is NULL. + +### standard_gate_symbolic_matrix(gate_name, params, len) + +Builds the symbolic unitary matrix of a standard gate selected by name (for example `"RX"`, `"RZZ"`; same name set as `circuit_multi_control`). + +- `gate_name` (`const char*`): standard gate name. +- `params` (`const CParameter* const*`): array of gate parameter handles. +- `len` (`uintptr_t`): number of parameters. + +Returns a newly allocated `CSymbolicMatrix*`; NULL on NULL input, invalid UTF-8, an unknown gate name, or a parameter-count mismatch. + +### circuit_to_symbolic_matrix(ptr, order, order_len) + +Computes the symbolic unitary matrix of a circuit. + +- `ptr` (`const CCircuit*`): target circuit handle. +- `order` (`const uint32_t*`): array of qubit ids ordered from least-significant to most-significant bit; NULL sorts by qubit index. +- `order_len` (`uintptr_t`): array length; 0 selects the default ordering. + +Returns a newly allocated `CSymbolicMatrix*`; NULL on NULL input, an order/qubit-set mismatch, or non-unitary operations. + +### symbolic_matrix_controlled(base, num_ctrls) + +Embeds `base` into the bottom-right block of a larger identity matrix, adding `num_ctrls` control qubits. + +- `base` (`const CSymbolicMatrix*`): base matrix. +- `num_ctrls` (`uintptr_t`): number of control qubits. + +Returns a newly allocated `CSymbolicMatrix*`; NULL on NULL input. + +### symbolic_matrix_free(ptr) + +Frees a symbolic matrix handle. + +- `ptr` (`CSymbolicMatrix*`): handle to free; NULL is allowed. + +No return value. + +--- + +## Shape and element queries + +### symbolic_matrix_rows(ptr) + +Returns the row count. + +- `ptr` (`const CSymbolicMatrix*`): symbolic matrix handle. + +Returns the row count, or 0 for NULL. + +### symbolic_matrix_cols(ptr) + +Returns the column count. + +- `ptr` (`const CSymbolicMatrix*`): symbolic matrix handle. + +Returns the column count, or 0 for NULL. + +### symbolic_matrix_element_str(ptr, row, col) + +Formats one matrix element as a human-readable parameter expression (for example `"cos(theta*0.5)"`). + +- `ptr` (`const CSymbolicMatrix*`): symbolic matrix handle. +- `row` (`uintptr_t`): row index. +- `col` (`uintptr_t`): column index. + +Returns a newly allocated C string (free with `cqlib_string_free`); NULL on NULL input or out-of-bounds indices. + +### symbolic_matrix_element(ptr, row, col) + +Returns a clone of the `[row, col]` element (free with `symbolic_complex_free`) for element-wise access. + +- `ptr` (`const CSymbolicMatrix*`): symbolic matrix handle. +- `row` (`uintptr_t`): row index. +- `col` (`uintptr_t`): column index. + +Returns a newly allocated `CSymbolicComplex*`; NULL on NULL input or out-of-bounds indices. + +### symbolic_matrix_evaluate_len(ptr) + +Returns the flattened element count (`rows * cols`), i.e. the buffer length required for numeric evaluation. + +- `ptr` (`const CSymbolicMatrix*`): symbolic matrix handle. + +Returns the element count, or 0 for NULL. + +--- + +## Simplification and substitution + +### symbolic_matrix_simplify(ptr) + +Simplifies every element of the matrix and returns the simplified result as a new matrix. + +- `ptr` (`const CSymbolicMatrix*`): source matrix handle. + +Returns a newly allocated `CSymbolicMatrix*`; NULL on NULL input or simplification failure. + +### symbolic_matrix_substitute(ptr, names, params, len) + +Simultaneously substitutes symbols in the matrix: `names[i]` is a symbol name and `params[i]` the replacement expression. + +- `ptr` (`const CSymbolicMatrix*`): source matrix handle. +- `names` (`const char* const*`): array of symbol names. +- `params` (`const CParameter* const*`): array of replacement expression handles. +- `len` (`uintptr_t`): number of substitutions. + +Returns a newly allocated `CSymbolicMatrix*`; NULL on NULL input, invalid UTF-8, or a substitution error (for example a reserved prefix). + +--- + +## Numeric evaluation + +### symbolic_matrix_evaluate(ptr, bindings, buffer, len) + +Evaluates every element under parameter bindings into `buffer` in row-major order. + +- `ptr` (`const CSymbolicMatrix*`): symbolic matrix handle. +- `bindings` (`const char*`): bindings string formatted as `"name:value,name:value"` (NULL means no bindings). +- `buffer` (`Complex64*`): array receiving the results. +- `len` (`uintptr_t`): array length, must be at least `symbolic_matrix_evaluate_len`. + +Returns 0 on success, -1 on NULL input, -3 on unevaluable symbols, -4 on a malformed bindings string, -8 when `len` is too small. + +--- + +## In-place gate application (apply_* family) + +The `apply_*` functions apply a quantum gate to a symbolic matrix **in place**: the effect is a left multiplication by the gate's operator restricted to the subspace spanned by the target bits, and the shape is preserved. Target-bit constraints shared by all functions: + +- the matrix row count must be a power of two and every bit in `bits` must be smaller than `log2(rows)`; +- the `bits` array must be non-empty and free of duplicates; the gate matrix dimension of a multi-bit gate must equal `1 << bits_len`. + +### Target-bit ordering + +The `bits` parameter of the `apply_*` functions uses the system's **Little-Endian (least-significant bit first)** convention: `bits[k]` corresponds to bit `k` of the gate's local row index. Contrast this with the circuit API, where controlled gates receive their qubits as `[control, target]` (most-significant first); `circuit_to_symbolic_matrix` reverses that sequence internally before applying. When reproducing a circuit's effect with `apply_standard_gate` you must therefore pass the **reversed order yourself**: `circuit_cx(c, 0, 1)` equals applying `"CX"` with `bits = [1, 0]`. + +### symbolic_matrix_apply_standard_gate(ptr, gate_name, bits, bits_len, params, params_len) + +Applies a standard gate by name (same name set as `circuit_multi_control`, e.g. `"H"`, `"CX"`, `"RX"`) with symbolic gate parameters. + +- `ptr` (`CSymbolicMatrix*`): target matrix (mutated in place). +- `gate_name` (`const char*`): standard gate name. +- `bits` (`const uintptr_t*`): target-bit array. +- `bits_len` (`uintptr_t`): number of target bits. +- `params` (`const CParameter* const*`): gate parameter handle array; NULL is allowed for parameterless gates. +- `params_len` (`uintptr_t`): number of gate parameters. + +Returns 0 on success, -1 when the matrix, gate name, or a parameter handle is NULL, -3 on a gate-bit/parameter-count mismatch with the matrix, -4 on a non-UTF-8 gate name, -8 on an unknown gate name or invalid bits. + +### symbolic_matrix_apply_gate(ptr, gate, bits, bits_len) + +Applies a symbolic gate matrix (dimension `1 << bits_len`) to the target bits. + +- `ptr` (`CSymbolicMatrix*`): target matrix (mutated in place). +- `gate` (`const CSymbolicMatrix*`): gate matrix handle. +- `bits` (`const uintptr_t*`): target-bit array. +- `bits_len` (`uintptr_t`): number of target bits. + +Returns 0 on success, -1 on NULL input, -3 when the gate dimension does not match the target bits, -8 on invalid bits. + +### symbolic_matrix_apply_gate_num(ptr, gate, gate_dim, bits, bits_len) + +Numeric variant of `symbolic_matrix_apply_gate`: `gate` points at `gate_dim * gate_dim` row-major `Complex64` values and `gate_dim` must equal `1 << bits_len`. + +- `ptr` (`CSymbolicMatrix*`): target matrix (mutated in place). +- `gate` (`const Complex64*`): gate matrix values. +- `gate_dim` (`uintptr_t`): gate matrix dimension. +- `bits` (`const uintptr_t*`): target-bit array. +- `bits_len` (`uintptr_t`): number of target bits. + +Returns 0 on success, -1 when the matrix or `gate` is NULL, -3 when the gate does not match the target bits, -8 when `gate_dim` is 0, differs from `1 << bits_len`, or the bits are invalid. + +### symbolic_matrix_apply_single_qubit_gate(ptr, gate, bit) + +Applies a `2x2` symbolic gate to a single target bit. + +- `ptr` (`CSymbolicMatrix*`): target matrix (mutated in place). +- `gate` (`const CSymbolicMatrix*`): `2x2` gate matrix handle. +- `bit` (`uintptr_t`): target bit. + +Returns 0 on success, -1 on NULL input, -8 when the gate is not `2x2`, the row count is not a power of two, or `bit` is out of range. + +### symbolic_matrix_apply_single_qubit_gate_num(ptr, gate, bit) + +Numeric variant of `symbolic_matrix_apply_single_qubit_gate`: `gate` points at 4 row-major `Complex64` values. + +- `ptr` (`CSymbolicMatrix*`): target matrix (mutated in place). +- `gate` (`const Complex64*`): the 4 gate matrix elements. +- `bit` (`uintptr_t`): target bit. + +Returns 0 on success, -1 on NULL input, -8 when the row count is not a power of two or `bit` is out of range. + +### symbolic_matrix_apply_two_qubit_gate(ptr, gate, b0, b1) + +Applies a `4x4` symbolic gate to two target bits. + +- `ptr` (`CSymbolicMatrix*`): target matrix (mutated in place). +- `gate` (`const CSymbolicMatrix*`): `4x4` gate matrix handle. +- `b0`, `b1` (`uintptr_t`): the two target bits. + +Returns 0 on success, -1 on NULL input, -8 when the gate is not `4x4`, the row count is not a power of two, or the bits are duplicated/out of range. + +### symbolic_matrix_apply_two_qubit_gate_num(ptr, gate, b0, b1) + +Numeric variant of `symbolic_matrix_apply_two_qubit_gate`: `gate` points at 16 row-major `Complex64` values. Same error codes as above. + +### symbolic_matrix_apply_general_gate(ptr, gate, bits, bits_len) + +Applies a symbolic gate of arbitrary arity (a square matrix of dimension `1 << bits_len`) to the target bits. + +- `ptr` (`CSymbolicMatrix*`): target matrix (mutated in place). +- `gate` (`const CSymbolicMatrix*`): gate matrix handle (square, power-of-two dimension). +- `bits` (`const uintptr_t*`): target-bit array. +- `bits_len` (`uintptr_t`): number of target bits. + +Returns 0 on success, -1 on NULL input, -8 when the gate shape does not match `1 << bits_len` or the bits are invalid. + +### symbolic_matrix_apply_general_gate_num(ptr, gate, bits, bits_len) + +Numeric variant of `symbolic_matrix_apply_general_gate`: `gate` points at `(1 << bits_len)` squared row-major `Complex64` values. + +- `ptr` (`CSymbolicMatrix*`): target matrix (mutated in place). +- `gate` (`const Complex64*`): gate matrix values. +- `bits` (`const uintptr_t*`): target-bit array. +- `bits_len` (`uintptr_t`): number of target bits. + +Returns 0 on success, -1 when the matrix or `gate` is NULL, -8 when `bits_len` overflows or the bits are invalid. + +### symbolic_matrix_apply_permutation_gate(ptr, indices, values, len, bits, bits_len) + +Applies a symbolic permutation gate in place: `permutation[i] = (indices[i], values[i])` means output local row `i` equals `values[i]` times input local row `indices[i]`. + +- `ptr` (`CSymbolicMatrix*`): target matrix (mutated in place). +- `indices` (`const uintptr_t*`): source local row indices. +- `values` (`const CSymbolicComplex* const*`): symbolic factor handles paired with `indices`. +- `len` (`uintptr_t`): permutation length, must equal `1 << bits_len`. +- `bits` (`const uintptr_t*`): target-bit array. +- `bits_len` (`uintptr_t`): number of target bits. + +Returns 0 on success, -1 when the matrix is NULL, an `indices[i]` is out of range, or a `values[i]` is NULL, -8 when `len` differs from `1 << bits_len`, `indices`/`values` is NULL, or the bits are invalid. + +### symbolic_matrix_apply_permutation_gate_num(ptr, indices, values, len, bits, bits_len) + +Numeric variant of `symbolic_matrix_apply_permutation_gate`: `values` points at `len` raw `Complex64` factors. + +- `ptr` (`CSymbolicMatrix*`): target matrix (mutated in place). +- `indices` (`const uintptr_t*`): source local row indices. +- `values` (`const Complex64*`): numeric factor array. +- `len` (`uintptr_t`): permutation length, must equal `1 << bits_len`. +- `bits` (`const uintptr_t*`): target-bit array. +- `bits_len` (`uintptr_t`): number of target bits. + +Returns 0 on success, -1 when the matrix or `values` is NULL, -8 when an `indices[i]` is out of range, `len` differs from `1 << bits_len`, `indices` is NULL, or the bits are invalid. + +### symbolic_matrix_apply_diagonal_gate(ptr, diagonal, len, bits, bits_len) + +Applies a symbolic diagonal gate in place: `diagonal[i]` scales local row `i`. + +- `ptr` (`CSymbolicMatrix*`): target matrix (mutated in place). +- `diagonal` (`const CSymbolicComplex* const*`): diagonal element handles. +- `len` (`uintptr_t`): diagonal length, must equal `1 << bits_len`. +- `bits` (`const uintptr_t*`): target-bit array. +- `bits_len` (`uintptr_t`): number of target bits. + +Returns 0 on success, -1 when the matrix is NULL or any diagonal handle is NULL, -8 when `len` differs from `1 << bits_len`, `diagonal` is NULL, or the bits are invalid. + +### symbolic_matrix_apply_diagonal_gate_num(ptr, diagonal, len, bits, bits_len) + +Numeric variant of `symbolic_matrix_apply_diagonal_gate`: `diagonal` points at `len` raw `Complex64` values. + +- `ptr` (`CSymbolicMatrix*`): target matrix (mutated in place). +- `diagonal` (`const Complex64*`): diagonal values. +- `len` (`uintptr_t`): diagonal length, must equal `1 << bits_len`. +- `bits` (`const uintptr_t*`): target-bit array. +- `bits_len` (`uintptr_t`): number of target bits. + +Returns 0 on success, -1 when the matrix or `diagonal` is NULL, -8 when `len` differs from `1 << bits_len` or the bits are invalid. + +### Example: reproducing a circuit matrix in place + +```c +/* Reproduce the symbolic matrix of circuit_h(c, 0) + circuit_cx(c, 0, 1). + * The CX [control, target] pair must be reversed to [target, control] + * for the apply_* Little-Endian convention. */ +struct CSymbolicMatrix *matrix = symbolic_eye(4); +const uintptr_t h_bits[1] = {0}; +const uintptr_t cx_bits[2] = {1, 0}; +symbolic_matrix_apply_standard_gate(matrix, "H", h_bits, 1, NULL, 0); +symbolic_matrix_apply_standard_gate(matrix, "CX", cx_bits, 2, NULL, 0); +/* matrix is now equivalent to circuit_to_symbolic_matrix(c, NULL, 0) */ +``` + +--- + +## Equivalence checks + +### symbolic_matrices_equivalent(lhs, rhs) + +Returns whether two symbolic matrices can be proven equivalent up to a global phase. + +- `lhs` (`const CSymbolicMatrix*`): left-hand matrix. +- `rhs` (`const CSymbolicMatrix*`): right-hand matrix. + +Returns 1 when equivalent, 0 when not, -1 on NULL input, -3 on simplification failure. + +### circuits_equivalent(lhs, rhs, order, order_len) + +Compares two circuits up to a global phase in the requested qubit order. + +- `lhs` (`const CCircuit*`): left-hand circuit handle. +- `rhs` (`const CCircuit*`): right-hand circuit handle. +- `order` (`const uint32_t*`): array of qubit ids ordered from least-significant to most-significant bit; NULL (with `order_len` 0) sorts by qubit index. +- `order_len` (`uintptr_t`): array length. + +Returns 1 when equivalent, 0 when not, -1 on NULL input, -3 on matrix construction failure (including an order/qubit-set mismatch), -8 when `order` is NULL while `order_len` is greater than 0. + +--- + +## Qubit order convention + +The `order` parameter of `circuit_to_symbolic_matrix` and `circuits_equivalent` fixes the correspondence between matrix basis indices and circuit qubits: + +- `order` NULL (`order_len` 0): default ordering by qubit index; +- explicit `order`: the qubit at array position `i` corresponds to bit `i` of the basis index (least-significant first); +- an explicit order must match the circuit's qubit set exactly, with no missing, duplicated, or unknown qubits. + +For example, when the circuit holds qubits 0 and 2, both `[0, 2]` and `[2, 0]` are valid orders, but they yield matrix representations under different axis orders. + +--- + +## Example: parameterized circuit → symbolic matrix → bound evaluation + +```c +#include +#include +#include "cqlib_c.h" + +int main(void) { + /* 1. Build a parameterized circuit: RX(theta) on qubit 0 */ + struct CCircuit *circuit = circuit_new(1); + struct CParameter *theta = param_parse("theta"); + if (circuit_rx_param(circuit, 0, theta) != 0) { + return 1; + } + + /* 2. Convert to a symbolic matrix (default order by qubit index) */ + struct CSymbolicMatrix *matrix = circuit_to_symbolic_matrix(circuit, NULL, 0); + if (matrix == NULL) { + return 1; + } + + /* Inspect an element's symbolic form, e.g. "cos(theta*0.5)" */ + char *element = symbolic_matrix_element_str(matrix, 0, 0); + if (element != NULL) { + printf("matrix[0][0] = %s\n", element); + cqlib_string_free(element); + } + + /* 3. Bind theta = pi/2 and evaluate into a numeric matrix */ + uintptr_t len = symbolic_matrix_evaluate_len(matrix); + Complex64 *values = malloc(len * sizeof(Complex64)); + int32_t status = symbolic_matrix_evaluate(matrix, "theta:1.5707963267948966", + values, len); + if (status == 0) { + /* values[0] is cos(pi/4); values is row-major */ + printf("matrix[0][0] = %f + %fi\n", values[0].re, values[0].im); + } + + free(values); + symbolic_matrix_free(matrix); + param_free(theta); + circuit_free(circuit); + return status; +} +``` + +See [Parameter](3_parameter.md) for parameter expression parsing (`param_parse` / `param_free`), [Standard Gates](5_gate_standard.md) for parameterized rotation gates (`circuit_rx_param`), and [Circuit](1_circuit.md) for circuit construction. diff --git a/docs/api/en/c/0_circuit/11_ansatz.md b/docs/api/en/c/0_circuit/11_ansatz.md new file mode 100644 index 00000000..ee5ebd4f --- /dev/null +++ b/docs/api/en/c/0_circuit/11_ansatz.md @@ -0,0 +1,1129 @@ +# Ansatz + +This page covers the variational circuit templates in the C API: TwoLocal, BasicEntanglerLayers, QAOAAnsatz, the feature-map family (ZFeatureMap, ZZFeatureMap, IQPFeatureMap, PauliFeatureMap), StronglyEntanglingLayers, and the Pauli evolution ansatz, plus the facade builders and entanglement topology helpers. Each template follows the flow "create handle → configure → validate → build circuit", and the generated circuit names its symbolic parameters with a `{prefix}` prefix. See [Overview](../0_overview.md) for the error-code and memory-management conventions. + +--- + +## Tag constants + +Entanglement topology tags (used by `two_local_entanglement`, the feature maps, the facade builders, and the topology helpers): + +| Topology tag | Value | Meaning | +| --- | --- | --- | +| `ENTANGLEMENT_LINEAR` | 0 | Linear nearest-neighbor chain. | +| `ENTANGLEMENT_CIRCULAR` | 1 | Linear chain plus wrap-around edge. | +| `ENTANGLEMENT_FULL` | 2 | All-to-all pairs. | +| `ENTANGLEMENT_CUSTOM` | 3 | Custom explicit pairs. | + +Evolution strategy tags (used by `qaoa_ansatz_evolution_strategy`): + +| Strategy tag | Value | Meaning | +| --- | --- | --- | +| `EVOLUTION_STRATEGY_EXACT` | 0 | Exact term-wise Pauli evolution. | +| `EVOLUTION_STRATEGY_AUTO` | 1 | Automatic exact/Trotter selection (uses `steps`). | +| `EVOLUTION_STRATEGY_TROTTER` | 2 | Explicit Trotter product formula (uses `mode` / `steps` / `seed`). | + +Trotter mode tags (`mode` values accepted by `EVOLUTION_STRATEGY_TROTTER`, plus the `trotter_mode` value reported by `pauli_evolution_ansatz_evolution_info`): + +| Mode tag | Value | Meaning | +| --- | --- | --- | +| `TROTTER_FIRST_ORDER` | 0 | First-order product formula. | +| `TROTTER_SECOND_ORDER` | 1 | Second-order product formula. | +| `TROTTER_RANDOMIZED` | 2 | Randomized first-order formula (`seed` is the random seed). | +| `TROTTER_MODE_NONE` | -1 | No Trotter decomposition was emitted (single-pass exact path); only reported in `CPauliEvolutionInfo.trotter_mode`, never accepted as a `mode` input. | + +--- + +## TwoLocal + +TwoLocal alternates single-qubit rotation layers with multi-qubit entanglement layers, suitable for VQE, quantum machine learning, and general variational circuit construction. `two_local_new` defaults to: `reps = 3`, rotation `[RY]`, entangler `CX`, linear topology, final rotation layer kept. + +### two_local_new(num_qubits) + +Creates a TwoLocal template handle. + +- `num_qubits` (`uintptr_t`): number of qubits. + +Returns a newly allocated `CTwoLocal*`. + +### two_local_free(ptr) + +Frees a TwoLocal handle. + +- `ptr` (`CTwoLocal*`): handle to free; NULL is allowed. + +No return value. + +### two_local_reps(ptr, reps) + +Sets the number of repetition layers. + +- `ptr` (`CTwoLocal*`): template handle. +- `reps` (`uintptr_t`): number of repetitions. + +Returns 0 on success, -1 on NULL input. + +### two_local_rotation_gates(ptr, names, len) + +Sets the per-layer single-qubit parameterized rotation gates by name (must be RX, RY, or RZ; validated on build). + +- `ptr` (`CTwoLocal*`): template handle. +- `names` (`const char* const*`): array of gate names. +- `len` (`uintptr_t`): array length. + +Returns 0 on success, -1 on NULL input, -4 on invalid UTF-8, -8 on an unknown gate name. + +### two_local_entanglement_gate(ptr, name) + +Sets the two-qubit entanglement gate by name (CX, CY, or CZ; validated on build). + +- `ptr` (`CTwoLocal*`): template handle. +- `name` (`const char*`): gate name. + +Returns 0 on success, -1 on NULL input, -4 on invalid UTF-8, -8 on an unknown name. + +### two_local_entanglement(ptr, topology, pairs, pairs_len) + +Sets the entanglement topology. For `ENTANGLEMENT_CUSTOM`, `pairs` is a flat array of `pairs_len / 2` control/target qubit-id pairs. + +- `ptr` (`CTwoLocal*`): template handle. +- `topology` (`int32_t`): an `ENTANGLEMENT_*` topology tag. +- `pairs` (`const uint32_t*`): flat array of custom pairs (NULL for other topologies). +- `pairs_len` (`uintptr_t`): length of the flat array. + +Returns 0 on success, -1 on NULL input (including `pairs` NULL while `pairs_len > 0`), -8 on an unknown topology or an odd pair count. + +### two_local_skip_final_rotation_layer(ptr, skip) + +Sets whether to skip the final rotation layer. + +- `ptr` (`CTwoLocal*`): template handle. +- `skip` (`bool`): true to skip. + +Returns 0 on success, -1 on NULL input. + +### two_local_validate(ptr) + +Validates the template configuration. + +- `ptr` (`const CTwoLocal*`): template handle. + +Returns 0 when the configuration is valid, -1 on NULL input, -3 on an invalid configuration. + +### two_local_num_parameters(ptr) + +Returns the number of independent parameters required by the generated circuit. + +- `ptr` (`const CTwoLocal*`): template handle. + +Returns the parameter count, or 0 for NULL. + +### two_local_num_qubits(ptr) + +Returns the number of qubits the template acts on. + +- `ptr` (`const CTwoLocal*`): template handle. + +Returns the qubit count, or 0 for NULL. + +### two_local_build_circuit(ptr, prefix) + +Builds the parameterized circuit from the current configuration; parameters are named `"{prefix}_{index}"`. + +- `ptr` (`const CTwoLocal*`): template handle. +- `prefix` (`const char*`): parameter name prefix. + +Returns a newly allocated `CCircuit*` (free with `circuit_free`); NULL on NULL input, invalid UTF-8, or build failure. + +### TwoLocal example + +```c +#include +#include "cqlib_c.h" + +int main(void) { + struct CTwoLocal *ansatz = two_local_new(3); + + two_local_reps(ansatz, 2); + const char *rotations[] = {"RY", "RZ"}; + two_local_rotation_gates(ansatz, rotations, 2); + two_local_entanglement_gate(ansatz, "CX"); + two_local_entanglement(ansatz, ENTANGLEMENT_LINEAR, NULL, 0); + + if (two_local_validate(ansatz) != 0) { + two_local_free(ansatz); + return 1; + } + + /* Parameters are named "theta_0", "theta_1", ... */ + struct CCircuit *circuit = two_local_build_circuit(ansatz, "theta"); + if (circuit == NULL) { + two_local_free(ansatz); + return 1; + } + printf("parameters: %zu, qubits: %zu\n", + two_local_num_parameters(ansatz), + two_local_num_qubits(ansatz)); + + circuit_free(circuit); + two_local_free(ansatz); + return 0; +} +``` + +--- + +## BasicEntanglerLayers + +BasicEntanglerLayers consists of single-qubit rotation layers and a fixed-pattern entanglement layer, suitable for training circuits with a simple structure and a controlled parameter count. `basic_entangler_layers_new` defaults to: `reps = 1`, rotation `RX`, entangler `CX`. + +### basic_entangler_layers_new(num_qubits) + +Creates a BasicEntanglerLayers template handle. + +- `num_qubits` (`uintptr_t`): number of qubits. + +Returns a newly allocated `CBasicEntanglerLayers*`. + +### basic_entangler_layers_free(ptr) + +Frees a BasicEntanglerLayers handle. + +- `ptr` (`CBasicEntanglerLayers*`): handle to free; NULL is allowed. + +No return value. + +### basic_entangler_layers_reps(ptr, reps) + +Sets the number of layers. + +- `ptr` (`CBasicEntanglerLayers*`): template handle. +- `reps` (`uintptr_t`): number of layers. + +Returns 0 on success, -1 on NULL input. + +### basic_entangler_layers_rotation_gate(ptr, name) + +Sets the single-qubit rotation gate by name (RX, RY, RZ, or Phase). + +- `ptr` (`CBasicEntanglerLayers*`): template handle. +- `name` (`const char*`): gate name. + +Returns 0 on success, -1 on NULL input, -4 on invalid UTF-8, -8 on an unknown gate name. + +### basic_entangler_layers_entanglement_gate(ptr, name) + +Sets the entanglement gate by name (CX, CY, or CZ). + +- `ptr` (`CBasicEntanglerLayers*`): template handle. +- `name` (`const char*`): gate name. + +Returns 0 on success, -1 on NULL input, -4 on invalid UTF-8, -8 on an unknown gate name. + +### basic_entangler_layers_num_parameters(ptr) + +Returns the number of independent parameters required by the generated circuit. + +- `ptr` (`const CBasicEntanglerLayers*`): template handle. + +Returns the parameter count, or 0 for NULL. + +### basic_entangler_layers_num_qubits(ptr) + +Returns the number of qubits the template acts on. + +- `ptr` (`const CBasicEntanglerLayers*`): template handle. + +Returns the qubit count, or 0 for NULL. + +### basic_entangler_layers_build_circuit(ptr, prefix) + +Builds the parameterized circuit from the current configuration; parameters are named `"{prefix}_{index}"`. + +- `ptr` (`const CBasicEntanglerLayers*`): template handle. +- `prefix` (`const char*`): parameter name prefix. + +Returns a newly allocated `CCircuit*` (free with `circuit_free`); NULL on NULL input, invalid UTF-8, or build failure. + +--- + +## QAOAAnsatz + +QAOAAnsatz builds parameterized circuits for the Quantum Approximate Optimization Algorithm (QAOA), alternating time evolution under the cost Hamiltonian and the mixer Hamiltonian: + +```text +U(β, γ) = ∏_l exp(-i β_l H_M) exp(-i γ_l H_C) +``` + +`qaoa_ansatz_new` infers the qubit count from the cost Hamiltonian, defaults the mixer to the standard X-mixer, and sets `reps = 1`. Each layer contributes one `γ_l` and one `β_l` parameter, so the total parameter count is `2 * reps`, with parameters named `"{prefix}_gamma_{layer}"` / `"{prefix}_beta_{layer}"`. + +### qaoa_ansatz_new(cost) + +Creates a QAOA template from a cost Hamiltonian (the Hamiltonian is cloned). + +- `cost` (`const CHamiltonian*`): cost Hamiltonian handle. + +Returns a newly allocated `CQAOAAnsatz*`; NULL on NULL input or an invalid cost operator. + +### qaoa_ansatz_free(ptr) + +Frees a QAOA handle. + +- `ptr` (`CQAOAAnsatz*`): handle to free; NULL is allowed. + +No return value. + +### qaoa_ansatz_reps(ptr, reps) + +Sets the number of alternating layers (QAOA depth p). + +- `ptr` (`CQAOAAnsatz*`): template handle. +- `reps` (`uintptr_t`): number of layers. + +Returns 0 on success, -1 on NULL input. + +### qaoa_ansatz_mixer(ptr, mixer) + +Overrides the mixer Hamiltonian (cloned; must match the cost operator's qubit count). + +- `ptr` (`CQAOAAnsatz*`): template handle. +- `mixer` (`const CHamiltonian*`): mixer Hamiltonian handle. + +Returns 0 on success, -1 on NULL input, -3 on a qubit-count mismatch or an invalid mixer. + +### qaoa_ansatz_initial_state(ptr, circuit) + +Prepends an initial-state circuit (cloned; must match the qubit count). + +- `ptr` (`CQAOAAnsatz*`): template handle. +- `circuit` (`const CCircuit*`): initial-state circuit handle. + +Returns 0 on success, -1 on NULL input, -3 on a qubit-count mismatch. + +### qaoa_ansatz_evolution_strategy(ptr, strategy, mode, steps, seed) + +Sets the Hamiltonian evolution strategy. `EVOLUTION_STRATEGY_EXACT` ignores the remaining arguments; `EVOLUTION_STRATEGY_AUTO` uses `steps`; `EVOLUTION_STRATEGY_TROTTER` uses `mode` (`TROTTER_FIRST_ORDER`, `TROTTER_SECOND_ORDER`, or `TROTTER_RANDOMIZED`), `steps`, and `seed`. + +- `ptr` (`CQAOAAnsatz*`): template handle. +- `strategy` (`int32_t`): an `EVOLUTION_STRATEGY_*` strategy tag. +- `mode` (`int32_t`): Trotter mode tag, used only by the TROTTER strategy. +- `steps` (`uintptr_t`): Trotter step count, ignored by the EXACT strategy. +- `seed` (`uint64_t`): random seed, used only by `TROTTER_RANDOMIZED`. + +Returns 0 on success, -1 on NULL input, -8 on an unknown tag. + +### qaoa_ansatz_validate(ptr) + +Validates the template configuration. + +- `ptr` (`const CQAOAAnsatz*`): template handle. + +Returns 0 when the configuration is valid, -1 on NULL input, -3 on an invalid configuration. + +### qaoa_ansatz_num_parameters(ptr) + +Returns the number of parameters (2 per layer). + +- `ptr` (`const CQAOAAnsatz*`): template handle. + +Returns the parameter count, or 0 for NULL. + +### qaoa_ansatz_num_qubits(ptr) + +Returns the number of qubits the template acts on. + +- `ptr` (`const CQAOAAnsatz*`): template handle. + +Returns the qubit count, or 0 for NULL. + +### qaoa_ansatz_build_circuit(ptr, prefix) + +Builds the parameterized circuit from the current configuration; parameters are named `"{prefix}_gamma_{layer}"` / `"{prefix}_beta_{layer}"`. + +- `ptr` (`const CQAOAAnsatz*`): template handle. +- `prefix` (`const char*`): parameter name prefix. + +Returns a newly allocated `CCircuit*` (free with `circuit_free`); NULL on NULL input, invalid UTF-8, or build failure. + +### QAOA example + +```c +#include +#include "cqlib_c.h" + +int main(void) { + /* Cost Hamiltonian: H_C = Z0 * Z1 */ + struct CPauliString *zz = pauli_string_parse("ZZ"); + struct CHamiltonian *cost = hamiltonian_from_pauli(zz); /* takes ownership of zz */ + + struct CQAOAAnsatz *ansatz = qaoa_ansatz_new(cost); + qaoa_ansatz_reps(ansatz, 2); + qaoa_ansatz_evolution_strategy(ansatz, EVOLUTION_STRATEGY_AUTO, + TROTTER_FIRST_ORDER, 1, 0); + + if (qaoa_ansatz_validate(ansatz) != 0) { + qaoa_ansatz_free(ansatz); + hamiltonian_free(cost); + return 1; + } + + /* Parameters are named "q_gamma_0", "q_beta_0", "q_gamma_1", "q_beta_1" */ + struct CCircuit *circuit = qaoa_ansatz_build_circuit(ansatz, "q"); + if (circuit == NULL) { + qaoa_ansatz_free(ansatz); + hamiltonian_free(cost); + return 1; + } + printf("parameters: %zu, qubits: %zu\n", + qaoa_ansatz_num_parameters(ansatz), + qaoa_ansatz_num_qubits(ansatz)); + + circuit_free(circuit); + qaoa_ansatz_free(ansatz); + hamiltonian_free(cost); + return 0; +} +``` + +--- + +## Feature maps + +Feature maps encode a classical feature vector `x_0, x_1, ...` into a quantum state and are the standard input layer for quantum machine learning and quantum kernel methods. All four templates (ZFeatureMap, ZZFeatureMap, IQPFeatureMap, PauliFeatureMap) share the same conventions: each repetition layer starts from a Hadamard layer, the symbolic parameters are named `"{prefix}_{index}"` with one parameter per input feature, and the same parameter objects are reused across repetition layers, so `*_num_parameters` reports `num_qubits` (0 when `reps = 0`, which builds an empty circuit). Note that `circuit_num_parameters` counts distinct angle-expression objects: a constant such as `pi` embedded inside an angle expression is not a separate parameter (see the ZZFeatureMap example below). + +### ZFeatureMap + +ZFeatureMap is the non-entangling first-order map: each repetition layer applies a Hadamard layer followed by an independent `RZ(2 * x_i)` rotation on every qubit. `z_feature_map_new` defaults to `reps = 2`. + +### z_feature_map_new(num_qubits) + +Creates a ZFeatureMap template handle. + +- `num_qubits` (`uintptr_t`): number of qubits (= number of input features). + +Returns a newly allocated `CZFeatureMap*`. + +### z_feature_map_free(ptr) + +Frees a ZFeatureMap handle. + +- `ptr` (`CZFeatureMap*`): handle to free; NULL is allowed. + +No return value. + +### z_feature_map_reps(ptr, reps) + +Sets the number of repetition layers. + +- `ptr` (`CZFeatureMap*`): template handle. +- `reps` (`uintptr_t`): number of repetitions. + +Returns 0 on success, -1 on NULL input. + +### z_feature_map_validate(ptr) + +Validates the template configuration (requires at least 1 qubit). + +- `ptr` (`const CZFeatureMap*`): template handle. + +Returns 0 when the configuration is valid, -1 on NULL input, -3 on an invalid configuration. + +### z_feature_map_num_parameters(ptr) + +Returns the number of input features (one parameter per qubit; 0 when `reps = 0`). + +- `ptr` (`const CZFeatureMap*`): template handle. + +Returns the parameter count, or 0 for NULL. + +### z_feature_map_num_qubits(ptr) + +Returns the number of qubits the template acts on. + +- `ptr` (`const CZFeatureMap*`): template handle. + +Returns the qubit count, or 0 for NULL. + +### z_feature_map_build_circuit(ptr, prefix) + +Builds the parameterized circuit from the current configuration; parameters are named `"{prefix}_{index}"`. + +- `ptr` (`const CZFeatureMap*`): template handle. +- `prefix` (`const char*`): parameter name prefix. + +Returns a newly allocated `CCircuit*` (free with `circuit_free`); NULL on NULL input, invalid UTF-8, or build failure. + +### ZZFeatureMap + +ZZFeatureMap adds second-order ZZ interactions: each repetition layer applies a Hadamard layer, `RZ(2 * x_i)` on each qubit, then for every pair `(i, j)` of the entanglement topology a Pauli evolution `e^{-i * 2 * (pi - x_i) * (pi - x_j) * Z_i Z_j}`. Defaults: `reps = 2`, full entanglement. Custom topologies are stored by the setter and checked by validation: out-of-bounds indices, self-loops, and duplicate (undirected) edges are rejected. + +### zz_feature_map_new(num_qubits) + +Creates a ZZFeatureMap template handle. + +- `num_qubits` (`uintptr_t`): number of qubits (= number of input features). + +Returns a newly allocated `CZZFeatureMap*`. + +### zz_feature_map_free(ptr) + +Frees a ZZFeatureMap handle. + +- `ptr` (`CZZFeatureMap*`): handle to free; NULL is allowed. + +No return value. + +### zz_feature_map_reps(ptr, reps) + +Sets the number of repetition layers. + +- `ptr` (`CZZFeatureMap*`): template handle. +- `reps` (`uintptr_t`): number of repetitions. + +Returns 0 on success, -1 on NULL input. + +### zz_feature_map_entanglement(ptr, topology, pairs, pairs_len) + +Sets the entanglement topology for the ZZ interactions. For `ENTANGLEMENT_CUSTOM`, `pairs` is a flat array of `pairs_len / 2` control/target qubit-id pairs. + +- `ptr` (`CZZFeatureMap*`): template handle. +- `topology` (`int32_t`): an `ENTANGLEMENT_*` topology tag. +- `pairs` (`const uint32_t*`): flat array of custom pairs (NULL for other topologies). +- `pairs_len` (`uintptr_t`): length of the flat array. + +Returns 0 on success, -1 on NULL input (including `pairs` NULL while `pairs_len > 0`), -8 on an unknown topology or an odd pair count. + +### zz_feature_map_validate(ptr) + +Validates the template configuration (at least 1 qubit; for custom topologies, in-bounds indices, no self-loops, no duplicate edges). + +- `ptr` (`const CZZFeatureMap*`): template handle. + +Returns 0 when the configuration is valid, -1 on NULL input, -3 on an invalid configuration. + +### zz_feature_map_num_parameters(ptr) + +Returns the number of input features (one parameter per qubit; 0 when `reps = 0`). + +- `ptr` (`const CZZFeatureMap*`): template handle. + +Returns the parameter count, or 0 for NULL. + +### zz_feature_map_num_qubits(ptr) + +Returns the number of qubits the template acts on. + +- `ptr` (`const CZZFeatureMap*`): template handle. + +Returns the qubit count, or 0 for NULL. + +### zz_feature_map_build_circuit(ptr, prefix) + +Builds the parameterized circuit from the current configuration; parameters are named `"{prefix}_{index}"`. + +- `ptr` (`const CZZFeatureMap*`): template handle. +- `prefix` (`const char*`): parameter name prefix. + +Returns a newly allocated `CCircuit*` (free with `circuit_free`); NULL on NULL input, invalid UTF-8, or build failure. + +### ZZFeatureMap example + +```c +#include +#include "cqlib_c.h" + +int main(void) { + struct CZZFeatureMap *map = zz_feature_map_new(2); + + /* Reports the 2 input features x_0, x_1. */ + printf("features: %zu\n", zz_feature_map_num_parameters(map)); + + /* circuit_num_parameters counts distinct angle expressions: two RZ + angles plus the shared ZZ expression 4 * (pi - x_i) * (pi - x_j); + the pi constant inside the expression is not a separate parameter. */ + struct CCircuit *circuit = zz_feature_map_build_circuit(map, "x"); + if (circuit == NULL) { + zz_feature_map_free(map); + return 1; + } + printf("circuit parameters: %zu\n", circuit_num_parameters(circuit)); + + circuit_free(circuit); + zz_feature_map_free(map); + return 0; +} +``` + +### IQPFeatureMap + +IQPFeatureMap is the IQP-style diagonal encoding. It applies the same circuit structure as ZZFeatureMap — Hadamard layer, first-order Z rotations, and second-order ZZ interactions following the entanglement topology — under a dedicated handle. Defaults: `reps = 2`, full entanglement. + +### iqp_feature_map_new(num_qubits) + +Creates an IQPFeatureMap template handle. + +- `num_qubits` (`uintptr_t`): number of qubits (= number of input features). + +Returns a newly allocated `CIQPFeatureMap*`. + +### iqp_feature_map_free(ptr) + +Frees an IQPFeatureMap handle. + +- `ptr` (`CIQPFeatureMap*`): handle to free; NULL is allowed. + +No return value. + +### iqp_feature_map_reps(ptr, reps) + +Sets the number of repetition layers. + +- `ptr` (`CIQPFeatureMap*`): template handle. +- `reps` (`uintptr_t`): number of repetitions. + +Returns 0 on success, -1 on NULL input. + +### iqp_feature_map_entanglement(ptr, topology, pairs, pairs_len) + +Sets the entanglement topology for the two-qubit diagonal interactions. For `ENTANGLEMENT_CUSTOM`, `pairs` is a flat array of `pairs_len / 2` control/target qubit-id pairs. + +- `ptr` (`CIQPFeatureMap*`): template handle. +- `topology` (`int32_t`): an `ENTANGLEMENT_*` topology tag. +- `pairs` (`const uint32_t*`): flat array of custom pairs (NULL for other topologies). +- `pairs_len` (`uintptr_t`): length of the flat array. + +Returns 0 on success, -1 on NULL input (including `pairs` NULL while `pairs_len > 0`), -8 on an unknown topology or an odd pair count. + +### iqp_feature_map_validate(ptr) + +Validates the template configuration (at least 1 qubit; for custom topologies, in-bounds indices, no self-loops, no duplicate edges). + +- `ptr` (`const CIQPFeatureMap*`): template handle. + +Returns 0 when the configuration is valid, -1 on NULL input, -3 on an invalid configuration. + +### iqp_feature_map_num_parameters(ptr) + +Returns the number of input features (one parameter per qubit; 0 when `reps = 0`). + +- `ptr` (`const CIQPFeatureMap*`): template handle. + +Returns the parameter count, or 0 for NULL. + +### iqp_feature_map_num_qubits(ptr) + +Returns the number of qubits the template acts on. + +- `ptr` (`const CIQPFeatureMap*`): template handle. + +Returns the qubit count, or 0 for NULL. + +### iqp_feature_map_build_circuit(ptr, prefix) + +Builds the parameterized circuit from the current configuration; parameters are named `"{prefix}_{index}"`. + +- `ptr` (`const CIQPFeatureMap*`): template handle. +- `prefix` (`const char*`): parameter name prefix. + +Returns a newly allocated `CCircuit*` (free with `circuit_free`); NULL on NULL input, invalid UTF-8, or build failure. + +### PauliFeatureMap + +PauliFeatureMap generalizes the Z and ZZ maps to arbitrary Pauli templates. Each repetition layer applies a Hadamard layer followed, for every configured Pauli template with support size `k` and every `k`-tuple of qubits drawn from the entanglement topology, by a Pauli evolution: + +- `k = 1`: `e^{-i * x_i * P}` with angle `2 * x_i`; +- `k >= 2`: `e^{-i * 2 * prod_j (pi - x_j) * P}` with angle `4 * prod_j (pi - x_j)`. + +Templates whose support is empty contribute only a global phase and are skipped. Defaults: `reps = 2`, Pauli templates `["Z", "ZZ"]`, full entanglement, parameter prefix `"x"`. Passing an empty prefix to `pauli_feature_map_build_circuit` falls back to the configured parameter prefix. + +### pauli_feature_map_new(num_qubits) + +Creates a PauliFeatureMap template handle. + +- `num_qubits` (`uintptr_t`): number of qubits (= number of input features). + +Returns a newly allocated `CPauliFeatureMap*`. + +### pauli_feature_map_free(ptr) + +Frees a PauliFeatureMap handle. + +- `ptr` (`CPauliFeatureMap*`): handle to free; NULL is allowed. + +No return value. + +### pauli_feature_map_reps(ptr, reps) + +Sets the number of repetition layers. + +- `ptr` (`CPauliFeatureMap*`): template handle. +- `reps` (`uintptr_t`): number of repetitions. + +Returns 0 on success, -1 on NULL input. + +### pauli_feature_map_paulis(ptr, texts, labels, len) + +Sets the Pauli evolution templates. `texts` and `labels` are parallel arrays of `len` NUL-terminated C strings; each text is parsed as a PauliString (for example `"Z"`, `"ZZ"`, `"XY"`) and each label names the template. A template longer than the qubit count is stored but fails validation. A failing call leaves the previous configuration in place. + +- `ptr` (`CPauliFeatureMap*`): template handle. +- `texts` (`const char* const*`): array of Pauli string texts. +- `labels` (`const char* const*`): array of template labels. +- `len` (`uintptr_t`): array length. + +Returns 0 on success, -1 on NULL input (handle, arrays while `len > 0`, or an individual element), -4 on invalid UTF-8, -8 on a Pauli string that fails to parse. + +### pauli_feature_map_entanglement(ptr, topology, pairs, pairs_len) + +Sets the entanglement topology for the multi-qubit interactions. For `ENTANGLEMENT_CUSTOM`, `pairs` is a flat array of `pairs_len / 2` control/target qubit-id pairs. + +- `ptr` (`CPauliFeatureMap*`): template handle. +- `topology` (`int32_t`): an `ENTANGLEMENT_*` topology tag. +- `pairs` (`const uint32_t*`): flat array of custom pairs (NULL for other topologies). +- `pairs_len` (`uintptr_t`): length of the flat array. + +Returns 0 on success, -1 on NULL input (including `pairs` NULL while `pairs_len > 0`), -8 on an unknown topology or an odd pair count. + +### pauli_feature_map_parameter_prefix(ptr, prefix) + +Sets the fallback parameter prefix used when `pauli_feature_map_build_circuit` receives an empty prefix. + +- `ptr` (`CPauliFeatureMap*`): template handle. +- `prefix` (`const char*`): parameter name prefix. + +Returns 0 on success, -1 on NULL input, -4 on invalid UTF-8. + +### pauli_feature_map_validate(ptr) + +Validates the template configuration (at least 1 qubit; no Pauli template longer than the qubit count; for custom topologies, in-bounds indices, no self-loops, no duplicate edges). + +- `ptr` (`const CPauliFeatureMap*`): template handle. + +Returns 0 when the configuration is valid, -1 on NULL input, -3 on an invalid configuration. + +### pauli_feature_map_num_parameters(ptr) + +Returns the number of input features (one parameter per qubit; 0 when `reps = 0`). + +- `ptr` (`const CPauliFeatureMap*`): template handle. + +Returns the parameter count, or 0 for NULL. + +### pauli_feature_map_num_qubits(ptr) + +Returns the number of qubits the template acts on. + +- `ptr` (`const CPauliFeatureMap*`): template handle. + +Returns the qubit count, or 0 for NULL. + +### pauli_feature_map_build_circuit(ptr, prefix) + +Builds the parameterized circuit from the current configuration; parameters are named `"{prefix}_{index}"`. An empty prefix falls back to the configured parameter prefix. + +- `ptr` (`const CPauliFeatureMap*`): template handle. +- `prefix` (`const char*`): parameter name prefix (empty string uses the configured fallback). + +Returns a newly allocated `CCircuit*` (free with `circuit_free`); NULL on NULL input, invalid UTF-8, or build failure. + +### PauliFeatureMap example + +```c +#include +#include "cqlib_c.h" + +int main(void) { + struct CPauliFeatureMap *map = pauli_feature_map_new(2); + + const char *texts[] = {"Z", "ZZ"}; + const char *labels[] = {"Z", "ZZ"}; + pauli_feature_map_paulis(map, texts, labels, 2); + pauli_feature_map_parameter_prefix(map, "p"); + + if (pauli_feature_map_validate(map) != 0) { + pauli_feature_map_free(map); + return 1; + } + + /* An empty prefix falls back to "p": parameters are named "p_0", "p_1". */ + struct CCircuit *circuit = pauli_feature_map_build_circuit(map, ""); + if (circuit == NULL) { + pauli_feature_map_free(map); + return 1; + } + printf("features: %zu\n", pauli_feature_map_num_parameters(map)); + + circuit_free(circuit); + pauli_feature_map_free(map); + return 0; +} +``` + +--- + +## Facade builders + +The facade constructors create pre-configured handles for common templates in one call. They return the same handle types as the corresponding `*_new` constructors, so the resulting objects work with all of the `two_local_*`, `zz_feature_map_*`, and `pauli_feature_map_*` functions. All four return NULL when the topology arguments are invalid (unknown tag, an odd custom pair count, or `pairs` NULL while `pairs_len > 0`); `pauli_feature_map_build` also returns NULL when a Pauli string fails to parse. + +### efficient_su2(num_qubits, reps, topology, pairs, pairs_len) + +Creates an EfficientSU2 ansatz: `[RY, RZ]` rotation layers with `CX` entanglement and the final rotation layer kept, giving `(reps + 1) * num_qubits * 2` parameters. + +- `num_qubits` (`uintptr_t`): number of qubits. +- `reps` (`uintptr_t`): number of repetition layers. +- `topology` (`int32_t`): an `ENTANGLEMENT_*` topology tag. +- `pairs` (`const uint32_t*`): flat array of custom pairs (NULL for other topologies). +- `pairs_len` (`uintptr_t`): length of the flat array. + +Returns a newly allocated `CTwoLocal*`, or NULL on invalid topology arguments. + +### real_amplitudes(num_qubits, reps, topology, pairs, pairs_len) + +Creates a RealAmplitudes ansatz: `[RY]` rotation layers with `CX` entanglement and the final rotation layer kept, giving `(reps + 1) * num_qubits` parameters. + +- `num_qubits` (`uintptr_t`): number of qubits. +- `reps` (`uintptr_t`): number of repetition layers. +- `topology` (`int32_t`): an `ENTANGLEMENT_*` topology tag. +- `pairs` (`const uint32_t*`): flat array of custom pairs (NULL for other topologies). +- `pairs_len` (`uintptr_t`): length of the flat array. + +Returns a newly allocated `CTwoLocal*`, or NULL on invalid topology arguments. + +### zz_feature_map_build(num_qubits, reps, topology, pairs, pairs_len) + +Creates a ZZFeatureMap with the given repetition count and topology. + +- `num_qubits` (`uintptr_t`): number of qubits (= number of input features). +- `reps` (`uintptr_t`): number of repetition layers. +- `topology` (`int32_t`): an `ENTANGLEMENT_*` topology tag. +- `pairs` (`const uint32_t*`): flat array of custom pairs (NULL for other topologies). +- `pairs_len` (`uintptr_t`): length of the flat array. + +Returns a newly allocated `CZZFeatureMap*`, or NULL on invalid topology arguments. + +### pauli_feature_map_build(num_qubits, reps, texts, labels, paulis_len, topology, pairs, pairs_len) + +Creates a PauliFeatureMap with explicit Pauli templates. `texts` and `labels` are parallel arrays of `paulis_len` NUL-terminated C strings; each text is parsed as a PauliString. + +- `num_qubits` (`uintptr_t`): number of qubits (= number of input features). +- `reps` (`uintptr_t`): number of repetition layers. +- `texts` (`const char* const*`): array of Pauli string texts. +- `labels` (`const char* const*`): array of template labels. +- `paulis_len` (`uintptr_t`): length of the two arrays. +- `topology` (`int32_t`): an `ENTANGLEMENT_*` topology tag. +- `pairs` (`const uint32_t*`): flat array of custom pairs (NULL for other topologies). +- `pairs_len` (`uintptr_t`): length of the flat array. + +Returns a newly allocated `CPauliFeatureMap*`, or NULL on invalid topology arguments or an unparsable Pauli string. + +### Facade example + +```c +#include +#include "cqlib_c.h" + +int main(void) { + /* EfficientSU2: [RY, RZ] + CX -> (1 + 1) * 2 * 2 = 8 parameters */ + struct CTwoLocal *su2 = efficient_su2(2, 1, ENTANGLEMENT_FULL, NULL, 0); + printf("efficient_su2 parameters: %zu\n", two_local_num_parameters(su2)); + two_local_free(su2); + + /* RealAmplitudes: [RY] + CX -> (2 + 1) * 3 = 9 parameters */ + struct CTwoLocal *ra = real_amplitudes(3, 2, ENTANGLEMENT_LINEAR, NULL, 0); + printf("real_amplitudes parameters: %zu\n", two_local_num_parameters(ra)); + two_local_free(ra); + + /* ZZ feature map with a circular topology */ + struct CZZFeatureMap *zz = zz_feature_map_build(3, 2, ENTANGLEMENT_CIRCULAR, NULL, 0); + printf("zz_feature_map features: %zu\n", zz_feature_map_num_parameters(zz)); + zz_feature_map_free(zz); + return 0; +} +``` + +--- + +## Entanglement topology helpers + +These helpers expand an `ENTANGLEMENT_*` topology tag into explicit qubit groups with the same semantics the templates use. They use a two-step output pattern: call the `*_len` function first to obtain the required flat length, allocate the buffer, then call the copy function. + +### entanglement_generate_pairs_len(topology, pairs, pairs_len, num_qubits) + +Returns the flattened length produced by `entanglement_generate_pairs` (two entries per pair), or 0 when the arguments are invalid (unknown topology tag, odd custom pair count, or `pairs` NULL while `pairs_len > 0`). For `ENTANGLEMENT_CUSTOM`, the custom pairs are echoed verbatim and `num_qubits` is ignored. + +- `topology` (`int32_t`): an `ENTANGLEMENT_*` topology tag. +- `pairs` (`const uint32_t*`): flat array of custom pairs (NULL for other topologies). +- `pairs_len` (`uintptr_t`): length of the flat array. +- `num_qubits` (`uintptr_t`): qubit count used by the predefined topologies. + +Returns the required length of the `out` buffer for `entanglement_generate_pairs`, or 0 on invalid arguments. + +### entanglement_generate_pairs(topology, pairs, pairs_len, num_qubits, out, len) + +Copies the flattened pairs `[c0, t0, c1, t1, ...]` into `out`. Predefined topologies generate: linear `(0,1), (1,2), ...`; circular adds the wrap-around edge `(n-1, 0)`; full all-to-all pairs in lexicographic order. + +- `topology` (`int32_t`): an `ENTANGLEMENT_*` topology tag. +- `pairs` (`const uint32_t*`): flat array of custom pairs (NULL for other topologies). +- `pairs_len` (`uintptr_t`): length of the flat array. +- `num_qubits` (`uintptr_t`): qubit count used by the predefined topologies. +- `out` (`uint32_t*`): output buffer. +- `len` (`uintptr_t`): buffer length returned by `entanglement_generate_pairs_len`. + +Returns 0 on success, -1 on NULL input (`out` NULL while `len > 0`), -8 when `len` does not match the required length or an argument is invalid. + +### entanglement_generate_k_tuples_len(topology, pairs, pairs_len, k, num_qubits) + +Returns the flattened length produced by `entanglement_generate_k_tuples` (`k` entries per tuple), or 0 on an invalid argument combination: unknown topology or malformed custom pairs, `k = 0`, `k > num_qubits`, or `num_qubits = 0`. + +- `topology` (`int32_t`): an `ENTANGLEMENT_*` topology tag. +- `pairs` (`const uint32_t*`): flat array of custom pairs (NULL for other topologies). +- `pairs_len` (`uintptr_t`): length of the flat array. +- `k` (`uintptr_t`): tuple size. +- `num_qubits` (`uintptr_t`): qubit count. + +Returns the required length of the `out` buffer for `entanglement_generate_k_tuples`, or 0 on invalid arguments. + +### entanglement_generate_k_tuples(topology, pairs, pairs_len, k, num_qubits, out, len) + +Copies the flattened k-tuples `[t0_0, t0_1, ..., t1_0, ...]` into `out`. `k = 1` yields each qubit on its own; `k = 2` yields the topology's pairs (custom pairs included); for `k >= 3`, linear yields consecutive windows `(i, ..., i+k-1)`, circular yields rotational windows mod n, and full yields lexicographic k-combinations — custom topologies only define pairs and fall back to full. + +- `topology` (`int32_t`): an `ENTANGLEMENT_*` topology tag. +- `pairs` (`const uint32_t*`): flat array of custom pairs (NULL for other topologies). +- `pairs_len` (`uintptr_t`): length of the flat array. +- `k` (`uintptr_t`): tuple size. +- `num_qubits` (`uintptr_t`): qubit count. +- `out` (`uint32_t*`): output buffer. +- `len` (`uintptr_t`): buffer length returned by `entanglement_generate_k_tuples_len`. + +Returns 0 on success, -1 on NULL input (`out` NULL while `len > 0`), -8 when `len` does not match the required length or an argument is invalid. + +### Entanglement helpers example + +```c +#include +#include +#include "cqlib_c.h" + +int main(void) { + /* Two-step pattern: query the length first, then copy. */ + uintptr_t len = entanglement_generate_pairs_len(ENTANGLEMENT_CIRCULAR, NULL, 0, 4); + uint32_t *pairs = malloc(len * sizeof(uint32_t)); + + if (entanglement_generate_pairs(ENTANGLEMENT_CIRCULAR, NULL, 0, 4, pairs, len) == 0) { + /* prints: 0 1 1 2 2 3 3 0 */ + for (uintptr_t i = 0; i < len; i++) { + printf("%u ", pairs[i]); + } + printf("\n"); + } + free(pairs); + return 0; +} +``` + +--- + +## StronglyEntanglingLayers + +StronglyEntanglingLayers applies, in each layer, a U gate (three parameters) on every qubit followed by an entangler connecting qubit `i` to qubit `(i + r) mod num_qubits`, where `r` is the layer's range. Defaults: `reps = 1`, entangler `CX`, ranges cycling through `1..num_qubits` (layer `l` uses `(l mod (num_qubits - 1)) + 1`). Parameters are named `"{prefix}_{index}"` and numbered consecutively; the total count is `reps * num_qubits * 3`. + +### strongly_entangling_layers_new(num_qubits) + +Creates a StronglyEntanglingLayers template handle. + +- `num_qubits` (`uintptr_t`): number of qubits. + +Returns a newly allocated `CStronglyEntanglingLayers*`. + +### strongly_entangling_layers_free(ptr) + +Frees a StronglyEntanglingLayers handle. + +- `ptr` (`CStronglyEntanglingLayers*`): handle to free; NULL is allowed. + +No return value. + +### strongly_entangling_layers_reps(ptr, reps) + +Sets the number of layers. + +- `ptr` (`CStronglyEntanglingLayers*`): template handle. +- `reps` (`uintptr_t`): number of layers. + +Returns 0 on success, -1 on NULL input. + +### strongly_entangling_layers_entanglement_gate(ptr, name) + +Sets the two-qubit entangling gate by name (CX, CY, or CZ). + +- `ptr` (`CStronglyEntanglingLayers*`): template handle. +- `name` (`const char*`): gate name. + +Returns 0 on success, -1 on NULL input, -4 on invalid UTF-8, -8 on an unknown gate name. + +### strongly_entangling_layers_ranges(ptr, ranges, len) + +Sets explicit entanglement ranges reused cyclically across layers (layer `l` uses `ranges[l mod len]`). Each range must be in `1..num_qubits` at validation time. An empty list (`len == 0`) stores the empty list, which fails validation until replaced. + +- `ptr` (`CStronglyEntanglingLayers*`): template handle. +- `ranges` (`const uintptr_t*`): array of ranges. +- `len` (`uintptr_t`): array length. + +Returns 0 on success, -1 on NULL input (handle, or `ranges` NULL while `len > 0`). + +### strongly_entangling_layers_validate(ptr) + +Validates the template configuration (at least 1 qubit, a known entangler, and — when set — a non-empty ranges list with every range in `1..num_qubits`). + +- `ptr` (`const CStronglyEntanglingLayers*`): template handle. + +Returns 0 when the configuration is valid, -1 on NULL input, -3 on an invalid configuration. + +### strongly_entangling_layers_num_parameters(ptr) + +Returns the number of independent parameters (`reps * num_qubits * 3`). + +- `ptr` (`const CStronglyEntanglingLayers*`): template handle. + +Returns the parameter count, or 0 for NULL. + +### strongly_entangling_layers_num_qubits(ptr) + +Returns the number of qubits the template acts on. + +- `ptr` (`const CStronglyEntanglingLayers*`): template handle. + +Returns the qubit count, or 0 for NULL. + +### strongly_entangling_layers_build_circuit(ptr, prefix) + +Builds the parameterized circuit from the current configuration; parameters are named `"{prefix}_{index}"`. + +- `ptr` (`const CStronglyEntanglingLayers*`): template handle. +- `prefix` (`const char*`): parameter name prefix. + +Returns a newly allocated `CCircuit*` (free with `circuit_free`); NULL on NULL input, invalid UTF-8, or build failure. + +### StronglyEntanglingLayers example + +```c +#include +#include "cqlib_c.h" + +int main(void) { + struct CStronglyEntanglingLayers *layers = strongly_entangling_layers_new(3); + + strongly_entangling_layers_reps(layers, 2); + uintptr_t ranges[] = {1, 2}; /* reused cyclically: layer 0 -> 1, layer 1 -> 2 */ + strongly_entangling_layers_ranges(layers, ranges, 2); + strongly_entangling_layers_entanglement_gate(layers, "CZ"); + + if (strongly_entangling_layers_validate(layers) != 0) { + strongly_entangling_layers_free(layers); + return 1; + } + + /* 2 layers * 3 qubits * 3 U-angles = 18 parameters named "t_0", "t_1", ... */ + struct CCircuit *circuit = strongly_entangling_layers_build_circuit(layers, "t"); + if (circuit == NULL) { + strongly_entangling_layers_free(layers); + return 1; + } + printf("parameters: %zu\n", strongly_entangling_layers_num_parameters(layers)); + + circuit_free(circuit); + strongly_entangling_layers_free(layers); + return 0; +} +``` + +--- + +## PauliEvolutionAnsatz + +PauliEvolutionAnsatz represents time evolution under a Hamiltonian. `pauli_evolution_ansatz_new` clones the input Hamiltonian and simplifies it (duplicate terms merged, phases absorbed into coefficients, near-zero terms removed); the default evolution strategy is automatic exact/Trotter selection with one step. + +### pauli_evolution_ansatz_new(hamiltonian) + +Creates a Pauli evolution ansatz from a Hamiltonian (cloned and simplified in the process). + +- `hamiltonian` (`const CHamiltonian*`): Hamiltonian handle. + +Returns a newly allocated `CPauliEvolutionAnsatz*`; NULL on NULL input or an empty operator. + +### pauli_evolution_ansatz_free(ptr) + +Frees a Pauli evolution ansatz handle. + +- `ptr` (`CPauliEvolutionAnsatz*`): handle to free; NULL is allowed. + +No return value. + +### pauli_evolution_ansatz_with_time_param_name(ptr, name) + +Overrides the time-parameter symbol used by the evolution lowering. By default the symbol is derived as `"{prefix}_t"`; setting a name uses that exact name. An empty name resets it to the default. + +- `ptr` (`CPauliEvolutionAnsatz*`): template handle. +- `name` (`const char*`): new time-parameter symbol (empty string resets to the default). + +Returns 0 on success, -1 on NULL input, -4 on invalid UTF-8. + +### pauli_evolution_ansatz_evolution_info(ptr, out) + +Reads the evolution strategy info (the C mirror of `PauliEvolutionAnsatz::evolution_info`) into `out`. `out` describes the strategy actually used: the decomposition emitted for the current configuration and the structure of the simplified Hamiltonian, which also reveals what `EVOLUTION_STRATEGY_AUTO` selected. + +- `ptr` (`const CPauliEvolutionAnsatz*`): template handle. +- `out` (`CPauliEvolutionInfo*`): output struct receiving the snapshot. + +Returns 0 on success, -1 on NULL input (`ptr` or `out`). + +`CPauliEvolutionInfo` is a transparent struct with a stable layout: + +| Field | Type | Meaning | +| --- | --- | --- | +| `is_exact` | `uint8_t` | 1 when the decomposition is mathematically exact, 0 otherwise. | +| `all_terms_commute` | `uint8_t` | 1 when all Hamiltonian terms mutually commute, 0 otherwise. | +| `_pad` | `uint8_t[6]` | Reserved padding to keep the struct layout stable. | +| `steps` | `uintptr_t` | Number of decomposition repetitions emitted into the circuit. | +| `num_terms` | `uintptr_t` | Number of Pauli terms in the (simplified) Hamiltonian. | +| `trotter_mode` | `int32_t` | `TROTTER_FIRST_ORDER`, `TROTTER_SECOND_ORDER`, or `TROTTER_RANDOMIZED`; `TROTTER_MODE_NONE` when the single-pass exact path was used. | +| `trotter_seed` | `uint64_t` | Seed for `TROTTER_RANDOMIZED`; 0 otherwise. | + +### PauliEvolutionAnsatz example + +```c +#include +#include "cqlib_c.h" + +int main(void) { + /* Evolution under H = X on one qubit. */ + struct CPauliString *px = pauli_string_parse("X"); + struct CHamiltonian *ham = hamiltonian_from_pauli(px); /* takes ownership of px */ + + struct CPauliEvolutionAnsatz *ansatz = pauli_evolution_ansatz_new(ham); + if (ansatz == NULL) { + hamiltonian_free(ham); + return 1; + } + + /* Rename the time parameter, then reset it to the default. */ + pauli_evolution_ansatz_with_time_param_name(ansatz, "tau"); + pauli_evolution_ansatz_with_time_param_name(ansatz, ""); + + /* Read the strategy actually used: the single commuting term takes the + single-pass exact path (trotter_mode == TROTTER_MODE_NONE). */ + struct CPauliEvolutionInfo info; + if (pauli_evolution_ansatz_evolution_info(ansatz, &info) == 0) { + printf("exact=%d commute=%d steps=%zu terms=%zu mode=%d\n", + info.is_exact, info.all_terms_commute, + info.steps, info.num_terms, info.trotter_mode); + } + + pauli_evolution_ansatz_free(ansatz); + hamiltonian_free(ham); + return 0; +} +``` + +--- + +See [Hamiltonian](../3_qis/7_hamiltonian.md) for the Hamiltonian constructors (`hamiltonian_from_pauli` / `hamiltonian_free`), [Pauli](../3_qis/6_pauli.md) for Pauli string parsing (`pauli_string_parse`), and [Circuit](1_circuit.md) for circuit handle management (`circuit_free`, `circuit_num_parameters`). diff --git a/docs/api/en/c/0_circuit/12_cfg.md b/docs/api/en/c/0_circuit/12_cfg.md new file mode 100644 index 00000000..8a191600 --- /dev/null +++ b/docs/api/en/c/0_circuit/12_cfg.md @@ -0,0 +1,615 @@ +# Circuit Control-Flow Graph (CCircuitCFG) + +`CCircuitCFG*` is the control-flow graph (CFG) analysis view of a quantum circuit: basic blocks hold sequences of operations that execute in order, block terminators describe control transfers, and control-flow regions record the boundaries and entries of structured `if` / `while` / `for` / `switch` constructs. Blocks are addressed by stable node indices (`uint32_t`), with `UINT32_MAX` as the "missing block / unset entry" sentinel; besides the common error codes, the interfaces on this page use `-2` for a missing block or a sentinel index. The typical workflow is `circuit_cfg_from_circuit` → inspect blocks / terminators / edges / regions → `circuit_cfg_validate` → `circuit_cfg_to_circuit`. For the common conventions (error codes, handle release, two-step array output) see [Overview](../0_overview.md). + +--- + +## Construction and conversion + +### circuit_cfg_new(num_qubits) + +Creates an empty CFG with densely numbered qubits `0..num_qubits`. The fresh graph has no blocks and no entry block; it must be edited before it can pass `circuit_cfg_validate`. + +- `num_qubits` (`uintptr_t`): number of logical qubits. + +Returns: a newly allocated `CCircuitCFG*`. + +### circuit_cfg_from_qubits(qubits, qubits_len) + +Creates an empty CFG from the supplied logical qubit ids in insertion order; duplicate ids are removed, which suits sparse logical numbering. + +- `qubits` (`const uint32_t*`): array of logical qubit ids. +- `qubits_len` (`uintptr_t`): length of the array. + +Returns: a newly allocated `CCircuitCFG*` (free with `circuit_cfg_free`), or NULL when `qubits` is NULL while `qubits_len > 0`. + +### circuit_cfg_from_circuit(ptr) + +Expands a structured circuit into a validated CFG. Every conditional, loop, and switch header owns a control-flow region recording body entries, merge blocks, and outer operation metadata; a linear circuit expands into a single entry block labeled `entry` with a `CFG_TERMINATOR_RETURN` terminator. + +- `ptr` (`const struct CCircuit*`): source circuit handle. + +Returns: a newly allocated `CCircuitCFG*` (free with `circuit_cfg_free`), or NULL when `ptr` is NULL or the expansion fails (control flow that cannot be structured). + +```c +struct CCircuit* circuit = circuit_new(2); +circuit_h(circuit, 0); +circuit_cx(circuit, 0, 1); + +struct CCircuitCFG* cfg = circuit_cfg_from_circuit(circuit); +/* circuit_cfg_num_blocks(cfg) == 1: a linear circuit has a single entry block */ +``` + +### circuit_cfg_free(ptr) + +Frees a CFG handle. + +- `ptr` (`struct CCircuitCFG*`): handle to free; passing NULL is allowed. + +### circuit_cfg_to_circuit(ptr) + +Reconstructs a structured circuit from this CFG. The symbol table, the parameter table, and the global phase are restored along with it; a structure check identical to `circuit_cfg_validate` runs first. + +- `ptr` (`const struct CCircuitCFG*`): CFG handle. + +Returns: a newly allocated `CCircuit*` (free with `circuit_free`), or NULL when `ptr` is NULL or the lowering fails (the graph is damaged, or a region cannot be mapped back to a structured form). + +### circuit_cfg_validate(ptr) + +Checks the graph, operation, parameter, edge, terminator, and region invariants: whether the entry block is valid, whether every block has exactly one terminator whose outgoing edge count and types agree, whether `Return` has no outgoing edge, whether all blocks are reachable from the entry, whether the region metadata agrees with the graph, whether the `Branch` condition is a `Bool` expression, whether the `Switch` target is a `UInt` expression with outgoing edges equal to the case count plus one, and more. + +- `ptr` (`const struct CCircuitCFG*`): CFG handle. + +Returns: `0` on success; `-1` when `ptr` is NULL; `-3` on validation failure. + +--- + +## Global information + +### circuit_cfg_num_qubits(ptr) + +Returns the number of logical qubits. + +- `ptr` (`const struct CCircuitCFG*`): CFG handle. + +Returns: the qubit count, or 0 when `ptr` is NULL. + +### circuit_cfg_num_blocks(ptr) + +Returns the number of blocks currently stored in the graph. + +- `ptr` (`const struct CCircuitCFG*`): CFG handle. + +Returns: the block count, or 0 when `ptr` is NULL. + +### circuit_cfg_qubits(ptr, buffer, len) + +Copies the logical qubit ids in insertion order into `buffer` (two-step: query the length with `circuit_cfg_num_qubits` first). + +- `ptr` (`const struct CCircuitCFG*`): CFG handle. +- `buffer` (`uint32_t*`): output buffer. +- `len` (`uintptr_t`): buffer capacity. + +Returns: `0` on success; `-1` when `ptr` is NULL, or when `buffer` is NULL while the id set is non-empty; `-8` when `len` is smaller than the qubit count. + +### circuit_cfg_classical_vars_len(ptr) + +Returns the length of the static type table of mutable classical variables. + +- `ptr` (`const struct CCircuitCFG*`): CFG handle. + +Returns: the variable count, or 0 when `ptr` is NULL. + +### circuit_cfg_classical_vars(ptr, buffer, len) + +Copies the mutable classical variable types as `(tag, width)` snapshots into `buffer` (two-step: query the length with `circuit_cfg_classical_vars_len`). `tag` is one of `CQLIB_CLASSICAL_TYPE_BIT` / `CQLIB_CLASSICAL_TYPE_BOOL` / `CQLIB_CLASSICAL_TYPE_UINT` / `CQLIB_CLASSICAL_TYPE_BIT_VEC`; the table is empty for a hand-built CFG and holds the source circuit's static snapshot for one built by `circuit_cfg_from_circuit`. + +- `ptr` (`const struct CCircuitCFG*`): CFG handle. +- `buffer` (`struct CClassicalType*`): output array whose elements are `{ uint32_t tag; uint32_t width; }`. +- `len` (`uintptr_t`): buffer capacity. + +Returns: `0` on success; `-1` when `ptr` is NULL, or when `buffer` is NULL while the table is non-empty; `-8` when `len` is too small. + +### circuit_cfg_classical_values_len(ptr) + +Returns the length of the static type table of immutable classical values. + +- `ptr` (`const struct CCircuitCFG*`): CFG handle. + +Returns: the value count, or 0 when `ptr` is NULL. + +### circuit_cfg_classical_values(ptr, buffer, len) + +Copies the immutable classical value types (such as the `BitVec` produced by a measurement) as `(tag, width)` snapshots into `buffer`, with the same conventions as `circuit_cfg_classical_vars`. + +- `ptr` (`const struct CCircuitCFG*`): CFG handle. +- `buffer` (`struct CClassicalType*`): output array. +- `len` (`uintptr_t`): buffer capacity. + +Returns: `0` on success; `-1` when `ptr` is NULL, or when `buffer` is NULL while the table is non-empty; `-8` when `len` is too small. + +### circuit_cfg_entry_block(ptr) + +Returns the designated entry block of the graph. + +- `ptr` (`const struct CCircuitCFG*`): CFG handle. + +Returns: the entry block index, or `UINT32_MAX` when none has been set or `ptr` is NULL. + +--- + +## Blocks and operations + +### circuit_cfg_add_block(ptr) + +Appends an empty block to the graph. + +- `ptr` (`struct CCircuitCFG*`): CFG handle. + +Returns: the new block's stable node index, or `UINT32_MAX` when `ptr` is NULL. + +### circuit_cfg_push_operation(ptr, node, op) + +Appends one ordinary (non-control-flow) operation to the end of a block; the operation is cloned and the source handle stays owned by the caller. + +- `ptr` (`struct CCircuitCFG*`): CFG handle. +- `node` (`uint32_t`): block index. +- `op` (`const struct COperation*`): operation handle (created with `operation_new`, see [Operations and Instructions](4_operation_instruction.md)). + +Returns: `0` on success; `-1` when `ptr` or `op` is NULL; `-2` when the block does not exist. + +### circuit_cfg_extend_operations(ptr, node, ops, len) + +Appends a batch of cloned operations to the end of block `node`. Every handle in `ops` is validated and cloned before any mutation, so the block is left untouched when any entry is NULL. + +- `ptr` (`struct CCircuitCFG*`): CFG handle. +- `node` (`uint32_t`): block index. +- `ops` (`const struct COperation* const*`): array of operation handles. +- `len` (`uintptr_t`): number of operations (may be 0). + +Returns: `0` on success; `-1` on NULL input; `-2` when `node` does not address a block. + +### circuit_cfg_blocks(ptr, buffer, len) + +Copies the stable node indices of all blocks in graph order into `buffer` (two-step: query the count with `circuit_cfg_num_blocks` first). + +- `ptr` (`const struct CCircuitCFG*`): CFG handle. +- `buffer` (`uint32_t*`): output buffer. +- `len` (`uintptr_t`): buffer capacity. + +Returns: `0` on success; `-1` when `ptr` is NULL, or when `buffer` is NULL while the graph is non-empty; `-8` when `len` is smaller than the block count. + +### circuit_cfg_block_label(ptr, node) + +Reads the block's diagnostic label. Blocks produced by `circuit_cfg_from_circuit` carry structured labels (such as `entry`, `while_body_…`). + +- `ptr` (`const struct CCircuitCFG*`): CFG handle. +- `node` (`uint32_t`): block index. + +Returns: a newly allocated C string (free with `cqlib_string_free`), or NULL when the block is unlabeled, does not exist, or `ptr` is NULL. + +### circuit_cfg_with_label(ptr, node, label) + +Sets or replaces the label of block `node`. + +- `ptr` (`struct CCircuitCFG*`): CFG handle. +- `node` (`uint32_t`): block index. +- `label` (`const char*`): new label string. + +Returns: `0` on success; `-1` on NULL input; `-2` when `node` does not address a block; `-4` when `label` is not valid UTF-8. + +### circuit_cfg_block_is_empty(ptr, node) + +Returns whether the block has neither operations nor a terminator. + +- `ptr` (`const struct CCircuitCFG*`): CFG handle. +- `node` (`uint32_t`): block index. + +Returns: `1` when empty; `0` otherwise; `-1` when `ptr` is NULL; `-2` when the block does not exist. + +### circuit_cfg_block_operations_len(ptr, node) + +Returns the number of ordinary operations in the block (the terminator excluded). + +- `ptr` (`const struct CCircuitCFG*`): CFG handle. +- `node` (`uint32_t`): block index. + +Returns: the operation count, or 0 when `ptr` is NULL or the block does not exist. + +### circuit_cfg_block_operations(ptr, node, out, len) + +Clones the block's ordinary operations into `out` (two-step: query the count with `circuit_cfg_block_operations_len`). Each written element is a freshly allocated `COperation*` freed separately with `operation_free`. + +- `ptr` (`const struct CCircuitCFG*`): CFG handle. +- `node` (`uint32_t`): block index. +- `out` (`struct COperation**`): output array. +- `len` (`uintptr_t`): array capacity. + +Returns: `0` on success; `-1` when `ptr` is NULL, or when `out` is NULL while the operations are non-empty; `-2` when the block does not exist; `-8` when `len` is too small. + +--- + +## Terminators + +In a valid CFG every basic block must carry exactly one terminator, whose outgoing edge count and types agree with it; `Return` allows no outgoing edge. `Branch` / `ForLoop` / `Switch` terminators are produced by `circuit_cfg_from_circuit` when expanding structured circuits built with `circuit_if_else` / `circuit_for_uint` / `circuit_switch` and friends — see [Classical Data and Control Flow](9_classical_control_flow.md); `Jump` / `Return` / `Break` / `Continue` can be replaced through the four setters below. + +### circuit_cfg_block_has_terminator(ptr, node) + +Returns whether a terminator has been assigned to the block. + +- `ptr` (`const struct CCircuitCFG*`): CFG handle. +- `node` (`uint32_t`): block index. + +Returns: `1` when assigned; `0` when not; `-1` when `ptr` is NULL; `-2` when the block does not exist. + +### circuit_cfg_block_terminator_tag(ptr, node) + +Returns the block's terminator tag. + +- `ptr` (`const struct CCircuitCFG*`): CFG handle. +- `node` (`uint32_t`): block index. + +Returns: one of the `CFG_TERMINATOR_*` constants; `0` when no terminator is set; `-1` when `ptr` is NULL; `-2` when the block does not exist. + +### circuit_cfg_terminator_target(ptr, node) + +Returns the jump target of a `Jump`, `Break`, or `Continue` terminator. + +- `ptr` (`const struct CCircuitCFG*`): CFG handle. +- `node` (`uint32_t`): block index. + +Returns: the target block index, or `UINT32_MAX` for other terminators, missing terminators, unknown blocks, or a NULL handle. + +### circuit_cfg_terminator_condition(ptr, node) + +Clones the condition of a `Branch` or `Switch` terminator. + +- `ptr` (`const struct CCircuitCFG*`): CFG handle. +- `node` (`uint32_t`): block index. + +Returns: a newly allocated `CClassicalExpr*` (free with `classical_expr_free`), or NULL on a NULL handle, an unknown block, a missing terminator, or a non-branching terminator. + +### circuit_cfg_terminator_for_info(ptr, node, var, start, stop, step) + +Writes cloned handles describing a `ForLoop` terminator: the loop variable plus the start, stop, and step expressions (half-open range `[start, stop)`). All four handles are clones: release `var` with `classical_var_free` and the expressions with `classical_expr_free`. + +- `ptr` (`const struct CCircuitCFG*`): CFG handle. +- `node` (`uint32_t`): block index. +- `var` (`struct CClassicalVar**`): loop variable output. +- `start` (`struct CClassicalExpr**`): start expression output. +- `stop` (`struct CClassicalExpr**`): stop expression output. +- `step` (`struct CClassicalExpr**`): step expression output. + +Returns: `0` on success; `-1` when `ptr` is NULL or any output pointer is NULL; `-2` when the block does not exist; `-8` when the terminator is not a `ForLoop`. + +### circuit_cfg_set_terminator_jump(ptr, node, target) + +Replaces the block terminator with an unconditional jump to `target`. + +- `ptr` (`struct CCircuitCFG*`): CFG handle. +- `node` (`uint32_t`): block index. +- `target` (`uint32_t`): target block index. + +Returns: `0` on success; `-1` when `ptr` is NULL; `-2` when the block does not exist. + +### circuit_cfg_set_terminator_return(ptr, node) + +Replaces the block terminator with `Return` (end of execution). + +- `ptr` (`struct CCircuitCFG*`): CFG handle. +- `node` (`uint32_t`): block index. + +Returns: `0` on success; `-1` when `ptr` is NULL; `-2` when the block does not exist. + +### circuit_cfg_set_terminator_break(ptr, node, target) + +Replaces the block terminator with a structured `Break` referencing the header of the loop or `switch` being broken out of. + +- `ptr` (`struct CCircuitCFG*`): CFG handle. +- `node` (`uint32_t`): block index. +- `target` (`uint32_t`): header block index. + +Returns: `0` on success; `-1` when `ptr` is NULL; `-2` when the block does not exist. + +### circuit_cfg_set_terminator_continue(ptr, node, target) + +Replaces the block terminator with a structured `Continue` referencing the loop header. + +- `ptr` (`struct CCircuitCFG*`): CFG handle. +- `node` (`uint32_t`): block index. +- `target` (`uint32_t`): loop header block index. + +Returns: `0` on success; `-1` when `ptr` is NULL; `-2` when the block does not exist. + +--- + +## Edges and traversal + +### circuit_cfg_add_edge(ptr, source, target, flow, case_lo, case_hi) + +Adds a directed semantic edge between two existing blocks. + +- `ptr` (`struct CCircuitCFG*`): CFG handle. +- `source` (`uint32_t`): source block index. +- `target` (`uint32_t`): target block index. +- `flow` (`uint32_t`): one of the `CFG_FLOW_*` tags. +- `case_lo` (`uint64_t`): least-significant 64 bits of the 128-bit case value for `CFG_FLOW_CASE`; pass 0 for other tags. +- `case_hi` (`uint64_t`): most-significant 64 bits of the case value. + +Returns: `0` on success; `-1` when `ptr` is NULL; `-2` when either endpoint does not exist (including the sentinel); `-8` on an unknown `flow` tag. + +### circuit_cfg_set_entry_block(ptr, node) + +Sets the graph entry block; whether the index points at a real block is checked by `circuit_cfg_validate`. + +- `ptr` (`struct CCircuitCFG*`): CFG handle. +- `node` (`uint32_t`): entry block index. + +Returns: `0` on success; `-1` when `ptr` is NULL; `-2` when `node` is the `UINT32_MAX` sentinel. + +### circuit_cfg_outgoing_edges_len(ptr, node) + +Returns the number of outgoing edges of a block. + +- `ptr` (`const struct CCircuitCFG*`): CFG handle. +- `node` (`uint32_t`): block index. + +Returns: the edge count, or 0 when `ptr` is NULL or the block does not exist. + +### circuit_cfg_outgoing_edges(ptr, node, out_targets, flow_tags, case_lo, case_hi, len) + +Copies the outgoing edges of a block into four parallel arrays (two-step: query the count with `circuit_cfg_outgoing_edges_len`). Edges are written in insertion order, with the true branch first for conditional headers. + +- `ptr` (`const struct CCircuitCFG*`): CFG handle. +- `node` (`uint32_t`): block index. +- `out_targets` (`uint32_t*`): array of target block indices. +- `flow_tags` (`uint32_t*`): array of `CFG_FLOW_*` tags. +- `case_lo` (`uint64_t*`): least-significant 64 bits of the 128-bit case value for `CFG_FLOW_CASE` edges; zeros otherwise. +- `case_hi` (`uint64_t*`): array of most-significant case value halves. +- `len` (`uintptr_t`): array capacity. + +Returns: `0` on success; `-1` when `ptr` is NULL, or when any array is NULL while the edges are non-empty; `-2` when the block does not exist; `-8` when `len` is too small. + +--- + +## Regions and loops + +Control-flow regions record the boundaries and semantics of structured control flow: graph edges alone cannot fully recover the structured meaning of `if` / `while` / `for` / `switch`, so region metadata describes each construct's branch entries, exit blocks, and merge blocks. Keep the region metadata in sync whenever the graph is edited — keeping regions consistent with the graph edges is the key to a successful `circuit_cfg_to_circuit` reconstruction. + +### circuit_cfg_region_tag(ptr, node) + +Returns the control-flow region tag owned by a block. + +- `ptr` (`const struct CCircuitCFG*`): CFG handle. +- `node` (`uint32_t`): block index. + +Returns: one of the `CFG_REGION_*` constants; `0` when the block owns no region; `-1` when `ptr` is NULL; `-2` when the block does not exist. + +### circuit_cfg_is_loop_header(ptr, node) + +Returns whether the block is a loop header: a block owning a `While` or `For` region returns `1`; a `Switch` region entry does not count as a loop header. + +- `ptr` (`const struct CCircuitCFG*`): CFG handle. +- `node` (`uint32_t`): block index. + +Returns: `1` when yes; `0` when no; `-1` when `ptr` is NULL; `-2` when the block does not exist. + +### circuit_cfg_region_if(ptr, node, then_entry, else_entry, merge_block, has_else) + +Writes the `If` region layout. + +- `ptr` (`const struct CCircuitCFG*`): CFG handle. +- `node` (`uint32_t`): header block index. +- `then_entry` (`uint32_t*`): entry block of the then branch. +- `else_entry` (`uint32_t*`): entry block of the else branch; the merge block when no else body exists. +- `merge_block` (`uint32_t*`): merge block. +- `has_else` (`int32_t*`): whether the source circuit has an else body (1/0). + +Returns: `0` on success; `-1` when `ptr` is NULL or any output pointer is NULL; `-2` when the block does not exist; `-8` when the block owns no `If` region. + +### circuit_cfg_region_loop(ptr, node, body_entry, exit_block) + +Writes the `While` / `For` region layout. + +- `ptr` (`const struct CCircuitCFG*`): CFG handle. +- `node` (`uint32_t`): header block index. +- `body_entry` (`uint32_t*`): loop body entry block. +- `exit_block` (`uint32_t*`): exit block. + +Returns: `0` on success; `-1` when `ptr` is NULL or an output pointer is NULL; `-2` when the block does not exist; `-8` when the block owns no loop region. + +### circuit_cfg_region_switch_cases_len(ptr, node) + +Returns the number of explicit cases of a `Switch` region. + +- `ptr` (`const struct CCircuitCFG*`): CFG handle. +- `node` (`uint32_t`): header block index. + +Returns: the case count, or 0 when `ptr` is NULL, the block does not exist, or the region is not a `Switch`. + +### circuit_cfg_region_switch(ptr, node, case_lo, case_hi, case_entries, len, default_entry, merge_block, has_default) + +Writes the `Switch` region layout (two-step: query the case count with `circuit_cfg_region_switch_cases_len` first). + +- `ptr` (`const struct CCircuitCFG*`): CFG handle. +- `node` (`uint32_t`): header block index. +- `case_lo` (`uint64_t*`): least-significant 64 bits of case `i`'s 128-bit match value. +- `case_hi` (`uint64_t*`): most-significant 64 bits of case `i`'s match value. +- `case_entries` (`uint32_t*`): entry block of case `i`'s branch. +- `len` (`uintptr_t`): capacity of the case arrays. +- `default_entry` (`uint32_t*`): default branch entry block; the merge block when no default body exists. +- `merge_block` (`uint32_t*`): merge block. +- `has_default` (`int32_t*`): whether the source circuit has a default body (1/0). + +Returns: `0` on success; `-1` when `ptr` is NULL or a required output pointer is NULL; `-2` when the block does not exist; `-8` when the block owns no `Switch` region or `len` is smaller than the case count. + +### circuit_cfg_set_if_region(ptr, node, then_entry, else_entry, merge_block, has_else, outer_op) + +Attaches an `If` region to a header block. Whether the entry indices point at real blocks is checked by `circuit_cfg_validate`. + +- `ptr` (`struct CCircuitCFG*`): CFG handle. +- `node` (`uint32_t`): header block index. +- `then_entry` (`uint32_t`): entry block of the then branch. +- `else_entry` (`uint32_t`): entry block of the else branch. +- `merge_block` (`uint32_t`): merge block. +- `has_else` (`int32_t`): whether an else body exists (1/0). +- `outer_op` (`const struct COperation*`): outer operation metadata (acted qubits, parameters, label); pass NULL for empty metadata. + +Returns: `0` on success; `-1` when `ptr` is NULL; `-2` when the header block does not exist. + +### circuit_cfg_set_while_region(ptr, node, body_entry, exit_block, outer_op) + +Attaches a `While` region to a header block. + +- `ptr` (`struct CCircuitCFG*`): CFG handle. +- `node` (`uint32_t`): header block index. +- `body_entry` (`uint32_t`): loop body entry block. +- `exit_block` (`uint32_t`): exit block. +- `outer_op` (`const struct COperation*`): outer operation metadata; may be NULL. + +Returns: `0` on success; `-1` when `ptr` is NULL; `-2` when the header block does not exist. + +### circuit_cfg_set_for_region(ptr, node, body_entry, exit_block, outer_op) + +Attaches a `For` region to a header block; the parameters mean the same as in `circuit_cfg_set_while_region`. + +- `ptr` (`struct CCircuitCFG*`): CFG handle. +- `node` (`uint32_t`): header block index. +- `body_entry` (`uint32_t`): loop body entry block. +- `exit_block` (`uint32_t`): exit block. +- `outer_op` (`const struct COperation*`): outer operation metadata; may be NULL. + +Returns: `0` on success; `-1` when `ptr` is NULL; `-2` when the header block does not exist. + +### circuit_cfg_set_switch_region(ptr, node, case_lo, case_hi, case_entries, len, default_entry, merge_block, has_default, outer_op) + +Attaches a `Switch` region to a header block: case `i`'s 128-bit match value is assembled from `case_lo[i]` (least-significant 64 bits) and `case_hi[i]` (most-significant 64 bits), with `case_entries[i]` as its branch entry block. + +- `ptr` (`struct CCircuitCFG*`): CFG handle. +- `node` (`uint32_t`): header block index. +- `case_lo` (`const uint64_t*`): least-significant halves of the case match values. +- `case_hi` (`const uint64_t*`): most-significant halves of the case match values. +- `case_entries` (`const uint32_t*`): branch entry blocks of the cases. +- `len` (`uintptr_t`): number of cases. +- `default_entry` (`uint32_t`): default branch entry block. +- `merge_block` (`uint32_t`): merge block. +- `has_default` (`int32_t`): whether a default body exists (1/0). +- `outer_op` (`const struct COperation*`): outer operation metadata; may be NULL. + +Returns: `0` on success; `-1` when `ptr` is NULL, or when any case array is NULL while `len > 0`; `-2` when the header block does not exist. + +--- + +## Tag constants + +Outgoing-edge tags (the `flow` argument of `circuit_cfg_add_edge`, the `flow_tags` array of `circuit_cfg_outgoing_edges`): + +| Constant | Value | Meaning | +| --- | --- | --- | +| `CFG_FLOW_TRUE_BRANCH` | 1 | True edge out of `if` / `while` / `for` headers | +| `CFG_FLOW_FALSE_BRANCH` | 2 | False edge out of `if` / `while` / `for` headers | +| `CFG_FLOW_UNCONDITIONAL` | 3 | Fallthrough jump / structured merge | +| `CFG_FLOW_CASE` | 4 | Exact-value switch case edge (128-bit value in the case halves) | +| `CFG_FLOW_DEFAULT_CASE` | 5 | Switch default edge | +| `CFG_FLOW_BREAK` | 6 | Structured `break` edge | +| `CFG_FLOW_CONTINUE` | 7 | Structured `continue` edge | + +Terminator tags (`circuit_cfg_block_terminator_tag`): + +| Constant | Value | Meaning | +| --- | --- | --- | +| `CFG_TERMINATOR_BRANCH` | 1 | Boolean branch header (`Branch`) | +| `CFG_TERMINATOR_FOR_LOOP` | 2 | Unsigned range loop header (`ForLoop`) | +| `CFG_TERMINATOR_SWITCH` | 3 | Exact-value multi-way branch header (`Switch`) | +| `CFG_TERMINATOR_JUMP` | 4 | Unconditional jump | +| `CFG_TERMINATOR_BREAK` | 5 | Structured break | +| `CFG_TERMINATOR_CONTINUE` | 6 | Structured continue | +| `CFG_TERMINATOR_RETURN` | 7 | End of execution (`Return`) | + +Region tags (`circuit_cfg_region_tag`): + +| Constant | Value | Meaning | +| --- | --- | --- | +| `CFG_REGION_IF` | 1 | Structured conditional region | +| `CFG_REGION_WHILE` | 2 | Structured while-loop region | +| `CFG_REGION_FOR` | 3 | Structured range-loop region | +| `CFG_REGION_SWITCH` | 4 | Structured exact-value switch region | + +--- + +## Example: building an if-else CFG and converting it back + +First build `if (flag) { x(1) } else { z(1) }` at the circuit level, then expand it into a CFG, read the `If` region layout and the header's outgoing edges, and finally validate and reconstruct the structured circuit: + +```c +#include +#include +#include "cqlib_c.h" + +/* then body: X(1) */ +static int32_t append_x(struct CCircuit* circuit, void* user_data) { + (void)user_data; + return circuit_x(circuit, 1); +} + +/* else body: Z(1) */ +static int32_t append_z(struct CCircuit* circuit, void* user_data) { + (void)user_data; + return circuit_z(circuit, 1); +} + +int main(void) { + struct CCircuit* circuit = circuit_new(2); + struct CClassicalVar* flag = circuit_var(circuit, CQLIB_CLASSICAL_TYPE_BOOL, 0); + struct CClassicalExpr* condition = classical_expr_var(flag); + if (circuit_if_else(circuit, condition, append_x, append_z, NULL) != 0) { + return 1; + } + + /* 1. Expand into a CFG */ + struct CCircuitCFG* cfg = circuit_cfg_from_circuit(circuit); + if (cfg == NULL) { + return 1; + } + + /* 2. Locate the if header and read the region layout and edges */ + uint32_t then_entry = UINT32_MAX; + uint32_t else_entry = UINT32_MAX; + uint32_t merge_block = UINT32_MAX; + int32_t has_else = 0; + uint32_t block_count = (uint32_t)circuit_cfg_num_blocks(cfg); + + for (uint32_t node = 0; node < block_count; node++) { + if (circuit_cfg_region_tag(cfg, node) == CFG_REGION_IF) { + circuit_cfg_region_if(cfg, node, &then_entry, &else_entry, + &merge_block, &has_else); + + /* The header terminator is Branch; its two edges are written in + * insertion order, the true branch first. */ + uint32_t targets[2] = {0, 0}; + uint32_t tags[2] = {0, 0}; + uint64_t lo[2] = {0, 0}; + uint64_t hi[2] = {0, 0}; + circuit_cfg_outgoing_edges(cfg, node, targets, tags, lo, hi, 2); + /* targets[0] == then_entry && tags[0] == CFG_FLOW_TRUE_BRANCH, + targets[1] == else_entry && tags[1] == CFG_FLOW_FALSE_BRANCH */ + break; + } + } + + /* 3. Validate and convert back to a structured circuit */ + struct CCircuit* lowered = NULL; + if (circuit_cfg_validate(cfg) == 0) { + lowered = circuit_cfg_to_circuit(cfg); + } + + circuit_free(lowered); + circuit_cfg_free(cfg); + classical_expr_free(condition); + classical_var_free(flag); + circuit_free(circuit); + return 0; +} +``` + +For the circuit-building entry points (`circuit_new`, `circuit_if_else`, and friends) see [Circuit](1_circuit.md); for the classical expression builders (`classical_expr_var` and friends) see [Classical Data and Control Flow](9_classical_control_flow.md). diff --git a/docs/api/en/c/0_circuit/13_circuit_to_matrix.md b/docs/api/en/c/0_circuit/13_circuit_to_matrix.md new file mode 100644 index 00000000..e3ea37b1 --- /dev/null +++ b/docs/api/en/c/0_circuit/13_circuit_to_matrix.md @@ -0,0 +1,116 @@ +# Circuit to Matrix + +`circuit_to_matrix_len` and `circuit_to_matrix` convert a numeric circuit into its unitary matrix: `n` qubits yield a `2^n × 2^n` matrix, written row-major as interleaved `(re, im)` doubles and including the circuit's global phase. The matrix size grows as `4^n` with the qubit count, so these entry points suit small circuits. For conversions that keep symbolic expressions see [Symbolic Matrix](10_symbolic_matrix.md); for the common conventions (error codes, handle release, two-step array output) see [Overview](../0_overview.md). + +--- + +## Two-step output + +### circuit_to_matrix_len(ptr, qubits_order, order_len) + +Returns the number of complex elements in the unitary matrix (rows × cols), used to allocate the output buffer. Pass `qubits_order` = NULL to use the default ordering. + +- `ptr` (`const struct CCircuit*`): circuit handle. +- `qubits_order` (`const uintptr_t*`): qubit order array; may be NULL. +- `order_len` (`uintptr_t`): length of the order array. + +Returns: the complex element count, or 0 when `ptr` is NULL or the conversion fails (including unresolved symbolic parameters, non-unitary operations, or an invalid qubit order). + +### circuit_to_matrix(ptr, qubits_order, order_len, out, buffer_len) + +Copies the unitary matrix into `out` as interleaved `(real, imag)` doubles and returns the number of complex elements written. + +- `ptr` (`const struct CCircuit*`): circuit handle. +- `qubits_order` (`const uintptr_t*`): qubit order array; may be NULL. +- `order_len` (`uintptr_t`): length of the order array. +- `out` (`double*`): output buffer; it must hold at least `2 × circuit_to_matrix_len(...)` doubles. +- `buffer_len` (`uintptr_t`): buffer capacity (in doubles). + +Returns: the number of complex elements written, or 0 when `ptr` or `out` is NULL, `buffer_len` is too small (rejected before writing), or the conversion fails. + +Complex element `i` is `{ out[2*i], out[2*i+1] }`, located at matrix row `i / 2^n`, column `i % 2^n` (row-major). + +--- + +## Qubit order + +`qubits_order` selects the qubit arrangement used for the basis-state indices of the matrix. It must be an exact permutation of the circuit's qubit set — no missing, duplicated, or unknown qubits — otherwise the conversion fails (both entry points return 0). + +- `NULL` / `order_len == 0`: qubits are sorted by numeric id; +- the first qubit in the order corresponds to the least-significant basis-state bit (little-endian); under the default ordering the lowest-numbered qubit (for example qubit 0) is the least-significant bit. + +```c +/* Explicit order: qubit 1 is the least-significant bit */ +uintptr_t order[2] = {1, 0}; +uintptr_t len = circuit_to_matrix_len(circuit, order, 2); +``` + +--- + +## Parameterized circuits + +The numeric matrix requires every symbolic parameter in the circuit to resolve to a concrete value. While unresolved symbolic parameters remain, both entry points return 0; bind the parameters first with `circuit_assign_params` (see [Circuit](1_circuit.md): + +```c +struct CParameter* theta = param_parse("theta"); + +struct CCircuit* circuit = circuit_new(1); +circuit_rx_param(circuit, 0, theta); + +/* theta is unbound: returns 0 */ +uintptr_t unbound = circuit_to_matrix_len(circuit, NULL, 0); + +/* Bind theta = 0.5, then convert: 1 qubit → 2 × 2 = 4 */ +struct CCircuit* bound = circuit_assign_params(circuit, "theta:0.5"); +uintptr_t len = circuit_to_matrix_len(bound, NULL, 0); +``` + +--- + +## Global phase + +The matrix includes the circuit's global phase `theta`: if `U` is the circuit matrix without the global phase, the conversion returns `exp(i * theta) * U`. + +--- + +## Example + +Two-step retrieval of the 4 × 4 unitary of a 2-qubit circuit (H + CX): + +```c +#include +#include +#include +#include "cqlib_c.h" + +int main(void) { + struct CCircuit* circuit = circuit_new(2); + circuit_h(circuit, 0); + circuit_cx(circuit, 0, 1); + + /* Step 1: query the complex element count (2^2 × 2^2 = 16) */ + uintptr_t len = circuit_to_matrix_len(circuit, NULL, 0); + if (len == 0) { + circuit_free(circuit); + return 1; + } + + /* Step 2: allocate at least 2 * len doubles and copy */ + double* buffer = (double*)malloc(len * 2 * sizeof(double)); + uintptr_t written = circuit_to_matrix(circuit, NULL, 0, buffer, len * 2); + if (written != len) { + free(buffer); + circuit_free(circuit); + return 1; + } + + /* For example, element [0][3] lives at buffer[6] (real) and buffer[7] (imag) */ + printf("U[0][3] = %g + %gi\n", buffer[6], buffer[7]); + + free(buffer); + circuit_free(circuit); + return 0; +} +``` + +For the circuit-building and parameter-binding entry points (`circuit_new`, `circuit_assign_params`, and friends) see [Circuit](1_circuit.md). diff --git a/docs/api/en/c/0_circuit/14_circuit_dag.md b/docs/api/en/c/0_circuit/14_circuit_dag.md new file mode 100644 index 00000000..d0f0167c --- /dev/null +++ b/docs/api/en/c/0_circuit/14_circuit_dag.md @@ -0,0 +1,943 @@ +# Circuit DAG + +`CCircuitDag*` is the operation-dependency view of a circuit: nodes represent operations, and edges represent the ordering constraints between two operations that share a resource. Shared resources include qubits, mutable classical variables, immutable classical values, and the global order that carries operations with no concrete data resource; each wire corresponds to a pair of input / output sentinel nodes in the graph. The typical workflow is `circuit_dag_from_circuit` → inspect and validate → `circuit_dag_to_circuit`; the DAG can also be queried in depth (node identification, traversal, layering, wire timelines, gate runs, control flow) and edited in place (append, remove, substitute) before being lowered back. For the common conventions (error codes, handle release, two-step array output) see [Overview](../0_overview.md). + +--- + +## Construction and release + +### circuit_dag_from_circuit(ptr) + +Builds a dependency DAG from an existing circuit, copying the circuit's qubits, parameter table, symbol names, classical variable and value type tables, and global phase; the DAG passes its internal validation by the time construction finishes. + +- `ptr` (`const struct CCircuit*`): source circuit handle. + +Returns: a newly allocated `CCircuitDag*` (free with `circuit_dag_free`), or NULL when `ptr` is NULL or the build fails. + +### circuit_dag_from_operations(qubits, qubits_len, ops, ops_len) + +Builds a dependency DAG from explicit qubits and an operation slice — the narrow entry point for local analysis: it takes no parameter table, classical declarations, or global phase. Each element of `qubits` is used directly as a qubit id, every operation must act on registered ids, and duplicate ids, out-of-set references, or non-self-contained operations (carrying parameter-table indices, classical variable / value references, or control-flow bodies) fail the build. + +- `qubits` (`const uint32_t*`): array of qubit ids. +- `qubits_len` (`uintptr_t`): number of qubits. +- `ops` (`const struct COperation* const*`): array of operation handles (created with `operation_new`, see [Operations and Instructions](4_operation_instruction.md)). +- `ops_len` (`uintptr_t`): number of operations. + +Returns: a newly allocated `CCircuitDag*` (free with `circuit_dag_free`), or NULL when an input array is NULL while its length is positive (`qubits` NULL with `qubits_len > 0`, `ops` NULL with `ops_len > 0`, a NULL entry inside the array) or the build fails. + +### circuit_dag_free(ptr) + +Frees a DAG handle. + +- `ptr` (`struct CCircuitDag*`): handle to free; passing NULL is allowed. + +--- + +## Conversion and validation + +### circuit_dag_to_circuit(ptr) + +Rebuilds the source-order circuit from the DAG, restoring the qubit set, classical type tables, and global phase; the source handle stays alive and unchanged. + +- `ptr` (`const struct CCircuitDag*`): DAG handle. + +Returns: a newly allocated `CCircuit*` (free with `circuit_free`), or NULL when `ptr` is NULL or the lowering fails (a cyclic graph, an invalid parameter index, and so on). + +### circuit_dag_validate(ptr) + +Validates DAG consistency: whether the graph is cyclic, whether every wire's input and output sentinels are legal, whether the qubits and parameter indices referenced by operations are valid, whether the resources carried by edges are legal, and more. + +- `ptr` (`const struct CCircuitDag*`): DAG handle. + +Returns: `0` on success; `-1` when `ptr` is NULL; `-3` on a cyclic or invalid graph. + +--- + +## Metadata and statistics + +### circuit_dag_num_qubits(ptr) + +Returns the number of qubits. + +- `ptr` (`const struct CCircuitDag*`): DAG handle. + +Returns: the qubit count, or 0 when `ptr` is NULL. + +### circuit_dag_num_ops(ptr) + +Returns the number of operation nodes. + +- `ptr` (`const struct CCircuitDag*`): DAG handle. + +Returns: the operation-node count, or 0 when `ptr` is NULL. + +### circuit_dag_is_empty(ptr) + +Returns whether the DAG has no operation nodes. + +- `ptr` (`const struct CCircuitDag*`): DAG handle. + +Returns: `1` when empty; `0` otherwise; `-1` when `ptr` is NULL. + +### circuit_dag_qubits_len(ptr) + +Returns the number of qubit ids in the DAG, used to size the output buffer. + +- `ptr` (`const struct CCircuitDag*`): DAG handle. + +Returns: the id count, or 0 when `ptr` is NULL. + +### circuit_dag_qubits(ptr, buffer, len) + +Copies the qubit ids in registration order into `buffer` (two-step: query the count with `circuit_dag_qubits_len` first). + +- `ptr` (`const struct CCircuitDag*`): DAG handle. +- `buffer` (`uint32_t*`): output buffer. +- `len` (`uintptr_t`): buffer capacity. + +Returns: `0` on success; `-1` when `ptr` is NULL, or when `buffer` is NULL while the id set is non-empty; `-8` when `len` is too small. + +### circuit_dag_parameters_len(ptr) + +Returns the number of registered parameters. + +- `ptr` (`const struct CCircuitDag*`): DAG handle. + +Returns: the parameter count, or 0 when `ptr` is NULL. + +### circuit_dag_parameters(ptr, out, len) + +Copies the parameter names in registration order into `out` (two-step: query the count with `circuit_dag_parameters_len`). Each written element is a freshly allocated C string freed separately with `cqlib_string_free`. + +- `ptr` (`const struct CCircuitDag*`): DAG handle. +- `out` (`char**`): output array. +- `len` (`uintptr_t`): array capacity. + +Returns: `0` on success; `-1` when `ptr` is NULL, or when `out` is NULL while parameters exist; `-8` when `len` is too small. + +### circuit_dag_add_parameter(ptr, param, out_index, out_inserted) + +Interns a copy of `param` into the DAG's parameter table: the parameter is inserted when absent, and all of its symbol names are registered. + +- `ptr` (`struct CCircuitDag*`): DAG handle. +- `param` (`const struct CParameter*`): parameter handle, created with `param_parse` (see [Parameters](3_parameter.md)). +- `out_index` (`uintptr_t*`): optional output for the parameter's index in the table; NULL is allowed. +- `out_inserted` (`int32_t*`): optional output flag; `1` when the parameter was newly inserted, `0` when it already existed; NULL is allowed. + +Returns: `0` on success; `-1` when `ptr` or `param` is NULL. + +### circuit_dag_symbols_len(ptr) + +Returns the number of free symbols referenced by the DAG. + +- `ptr` (`const struct CCircuitDag*`): DAG handle. + +Returns: the symbol count, or 0 when `ptr` is NULL. + +### circuit_dag_symbols(ptr, out, len) + +Copies the symbol names into `out` (two-step: query the count with `circuit_dag_symbols_len`). Each written element is a freshly allocated C string freed separately with `cqlib_string_free`. + +- `ptr` (`const struct CCircuitDag*`): DAG handle. +- `out` (`char**`): output array. +- `len` (`uintptr_t`): array capacity. + +Returns: `0` on success; `-1` when `ptr` is NULL, or when `out` is NULL while symbols exist; `-8` when `len` is too small. + +--- + +## Wires + +The resource carried by an edge is called a wire and doubles as the wire's identity. Qubit wires are all materialized at construction; classical variable and value wires are materialized only when referenced by an operation; the global-order wire is always present. A wire is identified by a `(tag, id)` pair: + +| Constant | Value | Meaning | +| --- | --- | --- | +| `DAG_WIRE_QUBIT` | 0 | Qubit timeline, `id` is the qubit id | +| `DAG_WIRE_CLASSICAL_VAR` | 1 | Mutable classical storage, `id` is the variable id | +| `DAG_WIRE_CLASSICAL_VALUE` | 2 | Immutable classical value, `id` is the value index | +| `DAG_WIRE_GLOBAL_ORDER` | 3 | Global ordering resource with no payload (`id` is 0) | + +### circuit_dag_wires_len(ptr) + +Returns the number of wires materialized in the DAG. + +- `ptr` (`const struct CCircuitDag*`): DAG handle. + +Returns: the wire count, or 0 when `ptr` is NULL. + +### circuit_dag_wires(ptr, tags, ids, len) + +Copies the tags and payload ids of the materialized wires into the parallel arrays `tags` and `ids` (two-step: query the count with `circuit_dag_wires_len`). The global-order wire comes first, followed by the qubit wires; the `id` of a `DAG_WIRE_GLOBAL_ORDER` wire is 0. + +- `ptr` (`const struct CCircuitDag*`): DAG handle. +- `tags` (`uint32_t*`): array of `DAG_WIRE_*` tags. +- `ids` (`uint32_t*`): array of payload ids. +- `len` (`uintptr_t`): array capacity. + +Returns: `0` on success; `-1` when `ptr` is NULL, or when either array is NULL while wires exist; `-8` when `len` is too small. + +### circuit_dag_has_wire(ptr, tag, id) + +Returns whether the DAG carries the given wire. + +- `ptr` (`const struct CCircuitDag*`): DAG handle. +- `tag` (`uint32_t`): `DAG_WIRE_QUBIT` (`id` is the qubit id) or `DAG_WIRE_GLOBAL_ORDER` (`id` is ignored). +- `id` (`uint32_t`): payload id of the wire. + +Returns: `1` when present; `0` when not; `-1` when `ptr` is NULL; `-8` for other `tag` values. + +--- + +## Node identification + +Every graph node — operation node or wire sentinel — is identified by a `uint32_t` node id. These helpers tell the kinds apart and fetch stored content. + +### circuit_dag_is_operation(ptr, node) + +Returns whether `node` is an operation node. + +- `ptr` (`const struct CCircuitDag*`): DAG handle. +- `node` (`uint32_t`): node id. + +Returns: `1` for an operation node; `0` for a wire sentinel; `-1` when `ptr` is NULL; `-2` for an unknown node id. + +### circuit_dag_node_kind(ptr, node) + +Returns the kind of `node` as a freshly allocated C string: `"wire_in"`, `"wire_out"`, or `"operation"`. + +- `ptr` (`const struct CCircuitDag*`): DAG handle. +- `node` (`uint32_t`): node id. + +Returns: the kind string (free with `cqlib_string_free`), or NULL when `ptr` is NULL or the node id is unknown. + +### circuit_dag_operation(ptr, node) + +Returns a copy of the operation stored at an operation node. + +- `ptr` (`const struct CCircuitDag*`): DAG handle. +- `node` (`uint32_t`): node id. + +Returns: a newly allocated `COperation*` (free with `operation_free`, see [Operations and Instructions](4_operation_instruction.md)), or NULL when `ptr` is NULL or the node is not an operation node. + +--- + +## Graph traversal + +All traversal helpers use the two-step array output: query the count with the `_len` variant, then call the copy variant. The queries differ in what they list: + +- `circuit_dag_op_nodes` / `circuit_dag_topological_op_nodes` list the operation nodes in source order and in deterministic topological order (ties are broken lexicographically, so the order is stable across runs). +- `circuit_dag_predecessors` / `circuit_dag_successors` return the raw graph neighbors of a node: wire sentinels included, a neighbor listed once per connecting edge (two nodes sharing several resources are connected by one edge per wire — parallel edges over the same wire are never created), and the order is unspecified. Sizing the buffer with the `_len` variant always suffices. +- The wire-filtered queries (`*_on_wire`, `quantum_*`, `classical_*`) return operation neighbors only, deduplicated and ordered by the deterministic topological key. + +### circuit_dag_op_nodes_len(ptr) + +Returns the number of operation nodes in source order. + +- `ptr` (`const struct CCircuitDag*`): DAG handle. + +Returns: the operation-node count, or 0 when `ptr` is NULL. + +### circuit_dag_op_nodes(ptr, buffer, len) + +Copies the operation node ids in source order into `buffer` (two-step: query the count with `circuit_dag_op_nodes_len`). + +- `ptr` (`const struct CCircuitDag*`): DAG handle. +- `buffer` (`uint32_t*`): output buffer. +- `len` (`uintptr_t`): buffer capacity. + +Returns: `0` on success; `-1` when `ptr` is NULL, or when `buffer` is NULL while nodes exist; `-8` when `len` is too small. + +### circuit_dag_topological_op_nodes_len(ptr) + +Returns the number of operation nodes in deterministic topological order. A cyclic graph also reports 0, so pair this with `circuit_dag_validate` when the graph's health is unknown. + +- `ptr` (`const struct CCircuitDag*`): DAG handle. + +Returns: the operation-node count, or 0 when `ptr` is NULL or the graph is cyclic. + +### circuit_dag_topological_op_nodes(ptr, buffer, len) + +Copies the operation node ids in deterministic topological order into `buffer` (two-step: query the count with `circuit_dag_topological_op_nodes_len`). + +- `ptr` (`const struct CCircuitDag*`): DAG handle. +- `buffer` (`uint32_t*`): output buffer. +- `len` (`uintptr_t`): buffer capacity. + +Returns: `0` on success; `-1` when `ptr` is NULL, or when `buffer` is NULL while nodes exist; `-3` on a cyclic graph; `-8` when `len` is too small. + +### circuit_dag_predecessors_len(ptr, node) + +Returns the number of raw direct-predecessor entries of `node` — sendinels included, one entry per connecting edge (see the section preamble). + +- `ptr` (`const struct CCircuitDag*`): DAG handle. +- `node` (`uint32_t`): node id. + +Returns: the entry count, or 0 when `ptr` is NULL or `node` is unknown. + +### circuit_dag_predecessors(ptr, node, buffer, len) + +Copies the raw direct predecessors of `node` into `buffer` (two-step: query the count with `circuit_dag_predecessors_len`). + +- `ptr` (`const struct CCircuitDag*`): DAG handle. +- `node` (`uint32_t`): node id. +- `buffer` (`uint32_t*`): output buffer. +- `len` (`uintptr_t`): buffer capacity. + +Returns: `0` on success; `-1` when `ptr` is NULL, or when `buffer` is NULL while entries exist; `-2` for an unknown node id; `-8` when `len` is too small. + +### circuit_dag_successors_len(ptr, node) + +Returns the number of raw direct-successor entries of `node` — sentinels included, one entry per connecting edge. + +- `ptr` (`const struct CCircuitDag*`): DAG handle. +- `node` (`uint32_t`): node id. + +Returns: the entry count, or 0 when `ptr` is NULL or `node` is unknown. + +### circuit_dag_successors(ptr, node, buffer, len) + +Copies the raw direct successors of `node` into `buffer` (two-step: query the count with `circuit_dag_successors_len`). + +- `ptr` (`const struct CCircuitDag*`): DAG handle. +- `node` (`uint32_t`): node id. +- `buffer` (`uint32_t*`): output buffer. +- `len` (`uintptr_t`): buffer capacity. + +Returns: `0` on success; `-1` when `ptr` is NULL, or when `buffer` is NULL while entries exist; `-2` for an unknown node id; `-8` when `len` is too small. + +### circuit_dag_predecessors_on_wire_len(ptr, node, tag, id) + +Returns the number of operation predecessors of `node` connected through the wire `(tag, id)`; results are deduplicated and ordered by the deterministic topological key. + +- `ptr` (`const struct CCircuitDag*`): DAG handle. +- `node` (`uint32_t`): node id. +- `tag` (`uint32_t`): `DAG_WIRE_QUBIT` (`id` is the qubit id) or `DAG_WIRE_GLOBAL_ORDER` (`id` is ignored); classical tags carry circuit-scoped handles that C cannot reconstruct and are rejected with `-8`. This applies to every `(tag, id)` wire input below. +- `id` (`uint32_t`): payload id of the wire. + +Returns: the neighbor count, or 0 when `ptr` is NULL, `node` is unknown, the tag is unknown, or the wire/node combination is invalid. + +### circuit_dag_predecessors_on_wire(ptr, node, tag, id, buffer, len) + +Copies the operation predecessors of `node` connected through the wire `(tag, id)` into `buffer` (two-step: query the count with `circuit_dag_predecessors_on_wire_len`). + +- `ptr` (`const struct CCircuitDag*`): DAG handle. +- `node` (`uint32_t`): node id. +- `tag` (`uint32_t`): `DAG_WIRE_QUBIT` or `DAG_WIRE_GLOBAL_ORDER`. +- `id` (`uint32_t`): payload id of the wire. +- `buffer` (`uint32_t*`): output buffer. +- `len` (`uintptr_t`): buffer capacity. + +Returns: `0` on success; `-1` when `ptr` is NULL, or when `buffer` is NULL while neighbors exist; `-2` for an unknown node id; `-3` for an invalid wire/node combination (for example a wire that is not part of this DAG); `-8` for an unknown wire tag or a too-small buffer. + +### circuit_dag_successors_on_wire_len(ptr, node, tag, id) + +Returns the number of operation successors of `node` connected through the wire `(tag, id)`; results are deduplicated and ordered by the deterministic topological key. + +- `ptr` (`const struct CCircuitDag*`): DAG handle. +- `node` (`uint32_t`): node id. +- `tag` (`uint32_t`): `DAG_WIRE_QUBIT` or `DAG_WIRE_GLOBAL_ORDER`. +- `id` (`uint32_t`): payload id of the wire. + +Returns: the neighbor count, or 0 when `ptr` is NULL, `node` is unknown, the tag is unknown, or the wire/node combination is invalid. + +### circuit_dag_successors_on_wire(ptr, node, tag, id, buffer, len) + +Copies the operation successors of `node` connected through the wire `(tag, id)` into `buffer` (two-step: query the count with `circuit_dag_successors_on_wire_len`). + +- `ptr` (`const struct CCircuitDag*`): DAG handle. +- `node` (`uint32_t`): node id. +- `tag` (`uint32_t`): `DAG_WIRE_QUBIT` or `DAG_WIRE_GLOBAL_ORDER`. +- `id` (`uint32_t`): payload id of the wire. +- `buffer` (`uint32_t*`): output buffer. +- `len` (`uintptr_t`): buffer capacity. + +Returns: `0` on success; `-1` when `ptr` is NULL, or when `buffer` is NULL while neighbors exist; `-2` for an unknown node id; `-3` for an invalid wire/node combination; `-8` for an unknown wire tag or a too-small buffer. + +### circuit_dag_quantum_predecessors_len(ptr, node) + +Returns the number of operation predecessors of `node` connected through any quantum wire; results are deduplicated and ordered by the deterministic topological key. + +- `ptr` (`const struct CCircuitDag*`): DAG handle. +- `node` (`uint32_t`): node id. + +Returns: the neighbor count, or 0 when `ptr` is NULL, `node` is unknown, or the query fails. + +### circuit_dag_quantum_predecessors(ptr, node, buffer, len) + +Copies the quantum-wire-connected operation predecessors of `node` into `buffer` (two-step: query the count with `circuit_dag_quantum_predecessors_len`). + +- `ptr` (`const struct CCircuitDag*`): DAG handle. +- `node` (`uint32_t`): node id. +- `buffer` (`uint32_t*`): output buffer. +- `len` (`uintptr_t`): buffer capacity. + +Returns: `0` on success; `-1` when `ptr` is NULL; `-2` for an unknown node id; `-3` on failure; `-8` when `len` is too small. + +### circuit_dag_quantum_successors_len(ptr, node) + +Returns the number of operation successors of `node` connected through any quantum wire; results are deduplicated and ordered by the deterministic topological key. + +- `ptr` (`const struct CCircuitDag*`): DAG handle. +- `node` (`uint32_t`): node id. + +Returns: the neighbor count, or 0 when `ptr` is NULL, `node` is unknown, or the query fails. + +### circuit_dag_quantum_successors(ptr, node, buffer, len) + +Copies the quantum-wire-connected operation successors of `node` into `buffer` (two-step: query the count with `circuit_dag_quantum_successors_len`). + +- `ptr` (`const struct CCircuitDag*`): DAG handle. +- `node` (`uint32_t`): node id. +- `buffer` (`uint32_t*`): output buffer. +- `len` (`uintptr_t`): buffer capacity. + +Returns: `0` on success; `-1` when `ptr` is NULL; `-2` for an unknown node id; `-3` on failure; `-8` when `len` is too small. + +### circuit_dag_classical_predecessors_len(ptr, node) + +Returns the number of operation predecessors of `node` connected through any classical wire (classical variables and classical values); results are deduplicated and ordered by the deterministic topological key. + +- `ptr` (`const struct CCircuitDag*`): DAG handle. +- `node` (`uint32_t`): node id. + +Returns: the neighbor count, or 0 when `ptr` is NULL, `node` is unknown, or the query fails. + +### circuit_dag_classical_predecessors(ptr, node, buffer, len) + +Copies the classical-wire-connected operation predecessors of `node` into `buffer` (two-step: query the count with `circuit_dag_classical_predecessors_len`). + +- `ptr` (`const struct CCircuitDag*`): DAG handle. +- `node` (`uint32_t`): node id. +- `buffer` (`uint32_t*`): output buffer. +- `len` (`uintptr_t`): buffer capacity. + +Returns: `0` on success; `-1` when `ptr` is NULL; `-2` for an unknown node id; `-3` on failure; `-8` when `len` is too small. + +### circuit_dag_classical_successors_len(ptr, node) + +Returns the number of operation successors of `node` connected through any classical wire; results are deduplicated and ordered by the deterministic topological key. + +- `ptr` (`const struct CCircuitDag*`): DAG handle. +- `node` (`uint32_t`): node id. + +Returns: the neighbor count, or 0 when `ptr` is NULL, `node` is unknown, or the query fails. + +### circuit_dag_classical_successors(ptr, node, buffer, len) + +Copies the classical-wire-connected operation successors of `node` into `buffer` (two-step: query the count with `circuit_dag_classical_successors_len`). + +- `ptr` (`const struct CCircuitDag*`): DAG handle. +- `node` (`uint32_t`): node id. +- `buffer` (`uint32_t*`): output buffer. +- `len` (`uintptr_t`): buffer capacity. + +Returns: `0` on success; `-1` when `ptr` is NULL; `-2` for an unknown node id; `-3` on failure; `-8` when `len` is too small. + +--- + +## Layering and depth + +Layering follows ASAP ("as soon as possible") semantics: every operation lands in the layer immediately after the latest layer among its operation predecessors, so layer 0 is the front layer and each operation appears in exactly one layer. Within a layer, nodes keep the deterministic topological order. + +`circuit_dag_layers` and the run collectors below use a CSR-style group output: `nodes` receives all groups' node ids flattened back-to-back, and `offsets` receives `groups_len + 1` boundaries with `offsets[0] == 0`; group `k` occupies `nodes[offsets[k]..offsets[k+1]]`. Size `nodes_cap` to hold every node and `offsets_cap` to hold the group count plus one. + +### circuit_dag_front_layer_len(ptr) + +Returns the number of operation nodes without an operation predecessor — the immediately runnable set, in deterministic topological order. + +- `ptr` (`const struct CCircuitDag*`): DAG handle. + +Returns: the front-layer size, or 0 when `ptr` is NULL or the graph is cyclic. + +### circuit_dag_front_layer(ptr, buffer, len) + +Copies the front-layer operation node ids into `buffer` (two-step: query the count with `circuit_dag_front_layer_len`). + +- `ptr` (`const struct CCircuitDag*`): DAG handle. +- `buffer` (`uint32_t*`): output buffer. +- `len` (`uintptr_t`): buffer capacity. + +Returns: `0` on success; `-1` when `ptr` is NULL, or when `buffer` is NULL while nodes exist; `-3` on a cyclic graph; `-8` when `len` is too small. + +### circuit_dag_layers_len(ptr) + +Returns the ASAP layer count, which equals the dependency depth. An empty DAG reports 0. + +- `ptr` (`const struct CCircuitDag*`): DAG handle. + +Returns: the layer count, or 0 when `ptr` is NULL or the graph is cyclic. + +### circuit_dag_layers(ptr, nodes, nodes_cap, offsets, offsets_cap) + +Copies the ASAP layers in CSR style: layer nodes flattened into `nodes` in layer order, boundaries into `offsets` (see the section preamble). + +- `ptr` (`const struct CCircuitDag*`): DAG handle. +- `nodes` (`uint32_t*`): flattened layer nodes; within each layer in deterministic topological order. +- `nodes_cap` (`uintptr_t`): capacity of `nodes`; must hold every operation node. +- `offsets` (`uintptr_t*`): layer boundaries, `offsets[0] == 0`. +- `offsets_cap` (`uintptr_t`): capacity of `offsets`; must hold the layer count plus one. + +Returns: `0` on success; `-1` when `ptr` is NULL, or when a buffer is NULL while data exists; `-3` on a cyclic graph; `-8` when either capacity is too small. + +### circuit_dag_node_layers_len(ptr) + +Returns the number of per-node layer entries, which equals the operation-node count. + +- `ptr` (`const struct CCircuitDag*`): DAG handle. + +Returns: the entry count, or 0 when `ptr` is NULL or the graph is cyclic. + +### circuit_dag_node_layers(ptr, nodes, layers, len) + +Copies the ASAP layer index of every operation node into the parallel arrays `nodes` and `layers` as `(node, layer)` pairs in deterministic topological order (two-step: query the count with `circuit_dag_node_layers_len`). + +- `ptr` (`const struct CCircuitDag*`): DAG handle. +- `nodes` (`uint32_t*`): node ids. +- `layers` (`uintptr_t*`): layer index of the node at the same position. +- `len` (`uintptr_t`): capacity of both arrays. + +Returns: `0` on success; `-1` when `ptr` is NULL, or when either array is NULL while entries exist; `-3` on a cyclic graph; `-8` when `len` is too small. + +### circuit_dag_depth(ptr) + +Returns the ASAP dependency depth: the number of layers. + +- `ptr` (`const struct CCircuitDag*`): DAG handle. + +Returns: the depth as a signed value (`0` for an empty DAG); `-1` when `ptr` is NULL; `-3` on a cyclic graph. + +--- + +## Wire timelines + +Each wire is a timeline: `circuit_dag_wire_in` / `circuit_dag_wire_out` report its endpoints and `circuit_dag_nodes_on_wire` walks the operations in the order the wire threads them. Every `(tag, id)` input in this section accepts `DAG_WIRE_QUBIT` (`id` is the qubit id) and `DAG_WIRE_GLOBAL_ORDER` (`id` is ignored); classical tags are rejected with `-8` because classical wires carry circuit-scoped handles that C cannot reconstruct — for node-based classical connectivity use `circuit_dag_classical_predecessors` / `circuit_dag_classical_successors` from [Graph traversal](#graph-traversal). + +### circuit_dag_is_wire_idle(ptr, tag, id) + +Returns whether the wire carries no operation node. + +- `ptr` (`const struct CCircuitDag*`): DAG handle. +- `tag` (`uint32_t`): `DAG_WIRE_QUBIT` or `DAG_WIRE_GLOBAL_ORDER`. +- `id` (`uint32_t`): payload id of the wire. + +Returns: `1` when idle; `0` when in use; `-1` when `ptr` is NULL; `-3` when the wire is not part of this DAG; `-8` for other `tag` values. + +### circuit_dag_nodes_on_wire_len(ptr, tag, id) + +Returns the number of operation nodes on the wire in wire order, or 0 when `ptr` is NULL, the tag is unknown, or the wire is not part of this DAG. + +- `ptr` (`const struct CCircuitDag*`): DAG handle. +- `tag` (`uint32_t`): `DAG_WIRE_QUBIT` or `DAG_WIRE_GLOBAL_ORDER`. +- `id` (`uint32_t`): payload id of the wire. + +Returns: the node count, or 0 on the invalid states above. + +### circuit_dag_nodes_on_wire(ptr, tag, id, buffer, len) + +Copies the operation nodes on the wire in wire order into `buffer` (two-step: query the count with `circuit_dag_nodes_on_wire_len`). + +- `ptr` (`const struct CCircuitDag*`): DAG handle. +- `tag` (`uint32_t`): `DAG_WIRE_QUBIT` or `DAG_WIRE_GLOBAL_ORDER`. +- `id` (`uint32_t`): payload id of the wire. +- `buffer` (`uint32_t*`): output buffer. +- `len` (`uintptr_t`): buffer capacity. + +Returns: `0` on success; `-1` when `ptr` is NULL, or when `buffer` is NULL while nodes exist; `-3` when the wire is not part of this DAG; `-8` for an unknown wire tag or a too-small buffer. + +### circuit_dag_wire_in(ptr, tag, id) + +Returns the input sentinel node id of the wire. + +- `ptr` (`const struct CCircuitDag*`): DAG handle. +- `tag` (`uint32_t`): `DAG_WIRE_QUBIT` or `DAG_WIRE_GLOBAL_ORDER`. +- `id` (`uint32_t`): payload id of the wire. + +Returns: the sentinel node id; `-1` when `ptr` is NULL; `-3` when the wire is not part of this DAG; `-8` for other `tag` values. + +### circuit_dag_wire_out(ptr, tag, id) + +Returns the output sentinel node id of the wire. + +- `ptr` (`const struct CCircuitDag*`): DAG handle. +- `tag` (`uint32_t`): `DAG_WIRE_QUBIT` or `DAG_WIRE_GLOBAL_ORDER`. +- `id` (`uint32_t`): payload id of the wire. + +Returns: the sentinel node id; `-1` when `ptr` is NULL; `-3` when the wire is not part of this DAG; `-8` for other `tag` values. + +--- + +## Control-flow and measurement flags + +### circuit_dag_has_control_flow(ptr) + +Returns whether any top-level operation is structured control flow (`if` / `while` / `for` / `switch` / `break` / `continue`). + +- `ptr` (`const struct CCircuitDag*`): DAG handle. + +Returns: `1` when true; `0` otherwise; `-1` when `ptr` is NULL. + +### circuit_dag_has_nested_control_flow(ptr) + +Returns whether any control-flow body itself contains structured control flow. + +- `ptr` (`const struct CCircuitDag*`): DAG handle. + +Returns: `1` when true; `0` otherwise; `-1` when `ptr` is NULL. + +### circuit_dag_has_measurement(ptr) + +Returns whether any top-level operation measures qubits, directly or recursively inside control-flow bodies. + +- `ptr` (`const struct CCircuitDag*`): DAG handle. + +Returns: `1` when true; `0` otherwise; `-1` when `ptr` is NULL. + +--- + +## Operation counting + +### circuit_dag_operation_count_by_name_len(ptr) + +Returns the number of distinct instruction names among the top-level operations. + +- `ptr` (`const struct CCircuitDag*`): DAG handle. + +Returns: the distinct-name count, or 0 when `ptr` is NULL. + +### circuit_dag_operation_count_by_name(ptr, names, counts, len) + +Copies the top-level per-name operation counts into the parallel arrays `names` and `counts`, in first-appearance (insertion) order (two-step: query the count with `circuit_dag_operation_count_by_name_len`). Each written name is a freshly allocated C string freed separately with `cqlib_string_free`. + +- `ptr` (`const struct CCircuitDag*`): DAG handle. +- `names` (`char**`): output array of instruction names. +- `counts` (`uintptr_t*`): output array of per-name counts. +- `len` (`uintptr_t`): capacity of both arrays. + +Returns: `0` on success; `-1` when `ptr` is NULL, or when an output array is NULL while entries exist; `-8` when `len` is too small. + +### circuit_dag_operation_count_by_name_recursive_len(ptr) + +Returns the number of distinct instruction names among top-level operations and, recursively, operations inside control-flow bodies. + +- `ptr` (`const struct CCircuitDag*`): DAG handle. + +Returns: the distinct-name count, or 0 when `ptr` is NULL. + +### circuit_dag_operation_count_by_name_recursive(ptr, names, counts, len) + +Copies the top-level and nested per-name operation counts into the parallel arrays `names` and `counts`, in first-appearance (insertion) order (two-step: query the count with `circuit_dag_operation_count_by_name_recursive_len`). Each written name is a freshly allocated C string freed separately with `cqlib_string_free`. + +- `ptr` (`const struct CCircuitDag*`): DAG handle. +- `names` (`char**`): output array of instruction names. +- `counts` (`uintptr_t*`): output array of per-name counts. +- `len` (`uintptr_t`): capacity of both arrays. + +Returns: `0` on success; `-1` when `ptr` is NULL, or when an output array is NULL while entries exist; `-8` when `len` is too small. + +--- + +## Contiguous run collection + +A run is a maximal stretch of consecutive operation nodes along one wire's timeline in which every node satisfies the criterion; runs are reported along the wire in timeline order. The criteria: + +| Constant | Value | Meaning | +| --- | --- | --- | +| `DAG_RUN_CRITERION_1Q_GATE` | 0 | One-qubit gate: the operation acts on exactly 1 qubit and its instruction is `Standard`, `McGate`, `UnitaryGate`, or `CircuitGate` | +| `DAG_RUN_CRITERION_2Q_GATE` | 1 | Two-qubit gate: the operation acts on exactly 2 qubits under the same instruction restrictions | + +The copy variants use the CSR-style group output described in [Layering and depth](#layering-and-depth): run nodes flattened back-to-back into `nodes`, boundaries into `offsets` with `offsets[0] == 0`. + +### circuit_dag_collect_1q_runs_len(ptr) + +Returns the number of contiguous one-qubit-gate runs over all qubit wires, or 0 when `ptr` is NULL or the collection fails. + +- `ptr` (`const struct CCircuitDag*`): DAG handle. + +Returns: the run count, or 0 on the invalid states above. + +### circuit_dag_collect_1q_runs(ptr, nodes, nodes_cap, offsets, offsets_cap) + +Copies the one-qubit-gate runs in CSR style (see the section preamble). + +- `ptr` (`const struct CCircuitDag*`): DAG handle. +- `nodes` (`uint32_t*`): flattened run nodes. +- `nodes_cap` (`uintptr_t`): capacity of `nodes`; must hold every run node. +- `offsets` (`uintptr_t*`): run boundaries, `offsets[0] == 0`. +- `offsets_cap` (`uintptr_t`): capacity of `offsets`; must hold the run count plus one. + +Returns: `0` on success; `-1` when `ptr` is NULL, or when a buffer is NULL while data exists; `-3` on a broken timeline; `-8` when either capacity is too small. + +### circuit_dag_collect_2q_runs_len(ptr) + +Returns the number of contiguous two-qubit-gate runs over all qubit wires, or 0 when `ptr` is NULL or the collection fails. + +- `ptr` (`const struct CCircuitDag*`): DAG handle. + +Returns: the run count, or 0 on the invalid states above. + +### circuit_dag_collect_2q_runs(ptr, nodes, nodes_cap, offsets, offsets_cap) + +Copies the two-qubit-gate runs in CSR style (see the section preamble). A two-qubit gate touches two qubit wires, so the same node may appear in the runs reported for both of its wires. + +- `ptr` (`const struct CCircuitDag*`): DAG handle. +- `nodes` (`uint32_t*`): flattened run nodes. +- `nodes_cap` (`uintptr_t`): capacity of `nodes`; must hold every run node. +- `offsets` (`uintptr_t*`): run boundaries, `offsets[0] == 0`. +- `offsets_cap` (`uintptr_t`): capacity of `offsets`; must hold the run count plus one. + +Returns: `0` on success; `-1` when `ptr` is NULL, or when a buffer is NULL while data exists; `-3` on a broken timeline; `-8` when either capacity is too small. + +### circuit_dag_collect_runs_on_wire_len(ptr, tag, id, criterion) + +Returns the number of contiguous gate runs on one wire matching `criterion`, or 0 when `ptr` is NULL, the tag or criterion is unknown, or the collection fails. + +- `ptr` (`const struct CCircuitDag*`): DAG handle. +- `tag` (`uint32_t`): `DAG_WIRE_QUBIT` or `DAG_WIRE_GLOBAL_ORDER`. +- `id` (`uint32_t`): payload id of the wire. +- `criterion` (`uint32_t`): `DAG_RUN_CRITERION_1Q_GATE` or `DAG_RUN_CRITERION_2Q_GATE`. + +Returns: the run count, or 0 on the invalid states above. + +### circuit_dag_collect_runs_on_wire(ptr, tag, id, criterion, nodes, nodes_cap, offsets, offsets_cap) + +Copies the contiguous gate runs on one wire matching `criterion` in CSR style (see the section preamble). + +- `ptr` (`const struct CCircuitDag*`): DAG handle. +- `tag` (`uint32_t`): `DAG_WIRE_QUBIT` or `DAG_WIRE_GLOBAL_ORDER`. +- `id` (`uint32_t`): payload id of the wire. +- `criterion` (`uint32_t`): `DAG_RUN_CRITERION_1Q_GATE` or `DAG_RUN_CRITERION_2Q_GATE`. +- `nodes` (`uint32_t*`): flattened run nodes. +- `nodes_cap` (`uintptr_t`): capacity of `nodes`; must hold every run node. +- `offsets` (`uintptr_t*`): run boundaries, `offsets[0] == 0`. +- `offsets_cap` (`uintptr_t`): capacity of `offsets`; must hold the run count plus one. + +Returns: `0` on success; `-1` when `ptr` is NULL, or when a buffer is NULL while data exists; `-3` when the wire is invalid or the timeline is broken; `-8` for an unknown wire tag or criterion, or undersized capacities. + +--- + +## Mutating the DAG + +These functions edit the DAG in place. Each mutation rebuilds the underlying graph, so node ids captured before a mutation may no longer identify the same nodes — re-query them (for example with `circuit_dag_op_nodes`) after every successful mutation. Operations passed in are copied (value-level operations are lowered first), so callers keep ownership of their handles. + +### circuit_dag_apply_operation_back(ptr, operation, out_node) + +Appends a copy of `operation` at the back of the DAG. + +- `ptr` (`struct CCircuitDag*`): DAG handle. +- `operation` (`const struct COperation*`): operation to append. +- `out_node` (`uint32_t*`): optional output for the new node id; NULL is allowed. + +Returns: `0` on success; `-1` when `ptr` or `operation` is NULL; `-3` on failure (for example the operation references resources the DAG does not know). + +### circuit_dag_apply_value_operation_back(ptr, operation, out_node) + +Lowers the self-contained value-level operation `operation` (see [Operations and Instructions](4_operation_instruction.md)) and appends it at the back of the DAG. + +- `ptr` (`struct CCircuitDag*`): DAG handle. +- `operation` (`const struct CValueOperation*`): value-level operation to append. +- `out_node` (`uint32_t*`): optional output for the new node id; NULL is allowed. + +Returns: `0` on success; `-1` when `ptr` or `operation` is NULL; `-3` on lowering or DAG failure. + +### circuit_dag_apply_operation_front(ptr, operation, out_node) + +Prepends a copy of `operation` at the front of the DAG. + +- `ptr` (`struct CCircuitDag*`): DAG handle. +- `operation` (`const struct COperation*`): operation to prepend. +- `out_node` (`uint32_t*`): optional output for the new node id; NULL is allowed. + +Returns: `0` on success; `-1` when `ptr` or `operation` is NULL; `-3` on failure. + +### circuit_dag_apply_value_operation_front(ptr, operation, out_node) + +Lowers the self-contained value-level operation `operation` and prepends it at the front of the DAG. + +- `ptr` (`struct CCircuitDag*`): DAG handle. +- `operation` (`const struct CValueOperation*`): value-level operation to prepend. +- `out_node` (`uint32_t*`): optional output for the new node id; NULL is allowed. + +Returns: `0` on success; `-1` when `ptr` or `operation` is NULL; `-3` on lowering or DAG failure. + +### circuit_dag_remove_op_node(ptr, node) + +Removes the operation node `node`; the wire timelines re-thread through the remaining operations. + +- `ptr` (`struct CCircuitDag*`): DAG handle. +- `node` (`uint32_t`): node id to remove. + +Returns: the removed operation as a newly allocated `COperation*` (free with `operation_free`), or NULL when `ptr` is NULL or the node is missing or not an operation node. + +### circuit_dag_substitute_node(ptr, node, operation) + +Replaces the operation stored at `node` with a copy of `operation`. + +- `ptr` (`struct CCircuitDag*`): DAG handle. +- `node` (`uint32_t`): node id to replace. +- `operation` (`const struct COperation*`): replacement operation. + +Returns: `0` on success; `-1` when `ptr` or `operation` is NULL; `-2` when `node` is not an operation node; `-3` when the new operation does not fit the DAG (unknown qubits, unregistered parameters, invalid classical references, and so on). + +### circuit_dag_substitute_value_node(ptr, node, operation) + +Lowers the self-contained value-level operation `operation` and replaces the operation stored at `node` with it. + +- `ptr` (`struct CCircuitDag*`): DAG handle. +- `node` (`uint32_t`): node id to replace. +- `operation` (`const struct CValueOperation*`): replacement value-level operation. + +Returns: `0` on success; `-1` when `ptr` or `operation` is NULL; `-2` when `node` is not an operation node; `-3` on lowering or DAG failure. + +### circuit_dag_substitute_node_with_dag(ptr, node, replacement) + +Splices a copy of the `replacement` DAG in place of the operation node `node`. Two rules keep the schedule legal, and violations fail with `-3`: + +- Resource footprint: every operation in `replacement` may only read wires the replaced node read or wrote, and may only write wires the replaced node wrote; the global-order resource is exempt. +- Control-flow shape: a control-flow operation can only be replaced by a replacement DAG holding exactly one operation of the same control-flow kind; ordinary operations can be replaced by multi-operation DAGs within the footprint rule. + +The replacement must also pass DAG validation on its own (a cyclic or invalid replacement fails with `-3`). + +- `ptr` (`struct CCircuitDag*`): DAG handle. +- `node` (`uint32_t`): node id to replace. +- `replacement` (`const struct CCircuitDag*`): replacement DAG. + +Returns: `0` on success; `-1` when `ptr` or `replacement` is NULL; `-2` when `node` is not an operation node; `-3` on validation failure. + +--- + +## Control-flow inspection + +`CDagControlFlow*` is a read-only snapshot of the (recursive) control-flow structure attached to an operation node. It is produced by `circuit_dag_control_flow` and freed with `dag_control_flow_free`. The structure's kind: + +| Constant | Value | Meaning | +| --- | --- | --- | +| `DAG_CONTROL_FLOW_IF` | 0 | `if` / `else`: bodies are the `then` body first, the `else` body second when present | +| `DAG_CONTROL_FLOW_WHILE` | 1 | `while`: a single body | +| `DAG_CONTROL_FLOW_FOR` | 2 | `for`: a single body | +| `DAG_CONTROL_FLOW_SWITCH` | 3 | `switch`: one body per case, in case order; the optional default body is reported separately | +| `DAG_CONTROL_FLOW_BREAK` | 4 | `break`: no body | +| `DAG_CONTROL_FLOW_CONTINUE` | 5 | `continue`: no body | + +Body DAGs returned here are independent deep copies (`CCircuitDag*` freed with `circuit_dag_free`); edits on a copy do not propagate back to the parent DAG. + +### circuit_dag_control_flow(ptr, node, out) + +Snapshots the control-flow structure attached to `node` into `out`. + +- `ptr` (`const struct CCircuitDag*`): DAG handle. +- `node` (`uint32_t`): node id. +- `out` (`struct CDagControlFlow**`): output for the snapshot handle. + +Returns: `1` on success (`*out` holds a freshly allocated `CDagControlFlow*`); `0` when the node is an ordinary operation (`*out` is set to NULL); `-1` when `ptr` or `out` is NULL; `-2` for an unknown node id or a non-operation node. + +### dag_control_flow_kind(ptr) + +Returns the kind tag of the snapshot. + +- `ptr` (`const struct CDagControlFlow*`): snapshot handle. + +Returns: a `DAG_CONTROL_FLOW_*` tag, or `-1` when `ptr` is NULL. + +### dag_control_flow_len(ptr) + +Returns the number of body DAGs: `if` carries its `then` plus an optional `else` body, `while` and `for` one body each, `switch` one per case, `break` and `continue` none. + +- `ptr` (`const struct CDagControlFlow*`): snapshot handle. + +Returns: the body count, or 0 when `ptr` is NULL. + +### dag_control_flow_body(ptr, index) + +Returns a copy of body `index`, in the order listed in the table above. + +- `ptr` (`const struct CDagControlFlow*`): snapshot handle. +- `index` (`uintptr_t`): body index. + +Returns: a newly allocated `CCircuitDag*` (free with `circuit_dag_free`), or NULL when `ptr` is NULL or `index` is out of range. + +### dag_control_flow_has_default(ptr) + +Returns whether a `switch` snapshot carries a default body. + +- `ptr` (`const struct CDagControlFlow*`): snapshot handle. + +Returns: `1` when present; `0` when absent or the structure is not a `switch`; `-1` when `ptr` is NULL. + +### dag_control_flow_default_body(ptr) + +Returns a copy of the `switch` default body. + +- `ptr` (`const struct CDagControlFlow*`): snapshot handle. + +Returns: a newly allocated `CCircuitDag*` (free with `circuit_dag_free`), or NULL when `ptr` is NULL, the structure is not a `switch`, or it has no default body. + +### dag_control_flow_body_value(ptr, index, lo, hi) + +Writes the 128-bit case value attached to `switch` body `index` into `lo` and `hi`. + +- `ptr` (`const struct CDagControlFlow*`): snapshot handle. +- `index` (`uintptr_t`): case index, aligned with `dag_control_flow_body`. +- `lo` (`uint64_t*`): output for the low 64 bits of the case value. +- `hi` (`uint64_t*`): output for the high 64 bits of the case value. + +Returns: `0` on success; `-1` when `ptr`, `lo`, or `hi` is NULL; `-8` when the structure is not a `switch` or `index` is out of range. + +### dag_control_flow_free(ptr) + +Frees a control-flow snapshot handle. + +- `ptr` (`struct CDagControlFlow*`): snapshot handle; passing NULL is allowed. + +--- + +## Example: circuit → DAG → statistics → back to circuit + +```c +#include +#include +#include "cqlib_c.h" + +int main(void) { + struct CCircuit* circuit = circuit_new(2); + circuit_h(circuit, 0); + circuit_cx(circuit, 0, 1); + + /* Circuit → DAG */ + struct CCircuitDag* dag = circuit_dag_from_circuit(circuit); + + /* Validate and collect statistics */ + int32_t ok = circuit_dag_validate(dag); /* 0 */ + uintptr_t num_ops = circuit_dag_num_ops(dag); /* 2 */ + int32_t empty = circuit_dag_is_empty(dag); /* 0 */ + + uint32_t ids[2] = {0, 0}; + circuit_dag_qubits(dag, ids, 2); /* ids = {0, 1} */ + + /* Wire inventory: the global-order wire first, then the two qubit wires */ + uintptr_t wires = circuit_dag_wires_len(dag); /* 3 */ + uint32_t tags[3] = {0, 0, 0}; + uint32_t wire_ids[3] = {0, 0, 0}; + circuit_dag_wires(dag, tags, wire_ids, 3); + /* tags = {DAG_WIRE_GLOBAL_ORDER, DAG_WIRE_QUBIT, DAG_WIRE_QUBIT} + wire_ids = {0, 0, 1} */ + + int32_t has_q0 = circuit_dag_has_wire(dag, DAG_WIRE_QUBIT, 0); /* 1 */ + int32_t has_q5 = circuit_dag_has_wire(dag, DAG_WIRE_QUBIT, 5); /* 0 */ + + /* DAG → circuit: the rebuild matches the source shape + * (2 qubits, 2 operations) */ + struct CCircuit* lowered = circuit_dag_to_circuit(dag); + + printf("validate=%d ops=%llu empty=%d wires=%llu lowered_ops=%llu\n", + (int)ok, (unsigned long long)num_ops, (int)empty, + (unsigned long long)wires, + (unsigned long long)circuit_num_operations(lowered)); + + circuit_free(lowered); + circuit_dag_free(dag); + circuit_free(circuit); + return 0; +} +``` + +For the circuit-building entry points see [Circuit](1_circuit.md); for building operation handles see [Operations and Instructions](4_operation_instruction.md). diff --git a/docs/api/en/c/0_circuit/1_circuit.md b/docs/api/en/c/0_circuit/1_circuit.md new file mode 100644 index 00000000..6611ed24 --- /dev/null +++ b/docs/api/en/c/0_circuit/1_circuit.md @@ -0,0 +1,429 @@ +# Circuit + +The `CCircuit*` handle is the main container for a quantum circuit, holding the set of logical qubits, the operation sequence, the parameter table, classical variables and values, control-flow scopes, and the global phase. This page covers circuit construction and release, property queries, structural transformations, checkpoint transactions, the global phase, and parameter binding; gate functions are covered in [Standard Gates](5_gate_standard.md) and symbolic parameter details in [Symbolic Parameters](3_parameter.md). Error codes and memory conventions are described in [Overview](../0_overview.md). + +--- + +## Construction and Release + +### circuit_new(num_qubits) + +Creates an empty circuit with contiguous logical qubits whose ids run from `0` to `num_qubits - 1`. + +- `num_qubits` (`uintptr_t`): the number of qubits. + +Returns: a newly allocated `CCircuit*`; free it with `circuit_free`. + +### circuit_from_qubits(qubits, len) + +Creates a circuit from an explicit list of qubit ids, allowing sparse numbering (e.g. `{2, 5}`). When `len` is 0 an empty circuit is created and `qubits` may be NULL. + +- `qubits` (`const uint32_t*`): array of qubit ids. +- `len` (`uintptr_t`): array length. + +Returns: a newly allocated `CCircuit*`; when `len > 0`, returns `NULL` if `qubits` is NULL or an id is duplicated. + +### circuit_from_operations(qubits, qubits_len, operations, operations_len) + +Creates a circuit from qubits plus a batch of resolved value-level operations (as produced by `circuit_index`, see [Operation / Instruction](4_operation_instruction.md)). Operations are appended in order through value-level lowering, so qubit membership is validated and symbolic parameters are interned into the new circuit's parameter table. + +- `qubits` (`const uint32_t*`): array of qubit ids. +- `qubits_len` (`uintptr_t`): array length; when 0 an empty qubit set is created and `qubits` may be NULL. +- `operations` (`const CValueOperation* const*`): array of resolved operation snapshots. +- `operations_len` (`uintptr_t`): number of operations. + +Returns: a newly allocated `CCircuit*`, freed with `circuit_free`; `NULL` on NULL input (including NULL entries inside `operations`), duplicate qubits, or any operation-level failure. + +### circuit_append_value_operation(ptr, op) + +Appends one resolved value-level operation (as produced by `circuit_index`) to the circuit. + +- `ptr` (`CCircuit*`): circuit handle. +- `op` (`const CValueOperation*`): resolved operation snapshot. + +Returns: `0` on success; `-1` for NULL; `-3` on application failure (e.g. the operation references a qubit outside the circuit). + +### circuit_free(ptr) + +Frees a circuit handle. Passing NULL is allowed. + +- `ptr` (`CCircuit*`): circuit handle. + +```c +/* Sparse numbering: qubit ids 2 and 5 */ +uint32_t ids[2] = {2, 5}; +CCircuit *qc = circuit_from_qubits(ids, 2); +if (qc != NULL) { + /* circuit_width(qc) == 2 */ + circuit_free(qc); +} +``` + +--- + +## Property Queries + +### circuit_num_qubits(ptr) + +Returns the number of qubits. + +- `ptr` (`const CCircuit*`): circuit handle. + +Returns: the qubit count; 0 for NULL. + +### circuit_width(ptr) + +Returns the circuit width, i.e. the number of qubits; identical in semantics to `circuit_num_qubits`. + +- `ptr` (`const CCircuit*`): circuit handle. + +Returns: the qubit count; 0 for NULL. + +### circuit_num_operations(ptr) + +Returns the number of top-level operations. + +- `ptr` (`const CCircuit*`): circuit handle. + +Returns: the operation count; 0 for NULL. + +### circuit_num_parameters(ptr) + +Returns the number of symbolic parameters interned in the circuit's parameter table; identical to `circuit_parameters_len`. + +- `ptr` (`const CCircuit*`): circuit handle. + +Returns: the parameter count; 0 for NULL. + +### circuit_qubits_len(ptr) + +Returns the qubit count, used to size the buffer for `circuit_qubits`. + +- `ptr` (`const CCircuit*`): circuit handle. + +Returns: the qubit count; 0 for NULL. + +### circuit_qubits(ptr, out, len) + +Copies the qubit ids into `out` in insertion order. When `out` is NULL or `len` is too small, it only returns the count without copying. + +- `ptr` (`const CCircuit*`): circuit handle. +- `out` (`uint32_t*`): output buffer; may be NULL (count query only). +- `len` (`uintptr_t`): buffer length. + +Returns: the total number of qubits. + +### circuit_depth(ptr, recurse) + +Computes the circuit depth. With `recurse = true`, operations inside control-flow bodies are expanded recursively. + +- `ptr` (`const CCircuit*`): circuit handle. +- `recurse` (`bool`): whether to recursively count operations inside control flow. + +Returns: the circuit depth; `-1` for a NULL handle, `-3` on failure (e.g. the circuit contains control flow but was not traversed recursively). + +### circuit_validate(ptr) + +Validates circuit consistency: classical handle ownership, control-flow scopes, data dependencies, and structural invariants. Circuits imported from external sources or generated automatically should be validated before further processing. + +- `ptr` (`const CCircuit*`): circuit handle. + +Returns: `0` on success; `-1` for NULL; `-3` when validation fails. + +```c +CCircuit *qc = circuit_new(2); +circuit_h(qc, 0); +circuit_cx(qc, 0, 1); + +if (circuit_validate(qc) == 0) { + /* num_qubits=2, num_operations=2, depth=2 */ + uintptr_t n = circuit_qubits_len(qc); + uint32_t *ids = malloc(n * sizeof(uint32_t)); + circuit_qubits(qc, ids, n); /* ids == {0, 1} */ + free(ids); +} +circuit_free(qc); +``` + +### circuit_contains_qubit(ptr, qubit) + +Returns whether `qubit` belongs to the circuit. + +- `ptr` (`const CCircuit*`): circuit handle. +- `qubit` (`uint32_t`): qubit id. + +Returns: `1` when yes; `0` when no; `-1` for NULL. + +### circuit_has_same_qubits(a, b) + +Returns whether both circuits share the same ordered qubit domain. + +- `a` (`const CCircuit*`): first circuit handle. +- `b` (`const CCircuit*`): second circuit handle. + +Returns: `1` when yes; `0` when no; `-1` for NULL. + +### circuit_operations_structurally_equal(a, b) + +Returns whether two circuits are structurally equal: same ordered qubits, parameter tables, and operations (with parameter indices remapped across the two tables, and circuit-local classical handles compared by structure). + +- `a` (`const CCircuit*`): first circuit handle. +- `b` (`const CCircuit*`): second circuit handle. + +Returns: `1` when yes; `0` when no; `-1` for NULL. + +--- + +## Qubit Management + +### circuit_add_qubits(ptr, qubits, count) + +Appends new logical qubits to the circuit. + +- `ptr` (`CCircuit*`): circuit handle. +- `qubits` (`const uint32_t*`): array of new qubit ids. +- `count` (`uintptr_t`): number of qubits to append. + +Returns: `0` on success; `-1` for NULL; `-3` on failure (e.g. an id duplicates an existing qubit). + +--- + +## Structural Transformations + +### circuit_compose(ptr, other, qubits_map, map_len) + +Appends `other` to the end of the current circuit. `qubits_map` maps `other`'s qubits, in `other`'s qubit order, onto the current circuit's qubits; passing a NULL map (`map_len` 0) merges by identity of ids, and qubits of `other` that do not exist in the current circuit are absorbed into it. + +- `ptr` (`CCircuit*`): target circuit (modified). +- `other` (`const CCircuit*`): circuit being composed in (not modified). +- `qubits_map` (`const uint32_t*`): qubit mapping array; may be NULL. +- `map_len` (`uintptr_t`): mapping length. + +Returns: `0` on success; `-1` for NULL; `-3` on failure (invalid mapping or structure check failure). + +```c +CCircuit *lhs = circuit_new(3); +CCircuit *rhs = circuit_new(2); +circuit_cx(rhs, 0, 1); + +/* rhs qubits 0 and 1 map onto lhs qubits 1 and 2, in order */ +uint32_t map[2] = {1, 2}; +int32_t rc = circuit_compose(lhs, rhs, map, 2); /* rc == 0 */ + +circuit_free(rhs); +circuit_free(lhs); +``` + +### circuit_inverse(ptr) + +Returns the inverse of the current circuit: operations in reverse order with each one inverted, and the global phase negated. The original circuit is unchanged. + +- `ptr` (`const CCircuit*`): circuit handle. + +Returns: a newly allocated `CCircuit*`, freed with `circuit_free`; returns `NULL` when the circuit contains non-invertible operations (measure, reset, ...) or on failure. + +### circuit_decompose(ptr) + +Returns a copy of the circuit with composite gates expanded: circuit-defined gates (composite gates appended via `circuit_circuit_gate`) are expanded into their underlying operations. The original circuit is unchanged. + +- `ptr` (`const CCircuit*`): circuit handle. + +Returns: a newly allocated `CCircuit*`; `NULL` on failure. + +### circuit_remove_operation(ptr, index) + +Deletes the top-level operation at `index`. + +- `ptr` (`CCircuit*`): circuit handle. +- `index` (`uintptr_t`): operation index. + +Returns: `0` on success; `-1` for NULL; `-3` when the index is out of bounds or a post-deletion structure check fails. + +### circuit_remove_operations(ptr, indices, len) + +Deletes top-level operations by index in one batch. `indices` are interpreted against the operation list before deletion; duplicate indices are ignored; any failure rolls the whole batch back (atomic). + +- `ptr` (`CCircuit*`): circuit handle. +- `indices` (`const uintptr_t*`): array of operation indices. +- `len` (`uintptr_t`): number of indices (may be 0). + +Returns: `0` on success (including an empty index list); `-1` for NULL; `-3` when any index is out of bounds or a classical value is still referenced. + +```c +CCircuit *qc = circuit_new(1); +circuit_rx(qc, 0, 0.7); + +CCircuit *inv = circuit_inverse(qc); /* inverse circuit: RX(-0.7) */ +if (inv != NULL && circuit_validate(inv) == 0) { + /* use inv ... */ +} +circuit_free(inv); +circuit_free(qc); +``` + +--- + +## Checkpoint Transactions + +The checkpoint family captures and restores circuit state (operation list, parameter and symbol tables, classical tables, control scopes) through an opaque `CCheckpoint*` token. A token is produced by `circuit_checkpoint` / `circuit_begin` and consumed exactly once by `circuit_commit`, `circuit_rollback_to`, or `circuit_rollback_control_body_transaction`; free an unconsumed token with `checkpoint_free`. + +```c +CCircuit *qc = circuit_new(1); +CCheckpoint *cp = circuit_checkpoint(qc); +circuit_h(qc, 0); /* tentative operations */ + +if (circuit_validate(qc) != 0) { + circuit_rollback_to(qc, cp); /* undo everything since the checkpoint */ +} else { + circuit_commit(qc, cp); /* keep the operations */ +} + +circuit_free(qc); +``` + +### circuit_checkpoint(ptr) + +Captures a checkpoint of the circuit state. The returned token is consumed exactly once by `circuit_commit`, `circuit_rollback_to`, or `circuit_rollback_control_body_transaction`. + +- `ptr` (`const CCircuit*`): circuit handle. + +Returns: a newly allocated `CCheckpoint*`; `NULL` on NULL input. + +### circuit_begin(ptr) + +Starts an externally driven construction transaction; equivalent to `circuit_checkpoint`. + +- `ptr` (`const CCircuit*`): circuit handle. + +Returns: a newly allocated `CCheckpoint*`; `NULL` on NULL input. + +### circuit_commit(ptr, checkpoint) + +Commits the state allocated since the token was captured and consumes the token. + +- `ptr` (`CCircuit*`): circuit handle. +- `checkpoint` (`CCheckpoint*`): token produced by `circuit_checkpoint` / `circuit_begin`. + +Returns: `0` on success; `-1` for NULL. + +### circuit_rollback_to(ptr, checkpoint) + +Rolls back every state allocated since the token was captured and consumes the token. + +- `ptr` (`CCircuit*`): circuit handle. +- `checkpoint` (`CCheckpoint*`): token produced by `circuit_checkpoint` / `circuit_begin`. + +Returns: `0` on success; `-1` for NULL. + +### circuit_rollback_control_body_transaction(ptr, checkpoint) + +Rolls back every state allocated since the token was captured and consumes the token; same behavior as `circuit_rollback_to`, kept under the core transaction name. + +- `ptr` (`CCircuit*`): circuit handle. +- `checkpoint` (`CCheckpoint*`): token produced by `circuit_checkpoint` / `circuit_begin`. + +Returns: `0` on success; `-1` for NULL. + +### checkpoint_free(ptr) + +Frees an unconsumed checkpoint token. Passing NULL is allowed. + +- `ptr` (`CCheckpoint*`): token to free. + +--- + +## Global Phase + +### circuit_global_phase(ptr, out) + +Reads the circuit's global phase. The result is written into a `CParameterValue`: a fixed number, or — in the symbolic case — a newly allocated `CParameter*` (free it with `param_free`; see [Symbolic Parameters](3_parameter.md)). + +- `ptr` (`const CCircuit*`): circuit handle. +- `out` (`CParameterValue*`): output struct. + +Returns: `0` on success; `-1` for NULL. + +### circuit_set_global_phase(ptr, phase) + +Sets the global phase to a numeric value. + +- `ptr` (`CCircuit*`): circuit handle. +- `phase` (`double`): global phase (radians). + +Returns: `0` on success; `-1` for NULL; `-3` when the phase is not finite. + +### circuit_set_global_phase_param(ptr, param) + +Sets the global phase to a symbolic parameter (the parameter is cloned). + +- `ptr` (`CCircuit*`): circuit handle. +- `param` (`const CParameter*`): symbolic parameter handle. + +Returns: `0` on success; `-1` for NULL. + +--- + +## Parameter Binding + +### circuit_assign_params(circuit, bindings) + +Returns a new circuit in which every interned parameter that can be evaluated from the binding string is replaced by its fixed numeric value, while the remaining parameters stay symbolic; the original circuit is unchanged and can be reused as a template. + +- `circuit` (`const CCircuit*`): circuit handle. +- `bindings` (`const char*`): binding string of the form `"name:value,name2:value2"` (same as `param_evaluate`); NULL or a malformed string is treated as no bindings. + +Returns: a newly allocated `CCircuit*`, freed with `circuit_free`; `NULL` when the circuit contains classical control flow or a substitution/evaluation failure occurs. + +```c +CCircuit *tpl = circuit_new(1); +CParameter *theta = param_parse("theta"); +circuit_rx_param(tpl, 0, theta); +param_free(theta); + +CCircuit *bound = circuit_assign_params(tpl, "theta:0.5"); +/* bound no longer references the symbol theta */ + +circuit_free(bound); +circuit_free(tpl); +``` + +--- + +## Non-Unitary Instructions and the Identity Gate + +### circuit_measure(ptr, qubit) + +Appends a computational-basis measurement on `qubit`, producing an immutable classical `Bit` value owned by the circuit. Variants that read measurement results or write into existing classical variables are covered in [Classical Data and Control Flow](9_classical_control_flow.md). + +- `ptr` (`CCircuit*`): circuit handle. +- `qubit` (`uint32_t`): the qubit being measured. + +Returns: `0` on success; `-1` for NULL; `-2` when the qubit is out of bounds; `-3` on failure. + +### circuit_reset(ptr, qubit) + +Appends a reset on `qubit`, returning the qubit to `|0>`. + +- `ptr` (`CCircuit*`): circuit handle. +- `qubit` (`uint32_t`): qubit id. + +Returns: `0` on success; `-1` for NULL; `-2` when the qubit is out of bounds; `-3` on failure. + +### circuit_barrier(ptr, qubits, count) + +Inserts a barrier over the given qubits, preventing related operations from being reordered across the boundary. Passing NULL `qubits` or `count` 0 inserts a global barrier over all circuit qubits. + +- `ptr` (`CCircuit*`): circuit handle. +- `qubits` (`const uint32_t*`): array of qubit ids; may be NULL. +- `count` (`uintptr_t`): number of qubits. + +Returns: `0` on success; `-1` for NULL; `-3` when a qubit does not exist or on failure. + +### circuit_id(ptr, qubit) + +Appends the identity gate on `qubit`; an alias of `circuit_i` kept for frontend naming compatibility. + +- `ptr` (`CCircuit*`): circuit handle. +- `qubit` (`uint32_t`): qubit id. + +Returns: `0` on success; `-1` for NULL; `-2` when the qubit is out of bounds; `-3` on failure. diff --git a/docs/api/en/c/0_circuit/2_qubit.md b/docs/api/en/c/0_circuit/2_qubit.md new file mode 100644 index 00000000..563ac9b4 --- /dev/null +++ b/docs/api/en/c/0_circuit/2_qubit.md @@ -0,0 +1,183 @@ +# Qubit (C) + +This page covers the C ABI of the `CQubit` logical-qubit identifier. `CQubit` is a lightweight handle passed by value; it wraps a `uint32_t` ID used to identify a logical qubit stably across circuits, compilation IR, mapping tables and set structures. Error-code and string-release conventions follow the [Overview](../0_overview.md). + +--- + +## Struct + +`CQubit` is passed by value and needs no release: + +```c +typedef struct CQubit { + uint32_t id; +} CQubit; +``` + +| Field | Type | Meaning | +| --- | --- | --- | +| `id` | `uint32_t` | Numeric identifier of the logical qubit. | + +The ID is a logical identity: it carries no circuit identity and does not correspond to a storage position in a state vector or matrix; the actual order is determined by the qubit list kept by `Circuit` and the ordering conventions of the conversion interfaces. IDs may be sparse (e.g. 10, 20); callers must not assume the value is a valid index into dense storage. + +--- + +## Creation and access + +### qubit_new(id) + +```c +struct CQubit qubit_new(uint32_t id); +``` + +Creates a logical qubit with the supplied numeric identifier, returned by value. + +- `id` (`uint32_t`): logical qubit identifier. + +### qubit_id(qubit) + +```c +uint32_t qubit_id(struct CQubit qubit); +``` + +Returns the raw underlying identifier (compact 4-byte representation). + +- `qubit` (`struct CQubit`): the logical qubit. + +### qubit_index(qubit) + +```c +uintptr_t qubit_index(struct CQubit qubit); +``` + +Returns the numeric identifier as a `uintptr_t` for index-style use. The value does not map the qubit to any position in a circuit, matrix or simulator. + +--- + +## Checked integer conversions + +### qubit_try_from_i64(value, out) + +```c +int32_t qubit_try_from_i64(int64_t value, struct CQubit *out); +``` + +Checked conversion from a signed integer: negative or out-of-range values return an error instead of being silently truncated. + +- `value` (`int64_t`): input integer. +- `out` (`struct CQubit*`): output qubit. + +Returns: `0` on success; `-1` when `out` is NULL; `-8` when `value` is negative or outside the `uint32_t` range. + +### qubit_try_from_u64(value, out) + +```c +int32_t qubit_try_from_u64(uint64_t value, struct CQubit *out); +``` + +Checked conversion from an unsigned integer: values outside the `uint32_t` range return an error. + +- `value` (`uint64_t`): input integer. +- `out` (`struct CQubit*`): output qubit. + +Returns: `0` on success; `-1` when `out` is NULL; `-8` when `value` is outside the `uint32_t` range. + +--- + +## Comparison and ordering + +Comparison, ordering and hashing are all based on the numeric identifier: two `CQubit` values with the same ID are the same logical qubit. + +### qubit_equal(a, b) + +```c +int32_t qubit_equal(struct CQubit a, struct CQubit b); +``` + +Returns `1` when both carry the same ID, `0` otherwise. + +- `a`, `b` (`struct CQubit`): qubits to compare. + +### qubit_compare(a, b) + +```c +int32_t qubit_compare(struct CQubit a, struct CQubit b); +``` + +Compares by numeric identifier: `-1` when `a < b`, `0` when equal, `1` when `a > b`. + +- `a`, `b` (`struct CQubit`): qubits to compare. + +--- + +## String representation + +### qubit_to_string(qubit) + +```c +char *qubit_to_string(struct CQubit qubit); +``` + +Formats the identifier as `Q` (e.g. `Q12`). Release the returned string with [cqlib_string_free](../0_overview.md); returns NULL on allocation failure. + +- `qubit` (`struct CQubit`): the logical qubit. + +--- + +## Error codes + +| Function | `0` | `-1` | `-8` | +| --- | --- | --- | --- | +| `qubit_try_from_i64` | converted | `out` is NULL | value negative or outside the `uint32_t` range | +| `qubit_try_from_u64` | converted | `out` is NULL | value outside the `uint32_t` range | + +`qubit_new`, `qubit_id`, `qubit_index`, `qubit_equal` and `qubit_compare` never fail; `qubit_to_string` returns NULL only on allocation failure. + +--- + +## Example + +```c +#include +#include +#include +#include + +int main(void) { + /* Creation and access */ + CQubit q = qubit_new(12); + assert(q.id == 12); + assert(qubit_id(q) == 12u); + assert(qubit_index(q) == 12u); + + /* Checked conversions: negative and out-of-range values error out */ + CQubit out = {0}; + assert(qubit_try_from_i64(-1, &out) == -8); + assert(qubit_try_from_i64(3, &out) == 0); + assert(qubit_id(out) == 3u); + + /* Comparison */ + assert(qubit_equal(q, qubit_new(12)) == 1); + assert(qubit_compare(qubit_new(0), qubit_new(1)) == -1); + + /* String representation */ + char *text = qubit_to_string(q); + assert(strcmp(text, "Q12") == 0); + cqlib_string_free(text); + + /* Interop with circuits: sparse logical IDs 10 and 20 */ + const uint32_t ids[2] = {qubit_id(qubit_new(10)), qubit_id(qubit_new(20))}; + CCircuit *qc = circuit_from_qubits(ids, 2); + assert(circuit_num_qubits(qc) == 2); + circuit_free(qc); + return 0; +} +``` + +--- + +## Related pages + +- [Circuit](1_circuit.md): `circuit_from_qubits` accepts explicit ID lists (sparse IDs included). +- [Qubit identifiers](../2_device/6_qubits.md): device-side `CLogicalQubit` / `CPhysicalQubit`. +- [Overview](../0_overview.md): error codes, memory ownership and global conventions. diff --git a/docs/api/en/c/0_circuit/3_parameter.md b/docs/api/en/c/0_circuit/3_parameter.md new file mode 100644 index 00000000..d2d17c69 --- /dev/null +++ b/docs/api/en/c/0_circuit/3_parameter.md @@ -0,0 +1,420 @@ +# Parameter + +The `CParameter*` handle is an opaque handle to a symbolic parameter expression: either a free symbol (e.g. `theta`) or a numeric constant, or an arithmetic combination such as `2 * theta + pi / 2`. This page covers expression parsing, construction, arithmetic and math functions, symbol and state queries, simplification/substitution/derivation, plus the circuit-side parameter registry, the storage form (`CCircuitParam`), resolved values (`CParameterValue`), and symbol-table queries; `circuit_assign_params`, which produces a new circuit from bindings, is covered in [Circuit](1_circuit.md). Error codes and memory conventions are described in [Overview](../0_overview.md). + +--- + +## Expression Parsing and Evaluation + +### param_parse(expr) + +Parses a symbolic parameter expression and returns a newly allocated handle. Expressions support free symbol names (including Unicode symbols such as `θ`), numeric constants, and arithmetic operations (`+`, `-`, `*`, `/`, unary minus); the constants `pi`/`π` and `e` are available automatically during evaluation, and explicit bindings can override them. + +- `expr` (`const char*`): NUL-terminated expression string. + +Returns: a newly allocated `CParameter*`, freed with `param_free`; `NULL` when `expr` is NULL, not valid UTF-8, or cannot be parsed as a valid expression. + +### param_free(ptr) + +Frees a parameter handle. Passing NULL is allowed. + +- `ptr` (`CParameter*`): parameter handle. + +### param_evaluate(ptr, bindings) + +Evaluates the expression to a number under the given bindings. + +- `ptr` (`const CParameter*`): parameter handle. +- `bindings` (`const char*`): binding string of the form `"name:value,name2:value2"`; NULL or a malformed string (a value that is not a finite floating-point number) is treated as no bindings. + +Returns: the evaluated result; `0.0` when `ptr` is NULL, the expression still contains unbound symbols, or evaluation fails (including a NaN result). + +```c +CParameter *expr = param_parse("2 * theta + pi / 2"); +double v = param_evaluate(expr, "theta:0.5"); /* v == 1.0 + pi/2 */ +param_free(expr); +``` + +--- + +## Expression Construction and Arithmetic + +### param_symbol(name) + +Constructs a free-symbol parameter from a symbol name. + +- `name` (`const char*`): NUL-terminated symbol name, including Unicode symbols such as `θ`. + +Returns: a newly allocated `CParameter*`, freed with `param_free`; `NULL` when `name` is NULL, not valid UTF-8, or is an invalid symbol name. + +### param_pi() / param_e() + +Constructs the built-in constants π and e. Both are constant expressions (`param_is_constant` returns 1) and evaluate without bindings. + +Returns: a newly allocated `CParameter*`. + +### param_from_double(value) + +Constructs a constant parameter from a number. + +- `value` (`double`): the constant value. + +Returns: a newly allocated `CParameter*`; `NULL` when `value` is not finite. + +### param_add(a, b) / param_sub(a, b) / param_mul(a, b) / param_div(a, b) + +Adds, subtracts, multiplies, or divides two expressions and returns the combined expression. Division does not check for a zero divisor at construction time; evaluation may produce a non-finite value. + +- `a`, `b` (`const CParameter*`): operand handles. + +Returns: a newly allocated `CParameter*`; `NULL` when either input is NULL. + +### param_neg(a) + +Unary negation: returns `-a`. + +- `a` (`const CParameter*`): operand handle. + +Returns: a newly allocated `CParameter*`; `NULL` when `a` is NULL. + +### param_pow(a, exp) + +Exponentiation: returns `a^exp`. + +- `a`, `exp` (`const CParameter*`): base and exponent handles. + +Returns: a newly allocated `CParameter*`; `NULL` when either input is NULL. + +```c +CParameter *theta = param_symbol("theta"); +CParameter *amp = param_mul(param_from_double(0.5), param_sin(theta)); +CParameter *shifted = param_add(amp, param_pi()); /* 0.5*sin(theta) + pi */ +param_free(shifted); +param_free(amp); +param_free(theta); +``` + +--- + +## Math Functions + +The following unary math functions share one signature: `param_xxx(const CParameter* a)` returns a newly allocated `CParameter*` (`NULL` when `a` is NULL) representing the math function applied to the expression. No domain checking happens at construction time; evaluation occurs during `param_evaluate` or simplification. + +### param_abs / param_sqrt / param_exp / param_ln + +Absolute value, square root, natural exponential, natural logarithm. + +### param_sin / param_cos / param_tan / param_asin / param_acos / param_atan + +Trigonometric and inverse trigonometric functions. + +### param_sinh / param_cosh / param_tanh + +Hyperbolic functions. + +### param_floor / param_ceil / param_round + +Floor, ceiling, and rounding. + +### param_log(a, base) + +Logarithm to an explicit base. + +- `a` (`const CParameter*`): the antilogarithm handle. +- `base` (`const CParameter*`): the base handle. + +Returns: a newly allocated `CParameter*`; `NULL` when either input is NULL. + +--- + +## Symbol and State Queries + +### param_symbols_len(a) + +Returns the number of free symbols in the expression (built-in constants such as π and e are excluded). + +- `a` (`const CParameter*`): parameter handle. + +Returns: the number of free symbols; `0` when `a` is NULL. + +### param_symbols(a, out, len) + +Two-step read of free symbol names: call `param_symbols_len` first to size the buffer, then fill `out` with the names sorted by name. + +- `a` (`const CParameter*`): parameter handle. +- `out` (`char**`): output buffer, may be NULL (count only); each written element is a newly allocated C string freed with `cqlib_string_free`. +- `len` (`uintptr_t`): buffer length. + +Returns: the total number of free symbols; `-1` on NULL; `-8` when the buffer is too small (nothing is written). + +### param_as_symbol(a) + +Returns the symbol name when the expression is exactly a single free symbol. + +Returns: a newly allocated C string freed with `cqlib_string_free`; `NULL` when the expression is not a plain symbol or the input is NULL. + +### param_is_constant(a) + +Checks whether the expression contains no free symbols (built-in constants such as π and e count as known quantities). + +Returns: `1` constant; `0` free symbols present; `-1` on NULL. + +### param_is_exact_zero(a) / param_is_zero(a) / param_is_one(a) + +- `param_is_exact_zero`: returns 1 only when the expression contains no free symbols and evaluates to exactly 0; returns 0 when free symbols are present. +- `param_is_zero`/`param_is_one`: evaluate directly without bindings and compare against 0 / 1 with tolerance `f64::EPSILON`; return 0 when the expression cannot be evaluated (free symbols present). + +Returns: `1`/`0`; `-1` on NULL. + +--- + +## Simplification, Substitution, and Derivatives + +### param_simplify(a) + +Applies domain-safe symbolic simplification to the expression. + +Returns: a newly allocated `CParameter*`; `NULL` on NULL input or when simplification fails. + +### param_canonicalized(a) + +Simplifies the expression; when the result contains no free symbols it is further folded into a numeric node (fails when the constant evaluates to a non-finite value). + +Returns: a newly allocated `CParameter*`; `NULL` on NULL input or when simplification fails. + +### param_replace(a, symbol, replacement) + +Replaces every occurrence of `symbol` with `replacement` and returns the new expression. + +- `a` (`const CParameter*`): parameter handle. +- `symbol` (`const char*`): the symbol name to replace. +- `replacement` (`const CParameter*`): the replacement parameter handle. + +Returns: a newly allocated `CParameter*`; `NULL` on NULL input or when `symbol` is not valid UTF-8. + +### param_substitute_many(a, names, values, len) + +Substitutes several symbols in one call and simplifies the result. `names` and `values` are parallel arrays of length `len`. + +- `a` (`const CParameter*`): parameter handle. +- `names` (`const char* const*`): symbol-name array; a NULL element is treated as an error. +- `values` (`const CParameter* const*`): parallel array of replacement handles. +- `len` (`uintptr_t`): array length; when 0, a clone of `a` is returned. + +Returns: a newly allocated `CParameter*`; `NULL` on NULL input, NULL array elements, or names that are not valid UTF-8. + +### param_derivative(a, var) + +Returns the symbolic partial derivative with respect to `var`. + +- `a` (`const CParameter*`): parameter handle. +- `var` (`const char*`): the differentiation variable name. + +Returns: a newly allocated `CParameter*`; `NULL` on NULL input, non-UTF-8 `var`, or when differentiation fails. + +```c +CParameter *expr = param_parse("2 * theta"); +CParameter *deriv = param_derivative(expr, "theta"); /* deriv == 2 */ +double v = param_evaluate(deriv, NULL); /* v == 2.0 */ +param_free(deriv); +param_free(expr); +``` + +--- + +## Equivalence Checks + +### param_provably_equal(a, b, tolerance) + +Conservatively checks whether two expressions are equal within `tolerance` (directly equal, or both evaluate without bindings and differ by at most the tolerance). + +- `a`, `b` (`const CParameter*`): handles to compare. +- `tolerance` (`double`): numeric tolerance. + +Returns: `1` equal; `0` not equal or unprovable; `-1` on NULL. + +### param_provably_equal_modulo(a, b, modulus, tolerance) + +Conservatively checks whether `a - b` is an integer multiple of `modulus` (direct difference test or modulo reduction, within `tolerance`). + +- `a`, `b` (`const CParameter*`): handles to compare. +- `modulus` (`const CParameter*`): modulus handle. +- `tolerance` (`double`): numeric tolerance. + +Returns: `1` equivalent; `0` not equivalent or unprovable; `-1` on NULL. + +```c +CParameter *p = param_pi(); +CParameter *three_pi = param_mul(param_from_double(3.0), param_pi()); +CParameter *mod = param_mul(param_from_double(2.0), param_pi()); +/* p and three_pi differ by an integer multiple of 2*pi */ +int32_t eq = param_provably_equal_modulo(p, three_pi, mod, 1e-9); /* eq == 1 */ +param_free(mod); +param_free(three_pi); +param_free(p); +``` + +--- + +## Circuit Parameter Registry + +The circuit maintains a parameter table: after canonicalization and interning, symbolic parameters appear in operations and the global phase in one of two storage forms — a fixed value or a table index. Interning registers the expression itself and does not mark the parameter as referenced. + +### circuit_add_parameter(ptr, param) + +Clones a symbolic parameter and interns it into the circuit's parameter table. Interning only registers the expression and its symbols, without marking it as used. + +- `ptr` (`CCircuit*`): circuit handle. +- `param` (`const CParameter*`): parameter handle. + +Returns: `0` on success; `-1` for NULL. + +### circuit_parameters_len(ptr) + +Returns the number of parameters interned in the circuit's parameter table. + +- `ptr` (`const CCircuit*`): circuit handle. + +Returns: the interned parameter count; 0 for NULL. + +### circuit_parameters(ptr, out, len) + +Two-step read of the parameter table: call `circuit_parameters_len` first to size the buffer, then this copies cloned parameter handles into `out`. + +- `ptr` (`const CCircuit*`): circuit handle. +- `out` (`CParameter**`): output buffer, may be NULL (count query only); each written element is a newly allocated `CParameter*` that the caller frees individually with `param_free`. +- `len` (`uintptr_t`): buffer length. + +Returns: the total number of interned parameters. + +--- + +## Storage Form and Resolved Values + +Parameters inside operations and the global phase use the storage form `CCircuitParam`; when reading them back you can either restore the full symbolic expression or resolve the value into a fixed number or a symbolic parameter. + +### CCircuitParam + +| Field | Type | Description | +| --- | --- | --- | +| `tag` | `uint8_t` | `CIRCUIT_PARAM_TAG_FIXED` (0, fixed value) or `CIRCUIT_PARAM_TAG_INDEX` (1, parameter-table index). | +| `index` | `uint32_t` | Parameter-table index, valid when `tag` is `INDEX`. | +| `value` | `double` | Fixed value, valid when `tag` is `FIXED`. | + +### CParameterValue + +| Field | Type | Description | +| --- | --- | --- | +| `tag` | `uint8_t` | `PARAMETER_VALUE_TAG_FIXED` (0, fixed value) or `PARAMETER_VALUE_TAG_PARAM` (1, symbolic parameter). | +| `value` | `double` | Fixed value, valid when `tag` is `FIXED`. | +| `param` | `CParameter*` | Newly allocated symbolic parameter handle valid when `tag` is `PARAM`; free it with `param_free`; NULL in the fixed case. | + +### circuit_map_param(ptr, param, out) + +Canonicalizes a symbolic parameter and interns it into the circuit's parameter table, writing out its storage form (fixed value or table index). + +- `ptr` (`CCircuit*`): circuit handle. +- `param` (`const CParameter*`): parameter handle. +- `out` (`CCircuitParam*`): storage form written out. + +Returns: `0` on success; `-1` for NULL; `-3` on evaluation failure. + +### circuit_resolve_parameter(ptr, param) + +Restores a storage-form parameter back into the full symbolic expression. + +- `ptr` (`const CCircuit*`): circuit handle. +- `param` (`const CCircuitParam*`): storage form. + +Returns: a newly allocated `CParameter*`, freed with `param_free`; `NULL` on NULL input, invalid tag, or resolution failure. + +### circuit_parameter_value(ptr, param, out) + +Resolves the current value of a storage-form parameter without interning anything new. + +- `ptr` (`const CCircuit*`): circuit handle. +- `param` (`const CCircuitParam*`): storage form. +- `out` (`CParameterValue*`): resolved value written out; in the symbolic case, free the `param` field with `param_free`. + +Returns: `0` on success; `-1` for NULL; `-3` for an invalid table index; `-8` for an invalid tag. + +```c +CCircuit *qc = circuit_new(1); +CParameter *theta = param_parse("theta"); +circuit_rx_param(qc, 0, theta); + +CCircuitParam storage; +CParameterValue value; +if (circuit_map_param(qc, theta, &storage) == 0 && + circuit_parameter_value(qc, &storage, &value) == 0) { + if (value.tag == PARAMETER_VALUE_TAG_PARAM) { + param_free(value.param); /* symbolic case: free the handle */ + } +} +param_free(theta); +circuit_free(qc); +``` + +--- + +## Symbol Table Queries + +The circuit distinguishes two symbol sets: the registry (which may include symbols interned but no longer referenced) and the symbols actually referenced by the executable IR (the global phase and operation parameters, including nested control-flow bodies). + +### circuit_symbols_len(ptr) + +Returns the number of symbol names in the registry; the registry may include symbols that are interned but no longer referenced by the circuit's operations. + +- `ptr` (`const CCircuit*`): circuit handle. + +Returns: the symbol count; 0 for NULL. + +### circuit_symbols(ptr, out, len) + +Two-step read of the registered symbol names: each written element is a freshly allocated C string the caller frees with `cqlib_string_free`. + +- `ptr` (`const CCircuit*`): circuit handle. +- `out` (`char**`): output buffer, may be NULL (count query only). +- `len` (`uintptr_t`): buffer length. + +Returns: the total number of registered symbols. + +### circuit_used_symbols_len(ptr) + +Returns the number of symbols referenced by the circuit's executable IR. + +- `ptr` (`const CCircuit*`): circuit handle. + +Returns: the symbol count; 0 for NULL. + +### circuit_used_symbols(ptr, out, len) + +Two-step read of the IR-referenced symbol names: each written element is a freshly allocated C string the caller frees with `cqlib_string_free`. + +- `ptr` (`const CCircuit*`): circuit handle. +- `out` (`char**`): output buffer, may be NULL (count query only). +- `len` (`uintptr_t`): buffer length. + +Returns: the total number of used symbols. + +### circuit_uses_symbol(ptr, name) + +Checks whether a symbol is referenced by the circuit's executable IR (the global phase and operation parameters, including nested control-flow bodies). + +- `ptr` (`const CCircuit*`): circuit handle. +- `name` (`const char*`): symbol name. + +Returns: `1` when used; `0` when not; `-1` for NULL; `-4` when the symbol name is not valid UTF-8. + +```c +uintptr_t n = circuit_used_symbols_len(qc); +char **names = malloc(n * sizeof(char *)); +circuit_used_symbols(qc, names, n); +for (uintptr_t i = 0; i < n; ++i) { + if (circuit_uses_symbol(qc, names[i]) == 1) { + /* names[i] is referenced by the IR */ + } + cqlib_string_free(names[i]); +} +free(names); +``` diff --git a/docs/api/en/c/0_circuit/4_operation_instruction.md b/docs/api/en/c/0_circuit/4_operation_instruction.md new file mode 100644 index 00000000..62f4a60c --- /dev/null +++ b/docs/api/en/c/0_circuit/4_operation_instruction.md @@ -0,0 +1,435 @@ +# Operation / Instruction + +`COperation*` and `CValueOperation*` are two read-only views of circuit operations. In the storage-layer view `COperation`, a parameter may be recorded as an index into the owning circuit's parameter table and can only be resolved with circuit context; `circuit_index` returns a `CValueOperation`, a resolved self-contained snapshot whose parameters are `CParameterValue` values (a fixed number or a full symbolic expression), independent of later circuit mutations. Delay instructions are covered at the end of this page. Gate functions and the gate-name list are covered in [Standard Gates](5_gate_standard.md); the memory rules of `CParameterValue` are described in [Parameter](3_parameter.md); error codes and memory conventions are described in [Overview](../0_overview.md). + +--- + +## Storage-Layer Operations + +A `COperation*` represents one concrete application of "instruction + acting qubits + parameters + optional label". `operation_new` creates a standard-gate operation from a gate name with fixed double parameters. + +### operation_new(gate_name, qubits, qubits_len, params, params_len) + +Creates an operation from a standard gate name (e.g. `"H"`, `"CX"`, `"RZZ"`); `params` holds fixed double values. + +- `gate_name` (`const char*`): standard gate name. +- `qubits` (`const uint32_t*`): array of acting qubit ids. +- `qubits_len` (`uintptr_t`): number of qubits. +- `params` (`const double*`): array of fixed parameter values. +- `params_len` (`uintptr_t`): number of parameters. + +Returns: a newly allocated `COperation*`, freed with `operation_free`; `NULL` when `gate_name` is NULL or names an unknown gate; also `NULL` when `qubits` is NULL while `qubits_len > 0`, or `params` is NULL while `params_len > 0`. + +### operation_free(ptr) + +Frees an operation handle. Passing NULL is allowed. + +- `ptr` (`COperation*`): operation handle. + +### operation_name(ptr) + +Returns the instruction name (e.g. `"H"`, `"RZZ"`). + +- `ptr` (`const COperation*`): operation handle. + +Returns: a newly allocated C string, freed with `cqlib_string_free`; `NULL` on NULL input. + +### operation_is_standard_gate(ptr) + +Returns whether the operation is driven by a standard-gate instruction. + +- `ptr` (`const COperation*`): operation handle. + +Returns: `1` when yes; `0` when no; `-1` for NULL. + +### operation_num_qubits(ptr) + +Returns the number of qubits the operation acts on. + +- `ptr` (`const COperation*`): operation handle. + +Returns: the qubit count; 0 for NULL. + +### operation_qubits_len(ptr) + +Returns the number of qubits the operation acts on, used to size the buffer for `operation_qubits`. + +- `ptr` (`const COperation*`): operation handle. + +Returns: the qubit count; 0 for NULL. + +### operation_qubits(ptr, buffer, len) + +Copies the qubit ids into `buffer` in application order. + +- `ptr` (`const COperation*`): operation handle. +- `buffer` (`uint32_t*`): output buffer. +- `len` (`uintptr_t`): buffer length. + +Returns: `0` on success; `-1` for NULL; `-8` when `len` is smaller than the qubit count. + +### operation_params_len(ptr) + +Returns the number of parameters carried by the operation. + +- `ptr` (`const COperation*`): operation handle. + +Returns: the parameter count; 0 for NULL. + +### operation_params(ptr, tags, values, len) + +Copies the parameters into the parallel arrays `tags` and `values`. `tags[i]` is one of the `OPERATION_PARAM_*` constants; `values[i]` is the fixed value, or — under the `OPERATION_PARAM_INDEX` tag — the parameter-table index (as a double). + +- `ptr` (`const COperation*`): operation handle. +- `tags` (`uint32_t*`): output array of parameter tags. +- `values` (`double*`): output array of parameter values. +- `len` (`uintptr_t`): buffer length. + +Returns: `0` on success; `-1` for NULL; `-8` when `len` is smaller than the parameter count. + +Parameter tags: + +| Constant | Value | Meaning | +| --- | --- | --- | +| `OPERATION_PARAM_FIXED` | 0 | Fixed value; `values[i]` is the value itself. | +| `OPERATION_PARAM_INDEX` | 1 | Index into the owning circuit's parameter table; `values[i]` is the index value and must be resolved against the circuit's table. | + +### operation_label(ptr) + +Returns the operation's optional metadata label. + +- `ptr` (`const COperation*`): operation handle. + +Returns: a newly allocated C string, freed with `cqlib_string_free`; `NULL` when the operation has no label or the handle is NULL. + +```c +uint32_t qubits[2] = {0, 1}; +double params[1] = {0.7}; +COperation *op = operation_new("RZZ", qubits, 2, params, 1); +if (op != NULL) { + char *name = operation_name(op); /* "RZZ" */ + cqlib_string_free(name); + + uintptr_t n = operation_qubits_len(op); + uint32_t *ids = malloc(n * sizeof(uint32_t)); + operation_qubits(op, ids, n); /* ids == {0, 1} */ + free(ids); + + operation_free(op); +} +``` + +--- + +## Instruction Kind Predicates + +Each predicate inspects which kind of instruction the operation carries; they all return `1` when yes, `0` when no, and `-1` for a NULL handle. + +### operation_is_unitary(ptr) + +Returns whether the operation uses a user-defined unitary instruction. + +- `ptr` (`const COperation*`): operation handle. + +Returns: `1` when yes; `0` when no; `-1` for NULL. + +### operation_is_mcgate(ptr) + +Returns whether the operation uses a multi-controlled-gate instruction. + +- `ptr` (`const COperation*`): operation handle. + +Returns: `1` when yes; `0` when no; `-1` for NULL. + +### operation_is_circuit_gate(ptr) + +Returns whether the operation uses a circuit-backed gate instruction. + +- `ptr` (`const COperation*`): operation handle. + +Returns: `1` when yes; `0` when no; `-1` for NULL. + +### operation_is_directive(ptr) + +Returns whether the operation uses a non-unitary directive instruction (barrier, measure, reset). + +- `ptr` (`const COperation*`): operation handle. + +Returns: `1` when yes; `0` when no; `-1` for NULL. + +### operation_is_delay(ptr) + +Returns whether the operation uses a delay instruction. + +- `ptr` (`const COperation*`): operation handle. + +Returns: `1` when yes; `0` when no; `-1` for NULL. + +### operation_is_classical_control(ptr) + +Returns whether the operation uses a structured classical-control-flow instruction (`if` / `while` / `for` / `switch` / `break` / `continue`). + +- `ptr` (`const COperation*`): operation handle. + +Returns: `1` when yes; `0` when no; `-1` for NULL. + +### operation_is_classical_data(ptr) + +Returns whether the operation uses a classical-data instruction (store, measure-into-value). + +- `ptr` (`const COperation*`): operation handle. + +Returns: `1` when yes; `0` when no; `-1` for NULL. + +### operation_is_quantum_gate(ptr) + +Returns whether the operation is a unitary quantum gate (standard, multi-controlled, user-defined unitary, or circuit-backed gate). + +- `ptr` (`const COperation*`): operation handle. + +Returns: `1` when yes; `0` when no; `-1` for NULL. + +### operation_is_instruction(ptr) + +Returns whether the operation carries a plain instruction rather than structured classical control flow. + +- `ptr` (`const COperation*`): operation handle. + +Returns: `1` when yes; `0` for classical-control operations; `-1` for NULL. + +--- + +## Instruction Introspection + +### operation_gate_arity(ptr, qubits, params) + +Writes the fixed `(qubit_count, parameter_count)` intrinsic to the instruction. + +- `ptr` (`const COperation*`): operation handle. +- `qubits` (`uintptr_t*`): written qubit count. +- `params` (`uintptr_t*`): written parameter count. + +Returns: `0` on success; `-1` for NULL; `-3` when the arity is variable (barrier, multi-qubit measurement, classical control). + +### operation_as_instruction(ptr) + +Returns an owned clone of the operation when it carries a plain (non-control-flow) instruction. + +- `ptr` (`const COperation*`): operation handle. + +Returns: a newly allocated `COperation*`, freed with `operation_free`; `NULL` on NULL input or when the operation is a classical-control instruction. + +### operation_directive(ptr) + +Returns the directive carried by the operation as one of the `OPERATION_DIRECTIVE_*` tags. + +- `ptr` (`const COperation*`): operation handle. + +Returns: the directive tag; `OPERATION_DIRECTIVE_NONE` (0) when the operation is not a directive; `-1` for NULL. + +Directive tags: + +| Constant | Value | Meaning | +| --- | --- | --- | +| `OPERATION_DIRECTIVE_NONE` | 0 | The operation carries no directive instruction. | +| `OPERATION_DIRECTIVE_BARRIER` | 1 | Barrier directive. | +| `OPERATION_DIRECTIVE_MEASURE` | 2 | Measure directive. | +| `OPERATION_DIRECTIVE_RESET` | 3 | Reset directive. | + +### operation_result(ptr, out) + +Writes a snapshot of the immutable classical value produced by a measurement operation into `out`: the circuit-local value table index plus the (tag, width) type parts (for the `CClassicalValueInfo` layout see [Classical Data and Control Flow](9_classical_control_flow.md)). + +- `ptr` (`const COperation*`): operation handle. +- `out` (`CClassicalValueInfo*`): output struct. + +Returns: `0` on success; `-1` for NULL; `-3` when the operation is not a measurement (no classical result). + +### operation_reads_value(ptr, expr) + +Returns whether the operation directly or recursively reads the immutable classical value referenced by `expr`. + +- `ptr` (`const COperation*`): operation handle. +- `expr` (`const CClassicalExpr*`): expression handle referencing a classical value. + +Returns: `1` when yes; `0` when no; `-1` for NULL; `-8` when `expr` is not a plain value expression. + +--- + +## Operation Matrix + +The three functions work together: query the length first, then fetch the dimensions and the data. Non-unitary instructions and operations with unresolved symbolic parameters have no numeric matrix. + +### operation_matrix_len(ptr) + +Returns the total number of complex elements (rows × cols) in the unitary matrix. + +- `ptr` (`const COperation*`): operation handle. + +Returns: the complex-element count; 0 on NULL input or when the operation has no numeric matrix (non-unitary instruction or unresolved symbolic parameter). + +### operation_matrix_dims(ptr, rows, cols) + +Writes the matrix shape into `rows` and `cols`. + +- `ptr` (`const COperation*`): operation handle. +- `rows` (`uintptr_t*`): written row count. +- `cols` (`uintptr_t*`): written column count. + +Returns: `0` on success; `-1` for NULL; `-3` when the operation has no numeric matrix. + +### operation_matrix(ptr, out, buffer_len) + +Copies the unitary matrix into `out` as interleaved `(re, im)` doubles in row-major order; the buffer must hold at least `2 × operation_matrix_len` doubles. + +- `ptr` (`const COperation*`): operation handle. +- `out` (`double*`): output buffer. +- `buffer_len` (`uintptr_t`): buffer length (number of doubles). + +Returns: the number of complex elements written; 0 on error. + +```c +uint32_t q0 = 0; +COperation *h = operation_new("H", &q0, 1, NULL, 0); +if (h != NULL) { + uintptr_t n = operation_matrix_len(h); /* 4 (2x2 complex elements) */ + double *buf = malloc(2 * n * sizeof(double)); + uintptr_t written = operation_matrix(h, buf, 2 * n); /* written == 4 */ + free(buf); + operation_free(h); +} +``` + +--- + +## Resolved Snapshots + +`circuit_index` returns a resolved snapshot `CValueOperation*` of a top-level operation: read-only and self-contained, with parameters as `CParameterValue` values (a fixed number or a full symbolic expression; free the `param` field of the symbolic case with `param_free`, see [Parameter](3_parameter.md)), unaffected by later circuit mutations. + +### circuit_index(ptr, index) + +Returns a resolved snapshot of the top-level operation at `index`. + +- `ptr` (`const CCircuit*`): circuit handle. +- `index` (`uintptr_t`): operation index. + +Returns: a newly allocated `CValueOperation*`, freed with `value_operation_free`; `NULL` on NULL input or an out-of-bounds index. + +### value_operation_free(ptr) + +Frees a snapshot handle. Passing NULL is allowed. + +- `ptr` (`CValueOperation*`): snapshot handle. + +### value_operation_name(ptr) + +Returns the operation's instruction name (e.g. `"H"`, `"delay"`). + +- `ptr` (`const CValueOperation*`): snapshot handle. + +Returns: a newly allocated C string, freed with `cqlib_string_free`; `NULL` on NULL input. + +### value_operation_instruction_type(ptr) + +Returns the operation's instruction category (e.g. `"standard"`, `"delay"`). + +- `ptr` (`const CValueOperation*`): snapshot handle. + +Returns: a newly allocated C string, freed with `cqlib_string_free`; `NULL` on NULL input. + +### value_operation_label(ptr) + +Returns the operation's optional metadata label. + +- `ptr` (`const CValueOperation*`): snapshot handle. + +Returns: a newly allocated C string, freed with `cqlib_string_free`; `NULL` when the operation has no label or the handle is NULL. + +### value_operation_num_qubits(ptr) + +Returns the number of qubits the operation acts on, used to size the buffer for `value_operation_qubits`. + +- `ptr` (`const CValueOperation*`): snapshot handle. + +Returns: the qubit count; 0 for NULL. + +### value_operation_qubits(ptr, out, len) + +Two-step read of the acting qubit ids: call `value_operation_num_qubits` first to size the buffer, then this copies the ids into `out`. + +- `ptr` (`const CValueOperation*`): snapshot handle. +- `out` (`uint32_t*`): output buffer, may be NULL (count query only). +- `len` (`uintptr_t`): buffer length. + +Returns: the total qubit count. + +### value_operation_num_params(ptr) + +Returns the number of parameters carried by the operation. + +- `ptr` (`const CValueOperation*`): snapshot handle. + +Returns: the parameter count; 0 for NULL. + +### value_operation_param(ptr, index, out) + +Reads the parameter at `index`, writing a `CParameterValue` to `out`. + +- `ptr` (`const CValueOperation*`): snapshot handle. +- `index` (`uintptr_t`): parameter index. +- `out` (`CParameterValue*`): resolved value written out; free the `param` field of the symbolic case with `param_free`. + +Returns: `0` on success; `-1` for NULL; `-3` when the index is out of bounds. + +```c +CCircuit *qc = circuit_new(1); +circuit_rx(qc, 0, 0.7); + +CValueOperation *op = circuit_index(qc, 0); +if (op != NULL) { + char *type = value_operation_instruction_type(op); /* "standard" */ + cqlib_string_free(type); + + CParameterValue v; + if (value_operation_param(op, 0, &v) == 0) { + /* v.tag == PARAMETER_VALUE_TAG_FIXED, v.value == 0.7 */ + } + value_operation_free(op); +} +circuit_free(qc); +``` + +--- + +## Delay Instructions + +### circuit_delay(ptr, qubit, duration) + +Appends a delay (idle period) with a numeric duration on `qubit`. + +- `ptr` (`CCircuit*`): circuit handle. +- `qubit` (`uint32_t`): qubit id. +- `duration` (`double`): duration. + +Returns: `0` on success; `-1` for NULL; `-2` when the qubit is out of bounds; `-3` when the duration is not finite. + +### circuit_delay_param(ptr, qubit, duration) + +Appends a delay with a symbolic duration on `qubit` (the duration parameter is cloned). + +- `ptr` (`CCircuit*`): circuit handle. +- `qubit` (`uint32_t`): qubit id. +- `duration` (`const CParameter*`): symbolic duration handle. + +Returns: `0` on success; `-1` for NULL; `-2` when the qubit is out of bounds; `-3` on failure. + +```c +CCircuit *qc = circuit_new(2); +circuit_delay(qc, 0, 100.0); /* numeric duration */ + +CParameter *tau = param_parse("tau"); +circuit_delay_param(qc, 1, tau); /* symbolic duration */ +param_free(tau); + +circuit_free(qc); +``` diff --git a/docs/api/en/c/0_circuit/5_gate_standard.md b/docs/api/en/c/0_circuit/5_gate_standard.md new file mode 100644 index 00000000..2140141f --- /dev/null +++ b/docs/api/en/c/0_circuit/5_gate_standard.md @@ -0,0 +1,169 @@ +# Standard Gates + +This page covers all C API functions that append standard quantum gates to a circuit; for global conventions on error codes and memory management, see [Overview](../0_overview.md). + +--- + +## Conventions + +- Every function on this page returns an `int32_t` error code: `0` on success, `-1` on NULL input, `-2` on an out-of-bounds qubit index, `-3` on a circuit error (a NaN or infinite numeric angle also returns `-3`). +- Numeric variants pass angles (in radians) as `double`; the matching `_param` variants pass angles as `const CParameter*` symbolic expressions and return `-1` when the parameter handle is NULL. `CParameter` handles are created with `param_parse` and released with `param_free`; see [Parameter](3_parameter.md). +- Each gate acts on a fixed number of qubits: one for single-qubit gates, two for two-qubit gates, three for the three-qubit gate; `qubit`/`control`/`target`/`a`/`b` are qubit indices within the circuit. + +--- + +## Single-qubit gates without parameters + +The following 13 functions all share the signature `(struct CCircuit *ptr, uint32_t qubit)` and append a fixed-matrix single-qubit gate. + +| Function | Gate | Description | +| --- | --- | --- | +| `circuit_h(ptr, qubit)` | H | Hadamard gate, converting between the computational and superposition bases. | +| `circuit_x(ptr, qubit)` | X | Pauli-X gate, a bit flip. | +| `circuit_y(ptr, qubit)` | Y | Pauli-Y gate, a bit flip with a phase. | +| `circuit_z(ptr, qubit)` | Z | Pauli-Z gate, a phase flip. | +| `circuit_i(ptr, qubit)` | I | Identity gate, leaves the quantum state unchanged. | +| `circuit_s(ptr, qubit)` | S | Clifford phase gate, diag(1, i). | +| `circuit_sdg(ptr, qubit)` | SDG | Inverse of S, diag(1, −i). | +| `circuit_t(ptr, qubit)` | T | π/8 phase gate, diag(1, e^(iπ/4)). | +| `circuit_tdg(ptr, qubit)` | TDG | Inverse of T, diag(1, e^(−iπ/4)). | +| `circuit_x2p(ptr, qubit)` | X2P | √X gate, a +90° rotation about the X axis. | +| `circuit_x2m(ptr, qubit)` | X2M | √X† gate, a −90° rotation about the X axis. | +| `circuit_y2p(ptr, qubit)` | Y2P | √Y gate, a +90° rotation about the Y axis. | +| `circuit_y2m(ptr, qubit)` | Y2M | √Y† gate, a −90° rotation about the Y axis. | + +--- + +## Parameterized single-qubit gates + +Numeric variants pass angles as `double`; `_param` variants pass a `const CParameter*`; both append the same standard gate. + +| Function | Gate | Description | +| --- | --- | --- | +| `circuit_rx(ptr, qubit, theta)` | RX | Rotation about the X axis by θ, exp(−iθX/2). | +| `circuit_rx_param(ptr, qubit, param_ptr)` | RX | Same gate, with a symbolic angle. | +| `circuit_ry(ptr, qubit, theta)` | RY | Rotation about the Y axis by θ, exp(−iθY/2). | +| `circuit_ry_param(ptr, qubit, param_ptr)` | RY | Same gate, with a symbolic angle. | +| `circuit_rz(ptr, qubit, theta)` | RZ | Rotation about the Z axis by θ, exp(−iθZ/2). | +| `circuit_rz_param(ptr, qubit, param_ptr)` | RZ | Same gate, with a symbolic angle. | +| `circuit_phase(ptr, qubit, lambda)` | Phase | Phase gate diag(1, e^(iλ)), applying a phase to the `1` component. | +| `circuit_phase_param(ptr, qubit, param)` | Phase | Same gate, with a symbolic phase. | +| `circuit_u(ptr, qubit, theta, phi, lambda)` | U | General single-qubit gate U(θ, φ, λ) with three angles. | +| `circuit_u_param(ptr, qubit, theta, phi, lambda)` | U | Same gate, with three symbolic angles. | +| `circuit_xy(ptr, qubit, theta)` | XY | XY-interaction-family gate with off-diagonal entries −i·e^(∓iθ). | +| `circuit_xy_param(ptr, qubit, param)` | XY | Same gate, with a symbolic phase. | +| `circuit_xy2p(ptr, qubit, theta)` | XY2P | +90° rotation about the XY-plane axis selected by θ. | +| `circuit_xy2p_param(ptr, qubit, param)` | XY2P | Same gate, with a symbolic axis angle. | +| `circuit_xy2m(ptr, qubit, theta)` | XY2M | −90° rotation about the XY-plane axis selected by θ. | +| `circuit_xy2m_param(ptr, qubit, param)` | XY2M | Same gate, with a symbolic axis angle. | +| `circuit_rxy(ptr, qubit, theta, phi)` | RXY | Rotation by θ about the XY-plane axis determined by φ. | +| `circuit_rxy_param(ptr, qubit, theta, phi)` | RXY | Same gate, with two symbolic angles. | + +--- + +## Two-qubit gates + +`cx`/`cy`/`cz`/`swap` are fixed-matrix gates; the rest are parameterized by angles, with `_param` variants taking a `const CParameter*`. + +| Function | Gate | Description | +| --- | --- | --- | +| `circuit_cx(ptr, control, target)` | CX | Controlled-X (CNOT); flips the target when the control is `1`. | +| `circuit_cy(ptr, control, target)` | CY | Controlled-Y; applies Y to the target when the control is `1`. | +| `circuit_cz(ptr, control, target)` | CZ | Controlled-Z; applies Z to the target when the control is `1`. | +| `circuit_swap(ptr, a, b)` | SWAP | Exchanges the states of two qubits. | +| `circuit_rxx(ptr, a, b, theta)` | RXX | Two-body Pauli rotation exp(−iθ X⊗X/2). | +| `circuit_rxx_param(ptr, a, b, param)` | RXX | Same gate, with a symbolic angle. | +| `circuit_ryy(ptr, a, b, theta)` | RYY | Two-body Pauli rotation exp(−iθ Y⊗Y/2). | +| `circuit_ryy_param(ptr, a, b, param)` | RYY | Same gate, with a symbolic angle. | +| `circuit_rzz(ptr, a, b, theta)` | RZZ | Two-body Pauli rotation exp(−iθ Z⊗Z/2). | +| `circuit_rzz_param(ptr, a, b, param)` | RZZ | Same gate, with a symbolic angle. | +| `circuit_rzx(ptr, a, b, theta)` | RZX | Two-body Pauli rotation exp(−iθ Z⊗X/2), with Z on `a` and X on `b`. | +| `circuit_rzx_param(ptr, a, b, param)` | RZX | Same gate, with a symbolic angle. | +| `circuit_crx(ptr, control, target, theta)` | CRX | Controlled RX(θ); rotates the target when the control is `1`. | +| `circuit_crx_param(ptr, control, target, param)` | CRX | Same gate, with a symbolic angle. | +| `circuit_cry(ptr, control, target, theta)` | CRY | Controlled RY(θ); rotates the target when the control is `1`. | +| `circuit_cry_param(ptr, control, target, param)` | CRY | Same gate, with a symbolic angle. | +| `circuit_crz(ptr, control, target, theta)` | CRZ | Controlled RZ(θ); rotates the target when the control is `1`. | +| `circuit_crz_param(ptr, control, target, param)` | CRZ | Same gate, with a symbolic angle. | +| `circuit_fsim(ptr, a, b, theta, phi)` | fSim | fSim gate: rotates the `01`/`10` block by θ and applies an e^(−iφ) phase to the `11` component. | +| `circuit_fsim_param(ptr, a, b, theta, phi)` | fSim | Same gate, with two symbolic angles. | + +--- + +## Three-qubit gate + +| Function | Gate | Description | +| --- | --- | --- | +| `circuit_ccx(ptr, control1, control2, target)` | CCX | Toffoli gate; flips the target when both controls are `1`. | + +--- + +## Mathematical conventions + +Rotation gates use the half-angle exponential definition: + +```text +RX(θ) = exp(-i θ X / 2) +RY(θ) = exp(-i θ Y / 2) +RZ(θ) = exp(-i θ Z / 2) +RXY(θ, φ) = rotation by θ about the XY-plane axis at azimuth φ (half-angle convention) +``` + +Two-body Pauli rotation gates use the same convention: + +```text +RXX(θ) = exp(-i θ X⊗X / 2) +RYY(θ) = exp(-i θ Y⊗Y / 2) +RZZ(θ) = exp(-i θ Z⊗Z / 2) +RZX(θ) = exp(-i θ Z⊗X / 2) +``` + +Matrix forms of the remaining gates: + +```text +U(θ, φ, λ) = [[cos(θ/2), -e^(-iλ)·sin(θ/2)], + [e^(iφ)·sin(θ/2), e^(i(φ+λ))·cos(θ/2)]] + +fSim(θ, φ) = [[1, 0, 0, 0 ], + [0, cos(θ), -i·sin(θ), 0 ], + [0, -i·sin(θ), cos(θ), 0 ], + [0, 0, 0, e^(-iφ)]] +``` + +--- + +## Example + +```c +#include "cqlib_c.h" + +/* Bell circuit: H(0) + CX(0, 1) */ +CCircuit *c = circuit_new(2); + +if (circuit_h(c, 0) != 0) { + /* handle error */ +} +if (circuit_cx(c, 0, 1) != 0) { + /* handle error */ +} + +/* Symbolic parameter: RZ(0, theta), where theta is a symbolic expression */ +CParameter *theta = param_parse("theta"); + +if (circuit_rz_param(c, 0, theta) != 0) { + /* handle error */ +} + +param_free(theta); +circuit_free(c); +``` + +--- + +## See also + +- [Parameter](3_parameter.md): creating, evaluating and freeing `CParameter`. +- [Circuit](1_circuit.md) and [Operation Instructions](4_operation_instruction.md): non-unitary instructions such as `circuit_measure`, `circuit_reset`, `circuit_barrier` and `circuit_delay`. +- [Unitary Gates](6_gate_unitary.md): matrix-defined custom unitary gates. +- [Multi-Controlled Gates](7_gate_mc_gate.md): adding control qubits to any standard gate. +- [Circuit to Matrix](13_circuit_to_matrix.md): numeric output of a whole circuit's unitary matrix. diff --git a/docs/api/en/c/0_circuit/6_gate_unitary.md b/docs/api/en/c/0_circuit/6_gate_unitary.md new file mode 100644 index 00000000..df92ccba --- /dev/null +++ b/docs/api/en/c/0_circuit/6_gate_unitary.md @@ -0,0 +1,183 @@ +# Unitary Gates + +This page covers the C API interfaces for appending matrix-defined custom unitary gates to a circuit; for global conventions on error codes and memory management, see [Overview](../0_overview.md). + +--- + +## circuit_unitary(ptr, label, num_qubits, matrix, qubits, qubits_len) + +Appends a custom unitary gate defined by a numeric matrix to the circuit. + +Parameters: + +- `ptr` (`struct CCircuit *`): target circuit handle. +- `label` (`const char *`): readable gate name, used for display, visualization and IR output. +- `num_qubits` (`uintptr_t`): number of qubits n the gate acts on, at most 20. +- `matrix` (`const Complex64 *`): row-major flattened 2^n × 2^n complex matrix with 2^n·2^n `Complex64` elements; each element is a `{double re; double im}` struct with interleaved real/imaginary parts, the same layout as the `circuit_to_matrix` output. +- `qubits` (`const uint32_t *`): array of qubit indices the gate acts on. +- `qubits_len` (`uintptr_t`): length of `qubits`; must equal `num_qubits`. + +Returns: `int32_t`, `0` on success; `-1` on NULL input (`ptr`, `label` or `matrix` is NULL); `-2` on out-of-bounds qubits; `-3` when the matrix is not unitary, the shape mismatches or application fails; `-4` on invalid UTF-8 in `label`; `-8` when `num_qubits` exceeds 20. + +The matrix is checked for unitarity when appended: U†U must be the identity matrix within tolerance, otherwise `-3` is returned. A gate defined by a numeric matrix takes no application parameters, and `qubits` maps onto the gate's qubits in array order. + +```c +#include +#include "cqlib_c.h" + +/* Custom 2x2 unitary gate: sqrt(X) */ +CCircuit *c = circuit_new(1); + +double s = 1.0 / sqrt(2.0); +Complex64 sqrt_x[4] = { + {s, 0.0}, {0.0, -s}, + {0.0, -s}, {s, 0.0}, +}; + +uint32_t qubits[1] = {0}; +if (circuit_unitary(c, "sqrt_x", 1, sqrt_x, qubits, 1) != 0) { + /* handle error */ +} + +circuit_free(c); +``` + +--- + +## circuit_unitary_with_params(ptr, label, num_qubits, re_exprs, im_exprs, param_names, param_names_len, qubits, qubits_len, params, params_len) + +Appends a parameterized custom unitary gate defined by a symbolic matrix to the circuit. + +Parameters: + +- `ptr` (`struct CCircuit *`): target circuit handle. +- `label` (`const char *`): readable gate name. +- `num_qubits` (`uintptr_t`): number of qubits n the gate acts on, at most 20. +- `re_exprs` (`const char *const *`): row-major flattened array of 2^n·2^n real-part expression strings, e.g. `"cos(theta)"`, `"1"`. +- `im_exprs` (`const char *const *`): array of imaginary-part expression strings with the same layout; may be NULL, in which case all imaginary parts are zero. +- `param_names` (`const char *const *`): list of positional parameter names declaring the order in which application arguments bind to the symbols in the matrix. +- `param_names_len` (`uintptr_t`): length of `param_names`. +- `qubits` (`const uint32_t *`): array of qubit indices the gate acts on. +- `qubits_len` (`uintptr_t`): length of `qubits`; must equal `num_qubits`. +- `params` (`const struct CParameter *const *`): array of parameter handles for the application; the i-th argument binds to `param_names[i]`. +- `params_len` (`uintptr_t`): length of `params`; must equal `param_names_len`. + +Returns: `int32_t`, `0` on success; `-1` on NULL input; `-2` on out-of-bounds qubits; `-3` on shape, symbol-declaration or application failure; `-4` on invalid UTF-8 or expression syntax errors; `-8` on invalid sizes (`num_qubits` exceeds 20 or `param_names_len` exceeds 65535). + +Expressions use the parameter-expression syntax (the same as `param_parse`); every free symbol appearing in the matrix must be declared in `param_names`. Numeric angles are written as expressions, for example `"pi/2"`. + +```c +#include "cqlib_c.h" + +/* Parameterized diagonal phase gate diag(1, e^(i·theta)), applied with theta = pi/2 */ +CCircuit *c = circuit_new(1); + +const char *re[4] = {"1", "0", "0", "cos(theta)"}; +const char *im[4] = {"0", "0", "0", "sin(theta)"}; +const char *names[1] = {"theta"}; + +CParameter *angle = param_parse("pi/2"); +const struct CParameter *params[1] = {angle}; + +uint32_t qubits[1] = {0}; +if (circuit_unitary_with_params(c, "phase_e", 1, + re, im, names, 1, + qubits, 1, params, 1) != 0) { + /* handle error */ +} + +param_free(angle); +circuit_free(c); +``` + +--- + +## Evaluating a Unitary Matrix at Parameter Values + +The C ABI has no standalone unitary-gate handle, so evaluating a gate definition outside a circuit reuses the `circuit_unitary_with_params` inputs: a symbolic matrix plus the ordered formal parameter names. + +### unitary_gate_matrix_for_params_len(matrix) + +```c +uintptr_t unitary_gate_matrix_for_params_len(const struct CSymbolicMatrix *matrix); +``` + +Returns the number of `Complex64` elements (`rows * cols`) that `unitary_gate_matrix_for_params` writes for `matrix`; 0 for NULL or a non-square symbolic matrix. + +### unitary_gate_matrix_for_params(matrix, param_names, param_names_len, params, params_len, buffer, buffer_len) + +```c +int32_t unitary_gate_matrix_for_params(const struct CSymbolicMatrix *matrix, + const char *const *param_names, + uintptr_t param_names_len, + const double *params, + uintptr_t params_len, + Complex64 *buffer, + uintptr_t buffer_len); +``` + +Evaluates a custom unitary gate's symbolic matrix at positional numeric parameter values and writes the resulting numeric matrix to `buffer` in row-major order. The gate is built and checked exactly as in `circuit_unitary_with_params` (shape, parameter count, finite values, unitarity). + +Parameters: + +- `matrix` (`const struct CSymbolicMatrix *`): symbolic `2^n × 2^n` matrix (construction in [Symbolic Matrix](10_symbolic_matrix.md)). +- `param_names` (`const char *const *`): ordered formal parameter names binding the application arguments to the matrix symbols; may be NULL when `param_names_len` is 0. +- `param_names_len` (`uintptr_t`): length of `param_names`, at most 65535. +- `params` (`const double *`): numeric parameter values; may be NULL when `params_len` is 0. +- `params_len` (`uintptr_t`): length of `params`. +- `buffer` (`Complex64 *`): output buffer receiving the evaluated row-major matrix. +- `buffer_len` (`uintptr_t`): buffer length; must be at least `unitary_gate_matrix_for_params_len(matrix)`. + +Returns: `int32_t`, `0` on success; `-1` on NULL input; `-3` on shape, parameter-count, non-finite-value or unitarity failure; `-4` on invalid UTF-8 parameter names; `-8` on invalid sizes (empty or non-power-of-two matrix, more than 20 qubits, or a too-small buffer). + +```c +#include +#include "cqlib_c.h" + +/* Evaluate U(theta) = cos(theta)*I + i*sin(theta)*X at theta = pi/2 (gives iX) */ +struct CParameter *zero = param_parse("0"); +struct CParameter *cos_t = param_parse("cos(theta)"); +struct CParameter *sin_t = param_parse("sin(theta)"); +struct CSymbolicComplex *e00 = symbolic_complex_new(cos_t, zero); +struct CSymbolicComplex *e01 = symbolic_complex_new(zero, sin_t); +struct CSymbolicComplex *e10 = symbolic_complex_new(zero, sin_t); +struct CSymbolicComplex *e11 = symbolic_complex_new(cos_t, zero); +const struct CSymbolicComplex *elements[4] = {e00, e01, e10, e11}; +struct CSymbolicMatrix *matrix = symbolic_matrix_new(2, 2, elements, 4); + +const char *names[1] = {"theta"}; +double params[1] = {3.141592653589793 / 2.0}; + +uintptr_t len = unitary_gate_matrix_for_params_len(matrix); /* 4 */ +Complex64 out[4]; +if (unitary_gate_matrix_for_params(matrix, names, 1, params, 1, out, len) == 0) { + /* out = {{0,0},{0,1},{0,1},{0,0}} -> the matrix iX */ +} + +symbolic_matrix_free(matrix); +symbolic_complex_free(e00); +symbolic_complex_free(e01); +symbolic_complex_free(e10); +symbolic_complex_free(e11); +param_free(zero); +param_free(cos_t); +param_free(sin_t); +``` + +--- + +## Comparison of the two definition styles + +| Function | Matrix definition | Application parameters | Typical use | +| --- | --- | --- | --- | +| `circuit_unitary` | Numeric `Complex64` array | None | Fixed black-box matrices, calibration gates, test oracles. | +| `circuit_unitary_with_params` | Real/imaginary expression strings | `CParameter` array bound positionally via `param_names` | Parameterized custom matrix gates. | + +--- + +## See also + +- [Standard Gates](5_gate_standard.md): the built-in standard gate set. +- [Parameter](3_parameter.md): creating `CParameter` and the expression syntax. +- [Circuit Gates](8_gate_circuit_gate.md): composite gates defined by sub-circuits. +- [Circuit to Matrix](13_circuit_to_matrix.md): matrix-layout output matching the `circuit_unitary` input. diff --git a/docs/api/en/c/0_circuit/7_gate_mc_gate.md b/docs/api/en/c/0_circuit/7_gate_mc_gate.md new file mode 100644 index 00000000..5eb73873 --- /dev/null +++ b/docs/api/en/c/0_circuit/7_gate_mc_gate.md @@ -0,0 +1,101 @@ +# Multi-Controlled Gates + +This page covers `circuit_multi_control`, the C API interface for adding control qubits to any standard gate; for global conventions on error codes and memory management, see [Overview](../0_overview.md). + +--- + +## circuit_multi_control(ptr, gate_name, controls, controls_len, targets, targets_len, params, params_len) + +Appends a multi-controlled standard gate to the circuit: the base gate acts on its target qubits only when every control qubit is in the `1` state. + +Parameters: + +- `ptr` (`struct CCircuit *`): target circuit handle. +- `gate_name` (`const char *`): name of the base standard gate; see the gate-name list below. +- `controls` (`const uint32_t *`): array of control qubit indices. +- `controls_len` (`uintptr_t`): number of control qubits; may be 0. +- `targets` (`const uint32_t *`): array of qubits the base gate acts on; its length must equal the base gate's own qubit count. +- `targets_len` (`uintptr_t`): length of `targets`. +- `params` (`const struct CParameter *const *`): array of parameter handles for the base gate; its length must equal the base gate's parameter count; pass `NULL, 0` for parameterless gates. +- `params_len` (`uintptr_t`): length of `params`. + +The operation's qubit list is `[controls..., targets...]`: the added control qubits come first, followed by the qubits the base gate itself requires; if the base gate carries built-in controls (such as `"CX"`), its internal control qubits keep their original order. For example, with base gate `"X"`, two controls and one target, the list `[c0, c1, t]` means `c0` and `c1` control and `t` is flipped; with base gate `"CX"` and one added control, the three qubits are in order `[new_control, cx_control, cx_target]`. + +Returns: `int32_t`, `0` on success; `-1` on NULL input (`ptr` or `gate_name` is NULL, or an array is NULL while its length is non-zero); `-2` on out-of-bounds qubits; `-3` when the base gate cannot be controlled or the qubit/parameter counts mismatch; `-4` on invalid UTF-8 in `gate_name`; `-8` on an unknown gate name. + +Parameters are passed as `CParameter` symbolic expressions (numeric angles are written as expressions, e.g. `"pi/2"`) and correspond positionally to the base gate's parameters (for example, `"U"` takes 3, `"RXY"` and `"fSim"` take 2 each, `"RX"` takes 1). + +### Supported base gate names + +`I`, `H`, `X`, `Y`, `Z`, `S`, `SDG`, `T`, `TDG`, `X2P`, `X2M`, `Y2P`, `Y2M`, `RX`, `RY`, `RZ`, `Phase`, `GPhase`, `U`, `XY`, `XY2P`, `XY2M`, `RXY`, `SWAP`, `RXX`, `RYY`, `RZZ`, `RZX`, `CX`, `CY`, `CZ`, `CCX`, `CRX`, `CRY`, `CRZ`, `fSim` (both `FSIM` and `fSim` are accepted). + +### MCX example + +```c +#include "cqlib_c.h" + +CCircuit *c = circuit_new(4); + +/* Doubly controlled X (Toffoli semantics): controls = {0, 1}, target = 2 */ +uint32_t ccx_controls[2] = {0, 1}; +uint32_t ccx_target[1] = {2}; + +if (circuit_multi_control(c, "X", + ccx_controls, 2, + ccx_target, 1, + NULL, 0) != 0) { + /* handle error */ +} + +/* Triply controlled H */ +uint32_t mch_controls[3] = {0, 1, 2}; +uint32_t mch_target[1] = {3}; + +if (circuit_multi_control(c, "H", + mch_controls, 3, + mch_target, 1, + NULL, 0) != 0) { + /* handle error */ +} + +/* Singly controlled RZ(theta) with a symbolic angle */ +CParameter *theta = param_parse("theta"); +const struct CParameter *crz_params[1] = {theta}; +uint32_t crz_control[1] = {0}; +uint32_t crz_target[1] = {1}; + +if (circuit_multi_control(c, "RZ", + crz_control, 1, + crz_target, 1, + crz_params, 1) != 0) { + /* handle error */ +} + +param_free(theta); +circuit_free(c); +``` + +--- + +## Relation to standard controlled gates + +Common controlled gates are special cases of `circuit_multi_control` with a specific base gate: + +| Standard gate function | Equivalent call | +| --- | --- | +| `circuit_cx(ptr, c, t)` | `circuit_multi_control(ptr, "X", {c}, 1, {t}, 1, NULL, 0)` | +| `circuit_cy(ptr, c, t)` | `circuit_multi_control(ptr, "Y", {c}, 1, {t}, 1, NULL, 0)` | +| `circuit_cz(ptr, c, t)` | `circuit_multi_control(ptr, "Z", {c}, 1, {t}, 1, NULL, 0)` | +| `circuit_ccx(ptr, c1, c2, t)` | `circuit_multi_control(ptr, "X", {c1, c2}, 2, {t}, 1, NULL, 0)` | +| `circuit_crx(ptr, c, t, θ)` | `circuit_multi_control(ptr, "RX", {c}, 1, {t}, 1, {θ}, 1)` | +| `circuit_cry(ptr, c, t, θ)` | `circuit_multi_control(ptr, "RY", {c}, 1, {t}, 1, {θ}, 1)` | +| `circuit_crz(ptr, c, t, θ)` | `circuit_multi_control(ptr, "RZ", {c}, 1, {t}, 1, {θ}, 1)` | + +Use `circuit_multi_control` when more control qubits are needed or controls must be added to an arbitrary standard gate; for singly or doubly controlled base gates, call the [standard gate](5_gate_standard.md) functions directly. + +--- + +## See also + +- [Standard Gates](5_gate_standard.md): the base gate set and each gate's parameter count. +- [Parameter](3_parameter.md): creating `CParameter` and the expression syntax. diff --git a/docs/api/en/c/0_circuit/8_gate_circuit_gate.md b/docs/api/en/c/0_circuit/8_gate_circuit_gate.md new file mode 100644 index 00000000..29f63ac4 --- /dev/null +++ b/docs/api/en/c/0_circuit/8_gate_circuit_gate.md @@ -0,0 +1,198 @@ +# Circuit Gates + +This page covers circuit gates in the C API: wrapping an already-built circuit into a named, reusable composite gate; for global conventions on error codes and memory management, see [Overview](../0_overview.md). + +--- + +## circuit_to_gate(ptr, name) + +Freezes a copy of the circuit into a named composite gate. The source circuit is cloned and stays usable after the call; it may be released afterwards. + +Parameters: + +- `ptr` (`const struct CCircuit *`): source circuit handle. +- `name` (`const char *`): composite gate name. + +Returns: a newly allocated `struct CCircuitGate *`, freed with `circuit_gate_free`; NULL on NULL input, invalid UTF-8 in the name or failure. + +When wrapping, the source circuit's free symbols form the composite gate's call signature in order of appearance, queryable through `circuit_gate_signature_params`. + +--- + +## circuit_gate_free(ptr) + +Frees a composite gate handle. Passing NULL is allowed; no return value. + +--- + +## circuit_gate_name(ptr) + +Returns the gate's name as a newly allocated C string. + +Parameters: + +- `ptr` (`const struct CCircuitGate *`): composite gate handle. + +Returns: `char *`, freed with `cqlib_string_free`; NULL on NULL input or error. + +--- + +## circuit_gate_num_qubits(ptr) + +Returns the number of qubits the gate acts on when applied. + +Parameters: + +- `ptr` (`const struct CCircuitGate *`): composite gate handle. + +Returns: `uintptr_t` qubit count; 0 for NULL. + +--- + +## circuit_gate_num_params(ptr) + +Returns the number of positional parameters in the gate's call signature. + +Parameters: + +- `ptr` (`const struct CCircuitGate *`): composite gate handle. + +Returns: `uintptr_t` parameter count; 0 for NULL. + +--- + +## circuit_gate_signature_params_len(ptr) + +Returns the number of positional parameter names in the gate's call signature; the first step of the two-step output pattern. + +Parameters: + +- `ptr` (`const struct CCircuitGate *`): composite gate handle. + +Returns: `uintptr_t` signature name count; 0 for NULL. + +--- + +## circuit_gate_signature_params(ptr, out, len) + +Copies the call signature's parameter names into `out` (two-step output pattern; call `circuit_gate_signature_params_len` first). + +Parameters: + +- `ptr` (`const struct CCircuitGate *`): composite gate handle. +- `out` (`char **`): output buffer allocated by the caller according to the length; passing NULL only queries the length. +- `len` (`uintptr_t`): number of elements `out` can hold. + +Returns: `uintptr_t` total number of signature names; each written string is freed by the caller with `cqlib_string_free`. + +--- + +## circuit_circuit_gate(ptr, gate, qubits, qubits_len, params, params_len) + +Appends the composite gate to the circuit, binding `params` positionally to the gate's call signature: the i-th argument replaces the i-th signature symbol name, with all substitutions applied simultaneously. + +Parameters: + +- `ptr` (`struct CCircuit *`): target circuit handle. +- `gate` (`const struct CCircuitGate *`): composite gate handle. +- `qubits` (`const uint32_t *`): qubit indices in the outer circuit the gate acts on, mapped onto the gate's qubits in array order. +- `qubits_len` (`uintptr_t`): length of `qubits`; must equal `circuit_gate_num_qubits(gate)`. +- `params` (`const struct CParameter *const *`): array of positional parameter handles; pass `NULL, 0` for parameterless gates. +- `params_len` (`uintptr_t`): length of `params`; must equal `circuit_gate_num_params(gate)`. + +Returns: `int32_t`, `0` on success; `-1` on NULL input (an array is NULL while its length is non-zero); `-2` on out-of-bounds qubits; `-3` on qubit/parameter count mismatch or application failure. + +--- + +## Example: wrapping a Bell circuit as a gate and reusing it + +```c +#include +#include "cqlib_c.h" + +int main(void) { + /* 1. Build the Bell sub-circuit */ + CCircuit *inner = circuit_new(2); + circuit_h(inner, 0); + circuit_cx(inner, 0, 1); + + /* 2. Wrap it into a named composite gate; the source circuit + is cloned and can be released right away */ + struct CCircuitGate *bell = circuit_to_gate(inner, "Bell"); + circuit_free(inner); + + if (bell == NULL) { + return 1; + } + + /* 3. Query gate metadata: num_qubits == 2, num_params == 0 */ + char *name = circuit_gate_name(bell); + printf("gate: %s, qubits: %zu, params: %zu\n", + name, + circuit_gate_num_qubits(bell), + circuit_gate_num_params(bell)); + cqlib_string_free(name); + + /* 4. Reuse the Bell gate twice in an outer circuit */ + CCircuit *outer = circuit_new(4); + uint32_t first[2] = {0, 1}; + uint32_t second[2] = {2, 3}; + + if (circuit_circuit_gate(outer, bell, first, 2, NULL, 0) != 0) { + /* handle error */ + } + if (circuit_circuit_gate(outer, bell, second, 2, NULL, 0) != 0) { + /* handle error */ + } + + circuit_gate_free(bell); + circuit_free(outer); + return 0; +} +``` + +A parameterized composite gate binds through its signature: the sub-circuit below uses the symbol `theta`, its signature after wrapping is `["theta"]`, and the outer call substitutes `alpha`. + +```c +#include +#include "cqlib_c.h" + +CCircuit *inner = circuit_new(1); +CParameter *theta = param_parse("theta"); + +circuit_rz_param(inner, 0, theta); +param_free(theta); + +struct CCircuitGate *rot = circuit_to_gate(inner, "RzBlock"); +circuit_free(inner); + +/* Signature query: circuit_gate_signature_params_len(rot) == 1 */ +uintptr_t n = circuit_gate_signature_params_len(rot); +char **names = malloc(n * sizeof(char *)); +circuit_gate_signature_params(rot, names, n); /* names[0] == "theta" */ +for (uintptr_t i = 0; i < n; ++i) { + cqlib_string_free(names[i]); +} +free(names); + +/* Outer call: bind theta to alpha */ +CCircuit *outer = circuit_new(1); +CParameter *alpha = param_parse("alpha"); +const struct CParameter *bind[1] = {alpha}; +uint32_t q[1] = {0}; + +circuit_circuit_gate(outer, rot, q, 1, bind, 1); + +param_free(alpha); +circuit_gate_free(rot); +circuit_free(outer); +``` + +--- + +## See also + +- [Circuit](1_circuit.md): circuit creation, appending and composition. +- [Parameter](3_parameter.md): creating and freeing `CParameter`. +- [Unitary Gates](6_gate_unitary.md): matrix-defined custom unitary gates. +- [Circuit to Matrix](13_circuit_to_matrix.md): whole-circuit matrix computation after composite gates are expanded. diff --git a/docs/api/en/c/0_circuit/9_classical_control_flow.md b/docs/api/en/c/0_circuit/9_classical_control_flow.md new file mode 100644 index 00000000..5b79e590 --- /dev/null +++ b/docs/api/en/c/0_circuit/9_classical_control_flow.md @@ -0,0 +1,708 @@ +# Classical Variables and Control Flow + +This page covers the C API functions for classical data and structured control flow: allocating and inspecting classical variables, building and introspecting classical expressions, measurement declarations, classical storage, and callback-driven `if` / `if-else` / `while` / `for` / `switch` control flow. See [Overview](../0_overview.md) for the error-code and memory-management conventions. + +--- + +## Type tags and snapshot structs + +The static type of classical data is described by a (tag, width) pair carried in a `CClassicalType` snapshot: + +```c +typedef struct CClassicalType { + uint32_t tag; /* one of CQLIB_CLASSICAL_TYPE_* */ + uint32_t width; /* width in bits, always 1 for Bit/Bool */ +} CClassicalType; +``` + +| Type tag | Value | Type | Description | +| --- | --- | --- | --- | +| `CQLIB_CLASSICAL_TYPE_BIT` | 0 | `Bit` | A single bit with value 0 or 1, typically a single-qubit measurement result. | +| `CQLIB_CLASSICAL_TYPE_BOOL` | 1 | `Bool` | A logical Boolean used for `if` / `while` conditions. | +| `CQLIB_CLASSICAL_TYPE_UINT` | 2 | `UInt(width)` | An unsigned integer of the given width. | +| `CQLIB_CLASSICAL_TYPE_BIT_VEC` | 3 | `BitVec(width)` | A bit vector of the given width. | + +Immutable classical values read by an expression are reported as `CClassicalValueInfo` snapshots: + +```c +typedef struct CClassicalValueInfo { + uint32_t index; /* circuit-local value table index */ + uint32_t tag; /* one of CQLIB_CLASSICAL_TYPE_* */ + uint32_t width; /* width in bits, always 1 for Bit/Bool */ +} CClassicalValueInfo; +``` + +The node kind of an expression is described by the tag written by `classical_expr_kind`: + +| Node tag | Value | Node | +| --- | --- | --- | +| `CQLIB_CLASSICAL_EXPR_VAR` | 0 | Variable read | +| `CQLIB_CLASSICAL_EXPR_VALUE` | 1 | Immutable value read | +| `CQLIB_CLASSICAL_EXPR_BOOL_LITERAL` | 2 | `Bool` literal | +| `CQLIB_CLASSICAL_EXPR_BIT_LITERAL` | 3 | `Bit` literal | +| `CQLIB_CLASSICAL_EXPR_UINT_LITERAL` | 4 | `UInt` literal | +| `CQLIB_CLASSICAL_EXPR_BIT_VEC_LITERAL` | 5 | `BitVec` literal | +| `CQLIB_CLASSICAL_EXPR_UNARY` | 6 | Unary (`not`) | +| `CQLIB_CLASSICAL_EXPR_BINARY` | 7 | Binary (`and` / `or` / `xor`) | +| `CQLIB_CLASSICAL_EXPR_COMPARE` | 8 | Compare (`eq` / `ne` / `lt` / `le` / `gt` / `ge`) | +| `CQLIB_CLASSICAL_EXPR_CAST` | 9 | Cast (`bit_to_bool` / `bit_vec_to_uint`) | +| `CQLIB_CLASSICAL_EXPR_SELECT` | 10 | Ternary select | +| `CQLIB_CLASSICAL_EXPR_EXTRACT_BIT` | 11 | Single-bit extraction | +| `CQLIB_CLASSICAL_EXPR_EXTRACT_BITS` | 12 | Contiguous bit-range extraction | +| `CQLIB_CLASSICAL_EXPR_CONCAT` | 13 | `BitVec` concatenation | +| `CQLIB_CLASSICAL_EXPR_PACK_BITS` | 14 | `Bit` packing | + +--- + +## Classical variables + +### circuit_var(ptr, ty_tag, width) + +Allocates a mutable classical variable owned by the circuit; the handle is owned by the caller. + +- `ptr` (`CCircuit*`): target circuit handle. +- `ty_tag` (`uint32_t`): type tag, one of `CQLIB_CLASSICAL_TYPE_*`. +- `width` (`uint32_t`): bit width, used only for `UInt` / `BitVec` (zero is rejected); ignored for `Bit` / `Bool`. + +Returns a newly allocated `CClassicalVar*`; returns NULL for a NULL circuit or invalid type parameters. + +### classical_var_free(ptr) + +Frees a classical variable handle. + +- `ptr` (`CClassicalVar*`): handle to free; NULL is allowed. + +No return value. + +### classical_var_id(ptr) + +Returns the circuit-local stable identity id of the variable. + +- `ptr` (`const CClassicalVar*`): variable handle. + +Returns the `uint32_t` id, or `UINT32_MAX` for NULL. + +### classical_var_index(ptr) + +Returns the variable's index in the circuit's variable table. + +- `ptr` (`const CClassicalVar*`): variable handle. + +Returns the `uint32_t` table index, or `UINT32_MAX` for NULL. + +### classical_var_ty(ptr, tag, width) + +Writes the variable's type (tag + width) to the out-parameters. + +- `ptr` (`const CClassicalVar*`): variable handle. +- `tag` (`uint32_t*`): out-parameter receiving a `CQLIB_CLASSICAL_TYPE_*` tag. +- `width` (`uint32_t*`): out-parameter receiving the width. + +Returns 0 on success, -1 on NULL pointers. + +### classical_var_expr(ptr) + +Creates a new expression that reads the current runtime value of the variable. + +- `ptr` (`const CClassicalVar*`): variable handle. + +Returns a newly allocated `CClassicalExpr*` (free with `classical_expr_free`); NULL on NULL input. + +### circuit_classical_vars_len(ptr) + +Returns the number of classical variables allocated by the circuit. + +- `ptr` (`const CCircuit*`): target circuit handle. + +Returns the count, or 0 for NULL. + +### circuit_classical_vars(ptr, buffer, len) + +Two-step copy of (tag, width) snapshots for all of the circuit's classical variables into `buffer` (call `circuit_classical_vars_len` first for the length). + +- `ptr` (`const CCircuit*`): target circuit handle. +- `buffer` (`CClassicalType*`): array receiving the snapshots. +- `len` (`uintptr_t`): array length, must equal `circuit_classical_vars_len`. + +Returns 0 on success, -1 on NULL pointers, -8 when `len` does not equal the variable count. + +### circuit_validate_classical_var(ptr, var) + +Checks that a classical variable belongs to this circuit and keeps its recorded type. + +- `ptr` (`const CCircuit*`): target circuit handle. +- `var` (`const CClassicalVar*`): variable handle to check. + +Returns 0 on success, -1 on NULL pointers, -3 otherwise. + +### circuit_validate_classical_expr(ptr, expr) + +Checks that every classical variable and value read by `expr` belongs to this circuit. + +- `ptr` (`const CCircuit*`): target circuit handle. +- `expr` (`const CClassicalExpr*`): expression handle to check. + +Returns 0 on success, -1 on NULL pointers, -3 otherwise. + +--- + +## Classical expression constructors + +All constructors return a newly allocated `CClassicalExpr*` (free with `classical_expr_free`) and NULL on failure. + +### classical_expr_free(ptr) + +Frees a classical expression handle. + +- `ptr` (`CClassicalExpr*`): handle to free; NULL is allowed. + +No return value. + +### classical_expr_bit_literal(value) + +Creates a `Bit` literal expression (false = 0, true = 1). + +- `value` (`bool`): literal value. + +Returns the new expression handle. + +### classical_expr_bool_literal(value) + +Creates a `Bool` literal expression. + +- `value` (`bool`): literal value. + +Returns the new expression handle. + +### classical_expr_uint_literal(width, lo, hi) + +Creates a `UInt(width)` literal. The 128-bit value is passed as two 64-bit halves (`lo` is least-significant). + +- `width` (`uint32_t`): bit width. +- `lo` (`uint64_t`): least-significant 64 bits. +- `hi` (`uint64_t`): most-significant 64 bits. + +Returns the new expression handle; NULL when the width is zero, exceeds 128, or the value does not fit. + +### classical_expr_bit_vec_literal(width, lo, hi) + +Creates a `BitVec(width)` literal. The 128-bit packed little-endian value is passed as two 64-bit halves (`lo` is least-significant). + +- `width` (`uint32_t`): bit width. +- `lo` (`uint64_t`): least-significant 64 bits of the packed value. +- `hi` (`uint64_t`): most-significant 64 bits of the packed value. + +Returns the new expression handle; NULL when the width is zero, exceeds 128, or the value does not fit. + +### classical_type_zero_literal(ty_tag, width) + +Creates the zero literal of a classical type. + +- `ty_tag` (`uint32_t`): type tag, one of `CQLIB_CLASSICAL_TYPE_*`. +- `width` (`uint32_t`): bit width, used for `UInt` / `BitVec`. + +Returns the new expression handle; NULL on invalid type parameters or a width exceeding the 128-bit literal representation. + +### classical_type_one_literal(ty_tag, width) + +Creates the one literal of a classical type. + +- `ty_tag` (`uint32_t`): type tag, one of `CQLIB_CLASSICAL_TYPE_*`. +- `width` (`uint32_t`): bit width, used for `UInt` / `BitVec`. + +Returns the new expression handle; NULL on invalid type parameters or a width exceeding the 128-bit literal representation. + +### classical_expr_var(var) + +Creates an expression that reads the current runtime value of `var`. + +- `var` (`const CClassicalVar*`): variable handle. + +Returns the new expression handle; NULL on NULL input. + +### classical_expr_not(expr) + +Creates a `not` expression over a `Bool` or `Bit` operand. + +- `expr` (`const CClassicalExpr*`): operand. + +Returns the new expression handle; NULL on NULL input or an invalid operand type. + +### classical_expr_and(lhs, rhs) + +Creates an `and` expression over matching `Bool` or matching `Bit` operands. + +- `lhs` (`const CClassicalExpr*`): left operand. +- `rhs` (`const CClassicalExpr*`): right operand. + +Returns the new expression handle; NULL on NULL input or a type mismatch. + +### classical_expr_or(lhs, rhs) + +Creates an `or` expression over matching `Bool` or matching `Bit` operands. + +- `lhs` (`const CClassicalExpr*`): left operand. +- `rhs` (`const CClassicalExpr*`): right operand. + +Returns the new expression handle; NULL on NULL input or a type mismatch. + +### classical_expr_xor(lhs, rhs) + +Creates an `xor` expression over matching `Bool` or matching `Bit` operands. + +- `lhs` (`const CClassicalExpr*`): left operand. +- `rhs` (`const CClassicalExpr*`): right operand. + +Returns the new expression handle; NULL on NULL input or a type mismatch. + +### classical_expr_eq(lhs, rhs) + +Creates an equality comparison over same-typed operands, producing `Bool`. + +- `lhs` (`const CClassicalExpr*`): left operand. +- `rhs` (`const CClassicalExpr*`): right operand. + +Returns the new expression handle; NULL on NULL input or a type mismatch. + +### classical_expr_ne(lhs, rhs) + +Creates an inequality comparison over same-typed operands, producing `Bool`. + +- `lhs` (`const CClassicalExpr*`): left operand. +- `rhs` (`const CClassicalExpr*`): right operand. + +Returns the new expression handle; NULL on NULL input or a type mismatch. + +### classical_expr_lt(lhs, rhs) + +Creates an unsigned less-than comparison over matching `UInt` operands, producing `Bool`. + +- `lhs` (`const CClassicalExpr*`): left operand. +- `rhs` (`const CClassicalExpr*`): right operand. + +Returns the new expression handle; NULL on NULL input or invalid operand types. + +### classical_expr_le(lhs, rhs) + +Creates an unsigned less-than-or-equal comparison over matching `UInt` operands, producing `Bool`. + +- `lhs` (`const CClassicalExpr*`): left operand. +- `rhs` (`const CClassicalExpr*`): right operand. + +Returns the new expression handle; NULL on NULL input or invalid types. + +### classical_expr_gt(lhs, rhs) + +Creates an unsigned greater-than comparison over matching `UInt` operands, producing `Bool`. + +- `lhs` (`const CClassicalExpr*`): left operand. +- `rhs` (`const CClassicalExpr*`): right operand. + +Returns the new expression handle; NULL on NULL input or invalid operand types. + +### classical_expr_ge(lhs, rhs) + +Creates an unsigned greater-than-or-equal comparison over matching `UInt` operands, producing `Bool`. + +- `lhs` (`const CClassicalExpr*`): left operand. +- `rhs` (`const CClassicalExpr*`): right operand. + +Returns the new expression handle; NULL on NULL input or invalid types. + +### classical_expr_select(condition, then_expr, else_expr) + +Creates a ternary select `condition ? then_expr : else_expr`. The condition must be `Bool` and both branches must have the same type. + +- `condition` (`const CClassicalExpr*`): condition expression. +- `then_expr` (`const CClassicalExpr*`): expression taken when the condition holds. +- `else_expr` (`const CClassicalExpr*`): expression taken otherwise. + +Returns the new expression handle; NULL on NULL input or invalid operand types. + +### classical_expr_extract_bit(value, index) + +Extracts one bit (index 0 = least-significant) from a `UInt` or `BitVec` expression, producing `Bit`. + +- `value` (`const CClassicalExpr*`): expression to extract from. +- `index` (`uint32_t`): bit index. + +Returns the new expression handle; NULL on NULL input, a non-integer operand, or an out-of-bounds index. + +### classical_expr_extract_bits(value, offset, width) + +Extracts the contiguous bit range `[offset, offset + width)` from a `UInt` or `BitVec` expression, producing `BitVec(width)`. Offset 0 starts at the least-significant bit. + +- `value` (`const CClassicalExpr*`): expression to extract from. +- `offset` (`uint32_t`): starting offset. +- `width` (`uint32_t`): range width. + +Returns the new expression handle; NULL on NULL input or an invalid range. + +### classical_expr_pack_bits(bits, len) + +Packs single-bit expressions into a `BitVec`; the first bit becomes output bit index 0. + +- `bits` (`const CClassicalExpr* const*`): array of expression handles. +- `len` (`uintptr_t`): array length. + +Returns the new expression handle; NULL on NULL entries, empty input, or non-`Bit` operands. + +### classical_expr_concat(parts, len) + +Concatenates `Bit` / `BitVec` expressions into a larger `BitVec`; the first part occupies the least-significant output bits. + +- `parts` (`const CClassicalExpr* const*`): array of expression handles. +- `len` (`uintptr_t`): array length. + +Returns the new expression handle; NULL on NULL entries, empty input, or invalid part types. + +### classical_expr_bit_to_bool(expr) + +Explicitly casts a `Bit` expression to `Bool`. + +- `expr` (`const CClassicalExpr*`): expression to cast. + +Returns the new expression handle; NULL on NULL input or a non-`Bit` operand. + +### classical_expr_bit_vec_to_uint(expr) + +Explicitly casts a `BitVec` expression to a little-endian `UInt` of the same width. + +- `expr` (`const CClassicalExpr*`): expression to cast. + +Returns the new expression handle; NULL on NULL input or a non-`BitVec` operand. + +### classical_expr_to_bool(expr) + +Converts a `Bit` expression to `Bool` (alias of `classical_expr_bit_to_bool`). + +- `expr` (`const CClassicalExpr*`): expression to convert. + +Returns the new expression handle; NULL on NULL input or a non-`Bit` operand. + +### classical_expr_to_uint(expr) + +Converts a `BitVec` expression to a little-endian `UInt` (alias of `classical_expr_bit_vec_to_uint`). + +- `expr` (`const CClassicalExpr*`): expression to convert. + +Returns the new expression handle; NULL on NULL input or a non-`BitVec` operand. + +### classical_expr_simplified(ptr) + +Returns a structurally simplified copy of the expression. Simplification eliminates literal-driven redundancies without evaluating runtime reads. + +- `ptr` (`const CClassicalExpr*`): source expression handle. + +Returns a newly allocated simplified copy; NULL on NULL input. + +--- + +## Expression queries + +### classical_expr_kind(ptr, kind) + +Writes the expression node kind tag (one of `CQLIB_CLASSICAL_EXPR_*`) to `kind`. + +- `ptr` (`const CClassicalExpr*`): expression handle. +- `kind` (`uint32_t*`): out-parameter receiving the tag. + +Returns 0 on success, -1 on NULL pointers. + +### classical_expr_ty(ptr, tag, width) + +Writes the expression's static type (tag + width) to the out-parameters. + +- `ptr` (`const CClassicalExpr*`): expression handle. +- `tag` (`uint32_t*`): out-parameter receiving a `CQLIB_CLASSICAL_TYPE_*` tag. +- `width` (`uint32_t*`): out-parameter receiving the width. + +Returns 0 on success, -1 on NULL pointers. + +### classical_expr_is_bit_true(ptr) + +Returns whether the expression is the `Bit` literal `true` (1). + +- `ptr` (`const CClassicalExpr*`): expression handle. + +Returns `1` when yes, `0` otherwise, or `-1` for NULL. + +### classical_expr_is_bit_false(ptr) + +Returns whether the expression is the `Bit` literal `false` (0). + +- `ptr` (`const CClassicalExpr*`): expression handle. + +Returns `1` when yes, `0` otherwise, or `-1` for NULL. + +### classical_expr_is_bool_true(ptr) + +Returns whether the expression is the `Bool` literal `true`. + +- `ptr` (`const CClassicalExpr*`): expression handle. + +Returns `1` when yes, `0` otherwise, or `-1` for NULL. + +### classical_expr_is_bool_false(ptr) + +Returns whether the expression is the `Bool` literal `false`. + +- `ptr` (`const CClassicalExpr*`): expression handle. + +Returns `1` when yes, `0` otherwise, or `-1` for NULL. + +### classical_expr_values_len(ptr) + +Returns the number of distinct immutable classical values read by the expression. + +- `ptr` (`const CClassicalExpr*`): expression handle. + +Returns the count, or 0 for NULL. + +### classical_expr_values(ptr, buffer, len) + +Two-step copy of (index, tag, width) snapshots for every immutable classical value read by the expression into `buffer` (call `classical_expr_values_len` first for the length). + +- `ptr` (`const CClassicalExpr*`): expression handle. +- `buffer` (`CClassicalValueInfo*`): array receiving the snapshots. +- `len` (`uintptr_t`): array length, must equal `classical_expr_values_len`. + +Returns 0 on success, -1 on NULL pointers, -8 when `len` does not equal the count. + +### classical_expr_vars_len(ptr) + +Returns the number of distinct classical variables read by the expression. + +- `ptr` (`const CClassicalExpr*`): expression handle. + +Returns the count, or 0 for NULL. + +### classical_expr_vars(ptr, buffer, len) + +Two-step fill of `buffer` with owned handles for every distinct classical variable read by the expression, in ascending id order (call `classical_expr_vars_len` first for the length). Each returned handle must be released with `classical_var_free`. + +- `ptr` (`const CClassicalExpr*`): expression handle. +- `buffer` (`CClassicalVar**`): array receiving owned handles. +- `len` (`uintptr_t`): array length, must equal `classical_expr_vars_len`. + +Returns 0 on success, -1 on NULL pointers, -8 when `len` does not equal the count. + +### classical_expr_remap_classical_ids(ptr, old_var_ids, new_var_ids, var_len, old_value_indices, new_value_indices, value_len) + +Returns a copy of the expression with circuit-local classical ids remapped. `old_var_ids[i]` maps the variable with that id to id `new_var_ids[i]`, and `old_value_indices[i]` maps the immutable value with that index to index `new_value_indices[i]`. Entries whose old id does not occur in the expression are ignored, but every variable and value actually read by the expression must be covered, otherwise the remap fails. + +- `ptr` (`const CClassicalExpr*`): source expression handle. +- `old_var_ids` (`const uint32_t*`): array of old variable ids. +- `new_var_ids` (`const uint32_t*`): array of new variable ids, paired by index. +- `var_len` (`uintptr_t`): length of the variable mapping arrays. +- `old_value_indices` (`const uint32_t*`): array of old immutable value indices. +- `new_value_indices` (`const uint32_t*`): array of new value indices, paired by index. +- `value_len` (`uintptr_t`): length of the value mapping arrays. + +Returns the new expression handle (free with `classical_expr_free`); NULL on NULL inputs (including NULL arrays for non-zero lengths) or a missing mapping. + +--- + +## Measurement declarations + +### circuit_measure_bits(ptr, qubits, len) + +Measures several qubits and returns an expression reading the immutable `BitVec` result. The first qubit maps to bit index 0 (least-significant). + +- `ptr` (`CCircuit*`): target circuit handle. +- `qubits` (`const uint32_t*`): array of qubit ids. +- `len` (`uintptr_t`): number of qubits. + +Returns a newly allocated `CClassicalExpr*`; NULL on error (NULL pointers, empty list, qubit out of bounds). + +### circuit_measure_into(ptr, qubit, target) + +Measures one qubit and stores the result into `target` (a `Bit` variable); returns an expression reading the immutable measurement value. + +- `ptr` (`CCircuit*`): target circuit handle. +- `qubit` (`uint32_t`): qubit id to measure. +- `target` (`const CClassicalVar*`): `Bit` variable handle receiving the result. + +Returns a newly allocated `CClassicalExpr*`; NULL on error (NULL pointers, qubit out of bounds, wrong target type). + +### circuit_measure_bits_into(ptr, qubits, len, target) + +Measures several qubits and stores the `BitVec` result into `target`. The target width must equal the number of qubits. + +- `ptr` (`CCircuit*`): target circuit handle. +- `qubits` (`const uint32_t*`): array of qubit ids. +- `len` (`uintptr_t`): number of qubits. +- `target` (`const CClassicalVar*`): `BitVec` variable handle receiving the result. + +Returns a `CClassicalExpr*` reading the immutable measurement value; NULL on error. + +--- + +## Classical storage + +### circuit_store(ptr, target, value) + +Stores the runtime value of `value` into the mutable variable `target`. The expression type must match the target type. + +- `ptr` (`CCircuit*`): target circuit handle. +- `target` (`const CClassicalVar*`): variable to write. +- `value` (`const CClassicalExpr*`): expression providing the value. + +Returns 0 on success, -1 on NULL pointers, -3 on validation failure. + +--- + +## Control flow + +Control-flow functions build branch and loop bodies through a C callback: + +```c +typedef int32_t (*CqlibBodyFn)(struct CCircuit *circuit, void *user_data); +``` + +Callback contract: + +- `circuit` is the same `CCircuit` pointer that was passed to the control-flow function and is valid only for the duration of the callback; body operations must be appended through it with the regular circuit functions (`circuit_h`, `circuit_x`, `circuit_measure`, nested control-flow builders, ...). +- `user_data` is passed through untouched, is never dereferenced by the bindings, and may be NULL. +- Return 0 on success. Any non-zero return aborts the whole control operation: everything the callback appended is rolled back and the value is propagated to the caller of the control-flow function. Returning the error code of a failed gate call (for example `return circuit_x(circuit, 0);`) is the idiomatic pattern. + +### circuit_if(ptr, condition, body_fn, user_data) + +Appends a structured `if` operation controlled by a boolean classical expression. `body_fn` is invoked once; every operation it appends becomes the `then` body. + +- `ptr` (`CCircuit*`): target circuit handle. +- `condition` (`const CClassicalExpr*`): condition expression. +- `body_fn` (`CqlibBodyFn`): callback building the branch body. +- `user_data` (`void*`): user pointer passed to the callback; may be NULL. + +Returns 0 on success, -1 for NULL arguments, -3 when the condition fails validation (for example a non-`Bool` type or handles from another circuit), or the non-zero status returned by `body_fn` after rollback. + +### circuit_if_else(ptr, condition, then_fn, else_fn, user_data) + +Appends a structured `if` / `else` operation controlled by a boolean classical expression. `then_fn` runs first, then `else_fn`; each callback's appended operations become the corresponding branch body. + +- `ptr` (`CCircuit*`): target circuit handle. +- `condition` (`const CClassicalExpr*`): condition expression. +- `then_fn` (`CqlibBodyFn`): callback building the then body. +- `else_fn` (`CqlibBodyFn`): callback building the else body. +- `user_data` (`void*`): user pointer passed to both callbacks; may be NULL. + +Returns 0 on success, -1 for NULL arguments, -3 on validation failure, or the non-zero status returned by either callback after rollback. + +### circuit_while(ptr, condition, body_fn, user_data) + +Appends a structured `while` loop controlled by a boolean classical expression. The condition is re-evaluated at runtime. `circuit_break_loop` / `circuit_continue_loop` may be called from the callback, but only as the final operation of the body (terminal control transfer). + +- `ptr` (`CCircuit*`): target circuit handle. +- `condition` (`const CClassicalExpr*`): loop condition expression. +- `body_fn` (`CqlibBodyFn`): callback building the loop body. +- `user_data` (`void*`): user pointer passed to the callback; may be NULL. + +Returns 0 on success, -1 for NULL arguments, -3 on validation failure, or the non-zero status returned by `body_fn` after rollback. + +### circuit_for_uint(ptr, var, start, stop, step, body_fn, user_data) + +Appends an unsigned runtime range loop with half-open `[start, stop)` semantics. `var` must be a `UInt` variable of this circuit; `start`, `stop`, and `step` must be `UInt` expressions of the same width. When the body needs to read the loop variable, rebuild its expression from `var` with `classical_expr_var`. + +- `ptr` (`CCircuit*`): target circuit handle. +- `var` (`const CClassicalVar*`): loop variable. +- `start` (`const CClassicalExpr*`): start expression. +- `stop` (`const CClassicalExpr*`): stop expression (exclusive). +- `step` (`const CClassicalExpr*`): step expression. +- `body_fn` (`CqlibBodyFn`): callback building the loop body. +- `user_data` (`void*`): user pointer passed to the callback; may be NULL. + +Returns 0 on success, -1 for NULL arguments, -3 on validation failure (type mismatch or foreign handles), or the non-zero status returned by `body_fn` after rollback. + +### circuit_switch(ptr, target, case_values_lo, case_values_hi, case_bodies, case_count, default_body, user_data) + +Appends a structured exact-value `switch` operation over a `UInt` expression. Case values are 128-bit quantities split into two 64-bit halves: `value = lo | (hi << 64)`. Every value must fit the target width and appear at most once; `default_body` may be NULL, in which case the switch has no default branch. All callbacks receive the same `user_data`. + +- `ptr` (`CCircuit*`): target circuit handle. +- `target` (`const CClassicalExpr*`): `UInt` target expression. +- `case_values_lo` (`const uint64_t*`): least-significant 64 bits of each case value. +- `case_values_hi` (`const uint64_t*`): most-significant 64 bits of each case value. +- `case_bodies` (`const CqlibBodyFn*`): callback array matching the case values. +- `case_count` (`uintptr_t`): number of cases. +- `default_body` (`CqlibBodyFn`): default-branch callback; may be NULL. +- `user_data` (`void*`): user pointer passed to all callbacks. + +Returns 0 on success, -1 for NULL arguments (including a NULL case body when `case_count > 0`), -3 on validation failure (non-`UInt` target, duplicate or out-of-range case values), or the first non-zero status returned by a callback after rollback. + +### circuit_break_loop(ptr) + +Appends a `break` to the nearest enclosing loop or switch body. Legal only inside a `circuit_while`, `circuit_for_uint`, or `circuit_switch` body callback (or a body nested inside one), and only as the final operation of that body. + +- `ptr` (`CCircuit*`): target circuit handle. + +Returns 0 on success, -1 for a NULL circuit, -3 when no enclosing loop or switch body is active or the transfer is not terminal. + +### circuit_continue_loop(ptr) + +Appends a `continue` to the nearest enclosing loop body. Legal only inside a `circuit_while` or `circuit_for_uint` body callback (or a body nested inside one), and only as the final operation of that body. + +- `ptr` (`CCircuit*`): target circuit handle. + +Returns 0 on success, -1 for a NULL circuit, -3 when no enclosing loop body is active or the transfer is not terminal. + +### circuit_append_control(ptr, source, index) + +Appends a validated copy of the control-flow operation stored at `index` in `source`'s operation list to `ptr`. The same circuit may be passed as both `ptr` and `source`, which duplicates one of its control operations. Cross-circuit copies require the operation to be self-contained (no classical handles from `source`). + +- `ptr` (`CCircuit*`): target circuit handle. +- `source` (`const CCircuit*`): source circuit handle. +- `index` (`uintptr_t`): index into the source operation list. + +Returns 0 on success, -1 for NULL arguments, -8 when `index` is out of range or the operation at `index` is not a control-flow operation, -3 when the copy fails validation (foreign classical handles, illegal `break` / `continue` scoping, or unknown qubits). + +--- + +## Complete example + +Declare a classical variable, build a boolean expression, then add an `if` / `else` branch: + +```c +#include +#include "cqlib_c.h" + +static int32_t then_body(struct CCircuit *circuit, void *user_data) { + (void)user_data; + return circuit_x(circuit, 1); /* condition holds: apply X on qubit 1 */ +} + +static int32_t else_body(struct CCircuit *circuit, void *user_data) { + (void)user_data; + return circuit_z(circuit, 1); /* condition fails: apply Z on qubit 1 */ +} + +int main(void) { + struct CCircuit *circuit = circuit_new(2); + if (circuit == NULL) { + return 1; + } + + /* 1. Declare a Bool classical variable */ + struct CClassicalVar *flag = circuit_var(circuit, CQLIB_CLASSICAL_TYPE_BOOL, 0); + + /* 2. Build the boolean expression: flag == true */ + struct CClassicalExpr *flag_read = classical_expr_var(flag); + struct CClassicalExpr *true_lit = classical_expr_bool_literal(true); + struct CClassicalExpr *cond = classical_expr_eq(flag_read, true_lit); + + /* 3. if / else branches */ + int32_t status = circuit_if_else(circuit, cond, then_body, else_body, NULL); + if (status != 0) { + fprintf(stderr, "circuit_if_else failed: %d\n", status); + } + + /* Free the expression and variable handles */ + classical_expr_free(cond); + classical_expr_free(true_lit); + classical_expr_free(flag_read); + classical_var_free(flag); + circuit_free(circuit); + return status; +} +``` + +See [Circuit](1_circuit.md) for the circuit constructors (`circuit_new` / `circuit_free`) and [Standard Gates](5_gate_standard.md) for the gate functions (`circuit_x` / `circuit_z`). diff --git a/docs/api/en/c/0_overview.md b/docs/api/en/c/0_overview.md new file mode 100644 index 00000000..0695be4a --- /dev/null +++ b/docs/api/en/c/0_overview.md @@ -0,0 +1,386 @@ +# C API Overview + +## Welcome to the Cqlib C API reference! + +The Cqlib C binding (`binding-c`) exposes quantum circuit construction, symbolic parameters, IR conversion, device modeling, quantum information, compilation, visualization and error mitigation through a pure C ABI. Every entry point follows the same style: free functions + opaque handles + `int32_t` error codes, declared in the auto-generated header [crates/binding-c/include/cqlib_c.h](../../../../crates/binding-c/include/cqlib_c.h). + +--- + +## Documentation navigation + +### Quantum Circuit (circuit) + +- [Overview](0_circuit/0_overview.md) +- [Circuit](0_circuit/1_circuit.md) +- [Qubit](0_circuit/2_qubit.md) +- [Parameters](0_circuit/3_parameter.md) +- [Operations / Instructions](0_circuit/4_operation_instruction.md) +- [Standard Gates](0_circuit/5_gate_standard.md) +- [Unitary Gates](0_circuit/6_gate_unitary.md) +- [Multi-Controlled Gates](0_circuit/7_gate_mc_gate.md) +- [Circuit Gates](0_circuit/8_gate_circuit_gate.md) +- [Classical / Control Flow](0_circuit/9_classical_control_flow.md) +- [Symbolic Matrix](0_circuit/10_symbolic_matrix.md) +- [Ansatz](0_circuit/11_ansatz.md) +- [CFG](0_circuit/12_cfg.md) +- [Circuit to Matrix](0_circuit/13_circuit_to_matrix.md) +- [Circuit DAG](0_circuit/14_circuit_dag.md) + +### Intermediate Representation (ir) + +- [Overview](1_ir/0_overview.md) +- [QCIS](1_ir/1_qcis.md) +- [OpenQASM 2.0](1_ir/2_qasm2.md) +- [OpenQASM 3.0](1_ir/3_qasm3.md) + +### Device (device) + +- [Overview](2_device/0_overview.md) +- [Topology](2_device/1_topology.md) +- [Device and noise properties](2_device/2_properties_device.md) +- [Layout](2_device/3_layout.md) +- [Noise](2_device/4_noise.md) +- [Result](2_device/5_result.md) +- [Qubit identifiers](2_device/6_qubits.md) + +### Quantum Information (qis) + +- [Overview](3_qis/0_overview.md) +- [Statevector](3_qis/1_statevector.md) +- [DensityMatrix](3_qis/2_density_matrix.md) +- [DensityMatrixNoise](3_qis/3_density_matrix_noise.md) +- [Stabilizer](3_qis/4_stabilizer.md) +- [ClassicalState](3_qis/5_classical_state.md) +- [Pauli](3_qis/6_pauli.md) +- [Hamiltonian](3_qis/7_hamiltonian.md) +- [Evolution](3_qis/8_evolution.md) +- [Metrics / Entropy](3_qis/9_metrics_entropy.md) + +### Compilation (compile) + +- [Overview](4_compile/0_overview.md) +- [Compiler](4_compile/1_compiler.md) +- [Transform](4_compile/2_transform.md) +- [Layout](4_compile/3_layout.md) +- [Routing](4_compile/4_routing.md) +- [SABRE](4_compile/5_sabre.md) +- [Decompose & Resynthesis](4_compile/6_decompose_resynthesis.md) +- [Knowledge Rules](4_compile/7_knowledge.md) +- [Resource](4_compile/8_resource.md) +- [Commutation](4_compile/9_commutation.md) + +### Visualization (visualization) + +- [Overview](5_visualization/0_overview.md) +- [Text circuit diagrams](5_visualization/1_draw_text.md) +- [SVG circuit diagrams](5_visualization/2_draw_figure.md) +- [State plots](5_visualization/3_state_plots.md) +- [Result plots](5_visualization/4_result_plots.md) +- [Write to file and inline display](5_visualization/5_render_to_file.md) +- [Visual IR](5_visualization/6_visual_ir.md) + +### Error Mitigation (error_mitigation) + +- [Overview](6_error_mitigation/0_overview.md) +- [Zero-noise extrapolation (ZNE)](6_error_mitigation/1_zne.md) +- [Virtual distillation](6_error_mitigation/2_virtual_distillation.md) +- [Unified pipeline](6_error_mitigation/3_unified.md) + +--- + +## Quick start: Bell state + +The following complete example builds a two-qubit Bell circuit, reads the probability distribution with the two-step pattern after exact simulation, and releases every resource: + +```c +#include +#include +#include + +int main(void) { + /* 1. Build the Bell circuit: H(0) + CX(0, 1) */ + CCircuit *qc = circuit_new(2); + if (qc == NULL) { + return 1; + } + if (circuit_h(qc, 0) != 0 || circuit_cx(qc, 0, 1) != 0) { + circuit_free(qc); + return 1; + } + + /* 2. Exact simulation */ + CStatevector *sv = statevector_from_circuit(qc); + if (sv == NULL) { + circuit_free(qc); + return 1; + } + + /* 3. Two-step output: query the length first, then fill the buffer */ + uintptr_t len = statevector_probabilities_len(sv); + double *probs = malloc(len * sizeof(double)); + if (probs == NULL || statevector_probabilities(sv, probs, len) != 0) { + free(probs); + statevector_free(sv); + circuit_free(qc); + return 1; + } + /* probs = {0.5, 0.0, 0.0, 0.5} */ + printf("P(00) = %f, P(11) = %f\n", probs[0], probs[3]); + + /* 4. Release (in reverse order of acquisition) */ + free(probs); + statevector_free(sv); + circuit_free(qc); + return 0; +} +``` + +### Build and link + +Windows (MinGW gcc, gnu target): + +```bash +cargo build --release -p binding-c --target x86_64-pc-windows-gnu + +gcc -Icrates/binding-c/include main.c -Ltarget/x86_64-pc-windows-gnu/release -lbinding_c -lntdll -lws2_32 -lbcrypt -luserenv -ladvapi32 -o bell +``` + +At runtime the executable must find `binding_c.dll`: add the absolute path of `target/x86_64-pc-windows-gnu/release` to `PATH`, or copy the DLL next to the executable. See [crates/binding-c/README.md](../../../../crates/binding-c/README.md) for macOS and Linux linking, test projects and further examples. + +--- + +## Error codes + +Functions returning `int32_t` use `0` for success and negative values for errors: + +| Value | Name | Typical scenario | +| --- | --- | --- | +| `0` | Ok | Success. | +| `-1` | NullPtr | NULL handle, NULL array or invalid C string passed in. | +| `-2` | QubitOutOfBounds | Qubit index outside the circuit or device range. | +| `-3` | CircuitError | Circuit structure error: validation failure, gate arity or parameter count mismatch, the same qubit reused within one operation, non-invertible operations, etc. | +| `-4` | ParseError | Parse error: unknown gate name, invalid expression syntax, invalid UTF-8, malformed IR text. | +| `-5` | IoError | File read/write failure. | +| `-6` | CompilerError | Compilation workflow error. | +| `-7` | SimulationError | Simulation error: a non-Clifford gate entering stabilizer simulation, not run yet, etc. | +| `-8` | InvalidParam | Invalid parameter: out-of-range probability, buffer too small, invalid tag value, etc. | + +Pointer-returning functions use `NULL` for errors; `param_evaluate` returns `0.0` when evaluation fails (including unbound symbols); `circuit_depth` returns a negative value on error. Boolean-style queries return `1`/`0`, with `-1` usually meaning NULL input. + +--- + +## Memory ownership and freeing + +1. **Constructors and transforms return heap handles**: `circuit_new`, `circuit_from_qubits`, `circuit_inverse`, `circuit_decompose`, `circuit_assign_params`, `param_parse`, `circuit_to_gate` and friends return newly allocated opaque handles; the caller releases them with the matching `*_free` (`circuit_free`, `param_free`, `circuit_gate_free`, ...). Every `*_free` accepts `NULL`. +2. **Strings are freed with `cqlib_string_free`**: every interface returning `char*` (`operation_name`, `value_operation_name`, `circuit_gate_name`, each element written by `circuit_symbols`, ...) is released with `cqlib_string_free`. +3. **Arrays use the two-step output pattern**: first call a `*_len` function to query the count and allocate a buffer, then call the fill function. Behavior on a too-small buffer falls into two classes: most fill functions return a negative error code (usually `-8`); some functions (such as `circuit_qubits`) only return the total count without copying. +4. **Nested lists come with matching frees**: each element written by `circuit_parameters` (a newly allocated `CParameter*`, released individually with `param_free`), `circuit_cfg_block_operations` (released with `operation_free`) and similar functions is an independent handle that must be freed one by one. +5. **Interfaces that explicitly hand over ownership**: `circuit_to_gate` clones the input circuit into a composite-gate handle; the `CValueOperation*` returned by `circuit_index` is a read-only snapshot independent of later circuit mutations (release with `value_operation_free`); when `CParameterValue.tag == PARAMETER_VALUE_TAG_PARAM` the embedded `param` field is a newly allocated `CParameter*` (release with `param_free`). Ownership of these objects belongs entirely to the caller. + +--- + +## Constant tags + +The header exports the following `#define` tag constants; all are part of the stable ABI. + +### Parameter storage tags + +| Constant | Value | Description | +| --- | --- | --- | +| `CIRCUIT_PARAM_TAG_FIXED` | 0 | `CCircuitParam.tag`: fixed number stored in `value`. | +| `CIRCUIT_PARAM_TAG_INDEX` | 1 | `CCircuitParam.tag`: interned expression in the circuit's parameter table; `index` holds the table index. | +| `PARAMETER_VALUE_TAG_FIXED` | 0 | `CParameterValue.tag`: fixed number stored in `value`; `param` is NULL. | +| `PARAMETER_VALUE_TAG_PARAM` | 1 | `CParameterValue.tag`: symbolic parameter; `param` points to a newly allocated `CParameter*`. | +| `OPERATION_PARAM_FIXED` | 0 | Parameter tag written by `operation_params`: fixed value. | +| `OPERATION_PARAM_INDEX` | 1 | Parameter tag written by `operation_params`: index into the owning circuit's parameter table; resolve it against the circuit. | + +### Ansatz topology and evolution strategy + +| Constant | Value | Description | +| --- | --- | --- | +| `ENTANGLEMENT_LINEAR` | 0 | TwoLocal entanglement topology: linear nearest-neighbor chain. | +| `ENTANGLEMENT_CIRCULAR` | 1 | Linear chain plus wrap-around edge. | +| `ENTANGLEMENT_FULL` | 2 | All-to-all pairs. | +| `ENTANGLEMENT_CUSTOM` | 3 | Custom explicit pairs of control/target ids. | +| `EVOLUTION_STRATEGY_EXACT` | 0 | QAOA evolution strategy: exact term-wise evolution. | +| `EVOLUTION_STRATEGY_AUTO` | 1 | Automatic exact/Trotter selection. | +| `EVOLUTION_STRATEGY_TROTTER` | 2 | Explicit Trotter product formula. | +| `TROTTER_FIRST_ORDER` | 0 | First-order product formula. | +| `TROTTER_SECOND_ORDER` | 1 | Second-order product formula. | +| `TROTTER_RANDOMIZED` | 2 | Randomized first-order formula. | +| `TROTTER_MODE_FIRST_ORDER` | 0 | Trotter-Suzuki decomposition mode: first-order Lie-Trotter. | +| `TROTTER_MODE_SECOND_ORDER` | 1 | Trotter-Suzuki decomposition mode: second-order Strang splitting. | + +### Control flow graph tags + +| Constant | Value | Description | +| --- | --- | --- | +| `CFG_FLOW_TRUE_BRANCH` | 1 | True edge out of `if`/`while`/`for` headers. | +| `CFG_FLOW_FALSE_BRANCH` | 2 | False edge out of the headers. | +| `CFG_FLOW_UNCONDITIONAL` | 3 | Fallthrough jump / structured merge edge. | +| `CFG_FLOW_CASE` | 4 | Exact-value switch case edge (value in the case halves). | +| `CFG_FLOW_DEFAULT_CASE` | 5 | Switch default edge. | +| `CFG_FLOW_BREAK` | 6 | Structured `break` edge. | +| `CFG_FLOW_CONTINUE` | 7 | Structured `continue` edge. | +| `CFG_TERMINATOR_BRANCH` | 1 | Boolean branch header terminator. | +| `CFG_TERMINATOR_FOR_LOOP` | 2 | Unsigned range loop header terminator. | +| `CFG_TERMINATOR_SWITCH` | 3 | Exact-value multi-way branch header terminator. | +| `CFG_TERMINATOR_JUMP` | 4 | Unconditional jump terminator. | +| `CFG_TERMINATOR_BREAK` | 5 | Structured `break` terminator. | +| `CFG_TERMINATOR_CONTINUE` | 6 | Structured `continue` terminator. | +| `CFG_TERMINATOR_RETURN` | 7 | End-of-execution terminator. | +| `CFG_REGION_IF` | 1 | Structured conditional region. | +| `CFG_REGION_WHILE` | 2 | Structured while-loop region. | +| `CFG_REGION_FOR` | 3 | Structured range-loop region. | +| `CFG_REGION_SWITCH` | 4 | Structured exact-value switch region. | + +### Classical types and expression nodes + +| Constant | Value | Description | +| --- | --- | --- | +| `CQLIB_CLASSICAL_TYPE_BIT` | 0 | Classical type `Bit`. | +| `CQLIB_CLASSICAL_TYPE_BOOL` | 1 | Classical type `Bool`. | +| `CQLIB_CLASSICAL_TYPE_UINT` | 2 | Classical type `UInt`. | +| `CQLIB_CLASSICAL_TYPE_BIT_VEC` | 3 | Classical type `BitVec`. | + +Node-kind tags written by `classical_expr_kind`: `CQLIB_CLASSICAL_EXPR_VAR` (0), `_VALUE` (1), `_BOOL_LITERAL` (2), `_BIT_LITERAL` (3), `_UINT_LITERAL` (4), `_BIT_VEC_LITERAL` (5), `_UNARY` (6), `_BINARY` (7), `_COMPARE` (8), `_CAST` (9), `_SELECT` (10), `_EXTRACT_BIT` (11), `_EXTRACT_BITS` (12), `_CONCAT` (13), `_PACK_BITS` (14). + +### DAG wire tags + +| Constant | Value | Description | +| --- | --- | --- | +| `DAG_WIRE_QUBIT` | 0 | Qubit timeline wire carrying a qubit id. | +| `DAG_WIRE_CLASSICAL_VAR` | 1 | Mutable classical storage wire carrying a variable id. | +| `DAG_WIRE_CLASSICAL_VALUE` | 2 | Immutable classical value wire carrying a value id. | +| `DAG_WIRE_GLOBAL_ORDER` | 3 | Global ordering resource wire with no payload. | + +### Compilation configuration tags + +| Constant | Value | Description | +| --- | --- | --- | +| `COMPILE_MODE_NORMAL` | 0 | Normal compilation mode. | +| `COMPILE_MODE_ENHANCED` | 1 | Enhanced compilation mode. | +| `COMPILE_TARGET_LOGICAL` | 0 | Logical target. | +| `COMPILE_TARGET_BASIS` | 1 | Basis gate-set target. | +| `COMPILE_TARGET_DEVICE` | 2 | Device target. | +| `COMPILE_TARGET_TOPOLOGY_BASIS` | 3 | Topology basis target. | + +### Compilation pass tags + +| Constant | Value | Description | +| --- | --- | --- | +| `REWRITE_MODE_OPTIMIZE` | 0 | Knowledge rewrite mode: optimization. | +| `REWRITE_MODE_LOWERING` | 1 | Knowledge rewrite mode: lowering toward the target basis. | +| `LAYOUT_OBJECTIVE_TOPOLOGY_ONLY` | 0 | Layout objective: topology only (distance + direction mismatch). | +| `LAYOUT_OBJECTIVE_FIDELITY_AWARE` | 1 | Layout objective: fidelity aware (adds error-rate terms). | +| `LAYOUT_OBJECTIVE_AUTO` | 2 | Layout objective: chosen automatically from device calibration data. | +| `VF2_EDGE_REQUIREMENT_POSITIVE_INTERACTIONS` | 0 | VF2 edge requirement: positive-weight interactions only. | +| `VF2_EDGE_REQUIREMENT_ALL_INTERACTIONS` | 1 | VF2 edge requirement: all interactions. | +| `SABRE_MAX_LOOKAHEAD_WEIGHTS` | 8 | Maximum length of the `CSabreConfigC.lookahead_weights` array. | +| `TWO_QUBIT_BASIS_PAULI_ROTATIONS` | 0 | Two-qubit synthesis target basis: Pauli rotations. | +| `TWO_QUBIT_BASIS_CX` | 1 | Two-qubit synthesis target basis: CX. | +| `TWO_QUBIT_BASIS_CY` | 2 | Two-qubit synthesis target basis: CY. | +| `TWO_QUBIT_BASIS_CZ` | 3 | Two-qubit synthesis target basis: CZ. | +| `TWO_QUBIT_BASIS_RZZ` | 4 | Two-qubit synthesis target basis: RZZ. | +| `KAK_FACTOR_K1L` | 0 | KAK local factor: first local gate (left). | +| `KAK_FACTOR_K1R` | 1 | KAK local factor: first local gate (right). | +| `KAK_FACTOR_K2L` | 2 | KAK local factor: second local gate (left). | +| `KAK_FACTOR_K2R` | 3 | KAK local factor: second local gate (right). | + +### Knowledge rule and resource tags + +| Constant | Value | Description | +| --- | --- | --- | +| `RULE_KIND_SIMPLIFY` | 0 | Knowledge rule kind: simplification. | +| `RULE_KIND_CANCEL` | 1 | Knowledge rule kind: cancellation. | +| `RULE_KIND_MERGE` | 2 | Knowledge rule kind: merging. | +| `RULE_KIND_COMMUTE` | 3 | Knowledge rule kind: commutation. | +| `RULE_KIND_DECOMPOSE` | 4 | Knowledge rule kind: decomposition. | +| `RULE_KIND_CANONICALIZE` | 5 | Knowledge rule kind: canonicalization. | +| `RULE_KIND_HARDWARE_NATIVE` | 6 | Knowledge rule kind: hardware-native. | +| `RULE_KIND_OTHER` | 7 | Knowledge rule kind: other. | +| `RESOURCE_REQUIREMENT_CLEAN_ZERO` | 0 | Resource requirement: a \|0⟩ (clean) ancilla qubit. | +| `RESOURCE_REQUIREMENT_DIRTY` | 1 | Resource requirement: a dirty ancilla qubit in any state. | + +### Commutation result tags + +| Constant | Value | Description | +| --- | --- | --- | +| `COMMUTATION_RESULT_UNPROVEN` | 0 | Commutation could not be proven. | +| `COMMUTATION_RESULT_EXACT` | 1 | The operations commute exactly. | +| `COMMUTATION_RESULT_UP_TO_GLOBAL_PHASE` | 2 | The operations commute up to a global phase. | + +### Noise channel tags + +| Constant | Value | Description | +| --- | --- | --- | +| `NOISE_BIT_FLIP` | 0 | Single-qubit bit-flip channel with flip probability p. | +| `NOISE_PHASE_FLIP` | 1 | Phase-flip channel with flip probability p. | +| `NOISE_DEPOLARIZING` | 2 | Depolarizing channel with depolarizing parameter p. | +| `NOISE_AMPLITUDE_DAMPING` | 3 | Amplitude damping channel with damping parameter gamma. | +| `NOISE_PHASE_DAMPING` | 4 | Phase damping channel with scattering probability. | +| `NOISE_PAULI` | 5 | General single-qubit Pauli channel with probabilities px/py/pz (each non-negative, sum <= 1). | +| `NOISE_TWO_DEPOLARIZING` | 0 | Two-qubit depolarizing channel. | +| `NOISE_TWO_INDEPENDENT` | 1 | Two-qubit independent channel (q0/q1 each carry a single-qubit channel). | +| `NOISE_TWO_CORRELATED_PAULI` | 2 | Correlated Pauli channel (probability p plus `NOISE_PAULI_OP_*` operators). | +| `NOISE_PAULI_OP_I` / `_X` / `_Y` / `_Z` | 0 / 1 / 2 / 3 | Pauli operators applied to q0/q1 by the correlated channel. | + +### Execution status tags + +| Constant | Value | Description | +| --- | --- | --- | +| `EXECUTION_STATUS_QUEUED` | 0 | Task submitted and queued. | +| `EXECUTION_STATUS_RUNNING` | 1 | Task currently running. | +| `EXECUTION_STATUS_COMPLETED` | 2 | Task completed successfully. | +| `EXECUTION_STATUS_FAILED` | 3 | Task failed (query the error code and message). | +| `EXECUTION_STATUS_CANCELLED` | 4 | Task cancelled by the user. | + +### Error mitigation tags + +| Constant | Value | Description | +| --- | --- | --- | +| `MITIGATION_ZNE` | 0 | Mitigation method: zero-noise extrapolation (extra parameter: fold-level array). | +| `MITIGATION_VIRTUAL_DISTILLATION` | 1 | Mitigation method: virtual distillation (extra parameter: copies). | +| `PROCESS_ZNE_POLYNOMIAL` | 0 | Mitigation process: ZNE + polynomial extrapolation. | +| `PROCESS_ZNE_EXPONENTIAL` | 1 | Mitigation process: ZNE + exponential extrapolation. | +| `PROCESS_VIRTUAL_DISTILLATION` | 2 | Mitigation process: virtual distillation. | +| `ZNE_EXTRAPOLATE_POLYNOMIAL` | 0 | `zne_extrapolate` method: polynomial. | +| `ZNE_EXTRAPOLATE_EXPONENTIAL` | 1 | `zne_extrapolate` method: exponential. | + +### Pauli operator tags + +| Constant | Value | Description | +| --- | --- | --- | +| `PAULI_I` | 0 | Identity operator I. | +| `PAULI_X` | 1 | Pauli X. | +| `PAULI_Y` | 2 | Pauli Y. | +| `PAULI_Z` | 3 | Pauli Z. | + +### Parameter display mode tags + +| Constant | Value | Description | +| --- | --- | --- | +| `PARAM_MODE_NUMERIC` | 0 | Numeric display mode. | +| `PARAM_MODE_SYMBOLIC` | 1 | Symbolic display mode. | +| `PARAM_MODE_SYMBOLIC_WITH_VALUE` | 2 | Symbolic display with the numeric value. | +| `PARAM_MODE_PI_FRACTION_PREFERRED` | 3 | Pi-fraction-preferred display mode. | + +--- + +## Threading and Complex64 layout + +### Threading and concurrency + +- There is no global initialization or teardown call: load the shared library (`binding_c.dll` on Windows, `libbinding_c.so` on Linux, `libbinding_c.dylib` on macOS) and call the entry points directly. +- Handles are independent heap objects owned by the caller; when several threads share one handle, the caller is responsible for serializing access. Distinct handles are independent of each other and can be used separately. + +### Complex64 layout + +```c +typedef struct Complex64 { + double re; + double im; +} Complex64; +``` + +Complex numbers are represented as consecutive interleaved `(re, im)` double pairs. Matrix interfaces (`circuit_to_matrix`, `operation_matrix`, ...) emit row-major interleaved double arrays; the buffer must hold `2 × element count` doubles. diff --git a/docs/api/en/c/1_ir/0_overview.md b/docs/api/en/c/1_ir/0_overview.md new file mode 100644 index 00000000..352f48b9 --- /dev/null +++ b/docs/api/en/c/1_ir/0_overview.md @@ -0,0 +1,61 @@ +# Intermediate Representation (C) + +The C binding's IR interface converts between `CCircuit` and three circuit text formats: QCIS, OpenQASM 2.0, and OpenQASM 3.0. Error codes and the conventions for freeing strings and handles follow the [Overview](../0_overview.md). + +--- + +## The four-function convention + +Each format exposes the same four entry points, with the `` being `qcis`, `qasm2`, or `qasm3`: + +| Action | Function | Returns | +| --- | --- | --- | +| string → `CCircuit` | `_loads(source)` | a newly allocated `CCircuit*` on success, NULL on error | +| file → `CCircuit` | `_load(path)` | a newly allocated `CCircuit*` on success, NULL on error | +| `CCircuit` → string | `_dumps(circuit)` | a heap-allocated C string on success, NULL on error | +| `CCircuit` → file | `_dump(circuit, path)` | 0 on success, a negative error code on failure | + +- Circuits returned by `loads` / `load` are released with `circuit_free` (see [Circuit](../0_circuit/1_circuit.md)). +- Strings returned by `dumps` are released with `cqlib_string_free`. +- `dump` returns an `int32_t` error code: -1 for NULL pointers, -8 when the path is not valid UTF-8, and -5 when dumping or writing fails. +- Parsing never modifies its input; `loads(dumps(circuit))` yields a circuit whose operation sequence is equivalent to the original. +- A circuit can be read with one format and written with another to convert between formats, for example reading OpenQASM 2.0 text and writing QCIS text. + +--- + +## Pages + +| Page | Contents | +| --- | --- | +| [QCIS](1_qcis.md) | `qcis_loads` / `qcis_load` / `qcis_dumps` / `qcis_dump` and the QCIS native gate set. | +| [OpenQASM 2.0](2_qasm2.md) | `qasm2_loads` / `qasm2_load` / `qasm2_dumps` / `qasm2_dump` and the supported 2.0 subset. | +| [OpenQASM 3.0](3_qasm3.md) | `qasm3_loads` / `qasm3_load` / `qasm3_dumps` / `qasm3_dump` and the supported 3.0 subset. | + +--- + +## Example + +```c +#include +#include "cqlib_c.h" + +/* string -> circuit -> string */ +struct CCircuit *c = qcis_loads("H Q0\nCZ Q0 Q1\n"); +if (c == NULL) { + /* parse failure */ +} + +char *text = qcis_dumps(c); +if (text != NULL) { + printf("%s", text); /* "H Q0\nCZ Q0 Q1\n" */ + cqlib_string_free(text); +} + +circuit_free(c); + +/* format conversion: QCIS text written as OpenQASM 2.0 text */ +struct CCircuit *c2 = qcis_loads("X Q0\nY Q1\n"); +char *qasm = qasm2_dumps(c2); /* contains the "OPENQASM 2.0;" header */ +cqlib_string_free(qasm); +circuit_free(c2); +``` diff --git a/docs/api/en/c/1_ir/1_qcis.md b/docs/api/en/c/1_ir/1_qcis.md new file mode 100644 index 00000000..b6ce5fd1 --- /dev/null +++ b/docs/api/en/c/1_ir/1_qcis.md @@ -0,0 +1,117 @@ +# QCIS (C) + +The C binding provides four entry points converting between QCIS text and `CCircuit`. Error codes and the conventions for freeing strings and handles follow the [Overview](../0_overview.md). + +--- + +## Interface overview + +| Function | Direction | +| --- | --- | +| `qcis_loads` | QCIS string → `CCircuit` | +| `qcis_load` | QCIS file → `CCircuit` | +| `qcis_dumps` | `CCircuit` → QCIS string | +| `qcis_dump` | `CCircuit` → QCIS file | + +--- + +## Parsing + +### qcis_loads(source) + +Parses a QCIS string into a new `CCircuit*`. + +Parameters: + +- `source` (`const char*`): the QCIS text, NUL-terminated. + +Returns: a newly allocated `CCircuit*` on success (free with `circuit_free`); NULL when `source` is NULL, not valid UTF-8, or parsing fails. + +Parse failures include: a qubit not written as `Q`, an unparseable id after `Q`, a gate whose qubit count or parameter count does not match its specification, an empty parameter expression, a gate name outside the QCIS instruction set, or an empty line / no valid content. + +### qcis_load(path) + +Reads a QCIS file and parses it into a new `CCircuit*`. + +Parameters: + +- `path` (`const char*`): the file path, NUL-terminated. + +Returns: a newly allocated `CCircuit*` on success; NULL when `path` is NULL, not valid UTF-8, reading fails, or parsing fails. + +--- + +## Dumping + +### qcis_dumps(circuit) + +Dumps a circuit to a QCIS string. + +Parameters: + +- `circuit` (`const struct CCircuit*`): the circuit to dump. + +Returns: a heap-allocated C string on success (free with `cqlib_string_free`); NULL when `circuit` is NULL or dumping fails. + +Dump failures include: a gate outside the QCIS native gate set (the standard identity gate, global phase, multi-controlled gates, custom gates, and unitary gates), classical storage, classical control flow, an unevaluable symbolic parameter, or a delay whose tick count is not a non-negative integer. Measurement (`M`) and barrier (`B`) can be dumped. + +### qcis_dump(circuit, path) + +Dumps a circuit to a QCIS string and writes it to a file. + +Parameters: + +- `circuit` (`const struct CCircuit*`): the circuit to dump. +- `path` (`const char*`): the target file path. + +Returns: 0 on success; -1 when either pointer is NULL; -8 when the path is not valid UTF-8; -5 when dumping or writing fails. + +--- + +## QCIS instruction set + +QCIS parsing and dumping support only the native gate set: + +- Single-qubit gates: `H`, `S`, `SD`, `T`, `TD`, `X`, `X2P`, `X2M`, `Y`, `Y2P`, `Y2M`, `Z` +- Parameterized single-qubit gates: `RX`, `RXY`, `RY`, `RZ`, `U`, `XY`, `XY2P`, `XY2M`, `PHASE` +- Multi-qubit gates: `RXX`, `RYY`, `RZX`, `RZZ`, `SWAP`, `CX`, `CCX`, `CY`, `CZ`, `CRX`, `CRY`, `CRZ`, `FSIM` + +Beyond gates, QCIS text also expresses measurement (`M`), barrier (`B`), and delay (`I Qn t`, where `t` is the number of delay ticks, in units of 0.5 ns, and must be a non-negative integer). + +When parsing, `SDG`, `TDG`, and `Barrier` are accepted as alternative spellings of `SD`, `TD`, and `B`; dumping always writes `SD`, `TD`, and `B`. The standard identity gate and global phase are not part of the native gate set, and QCIS `I` always denotes a delay. Decompose gates that cannot be expressed into the QCIS gate set before dumping. + +--- + +## Example + +```c +#include +#include "cqlib_c.h" + +/* Parsing: qubits are written Q; parameters accept expressions */ +const char *src = + "RX Q0 1.0\n" + "RY Q1 pi/2\n" + "X Q0\n" + "CZ Q0 Q1\n" + "M Q0 Q1\n"; + +struct CCircuit *c = qcis_loads(src); +if (c == NULL) { + /* parse failure */ +} + +uintptr_t n = circuit_num_qubits(c); /* 2 */ + +/* Dump back to text */ +char *text = qcis_dumps(c); +if (text != NULL) { + printf("%s", text); + cqlib_string_free(text); +} + +/* Write to a file */ +int32_t rc = qcis_dump(c, "out.qcis"); /* 0 on success, -5 on write failure */ + +circuit_free(c); +``` diff --git a/docs/api/en/c/1_ir/2_qasm2.md b/docs/api/en/c/1_ir/2_qasm2.md new file mode 100644 index 00000000..1cae6c46 --- /dev/null +++ b/docs/api/en/c/1_ir/2_qasm2.md @@ -0,0 +1,120 @@ +# OpenQASM 2.0 (C) + +The C binding provides four entry points converting between OpenQASM 2.0 text and `CCircuit`. Error codes and the conventions for freeing strings and handles follow the [Overview](../0_overview.md). + +--- + +## Interface overview + +| Function | Direction | +| --- | --- | +| `qasm2_loads` | OpenQASM 2.0 string → `CCircuit` | +| `qasm2_load` | OpenQASM 2.0 file → `CCircuit` | +| `qasm2_dumps` | `CCircuit` → OpenQASM 2.0 string | +| `qasm2_dump` | `CCircuit` → OpenQASM 2.0 file | + +--- + +## Parsing + +### qasm2_loads(source) + +Parses an OpenQASM 2.0 string into a new `CCircuit*`. + +Parameters: + +- `source` (`const char*`): the OpenQASM 2.0 text, NUL-terminated. + +Returns: a newly allocated `CCircuit*` on success (free with `circuit_free`); NULL when `source` is NULL, not valid UTF-8, or parsing fails. + +Parse failures include: syntax errors, AST-to-circuit conversion failures, references to undefined quantum or classical registers, references to undefined gates, redefinition of a gate name declared by qelib1, mismatched qubit or parameter counts, gate expansion exceeding the recursion limit or forming a cycle, parameter expression evaluation failures, a version declaration other than 2.0, duplicate declarations, `include` cycles, and an `opaque` gate being called. + +External files brought in via `include` are not resolved by `qasm2_loads`; the file entry point `qasm2_load` resolves relative includes against the input file's directory. + +### qasm2_load(path) + +Reads an OpenQASM 2.0 file and parses it into a new `CCircuit*`. + +Parameters: + +- `path` (`const char*`): the file path, NUL-terminated. + +Returns: a newly allocated `CCircuit*` on success; NULL when `path` is NULL, not valid UTF-8, reading fails, or parsing fails. + +--- + +## Dumping + +### qasm2_dumps(circuit) + +Dumps a circuit to an OpenQASM 2.0 string. The output contains an auto-generated comment, the `OPENQASM 2.0;` header, `include "qelib1.inc";`, and `qreg`/`creg` declarations. + +Parameters: + +- `circuit` (`const struct CCircuit*`): the circuit to dump. + +Returns: a heap-allocated C string on success (free with `cqlib_string_free`); NULL when `circuit` is NULL or dumping fails. + +Dump failures include: content generation failures, a measurement or reset inside a `gate` definition body, an invalid qubit index, a classical type that cannot be represented as `creg`, classical data operations or classical control that cannot be expressed, and distinct custom gates sharing one OpenQASM gate name. + +### qasm2_dump(circuit, path) + +Dumps a circuit to an OpenQASM 2.0 string and writes it to a file. + +Parameters: + +- `circuit` (`const struct CCircuit*`): the circuit to dump. +- `path` (`const char*`): the target file path. + +Returns: 0 on success; -1 when either pointer is NULL; -8 when the path is not valid UTF-8; -5 when dumping or writing fails. + +--- + +## Supported OpenQASM 2.0 subset + +- Standard and parameterized gates, including `ch`/`cu1`/`cu3` and `sx`/`sxdg`/`crx`/`cry`/`crz`/`rxx`/`ryy`/`rzz`/`fsim` +- Measurement, barrier, and reset statements +- `qreg`, `creg`, and `opaque` declarations, plus `if (creg == integer) qop;` conditionals +- `CircuitGate` dumped as a gate definition; matrix-only unitary gates dumped as `opaque` declarations +- Extension gates not covered directly are emitted as `gate` definitions (e.g. `crx`/`cry`/`rzz`/`rxx`/`ryy`/`rzx`) +- `include "qelib1.inc";` is handled as a built-in include; the file need not exist locally + +Dumping trades against the expressiveness of OpenQASM 2.0; the following structures fail: + +- `else` branches in classical control, loops, `switch`, and nested control flow +- Classical types that cannot be represented as `creg`, such as `bool` and unsigned integers +- Measurement and reset inside `gate` definition bodies +- Bit-wise conditions (e.g. `if (c[0] == 1)`) and conditional barriers + +`rzx` appears only in the dumped extension gate definitions and is not accepted when loading; global phase is not written as a statement but only as a comment, and is not restored when reloading. + +--- + +## Example + +```c +#include +#include "cqlib_c.h" + +const char *src = + "OPENQASM 2.0;\n" + "include \"qelib1.inc\";\n" + "qreg q[2];\n" + "h q[0];\n" + "cx q[0],q[1];\n"; + +struct CCircuit *c = qasm2_loads(src); +if (c == NULL) { + /* parse failure */ +} + +char *out = qasm2_dumps(c); +if (out != NULL) { + printf("%s", out); /* contains "OPENQASM 2.0;" */ + cqlib_string_free(out); +} + +int32_t rc = qasm2_dump(c, "out.qasm"); /* 0 on success */ + +circuit_free(c); +``` diff --git a/docs/api/en/c/1_ir/3_qasm3.md b/docs/api/en/c/1_ir/3_qasm3.md new file mode 100644 index 00000000..26bd2a29 --- /dev/null +++ b/docs/api/en/c/1_ir/3_qasm3.md @@ -0,0 +1,139 @@ +# OpenQASM 3.0 (C) + +The C binding provides four entry points converting between OpenQASM 3.0 text and `CCircuit`. Error codes and the conventions for freeing strings and handles follow the [Overview](../0_overview.md). + +--- + +## Interface overview + +| Function | Direction | +| --- | --- | +| `qasm3_loads` | OpenQASM 3.0 string → `CCircuit` | +| `qasm3_load` | OpenQASM 3.0 file → `CCircuit` | +| `qasm3_dumps` | `CCircuit` → OpenQASM 3.0 string | +| `qasm3_dump` | `CCircuit` → OpenQASM 3.0 file | + +--- + +## Parsing + +### qasm3_loads(source) + +Parses an OpenQASM 3.0 string into a new `CCircuit*`. + +Parameters: + +- `source` (`const char*`): the OpenQASM 3.0 text, NUL-terminated. + +Returns: a newly allocated `CCircuit*` on success (free with `circuit_free`); NULL when `source` is NULL, not valid UTF-8, or parsing fails. + +Parse failures include: syntax errors, semantic errors, conversion failures, unsupported features, undefined symbols or gates, type errors, mismatched parameter or qubit counts, and gate expansion exceeding the recursion limit or forming a cycle. + +### qasm3_load(path) + +Reads an OpenQASM 3.0 file and parses it into a new `CCircuit*`. Relative `include` paths are resolved against the input file's directory. + +Parameters: + +- `path` (`const char*`): the file path, NUL-terminated. + +Returns: a newly allocated `CCircuit*` on success; NULL when `path` is NULL, not valid UTF-8, reading fails, or parsing fails. + +--- + +## Dumping + +### qasm3_dumps(circuit) + +Dumps a circuit to an OpenQASM 3.0 string. The output is, in order: `OPENQASM 3.0;`, `include "stdgates.inc";`, extension gate definitions, custom gate definitions, quantum and classical declarations, and the main circuit body. + +Parameters: + +- `circuit` (`const struct CCircuit*`): the circuit to dump. + +Returns: a heap-allocated C string on success (free with `cqlib_string_free`); NULL when `circuit` is NULL or dumping fails. + +Dump failures include: instructions that cannot be represented (such as `delay`, `XY`, `RXY`), classical storage that cannot be represented, control flow that cannot be represented (such as `while`, `for`, `break` / `continue`), a measurement inside a gate definition body, and signature conflicts between same-named gate definitions. + +### qasm3_dump(circuit, path) + +Dumps a circuit to an OpenQASM 3.0 string and writes it to a file. + +Parameters: + +- `circuit` (`const struct CCircuit*`): the circuit to dump. +- `path` (`const char*`): the target file path. + +Returns: 0 on success; -1 when either pointer is NULL; -8 when the path is not valid UTF-8; -5 when dumping or writing fails. + +--- + +## Supported OpenQASM 3.0 subset + +- The `OPENQASM 3;` and `OPENQASM 3.0;` headers, plus `include "stdgates.inc";` (a built-in standard library; no file on disk is required) +- Scalar qubit and one-dimensional quantum register declarations +- Classical declarations `bit`, `bit[n]`, `bool`, `uint[n]`, plus angle and float `input` declarations +- Standard and extension gates that map onto the standard gates +- Custom gate definitions and calls +- Measurement, `reset`, `barrier`, and global phase +- Control flow: `if` / `else`, static `for`, constant-branch `switch` + +The following features produce an explicit error rather than partial degradation: + +- Timing and pulses: `delay`, `box`, `cal`, `defcal` +- Subprograms and externals: `def` subprograms, `extern` +- Hardware-related: hardware qubits, `output` declarations, and `input` declarations other than angle and float +- Other statements: `alias`, `pragma`, annotations, legacy declarations +- Gate modifiers: `negctrl`, `pow`; `ctrl` and `inv` only work when the target gate has a matching implementation +- Some standard gates: `ch`, `cp`, `cu`, `cswap` +- Control flow: `while` over a runtime condition is rejected by semantic checks; `for` only supports static expansion of constant ranges +- Complex classical arithmetic, complex lvalue slices, multi-dimensional indexing, and in-scope qubit declarations + +Dumping has further constraints: `delay`, matrix-only unitary gates, plain assignments to scalar `bit` variables, measurements inside gate definition bodies, `while` / `for` / `break` / `continue` control flow, and the `XY` and `RXY` standard gates are not dumped. + +--- + +## Round-trip behavior + +Dump output can be loaded again. Normalization rules: + +- The header is always `OPENQASM 3.0;` and `include "stdgates.inc";`, regardless of the input spelling. +- Top-level qubits are dumped uniformly as `qubit[n] q`; the single-qubit case is also written `qubit[1] q`. +- User classical variables are named `c0`, `c1`, ...; immutable measured values are named `v0`, `v1`, ... +- A measurement immediately feeding a compatible store collapses into an assignment such as `c0 = measure q;`. +- Stand-alone measurements go into an auto-generated `bit[k] meas` register, e.g. `meas[0] = measure q[0];`. +- Non-contiguous or reordered register measurements split into per-bit assignments such as `c0[0] = measure q[2];`. +- Gates already provided by `stdgates.inc` are called directly; extension gates get one `gate` definition before the main body and keep their names at call sites. +- Global phase is dumped as a `gphase(...);` statement and re-enters the circuit's global phase on reload. +- Auto-generated names avoid gate and register names already present in the circuit. + +--- + +## Example + +```c +#include +#include "cqlib_c.h" + +const char *src = + "OPENQASM 3;\n" + "include \"stdgates.inc\";\n" + "qubit[2] q;\n" + "h q[0];\n" + "cx q[0], q[1];\n"; + +struct CCircuit *c = qasm3_loads(src); +if (c == NULL) { + /* parse failure */ +} + +char *out = qasm3_dumps(c); +if (out != NULL) { + printf("%s", out); /* header normalized to "OPENQASM 3.0;" */ + cqlib_string_free(out); +} + +int32_t rc = qasm3_dump(c, "out.qasm"); /* 0 on success */ + +circuit_free(c); +``` diff --git a/docs/api/en/c/2_device/0_overview.md b/docs/api/en/c/2_device/0_overview.md new file mode 100644 index 00000000..534c6db5 --- /dev/null +++ b/docs/api/en/c/2_device/0_overview.md @@ -0,0 +1,71 @@ +# Device (C) + +The C binding's device interface describes quantum hardware through opaque handles (`CTopology`, `CDevice`, `CLayout`, `CNoiseModel`, `CExecutionResult`) plus free functions: the coupling topology, calibration properties, the logical-to-physical mapping, the noise model, and task execution results. Error codes and the conventions for freeing strings and handles follow the [Overview](../0_overview.md). + +--- + +## Concept relationships + +- A `CDevice` aggregates a `CTopology` and a set of property defaults: the topology describes the directed graph of physical qubits and couplings; the device layers a name, a native gate set, invalid qubits, a calibration time, and device-wide default properties on top of it. Qubit-property queries prefer local values and fall back to the defaults. +- A `CLayout` is the bidirectional mapping between logical and physical qubits used during routing; `layout_bind` / `layout_unbind` / `layout_swap_physical` are the basic operations for moving logical qubits. +- A `CNoiseModel` is built independently of any device and records noise channels and readout errors per gate + qubit combination. +- A `CExecutionResult` carries the outcome of one task: the state machine (queued → running → completed / failed / cancelled), measurement counts, and the probability distribution. + +--- + +## Pages + +| Page | Contents | +| --- | --- | +| [Topology](1_topology.md) | `CTopology`: construction, qubit and coupling editing, connectivity and degree queries. | +| [Device / Properties](2_properties_device.md) | `CDevice`: construction factories, the native gate set, validation, error properties, defaults, and the property structs. | +| [Layout](3_layout.md) | `CLayout`: building, binding, and swapping the logical↔physical mapping. | +| [NoiseModel](4_noise.md) | `CNoiseModel`: registering and querying single-qubit, two-qubit, and readout noise. | +| [ExecutionResult](5_result.md) | `CExecutionResult` and `CCountsList`: the task state machine, counts, and probabilities. | +| [Qubit identifiers](6_qubits.md) | `CLogicalQubit` / `CPhysicalQubit`: strongly typed device-side qubit identifiers and their conversions to/from `CQubit`. | + +--- + +## Shared conventions + +- Every handle is released with its matching `*_free` function, all of which accept NULL: `topology_free`, `device_free`, `layout_free`, `noise_model_free`, `execution_result_free`, `counts_list_free`. +- `device_topology` returns a clone of the topology; release it with `topology_free`. +- Functions returning `char*` (e.g. `device_name`, `counts_list_get_key`) are released with `cqlib_string_free`. +- Array outputs use the two-step pattern: call the `*_len` function for the total, allocate a buffer, then call the fill function (e.g. `device_usable_qubits_len` + `device_usable_qubits`); the fill function also returns the total count. + +--- + +## Example + +```c +#include +#include "cqlib_c.h" + +int main(void) { + /* line-topology device: 0 -> 1 -> 2 */ + struct CDevice *dev = device_line("demo", 3); + if (dev == NULL) { + return 1; + } + + char *name = device_name(dev); /* "demo" */ + uintptr_t usable = device_num_qubits(dev); /* 3 */ + cqlib_string_free(name); + + /* cloned topology */ + struct CTopology *topo = device_topology(dev); + int32_t directed = topology_supports_directed_coupling(topo, 0, 1); /* 1 */ + int32_t reverse = topology_supports_directed_coupling(topo, 1, 0); /* 0 */ + topology_free(topo); + + /* device-wide defaults and fallback query */ + device_set_default_t1(dev, 40.0); + double t1 = 0.0; + device_get_t1(dev, 0, &t1); /* 0 (success), t1 == 40.0 */ + + device_free(dev); + return 0; +} +``` + +The compiler interface takes a `CDevice` and a `CLayout` as the device-target inputs; see [Compiler](../4_compile/1_compiler.md). diff --git a/docs/api/en/c/2_device/1_topology.md b/docs/api/en/c/2_device/1_topology.md new file mode 100644 index 00000000..295baebb --- /dev/null +++ b/docs/api/en/c/2_device/1_topology.md @@ -0,0 +1,232 @@ +# Topology (C) + +`CTopology` is an opaque handle to the hardware connectivity graph: physical qubits and the directed couplings between them. Error codes and the conventions for freeing strings and handles follow the [Overview](../0_overview.md); every entry point on this page is declared in `cqlib_c.h`. + +A `CTopology` handle comes from one of two places: `topology_line` builds a line topology from a qubit-ID array, and `device_topology` returns a clone of a device's topology (see [Device / Properties](2_properties_device.md)). Couplings are directed: a coupling on `(control, target)` does not imply the reverse direction. + +--- + +## Construction and release + +### topology_line(qubits, num_qubits) + +Builds a directed line `qubits[0] -> qubits[1] -> ...` in the given order; coupling names are empty strings. + +Parameters: + +- `qubits` (`const uint32_t*`): array of physical qubit IDs. +- `num_qubits` (`uintptr_t`): array length. + +Returns: a newly allocated `CTopology*` on success; NULL on a NULL pointer or construction failure. + +### topology_free(ptr) + +Frees a `CTopology`; NULL is allowed. + +Parameters: + +- `ptr` (`struct CTopology*`): handle to free. + +### topology_num_qubits(ptr) + +Returns the number of qubits in the topology; 0 for NULL. + +Parameters: + +- `ptr` (`const struct CTopology*`): topology handle. + +### topology_num_couplings(ptr) + +Returns the number of coupling edges in the topology; 0 for NULL. + +Parameters: + +- `ptr` (`const struct CTopology*`): topology handle. + +--- + +## Editing qubits and couplings + +### topology_add_qubits(ptr, qubits, len) + +Adds a set of qubits to the topology. + +Parameters: + +- `ptr` (`struct CTopology*`): topology handle. +- `qubits` (`const uint32_t*`): array of qubit IDs. +- `len` (`uintptr_t`): array length. + +Returns: 0 on success; -1 on NULL; -8 when a qubit already exists in the topology or is listed twice. + +### topology_remove_qubits(ptr, qubits, len) + +Removes a set of qubits and every coupling touching them. + +Parameters: + +- `ptr` (`struct CTopology*`): topology handle. +- `qubits` (`const uint32_t*`): array of qubit IDs. +- `len` (`uintptr_t`): array length. + +Returns: 0 on success; -1 on NULL; -2 when a qubit is not in the topology; -8 when a qubit is listed twice. + +### topology_add_couplings(ptr, edges, num_edges, names) + +Adds directed coupling edges. `edges` points to `2 * num_edges` u32 values laid out as consecutive `(control, target)` pairs; `names` optionally points to `num_edges` C strings (NULL entries or a NULL array leave the couplings unnamed). + +Parameters: + +- `ptr` (`struct CTopology*`): topology handle. +- `edges` (`const uint32_t*`): coupling-edge array. +- `num_edges` (`uintptr_t`): number of edges. +- `names` (`const char *const*`): coupling-name array; may be NULL. + +Returns: 0 on success; -1 on NULL; -4 for invalid UTF-8 in a name; -2 when an endpoint qubit is not in the topology; -8 for duplicate or self couplings. + +### topology_remove_couplings(ptr, edges, num_edges) + +Removes directed coupling edges. `edges` uses the same layout as `topology_add_couplings`. + +Parameters: + +- `ptr` (`struct CTopology*`): topology handle. +- `edges` (`const uint32_t*`): coupling-edge array. +- `num_edges` (`uintptr_t`): number of edges. + +Returns: 0 on success; -1 on NULL; -2 when a qubit or coupling is not in the topology; -8 when a coupling is listed twice. + +--- + +## Queries and traversal + +### topology_contains_qubit(ptr, qubit) + +Checks whether `qubit` is part of the topology. + +Parameters: + +- `ptr` (`const struct CTopology*`): topology handle. +- `qubit` (`uint32_t`): qubit ID. + +Returns: 1 when present; 0 otherwise; -1 on NULL. + +### topology_get_coupling_name(ptr, control, target) + +Returns the name of the directed coupling `control -> target`; an existing but unnamed coupling yields an empty string. + +Parameters: + +- `ptr` (`const struct CTopology*`): topology handle. +- `control` (`uint32_t`): control qubit ID. +- `target` (`uint32_t`): target qubit ID. + +Returns: a heap-allocated C string on success (free with `cqlib_string_free`); NULL on NULL input or when the coupling does not exist. + +### topology_qubits_len(ptr) / topology_qubits(ptr, out, len) + +Two-step output of all qubit IDs in the topology. + +- `topology_qubits_len(ptr)` returns the total count; 0 for NULL. +- `topology_qubits(ptr, out, len)` copies the qubit IDs into `out` and returns the total count. + +### topology_neighbors_undirected_len(ptr, qubit) / topology_neighbors_undirected(ptr, qubit, out, len) + +Two-step output of the undirected neighbors of `qubit` (successors and predecessors, deduplicated, ascending). The length is 0 for NULL or an unknown qubit. + +Parameters: + +- `qubit` (`uint32_t`): qubit ID. + +### topology_predecessors_len(ptr, qubit) / topology_predecessors(ptr, qubit, out, len) + +Two-step output of the predecessors of `qubit` (qubits with a directed coupling into it, ascending). + +### topology_successors_len(ptr, qubit) / topology_successors(ptr, qubit, out, len) + +Two-step output of the successors of `qubit` (qubits reachable through outgoing couplings, ascending). + +### topology_undirected_edges_len(ptr) / topology_undirected_edges(ptr, out, len) + +Two-step output of the unique undirected coupling pairs: bidirectional couplings collapse to one pair, sorted ascending as `(low, high)`. `out` must have room for `2 * len` u32 values; the fill function returns the total number of pairs. + +--- + +## Degrees and support + +### topology_in_degree(ptr, qubit) + +Returns the number of incoming couplings to `qubit`; 0 for NULL or an unknown qubit. + +### topology_out_degree(ptr, qubit) + +Returns the number of outgoing couplings from `qubit`; 0 for NULL or an unknown qubit. + +### topology_supports_directed_coupling(ptr, control, target) + +Checks whether the directed coupling `control -> target` exists. + +Returns: 1 when it exists; 0 otherwise; -1 on NULL. + +### topology_supports_coupling_either_direction(ptr, a, b) + +Checks whether a coupling between the two qubits exists in either direction. + +Returns: 1 when it exists; 0 otherwise; -1 on NULL. + +--- + +## Example + +```c +#include +#include "cqlib_c.h" + +int main(void) { + uint32_t ids[3] = {0, 1, 2}; + struct CTopology *topo = topology_line(ids, 3); + if (topo == NULL) { + return 1; + } + + /* 3 qubits, 2 directed couplings: 0->1, 1->2 */ + printf("qubits=%llu couplings=%llu\n", + (unsigned long long)topology_num_qubits(topo), + (unsigned long long)topology_num_couplings(topo)); + + /* connectivity */ + int32_t fwd = topology_supports_directed_coupling(topo, 1, 2); /* 1 */ + int32_t bwd = topology_supports_directed_coupling(topo, 2, 1); /* 0 */ + int32_t either = topology_supports_coupling_either_direction(topo, 2, 1); /* 1 */ + + /* neighbors and degrees of qubit 1 */ + uintptr_t succ_len = topology_successors_len(topo, 1); /* 1 */ + uint32_t succ[1]; + topology_successors(topo, 1, succ, succ_len); /* {2} */ + + uintptr_t nb_len = topology_neighbors_undirected_len(topo, 1); /* 2 */ + uint32_t nb[2]; + topology_neighbors_undirected(topo, 1, nb, nb_len); /* {0, 2} */ + + uintptr_t in_deg = topology_in_degree(topo, 1); /* 1 */ + uintptr_t out_deg = topology_out_degree(topo, 1); /* 1 */ + + /* add and remove qubits and couplings */ + uint32_t extra[1] = {3}; + if (topology_add_qubits(topo, extra, 1) == 0) { + uint32_t edge[2] = {2, 3}; + const char *names[1] = {"CX"}; + topology_add_couplings(topo, edge, 1, names); + topology_remove_couplings(topo, edge, 1); + topology_remove_qubits(topo, extra, 1); + } + + /* undirected edges */ + uintptr_t pairs = topology_undirected_edges_len(topo); /* 2 */ + uint32_t buf[4]; + topology_undirected_edges(topo, buf, pairs); /* {0,1, 1,2} */ + + topology_free(topo); + return 0; +} +``` diff --git a/docs/api/en/c/2_device/2_properties_device.md b/docs/api/en/c/2_device/2_properties_device.md new file mode 100644 index 00000000..f8d36c82 --- /dev/null +++ b/docs/api/en/c/2_device/2_properties_device.md @@ -0,0 +1,647 @@ +# Device / Properties (C) + +`CDevice` is the opaque handle to a device: on top of a topology it aggregates the device name, native gate set, qubit registry, calibration time, and property defaults, and provides validation and error queries. Error codes and the conventions for freeing strings and handles follow the [Overview](../0_overview.md). + +Qubit-property queries prefer local values: `device_get_t1` and friends first read the properties recorded for that qubit and fall back to the device-wide default. Once a qubit or edge records its own native-instruction list, that list fully replaces the device-level capability at that location and never writes back to the default gate set. + +--- + +## Property structs + +### CQubitProp + +Snapshot of the properties recorded for one physical qubit, written by `device_qubit_properties`: + +| Field | Type | Meaning | +| --- | --- | --- | +| `readout_error` | `double` | Readout error rate in [0, 1]. | +| `t1` | `double` | T1 relaxation time in microseconds; NaN when unset. | +| `t2` | `double` | T2 dephasing time in microseconds; NaN when unset. | +| `prob_meas0_prep1` | `double` | P(measure 0 \| prepared 1); NaN when unset. | +| `prob_meas1_prep0` | `double` | P(measure 1 \| prepared 0); NaN when unset. | +| `frequency` | `double` | Frequency in GHz; NaN when unset. | +| `num_native_instructions` | `uintptr_t` | Number of native instructions recorded for the qubit; the index bound for `device_qubit_prop_native_instruction`. | + +### CNativeInstruction + +Snapshot of one native instruction capability, written by `device_qubit_prop_native_instruction` / `device_edge_prop_native_instruction`: + +| Field | Type | Meaning | +| --- | --- | --- | +| `name` | `char*` | Heap-allocated standard-gate name; free with `cqlib_string_free`. | +| `error_rate` | `double` | Error rate in [0, 1]. | +| `length` | `double` | Duration in nanoseconds; NaN when unset. | + +### CEdgeProp + +Snapshot of the properties recorded for one directed coupling, written by `device_edge_properties`: + +| Field | Type | Meaning | +| --- | --- | --- | +| `num_native_instructions` | `uintptr_t` | Number of native instructions recorded for the edge; the index bound for `device_edge_prop_native_instruction`. | + +### CQubitPropInput + +Input for `device_add_qubit_properties`. Optional `double` fields use NaN to leave the value unset; native instructions are supplied as parallel arrays of length `num_native_instructions` (gate names, error rates, durations). `native_lengths` may be NULL to leave all durations unset: + +| Field | Type | Meaning | +| --- | --- | --- | +| `readout_error` | `double` | Readout error rate in [0, 1]. | +| `t1` | `double` | T1 relaxation time in microseconds; NaN = unset. | +| `t2` | `double` | T2 dephasing time in microseconds; NaN = unset. | +| `prob_meas0_prep1` | `double` | P(measure 0 \| prepared 1); NaN = unset. | +| `prob_meas1_prep0` | `double` | P(measure 1 \| prepared 0); NaN = unset. | +| `frequency` | `double` | Frequency in GHz; NaN = unset. | +| `native_gate_names` | `const char *const*` | Array of standard-gate names, or NULL when none. | +| `native_error_rates` | `const double*` | Array of error rates, or NULL when none. | +| `native_lengths` | `const double*` | Array of durations in ns (NaN = unset), or NULL. | +| `num_native_instructions` | `uintptr_t` | Number of native instructions. | + +### CEdgePropInput + +Input for `device_add_edge_properties`; fields mirror the native-instruction part of `CQubitPropInput`, and only two-qubit standard gates are accepted: + +| Field | Type | Meaning | +| --- | --- | --- | +| `native_gate_names` | `const char *const*` | Array of standard-gate names, or NULL when none. | +| `native_error_rates` | `const double*` | Array of error rates, or NULL when none. | +| `native_lengths` | `const double*` | Array of durations in ns (NaN = unset), or NULL. | +| `num_native_instructions` | `uintptr_t` | Number of native instructions. | + +--- + +## Construction + +### device_new(name, num_qubits) + +Creates a device with `num_qubits` isolated physical qubits and no couplings; use `device_from_edges` for custom topologies. + +Parameters: + +- `name` (`const char*`): device name. +- `num_qubits` (`uint32_t`): number of physical qubits. + +Returns: a newly allocated `CDevice*` on success; NULL on error. + +### device_line(name, num_qubits) + +Creates a device with a directed line topology `0 -> 1 -> ... -> n-1`. + +### device_bidirectional_line(name, num_qubits) + +Creates a device with a bidirectional line topology. + +### device_ring(name, num_qubits) + +Creates a device with a bidirectional ring topology. + +### device_grid(name, rows, cols) + +Creates a device with a bidirectional grid topology; qubit IDs are row-major. + +Parameters: + +- `rows` (`uint32_t`): number of rows. +- `cols` (`uint32_t`): number of columns. + +### device_star(name, num_qubits, center) + +Creates a device with a bidirectional star topology around `center`. + +Parameters: + +- `num_qubits` (`uint32_t`): total number of qubits. +- `center` (`uint32_t`): center qubit ID. + +### device_from_edges(name, num_qubits, edges, num_edges) + +Creates a device from explicit directed edges. `edges` points to `2 * num_edges` u32 values laid out as consecutive `(control, target)` pairs. + +Parameters: + +- `num_qubits` (`uint32_t`): number of physical qubits. +- `edges` (`const uint32_t*`): coupling-edge array. +- `num_edges` (`uintptr_t`): number of edges. + +Returns: a newly allocated `CDevice*` on success; NULL on error. + +### device_line_from_qubits(name, qubits, num_qubits) + +Creates a device whose physical qubits (given by ID array) are connected as a directed line in the supplied order. + +Parameters: + +- `qubits` (`const uint32_t*`): array of physical qubit IDs. +- `num_qubits` (`uintptr_t`): array length. + +Returns: a newly allocated `CDevice*` on success; NULL on error. + +### device_free(ptr) + +Frees a `CDevice`; NULL is allowed. + +Parameters: + +- `ptr` (`struct CDevice*`): handle to free. + +--- + +## Basic information and native gates + +### device_name(ptr) + +Returns the device name. + +Returns: a heap-allocated C string on success (free with `cqlib_string_free`); NULL on error. + +### device_num_qubits(ptr) + +Returns the number of usable physical qubits; 0 for NULL. + +### device_native_gates(ptr) + +Returns the native gate names. + +Returns: a comma-separated heap-allocated C string on success (e.g. `"H,CX"`), freed with `cqlib_string_free`; NULL on error or when the gate set is empty. + +### device_with_native_gates(ptr, gate_names) + +Replaces the device's native gate set with the gates named in `gate_names` (comma separated, e.g. `"H,CX,RZ"`). + +Parameters: + +- `gate_names` (`const char*`): gate-name list. + +Returns: 0 on success; -4 when any name is unknown; other negative values on error. + +### device_topology(ptr) + +Returns a clone of the device topology; free it with `topology_free`. + +Returns: a newly allocated `CTopology*` on success; NULL on error. + +--- + +## Validation + +### device_validate_circuit(ptr, circuit) + +Validates that `circuit` is compatible with the device. + +Parameters: + +- `circuit` (`const struct CCircuit*`): circuit to validate. + +Returns: 0 when valid; -3 when validation fails; other negative values on error. + +### device_validate_operation(ptr, operation) + +Validates one resolved operation snapshot (from `circuit_index`) against the device in the physical-qubit ID space. + +Parameters: + +- `operation` (`const struct CValueOperation*`): the operation snapshot. + +Returns: 0 when valid; -3 when the device rejects the operation; -1 on NULL. + +### device_validate_value_operation(ptr, operation) + +Validates one resolved value-level operation snapshot (from `circuit_index`) against the device in the physical-qubit ID space; control-flow bodies are checked recursively. + +Parameters: + +- `operation` (`const struct CValueOperation*`): the operation snapshot. + +Returns: 0 when valid; -3 when the device rejects the operation; -1 on NULL. + +### device_supports_native_instruction(ptr, gate_name, qargs, num_qargs) + +Checks whether the standard gate `gate_name` can execute natively on the exact ordered physical `qargs`. + +Parameters: + +- `gate_name` (`const char*`): standard-gate name. +- `qargs` (`const uint32_t*`): array of physical qubit IDs; an empty list is valid for `GPhase`. +- `num_qargs` (`uintptr_t`): array length. + +Returns: 1 when supported; 0 when not; -1 on NULL; -4 for an unknown gate name. Calibration values do not affect this capability query. + +--- + +## Error and coherence properties + +### device_get_t1(ptr, qubit, out) + +Returns the T1 relaxation time (μs) for `qubit`, preferring the local property and falling back to the device default. + +Parameters: + +- `qubit` (`uint32_t`): physical qubit ID. +- `out` (`double*`): receives the value. + +Returns: 0 with the value in `*out`; -8 when no T1 data is available for the qubit. + +### device_get_t2(ptr, qubit, out) + +Returns the T2 dephasing time (μs) for `qubit`, with the same fallback rule. Returns: 0 with the value in `*out`; -8 when unset. + +### device_get_readout_error(ptr, qubit, out) + +Returns the readout error rate for `qubit`, with the same fallback rule. Returns: 0 with the value in `*out`; -8 when unset. + +### device_single_qubit_error(ptr, gate_name, qubit, out) + +Returns the error rate of the single-qubit standard gate `gate_name` on `qubit`, preferring the local native-instruction property and falling back to the device default single-qubit error rate. + +Parameters: + +- `gate_name` (`const char*`): standard-gate name. +- `qubit` (`uint32_t`): physical qubit ID. +- `out` (`double*`): receives the value. + +Returns: 0 with the value in `*out`; -4 for an unknown gate name; -8 when the qubit does not support the gate. + +### device_two_qubit_error(ptr, gate_name, control, target, out) + +Returns the error rate of the two-qubit standard gate `gate_name` on the directed coupling `control -> target`, with the same fallback rule. + +Returns: 0 with the value in `*out`; -4 for an unknown gate name; -8 when the coupling does not support the gate. + +### device_edge_error(ptr, control, target, out) + +Returns the direction-specific coupling error for `control -> target`: the best calibrated native two-qubit error on the edge, or the device default. + +Returns: 0 with the value in `*out`; -8 when the directed coupling does not exist. + +### device_calibration_time(ptr, out_unix_ms) + +Returns the system calibration timestamp as milliseconds since the Unix epoch. + +Parameters: + +- `out_unix_ms` (`int64_t*`): receives the value. + +Returns: 0 with the value in `*out`; -8 when unset. + +--- + +## Per-qubit property setters and getters + +The getters below read the values recorded for one qubit; unlike `device_get_t1`, the frequency and preparation/measurement-probability getters have no device-default fallback. The setters edit a single field of the properties already recorded for the qubit (for example via `device_add_qubit_properties`) and preserve every other recorded property; they do not create the record. The indexed variants address the native-instruction list, bounded by `num_native_instructions` from `device_qubit_properties`. An unset optional value is reported as -8 with NaN written to `*out`. + +### device_get_frequency(ptr, qubit, out) + +Returns the qubit frequency (GHz) recorded for `qubit` (no default fallback). + +Parameters: + +- `qubit` (`uint32_t`): physical qubit ID. +- `out` (`double*`): receives the value. + +Returns: 0 with the value in `*out`; -8 when the qubit has no recorded properties or no frequency is set. + +### device_get_prob_meas0_prep1(ptr, qubit, out) + +Returns P(measure 0 \| prepared 1) recorded for `qubit` (no default fallback). + +Returns: 0 with the value in `*out`; -8 when the qubit has no recorded properties or the probability is unset. + +### device_get_prob_meas1_prep0(ptr, qubit, out) + +Returns P(measure 1 \| prepared 0) recorded for `qubit` (no default fallback). + +Returns: 0 with the value in `*out`; -8 when the qubit has no recorded properties or the probability is unset. + +### device_get_error_rate(ptr, qubit, index, out) + +Returns the error rate of the native instruction at `index` recorded for `qubit`. + +Parameters: + +- `index` (`uintptr_t`): native-instruction index. +- `out` (`double*`): receives the value. + +Returns: 0 with the value in `*out`; -8 when the qubit has no recorded properties or `index` is out of bounds. + +### device_get_length(ptr, qubit, index, out) + +Returns the duration (ns) of the native instruction at `index` recorded for `qubit`. + +Returns: 0 with the value in `*out`; -8 when the qubit has no recorded properties, `index` is out of bounds, or the duration is unset. + +### device_set_t1(ptr, qubit, t1) + +Sets the T1 relaxation time (μs) recorded for `qubit`. + +Returns: 0 on success; -1 on NULL; -8 when the qubit has no recorded properties. + +### device_set_t2(ptr, qubit, t2) + +Sets the T2 dephasing time (μs) recorded for `qubit`. + +Returns: 0 on success; -1 on NULL; -8 when the qubit has no recorded properties. + +### device_set_frequency(ptr, qubit, frequency) + +Sets the qubit frequency (GHz) recorded for `qubit`. + +Returns: 0 on success; -1 on NULL; -8 when the qubit has no recorded properties. + +### device_set_prob_meas0_prep1(ptr, qubit, prob) + +Sets P(measure 0 \| prepared 1) recorded for `qubit`. + +Returns: 0 on success; -1 on NULL; -8 when the qubit has no recorded properties. + +### device_set_prob_meas1_prep0(ptr, qubit, prob) + +Sets P(measure 1 \| prepared 0) recorded for `qubit`. + +Returns: 0 on success; -1 on NULL; -8 when the qubit has no recorded properties. + +### device_set_error_rate(ptr, qubit, index, error_rate) + +Sets the error rate of the native instruction at `index` recorded for `qubit`. + +Returns: 0 on success; -1 on NULL; -8 when the qubit has no recorded properties or `index` is out of bounds. + +### device_set_length(ptr, qubit, index, length) + +Sets the duration (ns) of the native instruction at `index` recorded for `qubit`. + +Returns: 0 on success; -1 on NULL; -8 when the qubit has no recorded properties or `index` is out of bounds. + +### device_set_instruction(ptr, qubit, index, gate_name) + +Replaces the standard gate carried by the native instruction at `index` recorded for `qubit`. The edited list is revalidated, so changing a single-qubit gate to a two-qubit gate (or vice versa) is rejected. + +Parameters: + +- `index` (`uintptr_t`): native-instruction index. +- `gate_name` (`const char*`): replacement standard-gate name. + +Returns: 0 on success; -1 on NULL; -4 for an unknown gate name; -8 when the qubit has no recorded properties, `index` is out of bounds, or the edited list fails validation. + +### device_set_native_instruction(ptr, qubit, gate_name, error_rate, length) + +Appends one native single-qubit instruction (standard gate `gate_name` with `error_rate` and duration `length` in ns; NaN `length` = unset) to the recorded properties of `qubit`. + +Parameters: + +- `gate_name` (`const char*`): standard-gate name. +- `error_rate` (`double`): error rate in [0, 1]. +- `length` (`double`): duration in ns; NaN = unset. + +Returns: 0 on success; -1 on NULL; -4 for an unknown gate name; -8 when the qubit has no recorded properties or the instruction is rejected (wrong arity or invalid calibration values). + +--- + +## Defaults + +### device_default_t1(ptr, out) + +Returns the device-wide default T1 time (μs). Returns: 0 with the value in `*out`; -8 when no default is set. + +### device_default_t2(ptr, out) + +Returns the device-wide default T2 time (μs). Returns: 0 with the value in `*out`; -8 when unset. + +### device_default_readout_error(ptr, out) + +Returns the device-wide default readout error rate. Returns: 0 with the value in `*out`; -8 when unset. + +### device_default_single_qubit_error(ptr, out) + +Returns the device-wide default single-qubit gate error rate. Returns: 0 with the value in `*out`; -8 when unset. + +### device_default_two_qubit_error(ptr, out) + +Returns the device-wide default two-qubit gate error rate. Returns: 0 with the value in `*out`; -8 when unset. + +### device_set_default_t1(ptr, t1) + +Sets the device-wide default T1 time (μs). Returns: 0 on success; a negative code on error. + +### device_set_default_t2(ptr, t2) + +Sets the device-wide default T2 time (μs). Returns: 0 on success; a negative code on error. + +### device_set_default_readout_error(ptr, error) + +Sets the device-wide default readout error rate. Returns: 0 on success; a negative code on error. + +### device_set_default_single_qubit_error(ptr, error) + +Sets the device-wide default single-qubit gate error rate. Returns: 0 on success; a negative code on error. + +### device_set_default_two_qubit_error(ptr, error) + +Sets the device-wide default two-qubit gate error rate. Returns: 0 on success; a negative code on error. + +--- + +## Usable qubits + +### device_invalid_qubits_len(ptr) + +Returns the number of invalid (offline/faulty) qubits; 0 for NULL. + +### device_invalid_qubits(ptr, out, len) + +Two-step output of the invalid qubit IDs, paired with `device_invalid_qubits_len`; the fill function returns the total count. + +### device_set_invalid_qubits(ptr, qubits, len) + +Replaces the set of invalid qubits. + +Parameters: + +- `qubits` (`const uint32_t*`): array of qubit IDs. +- `len` (`uintptr_t`): array length. + +Returns: 0 on success; -1 on NULL; -2 when any qubit is not registered with the device (the existing set is then preserved). + +### device_is_usable_qubit(ptr, qubit) + +Checks whether `qubit` is registered with the device and not marked invalid. + +Returns: 1 when usable; 0 otherwise; -1 on NULL. + +### device_num_usable_qubits(ptr) + +Returns the number of usable qubits; 0 for NULL. + +### device_usable_qubits_len(ptr) / device_usable_qubits(ptr, out, len) + +Two-step output of the usable qubit IDs; the fill function returns the total count. + +### device_qubits_len(ptr) / device_qubits(ptr, out, len) + +Two-step output of all registered physical qubit IDs (including invalid ones); the fill function returns the total count. + +--- + +## Qubit and edge properties + +### device_qubit_properties(ptr, qubit, out) + +Writes the per-qubit property snapshot for `qubit` to `*out`. + +Parameters: + +- `qubit` (`uint32_t`): physical qubit ID. +- `out` (`struct CQubitProp*`): receives the snapshot. + +Returns: 0 on success; -8 when no properties have been recorded for the qubit (optional fields in the snapshot are NaN when unset). + +### device_qubit_prop_native_instruction(ptr, qubit, index, out) + +Writes the native instruction at `index` for `qubit` to `*out`. + +Parameters: + +- `qubit` (`uint32_t`): physical qubit ID. +- `index` (`uintptr_t`): instruction index, bounded by `num_native_instructions` from the snapshot. +- `out` (`struct CNativeInstruction*`): receives the snapshot; `name` must be freed with `cqlib_string_free`. + +Returns: 0 on success; -8 when the qubit has no recorded properties or `index` is out of bounds. + +### device_edge_properties(ptr, control, target, out) + +Writes the per-edge property snapshot for the directed coupling `control -> target` to `*out`. + +Returns: 0 on success; -8 when no properties have been recorded for the edge. + +### device_edge_prop_native_instruction(ptr, control, target, index, out) + +Writes the native instruction at `index` for the directed coupling `control -> target` to `*out` (two-qubit standard gates). + +Returns: 0 on success; -8 when the edge has no recorded properties or `index` is out of bounds; `name` must be freed with `cqlib_string_free`. + +### device_add_qubit_properties(ptr, qubit, input) + +Records (or replaces) the properties of `qubit`. + +Parameters: + +- `qubit` (`uint32_t`): physical qubit ID. +- `input` (`const struct CQubitPropInput*`): property input. + +Returns: 0 on success; -1 on NULL; -2 when the qubit is not registered with the device or absent from its topology; -4 for an unknown native gate name; -8 for invalid native-instruction calibration values. + +### device_add_edge_properties(ptr, control, target, input) + +Records (or replaces) the properties of the directed coupling `control -> target`. + +Parameters: + +- `input` (`const struct CEdgePropInput*`): property input. + +Returns: 0 on success; -1 on NULL; -2 when the directed coupling is not in the device topology; -4 for an unknown gate name; -8 when a native instruction is not a two-qubit standard gate or has invalid calibration values. + +--- + +## Example + +```c +#include +#include "cqlib_c.h" + +int main(void) { + /* line topology: 0 -> 1 -> 2 */ + struct CDevice *dev = device_line("mock_backend", 3); + if (dev == NULL) { + return 1; + } + + /* device-wide defaults */ + device_set_default_t1(dev, 50.0); + device_set_default_t2(dev, 35.0); + device_set_default_readout_error(dev, 0.05); + + /* local properties for qubit 0: readout error 0.02, T1 80, native H */ + struct CQubitPropInput input; + input.readout_error = 0.02; + input.t1 = 80.0; + input.t2 = 70.0; + input.prob_meas0_prep1 = 0.01; + input.prob_meas1_prep0 = 0.03; + input.frequency = 5.0; + + const char *names[1] = {"H"}; + const double rates[1] = {0.001}; + input.native_gate_names = names; + input.native_error_rates = rates; + input.native_lengths = NULL; + input.num_native_instructions = 1; + + int32_t rc = device_add_qubit_properties(dev, 0, &input); /* 0 */ + + /* local properties for edge 0 -> 1: native CX */ + struct CEdgePropInput einput; + const char *enames[1] = {"CX"}; + const double erates[1] = {0.02}; + einput.native_gate_names = enames; + einput.native_error_rates = erates; + einput.native_lengths = NULL; + einput.num_native_instructions = 1; + + rc = device_add_edge_properties(dev, 0, 1, &einput); /* 0 */ + + /* queries: local first, default fallback */ + double t1_0 = 0.0, t1_2 = 0.0; + device_get_t1(dev, 0, &t1_0); /* 0, t1_0 == 80.0 (local) */ + device_get_t1(dev, 2, &t1_2); /* 0, t1_2 == 50.0 (default) */ + + double sq_err = 0.0, tq_err = 0.0, edge_err = 0.0; + device_single_qubit_error(dev, "H", 0, &sq_err); /* 0, 0.001 */ + device_two_qubit_error(dev, "CX", 0, 1, &tq_err); /* 0, 0.02 */ + device_edge_error(dev, 0, 1, &edge_err); /* 0, 0.02 */ + + /* per-qubit property getters (no default fallback) */ + double freq = 0.0, pm01 = 0.0, pm10 = 0.0, rate = 0.0, len = 0.0; + device_get_frequency(dev, 0, &freq); /* 0, freq == 5.0 */ + device_get_prob_meas0_prep1(dev, 0, &pm01); /* 0, pm01 == 0.01 */ + device_get_prob_meas1_prep0(dev, 0, &pm10); /* 0, pm10 == 0.03 */ + device_get_error_rate(dev, 0, 0, &rate); /* 0, rate == 0.001 */ + device_get_length(dev, 0, 0, &len); /* -8: duration unset, len == NaN */ + + /* per-qubit property setters: one field per call, others preserved */ + device_set_t1(dev, 0, 85.0); /* 0 */ + device_set_frequency(dev, 0, 5.1); /* 0 */ + device_get_frequency(dev, 0, &freq); /* 0, freq == 5.1 */ + device_set_error_rate(dev, 0, 0, 0.002); /* 0 */ + device_set_length(dev, 0, 0, 50.0); /* 0 */ + device_set_t1(dev, 2, 1.0); /* -8: qubit 2 has no recorded properties */ + + /* native instruction edits */ + device_set_instruction(dev, 0, 0, "X"); /* 0: H -> X, calibration kept */ + device_set_native_instruction(dev, 0, "Z", 0.003, 80.0); /* 0: appended */ + + /* property snapshot and native instructions */ + struct CQubitProp prop; + if (device_qubit_properties(dev, 0, &prop) == 0) { + for (uintptr_t i = 0; i < prop.num_native_instructions; i++) { + struct CNativeInstruction ni; + if (device_qubit_prop_native_instruction(dev, 0, i, &ni) == 0) { + printf("%s rate=%f\n", ni.name, ni.error_rate); + cqlib_string_free(ni.name); + } + } + } + + /* invalid qubits */ + uint32_t bad[1] = {2}; + device_set_invalid_qubits(dev, bad, 1); + int32_t usable = device_is_usable_qubit(dev, 2); /* 0 */ + uintptr_t n_usable = device_num_usable_qubits(dev); /* 2 */ + + /* native gate set */ + rc = device_with_native_gates(dev, "H,CX,RZ"); /* 0 */ + char *gates = device_native_gates(dev); /* "H,CX,RZ" */ + cqlib_string_free(gates); + + device_free(dev); + return 0; +} +``` diff --git a/docs/api/en/c/2_device/3_layout.md b/docs/api/en/c/2_device/3_layout.md new file mode 100644 index 00000000..2f2a3fc9 --- /dev/null +++ b/docs/api/en/c/2_device/3_layout.md @@ -0,0 +1,236 @@ +# Layout (C) + +`CLayout` is an opaque handle to the logical-to-physical qubit mapping used during routing; it maintains the bidirectional logical↔physical mapping, and a physical qubit carrying no logical qubit is vacant. Error codes and the conventions for freeing strings and handles follow the [Overview](../0_overview.md). + +--- + +## Construction and release + +### layout_new(logical, num_logical, physical, num_physical) + +Creates a layout mapping the logical qubits in the `logical` array, in order, onto the physical qubits in the `physical` array. + +Parameters: + +- `logical` (`const uint32_t*`): array of logical qubit IDs. +- `num_logical` (`uintptr_t`): number of logical qubits; must be non-zero. +- `physical` (`const uint32_t*`): array of physical qubit IDs. +- `num_physical` (`uintptr_t`): number of physical qubits; must equal `num_logical`. + +Returns: a newly allocated `CLayout*` on success; NULL on error. + +### layout_from_pairs(pairs, num_pairs, physical_count) + +Creates a layout from `(logical, physical)` pairs. `pairs` points to `2 * num_pairs` u32 values laid out as consecutive `(logical, physical)` pairs; `physical_count` defines the total number of physical qubits (`0..physical_count`), and unreferenced ones stay vacant. + +Parameters: + +- `pairs` (`const uint32_t*`): pair array. +- `num_pairs` (`uintptr_t`): number of pairs; must be non-zero. +- `physical_count` (`uint32_t`): total number of physical qubits. + +Returns: a newly allocated `CLayout*` on success; NULL on error. + +### layout_free(ptr) + +Frees a `CLayout`; NULL is allowed. + +--- + +## Mapping queries + +### layout_get(ptr, logical) + +Returns the physical qubit mapped to `logical`. + +Returns: the physical qubit ID when mapped; `UINT32_MAX` when unmapped or on error. + +### layout_get_logical(ptr, physical) + +Returns the logical qubit carried by `physical` (the reverse lookup of `layout_get`). + +Returns: the logical qubit ID when occupied; `UINT32_MAX` when `physical` is vacant or unknown, which callers must treat as a sentinel. + +### layout_is_physical_vacant(ptr, physical) + +Checks whether `physical` belongs to the layout and is vacant. + +Returns: 1 when vacant; 0 when occupied or unknown; -1 on NULL. + +### layout_num_logical(ptr) + +Returns the number of mapped logical qubits; 0 for NULL. + +### layout_num_physical(ptr) + +Returns the total number of physical qubits; 0 for NULL. + +### layout_num_vacant_physical(ptr) + +Returns the number of vacant physical qubits; 0 for NULL. + +--- + +## Binding and swapping + +### layout_bind(ptr, logical, physical) + +Binds an unmapped `logical` qubit to a vacant `physical` qubit. + +Returns: 0 on success; -1 on NULL; -2 when `physical` does not belong to the layout; -8 when either side already participates in a mapping. + +### layout_unbind(ptr, logical, out_physical) + +Removes the mapping for `logical` and writes the released physical qubit ID to `*out_physical`. + +Returns: 0 on success; -1 on NULL; -8 when `logical` is not bound. + +### layout_swap_physical(ptr, phys_a, phys_b) + +Swaps the logical qubits carried by two physical qubits; either side may be vacant (a vacant swap moves the logical qubit). This is the basic operation for moving logical qubits during routing. + +Returns: 0 on success; -1 on NULL; -2 when either physical qubit does not belong to the layout. + +--- + +## Bulk reads + +The following entry points all use the two-step pattern: fetch the length, then fill the buffer; the fill function returns the total count. + +### layout_l2p_map_len(ptr) / layout_l2p_map(ptr, out, len) + +The logical→physical mapping as `(logical, physical)` pairs sorted by logical ID; `out` must have room for `2 * len` u32 values. + +### layout_p2l_map_len(ptr) / layout_p2l_map(ptr, out, len) + +The physical→logical mapping as `(physical, logical)` pairs sorted by physical ID; `out` must have room for `2 * len` u32 values. + +### layout_logical_qubits_len(ptr) / layout_logical_qubits(ptr, out, len) + +The mapped logical qubit IDs, ascending. + +### layout_physical_qubits_len(ptr) / layout_physical_qubits(ptr, out, len) + +All physical qubit IDs available to the layout (including vacant ones), ascending. + +### layout_vacant_physical_qubits_len(ptr) / layout_vacant_physical_qubits(ptr, out, len) + +The vacant physical qubit IDs, ascending. + +--- + +## Layout result helpers + +These helpers read the SABRE layout artifacts: the all-pairs distance table of a physical layout graph and the per-logical-qubit activity weights of a circuit layout analysis. The handles come from the compile-side layout API ([Initial Layout](../4_compile/3_layout.md)): `CPhysicalLayoutGraph` from `physical_layout_graph_from_device`, `CCircuitLayoutAnalysis` from `analyze_circuit_for_layout` (or from `layout_result_analysis`), and `CPreparedSabreCircuit` from `prepare_sabre_circuit`. + +### layout_result_distances(ptr, out, len) + +Copies the all-pairs undirected shortest-path distance table of the physical layout graph into `out` (two-step pattern). The table is row-major over the graph's physical qubits in ascending ID order (`n * n` entries for `n` qubits; `physical_layout_graph_physical_qubits` gives the axis order). Unreachable pairs are written as `UINT32_MAX`. + +Parameters: + +- `ptr` (`const struct CPhysicalLayoutGraph*`): physical layout graph. +- `out` (`uint32_t*`): receives the table; NULL only queries the length. +- `len` (`uintptr_t`): buffer capacity in entries; a smaller value copies only the first `len` entries. + +Returns: the total number of entries (`n * n`); 0 for NULL. + +### layout_result_logical_activity(ptr, out_qubits, out_activity, len) + +Copies the per-logical-qubit activity weights (the sum of incident interaction weights) into parallel arrays sorted by logical ID (two-step pattern): `out_qubits[i]` receives the logical qubit ID and `out_activity[i]` its activity. Either array may be NULL only when both are NULL (length query). + +Parameters: + +- `ptr` (`const struct CCircuitLayoutAnalysis*`): circuit layout analysis. +- `out_qubits` (`uint32_t*`): receives the logical qubit IDs. +- `out_activity` (`double*`): receives the activity weights. +- `len` (`uintptr_t`): buffer capacity in entries; a smaller value copies only the first `len` entries. + +Returns: the total number of entries; 0 for NULL. + +### layout_result_analysis(ptr) + +Extracts the reusable circuit layout analysis captured by the prepared SABRE circuit as an owned `CCircuitLayoutAnalysis*` — the same handle type used by `analyze_circuit_for_layout`; free it with `circuit_layout_analysis_free`. + +Parameters: + +- `ptr` (`const struct CPreparedSabreCircuit*`): prepared SABRE circuit. + +Returns: a newly allocated `CCircuitLayoutAnalysis*` on success; NULL on NULL input. + +--- + +## Example + +```c +#include +#include "cqlib_c.h" + +int main(void) { + /* logical {0, 1} mapped in order onto physical {10, 11}; 3 physical total (12 vacant) */ + uint32_t logical[2] = {0, 1}; + uint32_t physical[3] = {10, 11, 12}; + struct CLayout *layout = layout_new(logical, 2, physical, 3); + if (layout == NULL) { + return 1; + } + + uintptr_t n_logical = layout_num_logical(layout); /* 2 */ + uintptr_t n_physical = layout_num_physical(layout); /* 3 */ + uintptr_t n_vacant = layout_num_vacant_physical(layout); /* 1 */ + + uint32_t p = layout_get(layout, 0); /* 10 */ + uint32_t l = layout_get_logical(layout, 10); /* 0 */ + uint32_t vacant_l = layout_get_logical(layout, 12); /* UINT32_MAX (vacant) */ + int32_t is_vacant = layout_is_physical_vacant(layout, 12); /* 1 */ + + /* bulk read */ + uintptr_t pairs_len = layout_l2p_map_len(layout); /* 2 */ + uint32_t pairs[4]; + layout_l2p_map(layout, pairs, pairs_len); /* {0,10, 1,11} */ + + /* swap physical 11 and 12: logical 1 moves to 12 */ + layout_swap_physical(layout, 11, 12); + + /* unbind logical 0: physical 10 is released */ + uint32_t freed = 0; + layout_unbind(layout, 0, &freed); /* 0, freed == 10 */ + + /* rebind onto the vacant qubit 11 */ + int32_t rc = layout_bind(layout, 0, 11); /* 0 */ + + /* layout result helpers */ + struct CDevice *dev = device_line("mock_backend", 3); + struct CPhysicalLayoutGraph *graph = physical_layout_graph_from_device(dev); + uintptr_t n_dist = layout_result_distances(graph, NULL, 0); /* 9 (3x3) */ + uint32_t dist[9]; + layout_result_distances(graph, dist, n_dist); + /* dist == {0,1,2, 1,0,1, 2,1,0} row-major */ + + struct CCircuit *circuit = circuit_new(3); + circuit_cx(circuit, 0, 1); + circuit_cx(circuit, 0, 1); + circuit_cx(circuit, 1, 2); + + struct CCircuitLayoutAnalysis *analysis = analyze_circuit_for_layout(circuit); + uintptr_t n_act = layout_result_logical_activity(analysis, NULL, NULL, 0); /* 3 */ + uint32_t lq[3]; + double act[3]; + layout_result_logical_activity(analysis, lq, act, n_act); + /* lq == {0,1,2}, act == {2.0, 3.0, 1.0} */ + circuit_layout_analysis_free(analysis); + + /* reuse the analysis captured by the prepared SABRE circuit */ + struct CPreparedSabreCircuit *prepared = prepare_sabre_circuit(circuit); + struct CCircuitLayoutAnalysis *reused = layout_result_analysis(prepared); + circuit_layout_analysis_free(reused); + prepared_sabre_circuit_free(prepared); + + circuit_free(circuit); + physical_layout_graph_free(graph); + device_free(dev); + + layout_free(layout); + return 0; +} +``` diff --git a/docs/api/en/c/2_device/4_noise.md b/docs/api/en/c/2_device/4_noise.md new file mode 100644 index 00000000..44283be4 --- /dev/null +++ b/docs/api/en/c/2_device/4_noise.md @@ -0,0 +1,254 @@ +# NoiseModel (C) + +`CNoiseModel` is an opaque handle to a noise model that records noise channels per standard-gate + qubit combination and readout errors per qubit; one gate + qubit combination can carry multiple channels. Error codes and the conventions for freeing strings and handles follow the [Overview](../0_overview.md). + +--- + +## Channel constants + +Single-qubit channel tags (used for the `noise_type` argument of `noise_model_add_single_qubit` and for `CSingleQubitNoise.tag`): + +| Constant | Value | Channel | Parameter meaning | +| --- | --- | --- | --- | +| `NOISE_BIT_FLIP` | 0 | BitFlip | flip probability p | +| `NOISE_PHASE_FLIP` | 1 | PhaseFlip | flip probability p | +| `NOISE_DEPOLARIZING` | 2 | Depolarizing | depolarizing parameter p | +| `NOISE_AMPLITUDE_DAMPING` | 3 | AmplitudeDamping | damping parameter gamma | +| `NOISE_PHASE_DAMPING` | 4 | PhaseDamping | scattering probability | +| `NOISE_PAULI` | 5 | Pauli channel | probabilities `px`/`py`/`pz` (sum <= 1) | + +Two-qubit channel tags (`NOISE_TWO_DEPOLARIZING` for `noise_model_add_two_qubit`; all of them for `CTwoQubitNoise.tag`): + +| Constant | Value | Channel | Parameter meaning | +| --- | --- | --- | --- | +| `NOISE_TWO_DEPOLARIZING` | 0 | Depolarizing | depolarizing parameter p | +| `NOISE_TWO_INDEPENDENT` | 1 | Independent | per-qubit channels in q0/q1_noise | +| `NOISE_TWO_CORRELATED_PAULI` | 2 | CorrelatedPauli | probability p + `NOISE_PAULI_OP_*` operators | + +Pauli operator tags for the correlated-Pauli channel (`CTwoQubitNoise.op_q0` / `op_q1`): `NOISE_PAULI_OP_I` = 0, `NOISE_PAULI_OP_X` = 1, `NOISE_PAULI_OP_Y` = 2, `NOISE_PAULI_OP_Z` = 3. + +--- + +## Snapshot structs + +### CSingleQubitNoise + +Snapshot of a single-qubit noise channel, written by `noise_model_get_single_qubit_errors`. `tag` selects the channel; `p` carries the parameter for tags 0-4 and `px`/`py`/`pz` the Pauli probabilities for tag 5; fields that do not apply to the tagged channel are zero. + +| Field | Type | Meaning | +| --- | --- | --- | +| `tag` | `uint8_t` | One of the single-qubit `NOISE_*` tags (0-5). | +| `p` | `double` | Channel probability for tags 0-4. | +| `px` | `double` | Pauli-X probability for tag 5. | +| `py` | `double` | Pauli-Y probability for tag 5. | +| `pz` | `double` | Pauli-Z probability for tag 5. | + +### CTwoQubitNoise + +Snapshot of a two-qubit noise channel, written by `noise_model_get_two_qubit_errors`. Fields that do not apply to the tagged channel are zero. + +| Field | Type | Meaning | +| --- | --- | --- | +| `tag` | `uint8_t` | One of the `NOISE_TWO_*` tags. | +| `p` | `double` | Channel probability for the depolarizing and correlated-Pauli tags. | +| `q0_noise` | `struct CSingleQubitNoise` | Channel applied to q0 for the independent tag. | +| `q1_noise` | `struct CSingleQubitNoise` | Channel applied to q1 for the independent tag. | +| `op_q0` | `uint8_t` | Pauli operator applied to q0 for the correlated tag (`NOISE_PAULI_OP_*`). | +| `op_q1` | `uint8_t` | Pauli operator applied to q1 for the correlated tag (`NOISE_PAULI_OP_*`). | + +--- + +## Construction and release + +### noise_model_new() + +Creates a new empty noise model. + +Returns: a newly allocated `CNoiseModel*`. + +### noise_model_free(ptr) + +Frees a `CNoiseModel`; NULL is allowed. + +--- + +## Adding channels + +### noise_model_add_single_qubit(ptr, gate_name, qubit, noise_type, p) + +Adds a single-qubit noise channel to the standard gate `gate_name` acting on `qubit`. + +Parameters: + +- `gate_name` (`const char*`): standard-gate name. +- `qubit` (`uint32_t`): qubit ID. +- `noise_type` (`uint8_t`): one of the `NOISE_*` single-qubit tags (0-4). +- `p` (`double`): channel parameter. + +Returns: 0 on success; -1 on NULL; -4 for an unknown gate name or invalid UTF-8; -8 for an unknown noise tag or invalid probabilities. + +### noise_model_add_single_qubit_pauli(ptr, gate_name, qubit, px, py, pz) + +Adds a general single-qubit Pauli channel to the standard gate `gate_name` acting on `qubit`, with X/Y/Z probabilities `px`, `py`, `pz` (each >= 0, sum <= 1). + +Parameters: + +- `px` / `py` / `pz` (`double`): Pauli-X/Y/Z probabilities. + +Returns: 0 on success; -1 on NULL; -4 for an unknown gate name; -8 for invalid probabilities. + +### noise_model_add_two_qubit(ptr, gate_name, q0, q1, noise_type, p) + +Adds a two-qubit depolarizing channel to the standard gate `gate_name` acting on the ordered pair `(q0, q1)`. `noise_type` accepts only `NOISE_TWO_DEPOLARIZING` (0). + +Parameters: + +- `q0` / `q1` (`uint32_t`): qubit IDs. +- `noise_type` (`uint8_t`): `NOISE_TWO_DEPOLARIZING`. +- `p` (`double`): depolarizing parameter. + +Returns: 0 on success; -1 on NULL; -4 for an unknown gate name; -8 when the qubits collide or the probability is invalid. + +### noise_model_add_readout(ptr, qubit, p_0_given_1, p_1_given_0) + +Adds an asymmetric readout error for `qubit`. + +Parameters: + +- `qubit` (`uint32_t`): qubit ID. +- `p_0_given_1` (`double`): P(measure 0 | true 1). +- `p_1_given_0` (`double`): P(measure 1 | true 0). + +Returns: 0 on success; -8 for invalid probabilities. + +--- + +## Querying channels + +### noise_model_get_single_qubit_errors_len(ptr, gate_name, qubit) + +Returns the number of single-qubit noise channels recorded for the standard gate `gate_name` on `qubit`. + +Returns: the total count; 0 for NULL, an unknown gate name, or absent entries. + +### noise_model_get_single_qubit_errors(ptr, gate_name, qubit, out, len) + +Two-step output of those channels, paired with `noise_model_get_single_qubit_errors_len`; each entry is a `CSingleQubitNoise` snapshot whose valid fields depend on its `tag`. Returns the total count. + +### noise_model_get_two_qubit_errors_len(ptr, gate_name, q0, q1) + +Returns the number of two-qubit noise channels recorded for the standard gate `gate_name` on the ordered pair `(q0, q1)`. + +Returns: the total count; 0 for NULL, an unknown gate name, colliding qubits, or absent entries. + +### noise_model_get_two_qubit_errors(ptr, gate_name, q0, q1, out, len) + +Two-step output of those channels, paired with `noise_model_get_two_qubit_errors_len`; each entry is a `CTwoQubitNoise` snapshot whose valid fields depend on its `tag`. Returns the total count. + +### noise_model_get_readout_error(ptr, qubit, out_p_0_given_1, out_p_1_given_0) + +Writes the readout error recorded for `qubit` to the out parameters. + +Parameters: + +- `out_p_0_given_1` (`double*`): receives P(measure 0 | true 1). +- `out_p_1_given_0` (`double*`): receives P(measure 1 | true 0). + +Returns: 0 on success; -1 on NULL; -8 when no readout error is recorded for the qubit. + +--- + +## Channel validity and Kraus operators + +The entry points below address one recorded single-qubit channel by its `(gate_name, qubit, index)` key; `index` is bounded by `noise_model_get_single_qubit_errors_len`. Kraus operators are transferred as `Complex64` re/im pairs (a `struct Complex64` with `re` / `im` fields, defined in `cqlib_c.h`). + +### noise_model_is_valid(ptr, gate_name, qubit, index) + +Reports whether the single-qubit channel recorded for the standard gate `gate_name` on `qubit` at `index` carries valid noise parameters. + +Parameters: + +- `gate_name` (`const char*`): standard-gate name. +- `qubit` (`uint32_t`): qubit ID. +- `index` (`uintptr_t`): channel index. + +Returns: 1 when valid; 0 when not; -1 on NULL; -4 for an unknown gate name; -8 when no channel exists at that key/index. + +### noise_model_to_kraus_len(ptr, gate_name, qubit, index) + +Returns the total number of `Complex64` elements across all Kraus operators of the channel; each operator contributes `dim * dim` elements (a single-qubit operator is `2 * 2 = 4`). + +Returns: the total element count; 0 for NULL, an unknown gate name, or an absent key/index. + +### noise_model_to_kraus(ptr, gate_name, qubit, index, out, len) + +Two-step output of the Kraus operators, paired with `noise_model_to_kraus_len`: the operators are written one after another, each in row-major order as `dim * dim` `Complex64` re/im pairs. + +Parameters: + +- `out` (`Complex64*`): receives the elements; NULL only queries the length. +- `len` (`uintptr_t`): buffer capacity in elements; a smaller value copies only the first `len` elements. + +Returns: the total element count; 0 for NULL, an unknown gate name, or an absent key/index. + +--- + +## Example + +```c +#include +#include "cqlib_c.h" + +int main(void) { + struct CNoiseModel *nm = noise_model_new(); + + /* single-qubit channel: bit flip on the X gate */ + int32_t rc = noise_model_add_single_qubit(nm, "X", 0, NOISE_BIT_FLIP, 0.005); + + /* single-qubit Pauli channel */ + rc = noise_model_add_single_qubit_pauli(nm, "X", 0, 0.001, 0.002, 0.003); + + /* two-qubit depolarizing channel: CX gate on (0, 1) */ + rc = noise_model_add_two_qubit(nm, "CX", 0, 1, NOISE_TWO_DEPOLARIZING, 0.02); + + /* readout error */ + rc = noise_model_add_readout(nm, 0, 0.02, 0.01); + + /* query the single-qubit channels */ + uintptr_t n = noise_model_get_single_qubit_errors_len(nm, "X", 0); /* 2 */ + struct CSingleQubitNoise chans[2]; + noise_model_get_single_qubit_errors(nm, "X", 0, chans, n); + /* chans[0]: tag == NOISE_BIT_FLIP, p == 0.005 + chans[1]: tag == NOISE_PAULI, px/py/pz == 0.001/0.002/0.003 */ + + /* query the two-qubit channels */ + uintptr_t m = noise_model_get_two_qubit_errors_len(nm, "CX", 0, 1); /* 1 */ + struct CTwoQubitNoise tchans[1]; + noise_model_get_two_qubit_errors(nm, "CX", 0, 1, tchans, m); + /* tchans[0]: tag == NOISE_TWO_DEPOLARIZING, p == 0.02 */ + + /* query the readout error */ + double p01 = 0.0, p10 = 0.0; + rc = noise_model_get_readout_error(nm, 0, &p01, &p10); /* 0, 0.02 / 0.01 */ + + /* channel validity and Kraus operators */ + rc = noise_model_add_single_qubit(nm, "H", 0, NOISE_DEPOLARIZING, 0.1); + int32_t valid = noise_model_is_valid(nm, "H", 0, 0); /* 1 */ + uintptr_t klen = noise_model_to_kraus_len(nm, "H", 0, 0); /* 16: four 2x2 operators */ + Complex64 kraus[16]; + noise_model_to_kraus(nm, "H", 0, 0, kraus, klen); + /* kraus[0..4]: I*sqrt(0.9) row-major (kraus[0].re == sqrt(0.9)) + kraus[4..16]: X, Y, Z scaled by sqrt(0.1/3) */ + + /* amplitude damping on X@0 at index 2 (after bit flip and Pauli) */ + rc = noise_model_add_single_qubit(nm, "X", 0, NOISE_AMPLITUDE_DAMPING, 0.25); + klen = noise_model_to_kraus_len(nm, "X", 0, 2); /* 8: two 2x2 operators */ + + /* error codes */ + int32_t absent = noise_model_is_valid(nm, "H", 1, 0); /* -8: no channel at that key */ + int32_t unknown = noise_model_is_valid(nm, "nope", 0, 0); /* -4: unknown gate */ + + noise_model_free(nm); + return 0; +} +``` diff --git a/docs/api/en/c/2_device/5_result.md b/docs/api/en/c/2_device/5_result.md new file mode 100644 index 00000000..2fd6b2c2 --- /dev/null +++ b/docs/api/en/c/2_device/5_result.md @@ -0,0 +1,304 @@ +# ExecutionResult (C) + +`CExecutionResult` is the opaque handle to a task execution result, recording one task's lifecycle, measurement counts, and probability distribution; `CCountsList` is a bitstring-to-weight key/value list. Error codes and the conventions for freeing strings and handles follow the [Overview](../0_overview.md). + +Entry `i` of the `qubits` array corresponds to bit `i` of the outcome keys; bitstrings are written with the most significant bit on the left, the reverse of the `qubits` order. + +--- + +## Status constants + +Tags written by `execution_result_status`: + +| Constant | Value | Meaning | +| --- | --- | --- | +| `EXECUTION_STATUS_QUEUED` | 0 | Task submitted and queued. | +| `EXECUTION_STATUS_RUNNING` | 1 | Task currently running. | +| `EXECUTION_STATUS_COMPLETED` | 2 | Task completed successfully. | +| `EXECUTION_STATUS_FAILED` | 3 | Task failed (query the error code and message for details). | +| `EXECUTION_STATUS_CANCELLED` | 4 | Task cancelled by the user. | + +--- + +## Construction and release + +### execution_result_new(task_id, qubits, num_qubits, shots, backend) + +Creates an execution result in Queued status; the creation timestamp is the current time. + +Parameters: + +- `task_id` (`const char*`): task ID. +- `qubits` (`const uint32_t*`): array of measured-qubit IDs; entry `i` corresponds to bit `i` of the outcome keys. +- `num_qubits` (`uintptr_t`): number of measured qubits. +- `shots` (`uintptr_t`): shot count. +- `backend` (`const char*`): backend name; may be NULL. + +Returns: a newly allocated `CExecutionResult*` on success; NULL on error. + +### execution_result_from_counts(task_id, num_qubits, shots, bitstrings, counts, num_entries) + +Creates a completed (Completed) result directly from measurement counts; the measured qubits are `0..num_qubits-1`. + +Parameters: + +- `task_id` (`const char*`): task ID. +- `num_qubits` (`uintptr_t`): number of measured qubits; each bitstring must have exactly `num_qubits` characters. +- `shots` (`uintptr_t`): shot count. +- `bitstrings` (`const char *const*`): array of bitstrings, parallel to `counts`. +- `counts` (`const uint64_t*`): array of counts, parallel to `bitstrings`. +- `num_entries` (`uintptr_t`): number of entries. + +Returns: a newly allocated `CExecutionResult*` on success; NULL on error. + +### execution_result_free(ptr) + +Frees a `CExecutionResult`; NULL is allowed. + +--- + +## Lifecycle + +### execution_result_start(ptr) + +Marks the job as running and records the current time as the start timestamp. + +Returns: 0 on success; -1 on NULL. + +### execution_result_finish(ptr, bitstrings, counts, num_entries) + +Marks the job as completed and replaces the measurement counts; `bitstrings` and `counts` are parallel arrays, repeated bitstrings accumulate, and the finish timestamp is the current time. + +Parameters: + +- `bitstrings` (`const char *const*`): array of bitstrings. +- `counts` (`const uint64_t*`): array of counts. +- `num_entries` (`uintptr_t`): number of entries. + +Returns: 0 on success; -1 on NULL; -4 for an invalid bitstring (length or characters). + +### execution_result_fail(ptr, msg, code) + +Marks the job as failed and records an error message and code. + +Parameters: + +- `msg` (`const char*`): error message. +- `code` (`int32_t`): error code. + +Returns: 0 on success; -1 on NULL; -4 when the message is not valid UTF-8. + +### execution_result_cancel(ptr) + +Marks the job as cancelled. + +Returns: 0 on success; -1 on NULL. + +--- + +## Status and timestamps + +### execution_result_status(ptr, out_tag, out_error_code) + +Writes the execution status tag (one of the `EXECUTION_STATUS_*` constants). When the status is failed, `*out_error_code` receives the stored error code (0 otherwise); `out_error_code` may be NULL when not needed. + +Parameters: + +- `out_tag` (`uint8_t*`): receives the status tag. +- `out_error_code` (`int32_t*`): receives the error code; may be NULL. + +Returns: 0 on success; -1 on NULL. + +### execution_result_is_success(ptr) + +Returns whether the execution completed successfully. + +Returns: 1 when successful; 0 otherwise; -1 on NULL. + +### execution_result_is_terminal(ptr) + +Returns whether the execution reached a terminal state (completed, failed, or cancelled); Queued and Running are not terminal. + +Returns: 1 when terminal; 0 otherwise; -1 on NULL. + +### execution_result_error_message(ptr) + +Returns the error message stored by the failed status. + +Returns: a heap-allocated C string on success (free with `cqlib_string_free`); NULL on NULL input or when the status is not failed. + +### execution_result_task_id(ptr) + +Returns the task ID. + +Returns: a heap-allocated C string on success (free with `cqlib_string_free`); NULL on error. + +### execution_result_backend(ptr) + +Returns the backend name. + +Returns: a heap-allocated C string on success (free with `cqlib_string_free`); NULL on error or when no backend is set. + +### execution_result_created_at(ptr, out_ms) + +Writes the task creation timestamp in milliseconds since the Unix epoch. + +Returns: 0 with the value in `*out_ms`; -1 on NULL. + +### execution_result_started_at(ptr, out_ms) + +Writes the execution start timestamp in milliseconds since the Unix epoch. + +Returns: 0 with the value in `*out_ms`; -8 when the job has not started. + +### execution_result_finished_at(ptr, out_ms) + +Writes the execution finish timestamp in milliseconds since the Unix epoch. + +Returns: 0 with the value in `*out_ms`; -8 when the job has not finished. + +--- + +## Measurement results + +### execution_result_shots(ptr) + +Returns the number of shots; 0 for NULL. + +### execution_result_num_qubits(ptr) + +Returns the number of measured qubits; 0 for NULL. + +### execution_result_qubits_len(ptr) / execution_result_qubits(ptr, out, len) + +Two-step output of the measured-qubit IDs; entry `i` corresponds to bit `i` of the outcome keys. The fill function returns the total count. + +### execution_result_to_bitstring(ptr, outcome) + +Formats the raw outcome bitmask as a big-endian bitstring over the result's measured-qubit width: bit `i` of `outcome` is measured qubit `i`, and high bits beyond the width are truncated. + +Parameters: + +- `outcome` (`uint64_t`): raw outcome bitmask. + +Returns: a heap-allocated C string on success (free with `cqlib_string_free`); NULL on NULL input. + +### execution_result_counts(ptr) + +Returns the measurement counts. + +Returns: a newly allocated `CCountsList*` on success (bitstring keys, integer counts as f64 weights), freed with `counts_list_free`; NULL on error. + +### execution_result_probabilities(ptr) + +Returns the probability distribution (computed first if needed, by normalizing the counts). + +Returns: a newly allocated `CCountsList*` on success (bitstring keys, f64 probability weights), freed with `counts_list_free`; NULL on error. + +### execution_result_calc_probabilities_len(ptr) / execution_result_calc_probabilities(ptr, out, len) + +Two-step probability distribution: `calc_probabilities_len` computes the distribution (cached on the result) and returns the number of entries; `calc_probabilities` recomputes it and copies the probabilities into `out`. Entries are sorted ascending by outcome value, where bit `i` of the outcome value is measured qubit `i` (matching `execution_result_to_bitstring`). Both take a mutable handle; a smaller `len` copies only the first `len` entries. The fill function returns the total number of entries; 0 for NULL. + +--- + +## CCountsList + +The return value of `execution_result_counts` and `execution_result_probabilities`: keys are measurement-outcome bitstrings; the former carries counts as weights, the latter probabilities. + +### counts_list_len(ptr) + +Returns the number of entries in the list; 0 for NULL. + +### counts_list_get_key(ptr, index) + +Returns the bitstring key at `index`. + +Returns: a heap-allocated C string on success (free with `cqlib_string_free`); NULL on out-of-bounds. + +### counts_list_get_value(ptr, index) + +Returns the weight (count or probability) at `index`; NaN on error. + +### counts_list_free(ptr) + +Frees a `CCountsList`; NULL is allowed. + +--- + +## Example + +```c +#include +#include "cqlib_c.h" + +int main(void) { + /* path one: build a completed result directly from counts */ + const char *bits[2] = {"00", "11"}; + uint64_t cnts[2] = {750, 250}; + + struct CExecutionResult *r = execution_result_from_counts( + "task-1", 2, 1000, bits, cnts, 2); + if (r == NULL) { + return 1; + } + + uint8_t status = 255; + int32_t err_code = 0; + execution_result_status(r, &status, &err_code); /* EXECUTION_STATUS_COMPLETED */ + + struct CCountsList *counts = execution_result_counts(r); + if (counts != NULL) { + for (uintptr_t i = 0; i < counts_list_len(counts); i++) { + char *key = counts_list_get_key(counts, i); + double w = counts_list_get_value(counts, i); + printf("%s: %f\n", key, w); /* "00": 750, "11": 250 */ + cqlib_string_free(key); + } + counts_list_free(counts); + } + + struct CCountsList *probs = execution_result_probabilities(r); + /* same traversal: weights are 0.75 / 0.25 */ + counts_list_free(probs); + + /* status flags, bitstrings, and the two-step probability buffer */ + int32_t ok = execution_result_is_success(r); /* 1 */ + int32_t done = execution_result_is_terminal(r); /* 1 */ + + char *bs = execution_result_to_bitstring(r, 0b01); /* "01" */ + char *bs2 = execution_result_to_bitstring(r, 0b101); /* "11": bit 2 is beyond the width, truncated */ + cqlib_string_free(bs); + cqlib_string_free(bs2); + + uintptr_t n_probs = execution_result_calc_probabilities_len(r); /* 2 */ + double pvals[2]; + execution_result_calc_probabilities(r, pvals, n_probs); + /* ascending outcome order: pvals[0] == 0.75 ("00"), pvals[1] == 0.25 ("11") */ + + execution_result_free(r); + + /* path two: the full lifecycle */ + uint32_t qubits[2] = {0, 1}; + struct CExecutionResult *r2 = execution_result_new( + "task-2", qubits, 2, 1000, "sim"); + if (r2 == NULL) { + return 1; + } + + execution_result_start(r2); /* Queued -> Running */ + execution_result_finish(r2, bits, cnts, 2); /* Running -> Completed */ + + char *task = execution_result_task_id(r2); /* "task-2" */ + char *backend = execution_result_backend(r2); /* "sim" */ + cqlib_string_free(task); + cqlib_string_free(backend); + + int64_t created = 0, started = 0, finished = 0; + execution_result_created_at(r2, &created); + execution_result_started_at(r2, &started); + execution_result_finished_at(r2, &finished); + + execution_result_free(r2); + return 0; +} +``` diff --git a/docs/api/en/c/2_device/6_qubits.md b/docs/api/en/c/2_device/6_qubits.md new file mode 100644 index 00000000..ca3ecccd --- /dev/null +++ b/docs/api/en/c/2_device/6_qubits.md @@ -0,0 +1,214 @@ +# Qubit Identifiers (C) + +This page covers the C ABI of the strongly typed device-facing qubit identifiers `CLogicalQubit` and `CPhysicalQubit`: the former marks a circuit wire, the latter a hardware position on a device. Both types share the same numeric representation (a `uint32_t` ID) but stay distinct at the type level; each converts only to and from `CQubit`, the two never convert into each other directly, and equal numeric IDs do not make them the same identifier. Error-code and string-release conventions follow the [Overview](../0_overview.md). + +--- + +## Structs + +Both types are passed by value and need no release: + +```c +typedef struct CLogicalQubit { + uint32_t id; +} CLogicalQubit; + +typedef struct CPhysicalQubit { + uint32_t id; +} CPhysicalQubit; +``` + +| Field | Type | Meaning | +| --- | --- | --- | +| `id` | `uint32_t` | Numeric identifier. | + +--- + +## CLogicalQubit + +Logical qubit identifier used for circuit wires. + +### logical_qubit_new(id) + +```c +struct CLogicalQubit logical_qubit_new(uint32_t id); +``` + +Creates the identifier from its numeric ID, returned by value. + +- `id` (`uint32_t`): numeric identifier. + +### logical_qubit_from_qubit(qubit) + +```c +struct CLogicalQubit logical_qubit_from_qubit(struct CQubit qubit); +``` + +Wraps an existing circuit qubit as a logical identifier. + +- `qubit` (`struct CQubit`): circuit qubit identifier. + +### logical_qubit_qubit(qubit) + +```c +struct CQubit logical_qubit_qubit(struct CLogicalQubit qubit); +``` + +Returns the underlying circuit qubit identifier. + +- `qubit` (`struct CLogicalQubit`): logical identifier. + +### logical_qubit_id(qubit) + +```c +uint32_t logical_qubit_id(struct CLogicalQubit qubit); +``` + +Returns the numeric identifier. + +- `qubit` (`struct CLogicalQubit`): logical identifier. + +### logical_qubit_equal(a, b) / logical_qubit_compare(a, b) + +```c +int32_t logical_qubit_equal(struct CLogicalQubit a, struct CLogicalQubit b); +int32_t logical_qubit_compare(struct CLogicalQubit a, struct CLogicalQubit b); +``` + +Comparison and ordering use the numeric identifier: `equal` returns `1` (same) or `0` (different); `compare` returns `-1` (`a < b`), `0` (equal) or `1` (`a > b`). + +- `a`, `b` (`struct CLogicalQubit`): identifiers to compare. + +### logical_qubit_to_string(qubit) + +```c +char *logical_qubit_to_string(struct CLogicalQubit qubit); +``` + +Formats the identifier as `L` (e.g. `L3`). Release the returned string with [cqlib_string_free](../0_overview.md); returns NULL on allocation failure. + +- `qubit` (`struct CLogicalQubit`): logical identifier. + +--- + +## CPhysicalQubit + +Physical qubit identifier used for hardware positions. + +### physical_qubit_new(id) + +```c +struct CPhysicalQubit physical_qubit_new(uint32_t id); +``` + +Creates the identifier from its numeric ID, returned by value. + +- `id` (`uint32_t`): numeric identifier. + +### physical_qubit_from_qubit(qubit) + +```c +struct CPhysicalQubit physical_qubit_from_qubit(struct CQubit qubit); +``` + +Wraps an existing qubit-shaped identifier as a physical hardware ID. + +- `qubit` (`struct CQubit`): qubit-shaped identifier. + +### physical_qubit_qubit(qubit) + +```c +struct CQubit physical_qubit_qubit(struct CPhysicalQubit qubit); +``` + +Returns the underlying qubit-shaped identifier. + +- `qubit` (`struct CPhysicalQubit`): physical identifier. + +### physical_qubit_id(qubit) + +```c +uint32_t physical_qubit_id(struct CPhysicalQubit qubit); +``` + +Returns the numeric identifier. + +- `qubit` (`struct CPhysicalQubit`): physical identifier. + +### physical_qubit_equal(a, b) / physical_qubit_compare(a, b) + +```c +int32_t physical_qubit_equal(struct CPhysicalQubit a, struct CPhysicalQubit b); +int32_t physical_qubit_compare(struct CPhysicalQubit a, struct CPhysicalQubit b); +``` + +Comparison and ordering use the numeric identifier: `equal` returns `1` (same) or `0` (different); `compare` returns `-1` (`a < b`), `0` (equal) or `1` (`a > b`). + +- `a`, `b` (`struct CPhysicalQubit`): identifiers to compare. + +### physical_qubit_to_string(qubit) + +```c +char *physical_qubit_to_string(struct CPhysicalQubit qubit); +``` + +Formats the identifier as `P` (e.g. `P11`). Release the returned string with [cqlib_string_free](../0_overview.md); returns NULL on allocation failure. + +- `qubit` (`struct CPhysicalQubit`): physical identifier. + +--- + +## Error codes + +None of the interfaces on this page return error codes: constructors, accessors and comparisons never fail; `*_to_string` returns NULL only on allocation failure. + +--- + +## Example + +```c +#include +#include +#include +#include + +int main(void) { + /* A circuit qubit wraps as a logical identifier and unwraps back */ + CLogicalQubit logical = logical_qubit_from_qubit(qubit_new(3)); + assert(logical_qubit_id(logical) == 3u); + assert(qubit_id(logical_qubit_qubit(logical)) == 3u); + + /* Physical identifiers are constructed independently */ + CPhysicalQubit physical = physical_qubit_new(100); + assert(physical_qubit_id(physical) == 100u); + + /* Comparison */ + assert(logical_qubit_compare(logical_qubit_new(0), logical_qubit_new(2)) == -1); + assert(physical_qubit_compare(physical, physical_qubit_new(101)) == -1); + + /* String representation */ + char *ltext = logical_qubit_to_string(logical); + assert(strcmp(ltext, "L3") == 0); + cqlib_string_free(ltext); + + char *ptext = physical_qubit_to_string(physical); + assert(strcmp(ptext, "P100") == 0); + cqlib_string_free(ptext); + + /* Layout scenario: register and query the logical 1 -> physical 102 mapping */ + const uint32_t pairs[2] = {1u, 102u}; + CLayout *layout = layout_from_pairs(pairs, 1, 3); + assert(layout != NULL); + assert(layout_get(layout, 1) == physical_qubit_id(physical_qubit_new(102))); + layout_free(layout); + return 0; +} +``` + +--- + +## Related pages + +- [Qubit](../0_circuit/2_qubit.md): the `CQubit` circuit identifier, the conversion hub for both types. +- [Layout](3_layout.md): binding and querying the logical-to-physical mapping. +- [Overview](../0_overview.md): error codes, memory ownership and global conventions. diff --git a/docs/api/en/c/3_qis/0_overview.md b/docs/api/en/c/3_qis/0_overview.md new file mode 100644 index 00000000..cc94ee7b --- /dev/null +++ b/docs/api/en/c/3_qis/0_overview.md @@ -0,0 +1,86 @@ +# QIS (C) + +The C binding's quantum information interface provides local quantum simulation through opaque handles (`CStatevector`, `CDensityMatrix`, `CDensityMatrixNoise`, `CStabilizerState`, and others) plus free functions: state construction and evolution, gate operations, probabilities and measurement, expectation values, and noise simulation. Error codes and the conventions for freeing handles and strings follow the [Overview](../0_overview.md). + +--- + +## The three dense simulators + +| Simulator | State representation | Data size | Noise support | Readout noise | Typical use | +| --- | --- | --- | --- | --- | --- | +| [Statevector](1_statevector.md) | Pure state | `2^N` complex amplitudes | None | None | Ideal universal circuits, minimal memory | +| [DensityMatrix](2_density_matrix.md) | Mixed state | `4^N` matrix elements | Kraus-operator channels | None | Mixed states, quantum channels, partial trace | +| [DensityMatrixNoise](3_density_matrix_noise.md) | Mixed state | `4^N` matrix elements | Full noise model (gate noise applied automatically) | Yes | Device-level noisy simulation | + +The three simulators share the same set of gate functions, named `_apply_`: + +```c +statevector_apply_h(ptr, qubit); +density_matrix_apply_h(ptr, qubit); +density_matrix_noise_apply_h(ptr, qubit); +``` + +The gate set covers: non-parameterized single-qubit gates H, X, Y, Z, S, S†, T, T†, X2P, X2M, Y2P, Y2M; parameterized single-qubit gates Rx, Ry, Rz, P, U, XY, XY2P, XY2M, RXY, GPhase; two-qubit gates CX, CY, CZ, SWAP, RXX, RYY, RZZ, RZX, CrX, CrY, CrZ, FSIM; and the three-qubit gate CCX. Each simulator also provides a name-dispatched `apply_standard_gate` family and the whole-circuit entry point `apply_circuit`; Statevector and DensityMatrixNoise additionally provide the arbitrary-unitary entry point `apply_unitary_gate`. + +All three simulators interface with circuit handles: `*_from_circuit` executes a circuit and returns the evolved state; `*_apply_circuit` applies a circuit in place to an existing state, and the circuit's qubit count must match the state. See [Circuit](../0_circuit/1_circuit.md) for the circuit handle. + +--- + +## Shared conventions + +- **Handle lifetime**: constructors such as `*_new`, `*_from_circuit`, `*_from_state`, and `*_maximally_mixed` return heap-allocated handles released with the matching `*_free`; every `*_free` accepts NULL; constructors return `NULL` on failure. +- **Error codes**: functions returning `int32_t` use `0` for success and negative values for errors; the measurement entry points `*_measure` use `0`/`1` for the outcomes `|0>`/`|1>`, with negative values still indicating errors. Typical QIS error codes: `-1` (NULL pointer), `-2` (qubit index out of bounds), `-3` (circuit error), `-7` (unsupported operation), `-8` (invalid parameter: non-finite angle, buffer-length mismatch, non-normalized input state, qubit-count mismatch, unknown gate name). +- **Two-step array output**: for probabilities and matrix data, first call the `*_len` function for the element count, allocate a buffer, then call the fill function (e.g. `statevector_probabilities_len` + `statevector_probabilities`, `density_matrix_data_len` + `density_matrix_data`); when `len` does not match the actual count the fill function returns `-8`. +- **Strings**: `*_measure_all` returns a big-endian bitstring (MSB = qubit `N-1`, LSB = qubit `0`); release it with `cqlib_string_free`. +- **Complex64**: `{ double re; double im; }`; amplitudes and matrix elements are stored row-major with real and imaginary parts interleaved. +- **Sampling results**: `*_sample_shots` returns a `COutcomeList*` whose entries are big-endian bitstring `char*` values; use `outcome_list_len` for the count, `outcome_list_get` to fetch an entry (released with `cqlib_string_free`), and `outcome_list_free` to free the list — see [ClassicalState](5_classical_state.md). +- **Observables**: the expectation-value functions accept a `CHamiltonian*` handle; see [Hamiltonian](7_hamiltonian.md) for construction and [Pauli](6_pauli.md) for the Pauli-string components. + +--- + +## Pages + +| Page | Contents | +| --- | --- | +| [Statevector](1_statevector.md) | Pure-state simulator: amplitude access, gates, probabilities, measurement, sampling, and expectation values. | +| [DensityMatrix](2_density_matrix.md) | Mixed-state simulator: matrix data, physicality checks, Kraus channels, partial trace, and expectation values. | +| [DensityMatrixNoise](3_density_matrix_noise.md) | Noisy simulator: automatic gate noise and readout-error probability distributions. | +| [StabilizerState](4_stabilizer.md) | Stabilizer-state simulator for large Clifford circuits. | +| [ClassicalState](5_classical_state.md) | Runtime classical data and `COutcomeList`. | +| [Pauli](6_pauli.md) | Pauli operators, Pauli strings, and their matrix representation. | +| [Hamiltonian](7_hamiltonian.md) | Hamiltonians and observables. | +| [Evolution](8_evolution.md) | The Pauli-string evolution gate `circuit_pauli_evolution`. | +| [Metrics / Entropy](9_metrics_entropy.md) | Fidelity, purity, trace distance, entropy, and entanglement measures. | + +--- + +## Example + +```c +#include +#include "cqlib_c.h" + +int main(void) { + /* Run the same Bell circuit on both simulators */ + struct CStatevector *sv = statevector_new(2); + struct CDensityMatrix *dm = density_matrix_new(2); + + statevector_apply_h(sv, 0); + statevector_apply_cx(sv, 0, 1); + density_matrix_apply_h(dm, 0); + density_matrix_apply_cx(dm, 0, 1); + + /* Pure and mixed representations agree: P(00) = P(11) = 0.5 */ + double p_sv[4], p_dm[4]; + statevector_probabilities(sv, p_sv, 4); + density_matrix_probabilities(dm, p_dm, 4); + printf("P(00): sv=%.3f dm=%.3f\n", p_sv[0], p_dm[0]); + printf("P(11): sv=%.3f dm=%.3f\n", p_sv[3], p_dm[3]); + + statevector_free(sv); + density_matrix_free(dm); + return 0; +} +``` + +For constructing and populating the `CNoiseModel` handle, see [NoiseModel](../2_device/4_noise.md). diff --git a/docs/api/en/c/3_qis/1_statevector.md b/docs/api/en/c/3_qis/1_statevector.md new file mode 100644 index 00000000..9bc3e97c --- /dev/null +++ b/docs/api/en/c/3_qis/1_statevector.md @@ -0,0 +1,433 @@ +# Statevector + +The `CStatevector*` handle is the pure-state simulator: the quantum state is represented as `2^N` complex amplitudes, and every operation goes through the `statevector_*` free functions. This page covers construction and circuit ingestion, amplitude access, gate operations, probabilities and measurement, and expectation values. Error codes and memory conventions are described in [Overview](../0_overview.md); for the module summary see [QIS Overview](0_overview.md). + +--- + +## Construction and Release + +### statevector_new(num_qubits) + +```c +struct CStatevector *statevector_new(uintptr_t num_qubits); +``` + +Creates a pure-state simulator initialized to `|0...0>`: the first amplitude is 1, all others are 0. + +- `num_qubits` (`uintptr_t`): the number of qubits. + +Returns: a newly allocated `CStatevector*`; free it with `statevector_free`. + +### statevector_from_circuit(circuit) + +```c +struct CStatevector *statevector_from_circuit(const struct CCircuit *circuit); +``` + +Executes the circuit and returns the evolved state; the input circuit is not modified. The circuit is decomposed into basic gates and executed instruction by instruction; measurement declarations do not take part in the state evolution. + +- `circuit` (`const CCircuit*`): circuit handle, see [Circuit](../0_circuit/1_circuit.md). + +Returns: a newly allocated `CStatevector*`; `NULL` when `circuit` is NULL or execution fails. + +### statevector_from_state(num_qubits, initial_state, len) + +```c +struct CStatevector *statevector_from_state(uintptr_t num_qubits, + const Complex64 *initial_state, + uintptr_t len); +``` + +Creates a state from the given amplitude array: `len` must equal `2^num_qubits` and the array must be normalized. Entry `i` is the coefficient of the computational basis state `|i>`, with qubit 0 as the least significant bit. + +- `num_qubits` (`uintptr_t`): the number of qubits. +- `initial_state` (`const Complex64*`): amplitude array of length `2^num_qubits`. +- `len` (`uintptr_t`): array length. + +Returns: a newly allocated `CStatevector*`; `NULL` on NULL input, wrong dimension, or a non-normalized state. + +### statevector_free(ptr) + +```c +void statevector_free(struct CStatevector *ptr); +``` + +Frees the handle. Passing NULL is allowed. + +- `ptr` (`CStatevector*`): statevector handle. + +### statevector_num_qubits(ptr) + +```c +uintptr_t statevector_num_qubits(const struct CStatevector *ptr); +``` + +Returns the number of qubits. + +- `ptr` (`const CStatevector*`): statevector handle. + +Returns: the qubit count; 0 for NULL. + +### statevector_apply_circuit(ptr, circuit) + +```c +int32_t statevector_apply_circuit(struct CStatevector *ptr, const struct CCircuit *circuit); +``` + +Applies the circuit in place to the current state; the circuit's qubit count must match the state. + +- `ptr` (`CStatevector*`): statevector handle. +- `circuit` (`const CCircuit*`): circuit handle. + +Returns: `0` on success; `-1` NULL pointer; `-3` circuit error (a gate without a matrix representation, unresolved symbolic parameters, etc.); `-7` the circuit contains an unsupported operation; `-8` the circuit's qubit count does not match the state. + +--- + +## Amplitude Access + +### statevector_data_len(ptr) + +```c +uintptr_t statevector_data_len(const struct CStatevector *ptr); +``` + +Returns the number of complex amplitudes (`2^N`), for sizing the buffer passed to `statevector_data`. + +- `ptr` (`const CStatevector*`): statevector handle. + +Returns: the amplitude count; 0 for NULL. + +### statevector_data(ptr, buffer, len) + +```c +int32_t statevector_data(const struct CStatevector *ptr, Complex64 *buffer, uintptr_t len); +``` + +Copies all amplitudes row-major into `buffer` as `2^N` `Complex64` values. + +- `ptr` (`const CStatevector*`): statevector handle. +- `buffer` (`Complex64*`): output buffer. +- `len` (`uintptr_t`): buffer length; must equal `statevector_data_len`. + +Returns: `0` on success; `-1` NULL pointers; `-8` when `len` does not match the actual count. + +```c +uintptr_t n = statevector_data_len(sv); +Complex64 *amps = malloc(n * sizeof(Complex64)); +if (statevector_data(sv, amps, n) == 0) { + /* amps[i] is the amplitude of |i> */ +} +free(amps); +``` + +### statevector_data_mut(ptr, buffer, len) + +```c +int32_t statevector_data_mut(struct CStatevector *ptr, const Complex64 *buffer, uintptr_t len); +``` + +Overwrites the amplitudes by copying `len` `Complex64` values from `buffer` into the statevector; the writable counterpart of `statevector_data`. + +- `ptr` (`CStatevector*`): statevector handle. +- `buffer` (`const Complex64*`): input array of `2^N` amplitudes. +- `len` (`uintptr_t`): array length; must equal `statevector_data_len`. + +Returns: `0` on success; `-1` NULL pointers; `-8` when `len` does not match the actual count. + +Overwriting the amplitudes can break normalization; the caller is responsible for providing a valid statevector — the simulator keeps operating on the written data as-is. + +--- + +## Gate Operations + +All gate functions return `int32_t`: `0` on success; `-1` when the handle is NULL; `-2` when a qubit index is out of bounds; parameterized gates return `-8` when an angle is NaN or Inf. Single-qubit gates share the signature: + +```c +int32_t statevector_apply_h(struct CStatevector *ptr, uint32_t qubit); +``` + +### Non-parameterized single-qubit gates + +| Function | Gate | Description | +| --- | --- | --- | +| `statevector_apply_h(ptr, qubit)` | H | Hadamard gate. | +| `statevector_apply_x(ptr, qubit)` | X | Pauli-X, bit flip. | +| `statevector_apply_y(ptr, qubit)` | Y | Pauli-Y. | +| `statevector_apply_z(ptr, qubit)` | Z | Pauli-Z, phase flip. | +| `statevector_apply_s(ptr, qubit)` | S | S gate (√Z). | +| `statevector_apply_sdg(ptr, qubit)` | S† | Inverse of S. | +| `statevector_apply_t(ptr, qubit)` | T | T gate (√S). | +| `statevector_apply_tdg(ptr, qubit)` | T† | Inverse of T. | +| `statevector_apply_x2p(ptr, qubit)` | X2P | `Rx(π/2)`. | +| `statevector_apply_x2m(ptr, qubit)` | X2M | `Rx(-π/2)`. | +| `statevector_apply_y2p(ptr, qubit)` | Y2P | `Ry(π/2)`. | +| `statevector_apply_y2m(ptr, qubit)` | Y2M | `Ry(-π/2)`. | + +### Parameterized single-qubit gates + +| Function | Gate | Description | +| --- | --- | --- | +| `statevector_apply_rx(ptr, qubit, theta)` | Rx(θ) | Rotation by `θ` about the X axis. | +| `statevector_apply_ry(ptr, qubit, theta)` | Ry(θ) | Rotation by `θ` about the Y axis. | +| `statevector_apply_rz(ptr, qubit, theta)` | Rz(θ) | Rotation by `θ` about the Z axis. | +| `statevector_apply_phase(ptr, qubit, theta)` | P(θ) | Phase gate, multiplies the `\|1>` component by `e^(iθ)`. | +| `statevector_apply_u(ptr, qubit, theta, phi, lambda)` | U(θ, φ, λ) | Generic single-qubit gate. | +| `statevector_apply_xy(ptr, qubit, theta)` | XY(θ) | XY gate. | +| `statevector_apply_xy2p(ptr, qubit, theta)` | XY2P(θ) | XY2P gate. | +| `statevector_apply_xy2m(ptr, qubit, theta)` | XY2M(θ) | XY2M gate. | +| `statevector_apply_rxy(ptr, qubit, theta, phi)` | RXY(θ, φ) | Rotation by `θ` about the axis at azimuth `φ` in the XY plane. | +| `statevector_apply_gphase(ptr, phi)` | GPhase(φ) | Multiplies the whole state by the global phase `e^(iφ)`; the signature has no qubit parameter. | + +GPhase signature: + +```c +int32_t statevector_apply_gphase(struct CStatevector *ptr, double phi); +``` + +### Two-qubit gates + +Shared signature `int32_t statevector_apply_(struct CStatevector *ptr, uint32_t q0, uint32_t q1, ...)`; the two qubit parameters of controlled gates are named `control` / `target`. + +| Function | Gate | Description | +| --- | --- | --- | +| `statevector_apply_cx(ptr, control, target)` | CX | Controlled X. | +| `statevector_apply_cy(ptr, control, target)` | CY | Controlled Y. | +| `statevector_apply_cz(ptr, q0, q1)` | CZ | Controlled Z; the two parameters are symmetric. | +| `statevector_apply_swap(ptr, q0, q1)` | SWAP | Swaps two qubits. | +| `statevector_apply_rxx(ptr, q0, q1, theta)` | RXX(θ) | `exp(-i·θ/2·X⊗X)`. | +| `statevector_apply_ryy(ptr, q0, q1, theta)` | RYY(θ) | `exp(-i·θ/2·Y⊗Y)`. | +| `statevector_apply_rzz(ptr, q0, q1, theta)` | RZZ(θ) | `exp(-i·θ/2·Z⊗Z)`. | +| `statevector_apply_rzx(ptr, q0, q1, theta)` | RZX(θ) | `exp(-i·θ/2·Z⊗X)`. | +| `statevector_apply_crx(ptr, control, target, theta)` | CrX(θ) | Controlled `Rx(θ)`. | +| `statevector_apply_cry(ptr, control, target, theta)` | CrY(θ) | Controlled `Ry(θ)`. | +| `statevector_apply_crz(ptr, control, target, theta)` | CrZ(θ) | Controlled `Rz(θ)`. | +| `statevector_apply_fsim(ptr, q0, q1, theta, phi)` | FSIM | Fermionic simulation gate; `theta` is the iSWAP angle and `phi` the controlled-phase angle. | + +### Three-qubit gates + +| Function | Gate | Description | +| --- | --- | --- | +| `statevector_apply_ccx(ptr, c0, c1, target)` | CCX | Toffoli gate; flips the target when both controls are `\|1>`. | + +### Generic gate entry points + +#### statevector_apply_standard_gate(ptr, gate_name, qubits, num_target_qubits, params, num_params) + +```c +int32_t statevector_apply_standard_gate(struct CStatevector *ptr, + const char *gate_name, + const uint32_t *qubits, + uintptr_t num_target_qubits, + const double *params, + uintptr_t num_params); +``` + +Applies a standard gate by name (e.g. `"H"`, `"CX"`, `"RZZ"`, `"FSIM"`), dispatching to the dedicated implementation. + +- `ptr` (`CStatevector*`): statevector handle. +- `gate_name` (`const char*`): NUL-terminated standard-gate name. +- `qubits` (`const uint32_t*`): array of target qubits with length `num_target_qubits`. +- `num_target_qubits` (`uintptr_t`): number of target qubits; must match the gate's arity. +- `params` (`const double*`): gate-parameter array; may be NULL when `num_params` is 0. +- `num_params` (`uintptr_t`): number of parameters; must match the gate's arity. + +Returns: `0` on success; `-1` NULL pointers; `-2` out-of-bounds qubit; `-8` unknown gate name, gate name that is not valid UTF-8, qubit or parameter count that does not match the gate, or a non-finite parameter value. + +#### statevector_apply_unitary_gate(ptr, qubits, num_target_qubits, matrix, dim) + +```c +int32_t statevector_apply_unitary_gate(struct CStatevector *ptr, + const uint32_t *qubits, + uintptr_t num_target_qubits, + const Complex64 *matrix, + uintptr_t dim); +``` + +Applies an arbitrary unitary matrix to the given qubits. + +- `ptr` (`CStatevector*`): statevector handle. +- `qubits` (`const uint32_t*`): array of target qubits; indices must not repeat. +- `num_target_qubits` (`uintptr_t`): number of target qubits. +- `matrix` (`const Complex64*`): row-major `dim × dim` matrix. +- `dim` (`uintptr_t`): matrix dimension; must equal `2^num_target_qubits`. + +Returns: `0` on success; `-1` NULL pointers; `-2` out-of-bounds qubit; `-8` when `num_target_qubits` or `dim` is 0, the dimension does not match, or a qubit repeats. + +#### statevector_apply_pauli_rotation(ptr, pauli, theta) + +```c +int32_t statevector_apply_pauli_rotation(struct CStatevector *ptr, + const struct CPauliString *pauli, + double theta); +``` + +Applies the Pauli-string rotation `exp(-i·θ/2·P)` in place without gate decomposition. The Pauli string must span the full register and be Hermitian; see [Pauli](6_pauli.md) for construction. + +- `ptr` (`CStatevector*`): statevector handle. +- `pauli` (`const CPauliString*`): Pauli-string handle. +- `theta` (`double`): rotation angle. + +Returns: `0` on success; `-1` NULL pointers; `-8` when `theta` is not finite or the Pauli string's qubit count does not match the state. + +--- + +## Probabilities and Measurement + +### statevector_probabilities_len(ptr) + +```c +uintptr_t statevector_probabilities_len(const struct CStatevector *ptr); +``` + +Returns the length of the computational-basis probability distribution (`2^N`). + +- `ptr` (`const CStatevector*`): statevector handle. + +Returns: the probability count; 0 for NULL. + +### statevector_probabilities(ptr, buffer, len) + +```c +int32_t statevector_probabilities(const struct CStatevector *ptr, double *buffer, uintptr_t len); +``` + +Copies the measurement probabilities over all computational basis states (the squared modulus of each amplitude) into `buffer`. + +- `ptr` (`const CStatevector*`): statevector handle. +- `buffer` (`double*`): output buffer. +- `len` (`uintptr_t`): buffer length; must equal `statevector_probabilities_len`. + +Returns: `0` on success; `-1` NULL pointers; `-8` when `len` does not match the actual count. + +### statevector_measure(ptr, qubit) + +```c +int32_t statevector_measure(struct CStatevector *ptr, uint32_t qubit); +``` + +Measures the qubit in the Z basis and collapses the state; after the measurement the state is renormalized within the corresponding subspace. The operation is destructive. + +- `ptr` (`CStatevector*`): statevector handle. +- `qubit` (`uint32_t`): the measured qubit. + +Returns: `0` for outcome `|0>`, `1` for outcome `|1>`; `-1` NULL; `-2` out-of-bounds qubit. + +### statevector_measure_all(ptr) + +```c +char *statevector_measure_all(struct CStatevector *ptr); +``` + +Measures qubits in order `0..num_qubits`, collapsing the state, and returns the outcome as a big-endian bitstring (MSB = qubit `N-1`, LSB = qubit `0`). + +- `ptr` (`CStatevector*`): statevector handle. + +Returns: a heap-allocated C string, freed with `cqlib_string_free`; `NULL` on error. + +### statevector_sample_shots(ptr, shots) + +```c +struct COutcomeList *statevector_sample_shots(const struct CStatevector *ptr, uintptr_t shots); +``` + +Samples `shots` independent measurement outcomes in parallel from the current distribution without modifying the state. Use `outcome_list_len` for the count, `outcome_list_get` to fetch each big-endian bitstring (freed with `cqlib_string_free`), and `outcome_list_free` to free the list — see [ClassicalState](5_classical_state.md). + +- `ptr` (`const CStatevector*`): statevector handle. +- `shots` (`uintptr_t`): number of shots. + +Returns: a newly allocated `COutcomeList*`; `NULL` on error. + +### statevector_reset(ptr, qubit) + +```c +int32_t statevector_reset(struct CStatevector *ptr, uint32_t qubit); +``` + +Resets the qubit to `|0>`: performs a Z-basis measurement and applies an X correction when the outcome is `|1>`. + +- `ptr` (`CStatevector*`): statevector handle. +- `qubit` (`uint32_t`): target qubit. + +Returns: `0` on success; `-1` NULL; `-2` out-of-bounds qubit. + +--- + +## Expectation Values + +### statevector_expectation(ptr, observable, out) + +```c +int32_t statevector_expectation(const struct CStatevector *ptr, + const struct CHamiltonian *observable, + double *out); +``` + +Computes `⟨ψ|H|ψ⟩` and writes it to `out`. + +- `ptr` (`const CStatevector*`): statevector handle. +- `observable` (`const CHamiltonian*`): observable handle, see [Hamiltonian](7_hamiltonian.md). +- `out` (`double*`): output pointer. + +Returns: `0` on success; `-1` NULL pointers; `-8` when the observable's qubit count does not match the state. + +```c +/* of the Bell state equals 1 */ +struct CPauliString *zz = pauli_string_parse("ZZ"); +struct CHamiltonian *h = hamiltonian_from_pauli(zz); /* takes ownership of zz */ +double exp = 0.0; +statevector_expectation(sv, h, &exp); /* exp ≈ 1.0 */ +hamiltonian_free(h); +``` + +--- + +## Examples + +A complete workflow: prepare a Bell state, read probabilities, sample, compute an expectation value, and measure. + +```c +#include +#include +#include "cqlib_c.h" + +int main(void) { + /* Bell state (|00> + |11>)/sqrt(2) */ + struct CStatevector *sv = statevector_new(2); + if (sv == NULL) return 1; + statevector_apply_h(sv, 0); + statevector_apply_cx(sv, 0, 1); + + /* Probabilities: P(00) = P(11) = 0.5 */ + uintptr_t n = statevector_probabilities_len(sv); /* 4 */ + double *probs = malloc(n * sizeof(double)); + statevector_probabilities(sv, probs, n); + printf("P(00)=%.3f P(11)=%.3f\n", probs[0], probs[3]); + free(probs); + + /* Sample 100 shots: only "00" and "11" appear */ + struct COutcomeList *shots = statevector_sample_shots(sv, 100); + for (uintptr_t i = 0; i < outcome_list_len(shots); i++) { + char *bits = outcome_list_get(shots, i); + printf("%s ", bits); + cqlib_string_free(bits); + } + printf("\n"); + outcome_list_free(shots); + + /* Expectation = 1 */ + struct CPauliString *zz = pauli_string_parse("ZZ"); + struct CHamiltonian *h = hamiltonian_from_pauli(zz); + double exp = 0.0; + statevector_expectation(sv, h, &exp); + printf("=%.3f\n", exp); + hamiltonian_free(h); + + /* Destructive full measurement collapses the state */ + char *bits = statevector_measure_all(sv); + printf("measured: %s\n", bits); /* "00" or "11" */ + cqlib_string_free(bits); + + statevector_free(sv); + return 0; +} +``` diff --git a/docs/api/en/c/3_qis/2_density_matrix.md b/docs/api/en/c/3_qis/2_density_matrix.md new file mode 100644 index 00000000..d66548f6 --- /dev/null +++ b/docs/api/en/c/3_qis/2_density_matrix.md @@ -0,0 +1,520 @@ +# DensityMatrix + +The `CDensityMatrix*` handle is the mixed-state simulator: the quantum state is represented as a `2^N × 2^N` density matrix (`4^N` row-major matrix elements) and can describe pure states, mixed states, and the results of quantum channels. This page covers construction and circuit ingestion, matrix data and physicality checks, gate operations, quantum channels and the partial trace, probabilities and measurement, and expectation values. Error codes and memory conventions are described in [Overview](../0_overview.md); for the module summary see [QIS Overview](0_overview.md). + +--- + +## Construction and Release + +### density_matrix_new(num_qubits) + +```c +struct CDensityMatrix *density_matrix_new(uintptr_t num_qubits); +``` + +Creates a mixed-state simulator initialized to the pure state `|0...0><0...0|`. + +- `num_qubits` (`uintptr_t`): the number of qubits. + +Returns: a newly allocated `CDensityMatrix*`; free it with `density_matrix_free`. + +### density_matrix_maximally_mixed(num_qubits) + +```c +struct CDensityMatrix *density_matrix_maximally_mixed(uintptr_t num_qubits); +``` + +Creates the maximally mixed state `I / 2^N`: every computational basis state is equally probable with no coherences. + +- `num_qubits` (`uintptr_t`): the number of qubits. + +Returns: a newly allocated `CDensityMatrix*`; free it with `density_matrix_free`. + +### density_matrix_zeros(num_qubits) + +```c +struct CDensityMatrix *density_matrix_zeros(uintptr_t num_qubits); +``` + +Creates a density matrix filled entirely with zeros. This is not a valid physical state (trace = 0); it is useful as an accumulator during operations like Kraus channel application. + +- `num_qubits` (`uintptr_t`): the number of qubits. + +Returns: a newly allocated `CDensityMatrix*`; free it with `density_matrix_free`; `NULL` when the matrix size overflows the addressable range. + +### density_matrix_from_circuit(circuit) + +```c +struct CDensityMatrix *density_matrix_from_circuit(const struct CCircuit *circuit); +``` + +Executes the circuit and returns the evolved density matrix; the input circuit is not modified. The circuit is decomposed into basic gates and executed instruction by instruction; measurement declarations do not take part in the state evolution. + +- `circuit` (`const CCircuit*`): circuit handle, see [Circuit](../0_circuit/1_circuit.md). + +Returns: a newly allocated `CDensityMatrix*`; `NULL` when `circuit` is NULL or execution fails. + +### density_matrix_from_state(num_qubits, initial_state, len) + +```c +struct CDensityMatrix *density_matrix_from_state(uintptr_t num_qubits, + const Complex64 *initial_state, + uintptr_t len); +``` + +Constructs the density matrix as the outer product `ρ = |ψ><ψ|` of a normalized pure-state amplitude array; `len` must equal `2^num_qubits`. + +- `num_qubits` (`uintptr_t`): the number of qubits. +- `initial_state` (`const Complex64*`): normalized amplitude array of length `2^num_qubits`. +- `len` (`uintptr_t`): array length. + +Returns: a newly allocated `CDensityMatrix*`; `NULL` on NULL input, wrong dimension, or a non-normalized state. + +### density_matrix_free(ptr) + +```c +void density_matrix_free(struct CDensityMatrix *ptr); +``` + +Frees the handle. Passing NULL is allowed. + +- `ptr` (`CDensityMatrix*`): density-matrix handle. + +### density_matrix_num_qubits(ptr) + +```c +uintptr_t density_matrix_num_qubits(const struct CDensityMatrix *ptr); +``` + +Returns the number of qubits. + +- `ptr` (`const CDensityMatrix*`): density-matrix handle. + +Returns: the qubit count; 0 for NULL. + +### density_matrix_apply_circuit(ptr, circuit) + +```c +int32_t density_matrix_apply_circuit(struct CDensityMatrix *ptr, const struct CCircuit *circuit); +``` + +Applies the circuit in place to the current density matrix; the circuit's qubit count must match the state. + +- `ptr` (`CDensityMatrix*`): density-matrix handle. +- `circuit` (`const CCircuit*`): circuit handle. + +Returns: `0` on success; `-1` NULL pointer; `-3` circuit error; `-7` the circuit contains an unsupported operation; `-8` the circuit's qubit count does not match the state. + +--- + +## Matrix Data and Physicality + +### density_matrix_data_len(ptr) + +```c +uintptr_t density_matrix_data_len(const struct CDensityMatrix *ptr); +``` + +Returns the number of matrix elements (`4^N`), for sizing the buffer passed to `density_matrix_data`. + +- `ptr` (`const CDensityMatrix*`): density-matrix handle. + +Returns: the element count; 0 for NULL. + +### density_matrix_data(ptr, buffer, len) + +```c +int32_t density_matrix_data(const struct CDensityMatrix *ptr, Complex64 *buffer, uintptr_t len); +``` + +Copies the flattened row-major matrix elements into `buffer` as `4^N` `Complex64` values; element `r·2^N + c` is row `r`, column `c` of the matrix. + +- `ptr` (`const CDensityMatrix*`): density-matrix handle. +- `buffer` (`Complex64*`): output buffer. +- `len` (`uintptr_t`): buffer length; must equal `density_matrix_data_len`. + +Returns: `0` on success; `-1` NULL pointers; `-8` when `len` does not match the actual count. + +### density_matrix_trace(ptr, out_re, out_im) + +```c +int32_t density_matrix_trace(const struct CDensityMatrix *ptr, double *out_re, double *out_im); +``` + +Computes the trace and writes its real and imaginary parts to `out_re` / `out_im`; the trace of a valid physical state equals `1.0`. + +- `ptr` (`const CDensityMatrix*`): density-matrix handle. +- `out_re` (`double*`): output for the real part. +- `out_im` (`double*`): output for the imaginary part. + +Returns: `0` on success; `-1` when any pointer is NULL. + +### density_matrix_is_hermitian(ptr, tol, out) + +```c +int32_t density_matrix_is_hermitian(const struct CDensityMatrix *ptr, double tol, bool *out); +``` + +Checks whether the matrix satisfies `ρ = ρ†` within tolerance `tol` and writes the result to `out`. + +- `ptr` (`const CDensityMatrix*`): density-matrix handle. +- `tol` (`double`): tolerance; must be finite. +- `out` (`bool*`): result output. + +Returns: `0` on success; `-1` NULL pointers; `-8` when `tol` is not finite. + +### density_matrix_is_positive_semidefinite(ptr, tol, out) + +```c +int32_t density_matrix_is_positive_semidefinite(const struct CDensityMatrix *ptr, double tol, bool *out); +``` + +Checks whether the matrix is positive semidefinite within tolerance `tol` (all eigenvalues satisfy `λ >= -tol`) and writes the result to `out`. + +- `ptr` (`const CDensityMatrix*`): density-matrix handle. +- `tol` (`double`): tolerance; must be finite. +- `out` (`bool*`): result output. + +Returns: `0` on success; `-1` NULL pointers; `-8` when `tol` is not finite. + +### density_matrix_validate_physical(ptr, tol) + +```c +int32_t density_matrix_validate_physical(const struct CDensityMatrix *ptr, double tol); +``` + +Validates the physicality constraints of the density matrix within tolerance `tol`, in order: Hermiticity (`ρ = ρ†`), positive semidefiniteness, and unit trace. The matrix is only read. + +- `ptr` (`const CDensityMatrix*`): density-matrix handle. +- `tol` (`double`): tolerance; must be finite. + +Returns: `0` when the state is physical; `-1` NULL handle; `-7` not Hermitian or not positive semidefinite; `-8` non-finite `tol` or trace not equal to `1`. + +--- + +## Gate Operations + +All gate functions return `int32_t`: `0` on success; `-1` when the handle is NULL; `-2` when a qubit index is out of bounds; parameterized gates return `-8` when an angle is NaN or Inf. Single-qubit gates share the signature: + +```c +int32_t density_matrix_apply_h(struct CDensityMatrix *ptr, uint32_t qubit); +``` + +### Non-parameterized single-qubit gates + +| Function | Gate | Description | +| --- | --- | --- | +| `density_matrix_apply_h(ptr, qubit)` | H | Hadamard gate. | +| `density_matrix_apply_x(ptr, qubit)` | X | Pauli-X, bit flip. | +| `density_matrix_apply_y(ptr, qubit)` | Y | Pauli-Y. | +| `density_matrix_apply_z(ptr, qubit)` | Z | Pauli-Z, phase flip. | +| `density_matrix_apply_s(ptr, qubit)` | S | S gate (√Z). | +| `density_matrix_apply_sdg(ptr, qubit)` | S† | Inverse of S. | +| `density_matrix_apply_t(ptr, qubit)` | T | T gate (√S). | +| `density_matrix_apply_tdg(ptr, qubit)` | T† | Inverse of T. | +| `density_matrix_apply_x2p(ptr, qubit)` | X2P | `Rx(π/2)`. | +| `density_matrix_apply_x2m(ptr, qubit)` | X2M | `Rx(-π/2)`. | +| `density_matrix_apply_y2p(ptr, qubit)` | Y2P | `Ry(π/2)`. | +| `density_matrix_apply_y2m(ptr, qubit)` | Y2M | `Ry(-π/2)`. | + +### Parameterized single-qubit gates + +| Function | Gate | Description | +| --- | --- | --- | +| `density_matrix_apply_rx(ptr, qubit, theta)` | Rx(θ) | Rotation by `θ` about the X axis. | +| `density_matrix_apply_ry(ptr, qubit, theta)` | Ry(θ) | Rotation by `θ` about the Y axis. | +| `density_matrix_apply_rz(ptr, qubit, theta)` | Rz(θ) | Rotation by `θ` about the Z axis. | +| `density_matrix_apply_phase(ptr, qubit, theta)` | P(θ) | Phase gate, multiplies the `\|1>` component by `e^(iθ)`. | +| `density_matrix_apply_u(ptr, qubit, theta, phi, lambda)` | U(θ, φ, λ) | Generic single-qubit gate. | +| `density_matrix_apply_xy(ptr, qubit, theta)` | XY(θ) | XY gate. | +| `density_matrix_apply_xy2p(ptr, qubit, theta)` | XY2P(θ) | XY2P gate. | +| `density_matrix_apply_xy2m(ptr, qubit, theta)` | XY2M(θ) | XY2M gate. | +| `density_matrix_apply_rxy(ptr, qubit, theta, phi)` | RXY(θ, φ) | Rotation by `θ` about the axis at azimuth `φ` in the XY plane. | +| `density_matrix_apply_gphase(ptr, phi)` | GPhase(φ) | Global phase. A density matrix is invariant under a global phase, so the call always returns `0` without changing the state; the signature has no qubit parameter. | + +GPhase signature: + +```c +int32_t density_matrix_apply_gphase(struct CDensityMatrix *ptr, double phi); +``` + +### Two-qubit gates + +Shared signature `int32_t density_matrix_apply_(struct CDensityMatrix *ptr, uint32_t q0, uint32_t q1, ...)`; the two qubit parameters of controlled gates are named `control` / `target`. + +| Function | Gate | Description | +| --- | --- | --- | +| `density_matrix_apply_cx(ptr, control, target)` | CX | Controlled X. | +| `density_matrix_apply_cy(ptr, control, target)` | CY | Controlled Y. | +| `density_matrix_apply_cz(ptr, q0, q1)` | CZ | Controlled Z; the two parameters are symmetric. | +| `density_matrix_apply_swap(ptr, q0, q1)` | SWAP | Swaps two qubits. | +| `density_matrix_apply_rxx(ptr, q0, q1, theta)` | RXX(θ) | `exp(-i·θ/2·X⊗X)`. | +| `density_matrix_apply_ryy(ptr, q0, q1, theta)` | RYY(θ) | `exp(-i·θ/2·Y⊗Y)`. | +| `density_matrix_apply_rzz(ptr, q0, q1, theta)` | RZZ(θ) | `exp(-i·θ/2·Z⊗Z)`. | +| `density_matrix_apply_rzx(ptr, q0, q1, theta)` | RZX(θ) | `exp(-i·θ/2·Z⊗X)`. | +| `density_matrix_apply_crx(ptr, control, target, theta)` | CrX(θ) | Controlled `Rx(θ)`. | +| `density_matrix_apply_cry(ptr, control, target, theta)` | CrY(θ) | Controlled `Ry(θ)`. | +| `density_matrix_apply_crz(ptr, control, target, theta)` | CrZ(θ) | Controlled `Rz(θ)`. | +| `density_matrix_apply_fsim(ptr, q0, q1, theta, phi)` | FSIM | Fermionic simulation gate; `theta` is the iSWAP angle and `phi` the controlled-phase angle. | + +### Three-qubit gates + +| Function | Gate | Description | +| --- | --- | --- | +| `density_matrix_apply_ccx(ptr, c0, c1, target)` | CCX | Toffoli gate; flips the target when both controls are `\|1>`. | + +### Generic gate entry points + +#### density_matrix_apply_standard_gate(ptr, gate_name, qubits, num_target_qubits, params, num_params) + +```c +int32_t density_matrix_apply_standard_gate(struct CDensityMatrix *ptr, + const char *gate_name, + const uint32_t *qubits, + uintptr_t num_target_qubits, + const double *params, + uintptr_t num_params); +``` + +Applies a standard gate by name (e.g. `"H"`, `"CX"`, `"RZZ"`, `"FSIM"`), dispatching to the dedicated implementation. + +- `ptr` (`CDensityMatrix*`): density-matrix handle. +- `gate_name` (`const char*`): NUL-terminated standard-gate name. +- `qubits` (`const uint32_t*`): array of target qubits with length `num_target_qubits`. +- `num_target_qubits` (`uintptr_t`): number of target qubits; must match the gate's arity. +- `params` (`const double*`): gate-parameter array; may be NULL when `num_params` is 0. +- `num_params` (`uintptr_t`): number of parameters; must match the gate's arity. + +Returns: `0` on success; `-1` NULL pointers; `-2` out-of-bounds qubit; `-8` unknown gate name, gate name that is not valid UTF-8, qubit or parameter count that does not match the gate, or a non-finite parameter value. + +--- + +## Quantum Channels and Partial Trace + +### density_matrix_apply_kraus(ptr, ops, num_ops, op_dim, qubits, num_target_qubits) + +```c +int32_t density_matrix_apply_kraus(struct CDensityMatrix *ptr, + const Complex64 *ops, + uintptr_t num_ops, + uintptr_t op_dim, + const uint32_t *qubits, + uintptr_t num_target_qubits); +``` + +Applies the quantum channel given by Kraus operators, evolving `ρ → Σ_k K_k ρ K_k†`. + +- `ptr` (`CDensityMatrix*`): density-matrix handle. +- `ops` (`const Complex64*`): flattened operator array holding `num_ops × op_dim × op_dim` row-major `Complex64` values, one operator after another. +- `num_ops` (`uintptr_t`): number of operators. +- `op_dim` (`uintptr_t`): dimension of a single operator, equal to `2^num_target_qubits`. +- `qubits` (`const uint32_t*`): array of target qubits. +- `num_target_qubits` (`uintptr_t`): number of target qubits. + +Returns: `0` on success; `-1` NULL pointers; `-2` out-of-bounds qubit; `-8` when `num_ops`, `op_dim`, or `num_target_qubits` is 0, or an operator's size does not match the qubit count. + +```c +/* Bit-flip channel with p = 0.3 on qubit 0 */ +double p = 0.3, s0 = sqrt(1.0 - p), s1 = sqrt(p); +Complex64 ops[8] = { + {s0, 0}, {0, 0}, {0, 0}, {s0, 0}, /* K0 = sqrt(1-p) * I */ + {0, 0}, {s1, 0}, {s1, 0}, {0, 0}, /* K1 = sqrt(p) * X */ +}; +uint32_t q = 0; +density_matrix_apply_kraus(dm, ops, 2, 2, &q, 1); +``` + +### density_matrix_partial_trace(ptr, keep_qubits, num_keep) + +```c +struct CDensityMatrix *density_matrix_partial_trace(const struct CDensityMatrix *ptr, + const uint32_t *keep_qubits, + uintptr_t num_keep); +``` + +Traces out every qubit not listed in `keep_qubits` and returns a new density matrix with `num_keep` qubits. + +- `ptr` (`const CDensityMatrix*`): density-matrix handle. +- `keep_qubits` (`const uint32_t*`): array of qubits to keep; may be NULL when `num_keep` is 0. +- `num_keep` (`uintptr_t`): number of qubits to keep. + +Returns: a newly allocated `CDensityMatrix*`, freed with `density_matrix_free`; `NULL` on error. + +--- + +## Probabilities and Measurement + +### density_matrix_probabilities_len(ptr) + +```c +uintptr_t density_matrix_probabilities_len(const struct CDensityMatrix *ptr); +``` + +Returns the length of the computational-basis probability distribution (`2^N`). + +- `ptr` (`const CDensityMatrix*`): density-matrix handle. + +Returns: the probability count; 0 for NULL. + +### density_matrix_probabilities(ptr, buffer, len) + +```c +int32_t density_matrix_probabilities(const struct CDensityMatrix *ptr, + double *buffer, + uintptr_t len); +``` + +Copies the diagonal of the density matrix (the measurement probability of each computational basis state) into `buffer`. + +- `ptr` (`const CDensityMatrix*`): density-matrix handle. +- `buffer` (`double*`): output buffer. +- `len` (`uintptr_t`): buffer length; must equal `density_matrix_probabilities_len`. + +Returns: `0` on success; `-1` NULL pointers; `-8` when `len` does not match the actual count. + +### density_matrix_measure(ptr, qubit) + +```c +int32_t density_matrix_measure(struct CDensityMatrix *ptr, uint32_t qubit); +``` + +Measures the qubit in the Z basis and collapses the density matrix: `ρ' = Π_b ρ Π_b / Tr(Π_b ρ)`. + +- `ptr` (`CDensityMatrix*`): density-matrix handle. +- `qubit` (`uint32_t`): the measured qubit. + +Returns: `0` for outcome `|0>`, `1` for outcome `|1>`; `-1` NULL; `-2` out-of-bounds qubit. + +### density_matrix_measure_all(ptr) + +```c +char *density_matrix_measure_all(struct CDensityMatrix *ptr); +``` + +Measures qubits in order `0..num_qubits`, collapsing the state, and returns the outcome as a big-endian bitstring (MSB = qubit `N-1`, LSB = qubit `0`). + +- `ptr` (`CDensityMatrix*`): density-matrix handle. + +Returns: a heap-allocated C string, freed with `cqlib_string_free`; `NULL` on error. + +### density_matrix_sample_shots(ptr, shots) + +```c +struct COutcomeList *density_matrix_sample_shots(const struct CDensityMatrix *ptr, uintptr_t shots); +``` + +Samples `shots` independent measurement outcomes in parallel from the current distribution without modifying the state. Use `outcome_list_len` for the count, `outcome_list_get` to fetch each big-endian bitstring (freed with `cqlib_string_free`), and `outcome_list_free` to free the list — see [ClassicalState](5_classical_state.md). + +- `ptr` (`const CDensityMatrix*`): density-matrix handle. +- `shots` (`uintptr_t`): number of shots. + +Returns: a newly allocated `COutcomeList*`; `NULL` on error. + +### density_matrix_reset(ptr, qubit) + +```c +int32_t density_matrix_reset(struct CDensityMatrix *ptr, uint32_t qubit); +``` + +Resets the qubit to `|0>`. + +- `ptr` (`CDensityMatrix*`): density-matrix handle. +- `qubit` (`uint32_t`): target qubit. + +Returns: `0` on success; `-1` NULL; `-2` out-of-bounds qubit. + +--- + +## Expectation Values + +### density_matrix_expectation(ptr, observable, out) + +```c +int32_t density_matrix_expectation(const struct CDensityMatrix *ptr, + const struct CHamiltonian *observable, + double *out); +``` + +Computes `Tr(ρ·H)` and writes it to `out`. + +- `ptr` (`const CDensityMatrix*`): density-matrix handle. +- `observable` (`const CHamiltonian*`): observable handle, see [Hamiltonian](7_hamiltonian.md). +- `out` (`double*`): output pointer. + +Returns: `0` on success; `-1` NULL pointers; `-8` when the observable's qubit count does not match the state. + +--- + +## Examples + +### 1. Bell state, matrix data, and probabilities + +```c +#include +#include +#include "cqlib_c.h" + +int main(void) { + struct CDensityMatrix *dm = density_matrix_new(2); + density_matrix_apply_h(dm, 0); + density_matrix_apply_cx(dm, 0, 1); + + /* Probabilities: P(00) = P(11) = 0.5 */ + double probs[4]; + density_matrix_probabilities(dm, probs, 4); + printf("P(00)=%.3f P(11)=%.3f\n", probs[0], probs[3]); + + /* Row-major matrix data: 16 Complex64 elements */ + uintptr_t n = density_matrix_data_len(dm); /* 16 */ + Complex64 *data = malloc(n * sizeof(Complex64)); + density_matrix_data(dm, data, n); + /* data[0] is rho[0][0], data[3] is rho[0][3], data[15] is rho[3][3] */ + printf("rho[3][3].re=%.3f\n", data[15].re); + free(data); + + density_matrix_free(dm); + return 0; +} +``` + +### 2. Maximally mixed state, trace, and Hermiticity check + +```c +struct CDensityMatrix *dm = density_matrix_maximally_mixed(2); +/* 4^2 = 16 matrix elements, trace = 1, Hermitian */ +double tr_re, tr_im; +density_matrix_trace(dm, &tr_re, &tr_im); /* tr_re = 1.0, tr_im = 0.0 */ +bool herm = false; +density_matrix_is_hermitian(dm, 1e-10, &herm); /* herm = true */ +density_matrix_free(dm); +``` + +### 3. Partial trace + +```c +struct CDensityMatrix *dm = density_matrix_new(2); +density_matrix_apply_h(dm, 0); +density_matrix_apply_cx(dm, 0, 1); + +/* Trace out qubit 1: qubit 0 becomes maximally mixed */ +uint32_t keep[1] = {0}; +struct CDensityMatrix *reduced = density_matrix_partial_trace(dm, keep, 1); +/* density_matrix_num_qubits(reduced) == 1, P(0) = P(1) = 0.5 */ +density_matrix_free(reduced); +density_matrix_free(dm); +``` + +### 4. Expectation value + +```c +struct CDensityMatrix *dm = density_matrix_new(2); +density_matrix_apply_h(dm, 0); +density_matrix_apply_cx(dm, 0, 1); + +struct CPauliString *zz = pauli_string_parse("ZZ"); +struct CHamiltonian *h = hamiltonian_from_pauli(zz); /* takes ownership of zz */ +double exp = 0.0; +density_matrix_expectation(dm, h, &exp); /* exp ≈ 1.0 */ +hamiltonian_free(h); +density_matrix_free(dm); +``` diff --git a/docs/api/en/c/3_qis/3_density_matrix_noise.md b/docs/api/en/c/3_qis/3_density_matrix_noise.md new file mode 100644 index 00000000..74b85ee7 --- /dev/null +++ b/docs/api/en/c/3_qis/3_density_matrix_noise.md @@ -0,0 +1,398 @@ +# DensityMatrixNoise + +The `CDensityMatrixNoise*` handle is the noisy mixed-state simulator: it layers a `CNoiseModel` on top of the density-matrix simulator, so that after every gate the model is consulted for a noise entry matching the same gate on the same qubits and the corresponding channel is applied; readout errors only change the reported probability distribution. This page covers construction and circuit ingestion, gate operations, probabilities and readout noise, measurement and sampling, and expectation values. Error codes and memory conventions are described in [Overview](../0_overview.md); for the module summary see [QIS Overview](0_overview.md). + +--- + +## Construction and Release + +### density_matrix_noise_new(num_qubits, noise_model) + +```c +struct CDensityMatrixNoise *density_matrix_noise_new(uintptr_t num_qubits, + const struct CNoiseModel *noise_model); +``` + +Creates the noisy simulator initialized to `|0...0><0...0|`; pass NULL as `noise_model` for ideal (noiseless) simulation. + +- `num_qubits` (`uintptr_t`): the number of qubits. +- `noise_model` (`const CNoiseModel*`): noise-model handle, may be NULL; it is cloned at construction, so the model handle can be released independently afterwards. + +Returns: a newly allocated `CDensityMatrixNoise*`; free it with `density_matrix_noise_free`. + +### density_matrix_noise_from_circuit(circuit, noise_model) + +```c +struct CDensityMatrixNoise *density_matrix_noise_from_circuit(const struct CCircuit *circuit, + const struct CNoiseModel *noise_model); +``` + +Executes the circuit and returns the noisy simulator: the circuit is first decomposed into basic gates, with noise applied after each gate; pass NULL as `noise_model` for ideal simulation. + +- `circuit` (`const CCircuit*`): circuit handle, see [Circuit](../0_circuit/1_circuit.md). +- `noise_model` (`const CNoiseModel*`): noise-model handle, may be NULL. + +Returns: a newly allocated `CDensityMatrixNoise*`; `NULL` when `circuit` is NULL or execution fails. + +### density_matrix_noise_free(ptr) + +```c +void density_matrix_noise_free(struct CDensityMatrixNoise *ptr); +``` + +Frees the handle. Passing NULL is allowed. + +- `ptr` (`CDensityMatrixNoise*`): noisy-simulator handle. + +### density_matrix_noise_num_qubits(ptr) + +```c +uintptr_t density_matrix_noise_num_qubits(const struct CDensityMatrixNoise *ptr); +``` + +Returns the number of qubits. + +- `ptr` (`const CDensityMatrixNoise*`): noisy-simulator handle. + +Returns: the qubit count; 0 for NULL. + +### density_matrix_noise_apply_circuit(ptr, circuit) + +```c +int32_t density_matrix_noise_apply_circuit(struct CDensityMatrixNoise *ptr, + const struct CCircuit *circuit); +``` + +Applies the circuit in place to the current simulator: the circuit is first decomposed, and after each gate the matching gate noise from the noise model is injected. + +- `ptr` (`CDensityMatrixNoise*`): noisy-simulator handle. +- `circuit` (`const CCircuit*`): circuit handle. + +Returns: `0` on success; `-1` NULL pointer; `-3` circuit error; `-7` the circuit contains an unsupported operation; `-8` the circuit's qubit count does not match the simulator. + +--- + +## Gate Operations + +All gate functions return `int32_t`: `0` on success; `-1` when the handle is NULL; `-2` when a qubit index is out of bounds; parameterized gates return `-8` when an angle is NaN or Inf. After each gate, matching gate noise from the noise model is injected; when no model is configured or no entry matches, only the ideal gate is applied. Single-qubit gates share the signature: + +```c +int32_t density_matrix_noise_apply_h(struct CDensityMatrixNoise *ptr, uint32_t qubit); +``` + +### Non-parameterized single-qubit gates + +| Function | Gate | Description | +| --- | --- | --- | +| `density_matrix_noise_apply_h(ptr, qubit)` | H | Hadamard gate. | +| `density_matrix_noise_apply_x(ptr, qubit)` | X | Pauli-X, bit flip. | +| `density_matrix_noise_apply_y(ptr, qubit)` | Y | Pauli-Y. | +| `density_matrix_noise_apply_z(ptr, qubit)` | Z | Pauli-Z, phase flip. | +| `density_matrix_noise_apply_s(ptr, qubit)` | S | S gate (√Z). | +| `density_matrix_noise_apply_sdg(ptr, qubit)` | S† | Inverse of S. | +| `density_matrix_noise_apply_t(ptr, qubit)` | T | T gate (√S). | +| `density_matrix_noise_apply_tdg(ptr, qubit)` | T† | Inverse of T. | +| `density_matrix_noise_apply_x2p(ptr, qubit)` | X2P | `Rx(π/2)`. | +| `density_matrix_noise_apply_x2m(ptr, qubit)` | X2M | `Rx(-π/2)`. | +| `density_matrix_noise_apply_y2p(ptr, qubit)` | Y2P | `Ry(π/2)`. | +| `density_matrix_noise_apply_y2m(ptr, qubit)` | Y2M | `Ry(-π/2)`. | + +### Parameterized single-qubit gates + +| Function | Gate | Description | +| --- | --- | --- | +| `density_matrix_noise_apply_rx(ptr, qubit, theta)` | Rx(θ) | Rotation by `θ` about the X axis. | +| `density_matrix_noise_apply_ry(ptr, qubit, theta)` | Ry(θ) | Rotation by `θ` about the Y axis. | +| `density_matrix_noise_apply_rz(ptr, qubit, theta)` | Rz(θ) | Rotation by `θ` about the Z axis. | +| `density_matrix_noise_apply_phase(ptr, qubit, theta)` | P(θ) | Phase gate, multiplies the `\|1>` component by `e^(iθ)`. | +| `density_matrix_noise_apply_u(ptr, qubit, theta, phi, lambda)` | U(θ, φ, λ) | Generic single-qubit gate. | +| `density_matrix_noise_apply_xy(ptr, qubit, theta)` | XY(θ) | XY gate. | +| `density_matrix_noise_apply_xy2p(ptr, qubit, theta)` | XY2P(θ) | XY2P gate. | +| `density_matrix_noise_apply_xy2m(ptr, qubit, theta)` | XY2M(θ) | XY2M gate. | +| `density_matrix_noise_apply_rxy(ptr, qubit, theta, phi)` | RXY(θ, φ) | Rotation by `θ` about the axis at azimuth `φ` in the XY plane. | +| `density_matrix_noise_apply_gphase(ptr, phi)` | GPhase(φ) | Multiplies the whole state by the global phase `e^(iφ)`; the signature has no qubit parameter. | + +GPhase signature: + +```c +int32_t density_matrix_noise_apply_gphase(struct CDensityMatrixNoise *ptr, double phi); +``` + +### Two-qubit gates + +Shared signature `int32_t density_matrix_noise_apply_(struct CDensityMatrixNoise *ptr, uint32_t q0, uint32_t q1, ...)`; the two qubit parameters of controlled gates are named `control` / `target`. + +| Function | Gate | Description | +| --- | --- | --- | +| `density_matrix_noise_apply_cx(ptr, control, target)` | CX | Controlled X. | +| `density_matrix_noise_apply_cy(ptr, control, target)` | CY | Controlled Y. | +| `density_matrix_noise_apply_cz(ptr, q0, q1)` | CZ | Controlled Z; the two parameters are symmetric. | +| `density_matrix_noise_apply_swap(ptr, q0, q1)` | SWAP | Swaps two qubits. | +| `density_matrix_noise_apply_rxx(ptr, q0, q1, theta)` | RXX(θ) | `exp(-i·θ/2·X⊗X)`. | +| `density_matrix_noise_apply_ryy(ptr, q0, q1, theta)` | RYY(θ) | `exp(-i·θ/2·Y⊗Y)`. | +| `density_matrix_noise_apply_rzz(ptr, q0, q1, theta)` | RZZ(θ) | `exp(-i·θ/2·Z⊗Z)`. | +| `density_matrix_noise_apply_rzx(ptr, q0, q1, theta)` | RZX(θ) | `exp(-i·θ/2·Z⊗X)`. | +| `density_matrix_noise_apply_crx(ptr, control, target, theta)` | CrX(θ) | Controlled `Rx(θ)`. | +| `density_matrix_noise_apply_cry(ptr, control, target, theta)` | CrY(θ) | Controlled `Ry(θ)`. | +| `density_matrix_noise_apply_crz(ptr, control, target, theta)` | CrZ(θ) | Controlled `Rz(θ)`. | +| `density_matrix_noise_apply_fsim(ptr, q0, q1, theta, phi)` | FSIM | Fermionic simulation gate; `theta` is the iSWAP angle and `phi` the controlled-phase angle. | + +### Three-qubit gates + +| Function | Gate | Description | +| --- | --- | --- | +| `density_matrix_noise_apply_ccx(ptr, c0, c1, target)` | CCX | Toffoli gate; flips the target when both controls are `\|1>`. | + +### Generic gate entry points + +#### density_matrix_noise_apply_standard_gate_noise(ptr, gate_name, qubits, qubit_len, params, param_len) + +```c +int32_t density_matrix_noise_apply_standard_gate_noise(struct CDensityMatrixNoise *ptr, + const char *gate_name, + const uint32_t *qubits, + uintptr_t qubit_len, + const double *params, + uintptr_t param_len); +``` + +Applies a standard gate by name (e.g. `"H"`, `"CX"`, `"RZZ"`, `"GPhase"`, `"FSIM"`): the ideal gate is applied first, then the gate noise matching the noise model. + +- `ptr` (`CDensityMatrixNoise*`): noisy-simulator handle. +- `gate_name` (`const char*`): NUL-terminated standard-gate name. +- `qubits` (`const uint32_t*`): array of target qubits with length `qubit_len`; may be NULL when `qubit_len` is 0. +- `qubit_len` (`uintptr_t`): number of target qubits; must match the gate's arity. +- `params` (`const double*`): gate-parameter array; may be NULL when `param_len` is 0. +- `param_len` (`uintptr_t`): number of parameters; must match the gate's arity. + +Returns: `0` on success; `-1` NULL pointers; `-2` out-of-bounds qubit; `-4` the gate name is not valid UTF-8; `-8` unknown gate name, qubit or parameter count that does not match the gate, or a non-finite parameter value. + +#### density_matrix_noise_apply_unitary_gate(ptr, qubits, qubit_len, matrix, matrix_dim) + +```c +int32_t density_matrix_noise_apply_unitary_gate(struct CDensityMatrixNoise *ptr, + const uint32_t *qubits, + uintptr_t qubit_len, + const Complex64 *matrix, + uintptr_t matrix_dim); +``` + +Applies an arbitrary unitary as `ρ → U ρ U†`. A generic unitary has no corresponding standard-gate type, so no gate noise is injected; use the dedicated gate functions when noise modeling is required. + +- `ptr` (`CDensityMatrixNoise*`): noisy-simulator handle. +- `qubits` (`const uint32_t*`): array of target qubits; indices must not repeat. +- `qubit_len` (`uintptr_t`): number of target qubits. +- `matrix` (`const Complex64*`): row-major `matrix_dim × matrix_dim` matrix. +- `matrix_dim` (`uintptr_t`): matrix dimension; must equal `2^qubit_len`. + +Returns: `0` on success; `-1` NULL pointers; `-2` out-of-bounds qubit; `-8` when `qubit_len` or `matrix_dim` is 0, the dimension does not match, or a qubit repeats. + +--- + +## Probabilities and Readout Noise + +The simulator separates noise of the quantum state from noise of the readout: gate noise acts on the density matrix itself, while readout errors only change the reported probability distribution. + +### density_matrix_noise_probabilities_len(ptr) + +```c +uintptr_t density_matrix_noise_probabilities_len(const struct CDensityMatrixNoise *ptr); +``` + +Returns the length of the computational-basis probability distribution (`2^N`). + +- `ptr` (`const CDensityMatrixNoise*`): noisy-simulator handle. + +Returns: the probability count; 0 for NULL. + +### density_matrix_noise_probabilities(ptr, buffer, len) + +```c +int32_t density_matrix_noise_probabilities(const struct CDensityMatrixNoise *ptr, + double *buffer, + uintptr_t len); +``` + +Copies the ideal probability distribution (the density-matrix diagonal, including applied gate noise but excluding readout errors) into `buffer`. + +- `ptr` (`const CDensityMatrixNoise*`): noisy-simulator handle. +- `buffer` (`double*`): output buffer. +- `len` (`uintptr_t`): buffer length; must equal `density_matrix_noise_probabilities_len`. + +Returns: `0` on success; `-1` NULL pointers; `-8` when `len` does not match the actual count. + +### density_matrix_noise_probabilities_with_readout_len(ptr) + +```c +uintptr_t density_matrix_noise_probabilities_with_readout_len(const struct CDensityMatrixNoise *ptr); +``` + +Returns the buffer length of the readout-error probability distribution (`2^N`); the buffer size is independent of the qubit selection passed to `density_matrix_noise_probabilities_with_readout`. + +- `ptr` (`const CDensityMatrixNoise*`): noisy-simulator handle. + +Returns: the probability count; 0 for NULL. + +### density_matrix_noise_probabilities_with_readout(ptr, buffer, len, qubits, qubit_len) + +```c +int32_t density_matrix_noise_probabilities_with_readout(const struct CDensityMatrixNoise *ptr, + double *buffer, + uintptr_t len, + const uint32_t *qubits, + uintptr_t qubit_len); +``` + +Takes the ideal probability distribution and applies the readout errors registered in the noise model for the qubits listed in `qubits`, copying the result into `buffer`. + +- `ptr` (`const CDensityMatrixNoise*`): noisy-simulator handle. +- `buffer` (`double*`): output buffer. +- `len` (`uintptr_t`): buffer length; must equal `density_matrix_noise_probabilities_with_readout_len`. +- `qubits` (`const uint32_t*`): array of qubits to apply readout errors to; may be NULL when `qubit_len` is 0, in which case the result matches `density_matrix_noise_probabilities`. +- `qubit_len` (`uintptr_t`): number of qubits. + +Returns: `0` on success; `-1` NULL pointers; `-2` out-of-bounds qubit; `-8` when `len` does not match the actual count. + +--- + +## Measurement, Reset, and Sampling + +### density_matrix_noise_measure(ptr, qubit) + +```c +int32_t density_matrix_noise_measure(struct CDensityMatrixNoise *ptr, uint32_t qubit); +``` + +Measures the qubit in the Z basis and collapses the underlying density matrix; the measurement itself applies no noise. + +- `ptr` (`CDensityMatrixNoise*`): noisy-simulator handle. +- `qubit` (`uint32_t`): the measured qubit. + +Returns: `0` for outcome `|0>`, `1` for outcome `|1>`; `-1` NULL; `-2` out-of-bounds qubit. + +### density_matrix_noise_measure_all(ptr) + +```c +char *density_matrix_noise_measure_all(struct CDensityMatrixNoise *ptr); +``` + +Measures qubits in order `0..num_qubits`, collapsing the state, and returns the outcome as a big-endian bitstring (MSB = qubit `N-1`, LSB = qubit `0`). + +- `ptr` (`CDensityMatrixNoise*`): noisy-simulator handle. + +Returns: a heap-allocated C string, freed with `cqlib_string_free`; `NULL` on error. + +### density_matrix_noise_reset(ptr, qubit) + +```c +int32_t density_matrix_noise_reset(struct CDensityMatrixNoise *ptr, uint32_t qubit); +``` + +Resets the qubit to `|0>`; the reset applies no readout error. + +- `ptr` (`CDensityMatrixNoise*`): noisy-simulator handle. +- `qubit` (`uint32_t`): target qubit. + +Returns: `0` on success; `-1` NULL; `-2` out-of-bounds qubit. + +### density_matrix_noise_sample_shots(ptr, shots) + +```c +struct COutcomeList *density_matrix_noise_sample_shots(const struct CDensityMatrixNoise *ptr, + uintptr_t shots); +``` + +Samples `shots` independent measurement outcomes in parallel from the current distribution without modifying the state; the sampling draws from the quantum state's own distribution and excludes readout errors. Use `outcome_list_len` for the count, `outcome_list_get` to fetch each big-endian bitstring (freed with `cqlib_string_free`), and `outcome_list_free` to free the list — see [ClassicalState](5_classical_state.md). + +- `ptr` (`const CDensityMatrixNoise*`): noisy-simulator handle. +- `shots` (`uintptr_t`): number of shots. + +Returns: a newly allocated `COutcomeList*`; `NULL` on error. + +--- + +## Expectation Values + +### density_matrix_noise_expectation(ptr, observable, out) + +```c +int32_t density_matrix_noise_expectation(const struct CDensityMatrixNoise *ptr, + const struct CHamiltonian *observable, + double *out); +``` + +Computes ` = Tr(H·ρ)` and writes it to `out`; the result includes the applied gate noise but excludes readout errors (readout errors do not affect the quantum state). + +- `ptr` (`const CDensityMatrixNoise*`): noisy-simulator handle. +- `observable` (`const CHamiltonian*`): observable handle, see [Hamiltonian](7_hamiltonian.md). +- `out` (`double*`): output pointer. + +Returns: `0` on success; `-1` NULL pointers; `-8` when the observable's qubit count does not match the simulator. + +--- + +## Configuring the noise model + +The `CNoiseModel` handle comes from the device module; its construction and registration functions include `noise_model_new`, `noise_model_add_single_qubit` (selecting a channel by the `NOISE_BIT_FLIP` style tags), `noise_model_add_single_qubit_pauli`, `noise_model_add_two_qubit` (`NOISE_TWO_DEPOLARIZING`), and `noise_model_add_readout` (asymmetric readout errors `p_0_given_1` / `p_1_given_0`). See [NoiseModel](../2_device/4_noise.md). + +--- + +## Examples + +### 1. Bell state with gate noise and readout noise + +```c +#include +#include "cqlib_c.h" + +int main(void) { + /* 5% depolarizing on CX(0,1), 10% readout error on qubit 0 */ + struct CNoiseModel *model = noise_model_new(); + noise_model_add_two_qubit(model, "CX", 0, 1, NOISE_TWO_DEPOLARIZING, 0.05); + noise_model_add_readout(model, 0, 0.0, 0.1); + + struct CDensityMatrixNoise *sim = density_matrix_noise_new(2, model); + density_matrix_noise_apply_h(sim, 0); + density_matrix_noise_apply_cx(sim, 0, 1); + + /* Ideal distribution: gate noise only, no readout error */ + double ideal[4]; + density_matrix_noise_probabilities(sim, ideal, 4); + printf("ideal P(00)=%.3f P(11)=%.3f\n", ideal[0], ideal[3]); + + /* With readout error applied to qubit 0 */ + double with_ro[4]; + uint32_t measured[1] = {0}; + density_matrix_noise_probabilities_with_readout(sim, with_ro, 4, measured, 1); + printf("readout P(00)=%.3f P(11)=%.3f\n", with_ro[0], with_ro[3]); + + density_matrix_noise_free(sim); + noise_model_free(model); + return 0; +} +``` + +### 2. Constructing from a circuit and computing an expectation value + +```c +struct CCircuit *qc = circuit_new(2); +circuit_h(qc, 0); +circuit_cx(qc, 0, 1); + +struct CNoiseModel *model = noise_model_new(); +noise_model_add_two_qubit(model, "CX", 0, 1, NOISE_TWO_DEPOLARIZING, 0.05); + +struct CDensityMatrixNoise *sim = density_matrix_noise_from_circuit(qc, model); + +/* stays close to 1 under weak depolarizing noise */ +struct CPauliString *zz = pauli_string_parse("ZZ"); +struct CHamiltonian *h = hamiltonian_from_pauli(zz); /* takes ownership of zz */ +double exp = 0.0; +density_matrix_noise_expectation(sim, h, &exp); +printf("=%.3f\n", exp); +hamiltonian_free(h); + +density_matrix_noise_free(sim); +noise_model_free(model); +circuit_free(qc); +``` diff --git a/docs/api/en/c/3_qis/4_stabilizer.md b/docs/api/en/c/3_qis/4_stabilizer.md new file mode 100644 index 00000000..5eebdd70 --- /dev/null +++ b/docs/api/en/c/3_qis/4_stabilizer.md @@ -0,0 +1,249 @@ +# Stabilizer (C) + +`CStabilizerState` is an opaque handle to the stabilizer-state simulator: it describes a quantum state with a set of mutually commuting Pauli operators (stabilizer generators), can only represent stabilizer states reachable by Clifford circuits, and scales to far more qubits than dense representations; arbitrary-angle rotations, `T` gates, and other non-Clifford operations return errors. Error codes, string and handle ownership conventions follow the [Overview](../0_overview.md). + +--- + +## Construction and Release + +### stabilizer_new(num_qubits) + +Constructs the `|0...0>` stabilizer state with per-qubit destabilizer `X_i` and stabilizer `Z_i` generators, all with phase `+1`. + +Parameters: + +- `num_qubits` (`uintptr_t`): number of qubits. + +Returns: a new `CStabilizerState*` on success; NULL when `num_qubits` is 0. + +### stabilizer_free(ptr) + +Frees a stabilizer state handle; NULL is allowed. + +### stabilizer_from_circuit(circuit) + +Executes a Clifford circuit and returns the final stabilizer state. Terminal measurement declarations are treated as output declarations and ignored: the state does not collapse; barriers and delays have no side effect. + +Parameters: + +- `circuit` (`const CCircuit*`): Clifford circuit, may contain `I`, `H`, `X`, `Y`, `Z`, `S`, `SDG`, `X2P`, `X2M`, `Y2P`, `Y2M`, `CX`, `CY`, `CZ`, `SWAP`, and `Reset`. + +Returns: a new `CStabilizerState*` on success; NULL on NULL input, a non-Clifford gate, or execution failure. + +### stabilizer_run_circuit(circuit) + +Executes a Clifford circuit from `|0...0>` and returns the final state. Unlike `stabilizer_from_circuit`, this entry point executes `MeasureBit` / `MeasureBits` operations, so the returned state is already collapsed. + +Parameters: + +- `circuit` (`const CCircuit*`): Clifford circuit. + +Returns: a collapsed `CStabilizerState*` on success; NULL on NULL input or execution failure. + +--- + +## Applying Circuits + +### stabilizer_apply_circuit(ptr, circuit) + +Applies a Clifford circuit to the state in place. + +Parameters: + +- `ptr` (`CStabilizerState*`): stabilizer state handle. +- `circuit` (`const CCircuit*`): Clifford circuit whose qubit count must match the state. + +Returns: 0 on success; -1 for NULL pointers; -3 when the circuit contains gates that cannot execute or unresolved symbolic parameters; -7 for unsupported operations such as control flow; -8 when the circuit width differs from the state. + +--- + +## Single-Qubit Clifford Gates + +The following entry points apply the corresponding gate to `qubit` in place; the gate-name column gives the semantics of each one: + +| Gate | Function | Description | +| --- | --- | --- | +| `H` | `stabilizer_apply_h(ptr, qubit)` | Hadamard gate. | +| `X` | `stabilizer_apply_x(ptr, qubit)` | Pauli-X. | +| `Y` | `stabilizer_apply_y(ptr, qubit)` | Pauli-Y. | +| `Z` | `stabilizer_apply_z(ptr, qubit)` | Pauli-Z. | +| `S` | `stabilizer_apply_s(ptr, qubit)` | S gate. | +| `SDG` | `stabilizer_apply_sdg(ptr, qubit)` | S† gate. | +| `X2P` | `stabilizer_apply_x2p(ptr, qubit)` | √X, i.e. Rx(π/2). | +| `X2M` | `stabilizer_apply_x2m(ptr, qubit)` | √X†, i.e. Rx(-π/2). | +| `Y2P` | `stabilizer_apply_y2p(ptr, qubit)` | √Y, i.e. Ry(π/2). | +| `Y2M` | `stabilizer_apply_y2m(ptr, qubit)` | √Y†, i.e. Ry(-π/2). | + +Parameters: + +- `ptr` (`CStabilizerState*`): stabilizer state handle. +- `qubit` (`uint32_t`): target qubit index. + +Returns: 0 on success; -1 for NULL pointers; -2 for an out-of-bounds qubit; -3 when the gate cannot be executed. + +--- + +## Two-Qubit Clifford Gates + +| Gate | Function | Description | +| --- | --- | --- | +| `CX` | `stabilizer_apply_cx(ptr, control, target)` | Controlled-X. | +| `CY` | `stabilizer_apply_cy(ptr, control, target)` | Controlled-Y. | +| `CZ` | `stabilizer_apply_cz(ptr, q0, q1)` | Controlled-Z. | +| `SWAP` | `stabilizer_apply_swap(ptr, q0, q1)` | Swaps two qubits. | + +Parameters: + +- `ptr` (`CStabilizerState*`): stabilizer state handle. +- `control` / `target` / `q0` / `q1` (`uint32_t`): target qubit indices. + +Returns: 0 on success; -1 for NULL pointers; -2 for an out-of-bounds qubit; -8 when both qubits are identical. + +### stabilizer_apply_standard_gate(ptr, gate_name, qubits, num_target_qubits, params, num_params) + +Applies a Clifford standard gate identified by name (e.g. `"H"`, `"CX"`, `"SWAP"`). + +Parameters: + +- `ptr` (`CStabilizerState*`): stabilizer state handle. +- `gate_name` (`const char*`): standard gate name. +- `qubits` (`const uint32_t*`): array of `num_target_qubits` target indices. +- `num_target_qubits` (`uintptr_t`): number of target qubits. +- `params` (`const double*`): array of `num_params` gate parameters; NULL is allowed when `num_params` is 0. +- `num_params` (`uintptr_t`): number of gate parameters. + +Returns: 0 on success; -1 for NULL pointers; -2 for an out-of-bounds qubit; -8 for an unknown gate name or invalid argument counts; -7 for a non-Clifford gate. + +--- + +## Measurement, Reset, and Sampling + +### stabilizer_measure(ptr, qubit) + +Measures `qubit` in the Z basis, collapsing the state. + +Returns: the measurement outcome `0` or `1`; -1 for NULL pointers; -2 for an out-of-bounds qubit. + +### stabilizer_measure_all(ptr) + +Measures all qubits in order, collapsing the state. + +Returns: a big-endian bitstring (leftmost character is qubit N-1, rightmost is qubit 0); free with `cqlib_string_free`. NULL input returns NULL. + +### stabilizer_reset(ptr, qubit) + +Resets `qubit` to `|0>` (one Z-basis measurement plus a conditional X correction). + +Returns: 0 on success; -1 for NULL pointers; -2 for an out-of-bounds qubit. + +### stabilizer_sample_shots(ptr, shots) + +Samples `shots` independent measurement outcomes in parallel; the same initial state always produces the same sample set. + +Parameters: + +- `ptr` (`const CStabilizerState*`): stabilizer state handle. +- `shots` (`uintptr_t`): number of samples. + +Returns: a `COutcomeList*` (free with `outcome_list_free`); NULL input returns NULL. See [Classical State](5_classical_state.md) for traversing the bitstrings. + +--- + +## Probability Distribution + +### stabilizer_probabilities_len(ptr) / stabilizer_probabilities(ptr, buffer, len) + +Two-step read of the probability distribution over all computational basis states: `*_len` returns the length `2^N` (0 for NULL), and the fill function copies the distribution into `buffer`, where index `i` corresponds to the basis state whose binary representation is `i` (qubit 0 is the least-significant bit). Only suitable for small systems. + +Returns (fill function): 0 on success; -1 for NULL pointers; -8 when `len` differs from `2^N` or the system exceeds the size limit. + +### stabilizer_probability_of(ptr, bits, len, out) + +Returns the probability of measuring a specific computational-basis outcome: a bitstring compatible with the stabilizer state returns `1/2^k`, where `k` is the number of qubits with random outcomes; an incompatible one returns 0. The call does not modify the state. + +Parameters: + +- `ptr` (`const CStabilizerState*`): stabilizer state handle. +- `bits` (`const bool*`): `len` booleans where `bits[0]` is qubit 0. +- `len` (`uintptr_t`): must equal the number of qubits. +- `out` (`double*`): probability output. + +Returns: 0 on success; -1 for NULL pointers; -8 on a length mismatch. + +--- + +## Generators and Pauli Expectations + +### stabilizer_num_qubits(ptr) + +Returns the number of qubits; 0 for NULL. + +### stabilizer_get_stabilizers_len(ptr) / stabilizer_get_stabilizer(ptr, index) + +Reads the stabilizer generators: `*_len` returns the generator count (always N), and `get_stabilizer` returns the `index`-th generator as a `"+XYZ"`-style Pauli label (bit-order convention in [Pauli](6_pauli.md)); free with `cqlib_string_free`. Out-of-bounds indices return NULL. + +### stabilizer_get_destabilizers_len(ptr) / stabilizer_get_destabilizer(ptr, index) + +Reads the destabilizer generators: `*_len` returns the generator count (always N), and `get_destabilizer` returns the `index`-th generator as a Pauli label in the same `"+XYZ"` format as `stabilizer_get_stabilizer`; free with `cqlib_string_free`. Out-of-bounds indices return NULL. + +### stabilizer_pauli_expectation(ptr, pauli, out) + +Computes the Pauli expectation value ⟨P⟩: `1` when `P` belongs to the stabilizer group, `-1` when it belongs to the negated stabilizer group, and `0` otherwise. The test covers products of generators, not just individual generators. + +Parameters: + +- `ptr` (`const CStabilizerState*`): stabilizer state handle. +- `pauli` (`const CPauliString*`): Pauli string whose qubit count must match the state. +- `out` (`int32_t*`): expectation output (-1, 0, or +1). + +Returns: 0 on success; -1 for NULL pointers; -8 when the Pauli string width differs from the state. + +### stabilizer_to_stim_format(ptr) + +Exports the generator table in Stim-compatible text format (one `"+XYZ"` Pauli label per line). + +Returns: a C string; free with `cqlib_string_free`. NULL input returns NULL. + +--- + +## Example + +```c +#include +#include "cqlib_c.h" + +int main(void) { + /* 1. Bell state: H(0) + CX(0, 1) */ + struct CStabilizerState *s = stabilizer_new(2); + if (s == NULL) { + return 1; + } + stabilizer_apply_h(s, 0); + stabilizer_apply_cx(s, 0, 1); + + /* 2. Read a generator label ("+XY" style) and a Pauli expectation */ + char *stab = stabilizer_get_stabilizer(s, 0); + struct CPauliString *zz = pauli_string_parse("ZZ"); + int32_t e_zz = 0; + stabilizer_pauli_expectation(s, zz, &e_zz); /* 1: ZZ stabilizes the Bell state */ + cqlib_string_free(stab); + pauli_string_free(zz); + + /* 3. Probability of a specific basis state: |00> and |11> split evenly */ + bool bits[2] = {false, false}; + double p00 = 0.0; + stabilizer_probability_of(s, bits, 2, &p00); /* 0.5 */ + + /* 4. Sample 500 shots and traverse the OutcomeList */ + struct COutcomeList *shots = stabilizer_sample_shots(s, 500); + uintptr_t n = outcome_list_len(shots); /* 500 */ + for (uintptr_t i = 0; i < n; i++) { + char *bs = outcome_list_get(shots, i); /* "00" or "11" */ + cqlib_string_free(bs); + } + outcome_list_free(shots); + + stabilizer_free(s); + return 0; +} +``` diff --git a/docs/api/en/c/3_qis/5_classical_state.md b/docs/api/en/c/3_qis/5_classical_state.md new file mode 100644 index 00000000..c8dcb252 --- /dev/null +++ b/docs/api/en/c/3_qis/5_classical_state.md @@ -0,0 +1,164 @@ +# Classical State (C) + +This page covers three handle kinds: the measurement descriptor `CMeasurement` (binding measured qubits to the result bit order), the immutable classical value `CClassicalValue` produced by a measurement, and the sampling outcome list `COutcomeList`. Error codes, string and handle ownership conventions follow the [Overview](../0_overview.md). + +--- + +## Measurement Descriptors + +### measurement_new(value_index, ty_tag, ty_width, qubits, len) + +Creates a standalone measurement descriptor from raw components: `qubits[0]` becomes result bit 0 (the least-significant bit), `qubits[1]` becomes result bit 1, and so on. + +This constructor targets the self-contained qubit-order utilities (`measurement_project`, `measurement_project_basis`, `measurement_check_qubits`): its internal classical value is built with a fresh circuit identity and belongs to no circuit, so expressions derived from it are rejected by `circuit_validate_classical_expr`; build circuit-integrated measurements with `circuit_measure` / `circuit_measure_bits` instead (see [Classical Data and Control Flow](../0_circuit/9_classical_control_flow.md)). + +Parameters: + +- `value_index` (`uint32_t`): value-table index. +- `ty_tag` (`uint32_t`): result type tag, one of `CQLIB_CLASSICAL_TYPE_*` (`Bit` and `BitVec` are the direct measurement-target types). +- `ty_width` (`uint32_t`): result type width. +- `qubits` (`const uint32_t*`): measured qubit indices in result bit order. +- `len` (`uintptr_t`): number of measured qubits. + +Returns: a new `CMeasurement*` on success; NULL for NULL `qubits`, an empty list, or an invalid type tag. + +### measurement_free(ptr) + +Frees a measurement handle; NULL is allowed. + +### measurement_value(ptr) + +Returns the immutable classical value produced by this measurement. + +Returns: a new `CClassicalValue*` (free with `classical_value_free`); NULL on NULL input. + +### measurement_expr(ptr) + +Creates an expression that reads this measurement's immutable value. + +Returns: a new `CClassicalExpr*` (free with `classical_expr_free`); NULL on NULL input. + +### measurement_qubits_len(ptr) / measurement_qubits(ptr, buffer, len) + +Two-step read of the measured qubit indices (in result bit order): `*_len` returns the count (0 for NULL), and the fill function copies the indices into `buffer`. + +Returns (fill function): 0 on success; -1 for NULL pointers; -8 when `len` differs from `measurement_qubits_len`. + +### measurement_width(ptr) + +Returns the number of measured bits; 0 for NULL. + +### measurement_ty(ptr, tag, width) + +Writes the result type to the out-parameters. + +Returns: 0 on success (`*tag` is one of `CQLIB_CLASSICAL_TYPE_*`, `*width` is the type width); -1 for NULL pointers. + +### measurement_check_qubits(ptr, num_qubits) + +Checks that all measured qubits are valid for a state of `num_qubits` qubits. + +Returns: 0 on success; -1 for NULL input; -2 when a qubit index is out of bounds. + +### measurement_project(ptr, bitstring) + +Projects a full-register outcome onto this measurement's qubit order: `bitstring` is a big-endian `'0'`/`'1'` string over the full register (leftmost character is the highest qubit, matching `statevector_measure_all` output); if `qubits[i]` is one in the outcome, result bit `i` is set. + +Returns: the projected outcome of `measurement_width` bits as a big-endian bitstring; free with `cqlib_string_free`. NULL on NULL input, invalid UTF-8, or an invalid bitstring. + +### measurement_project_basis(ptr, basis) + +Projects a computational-basis index onto this measurement's qubit order: if bit `qubits[i]` is one in `basis`, result bit `i` is set. + +Returns: the projected outcome of `measurement_width` bits as a big-endian bitstring; free with `cqlib_string_free`. NULL on NULL input. + +--- + +## Classical Values + +### classical_value_free(ptr) + +Frees a classical value handle; NULL is allowed. + +### classical_value_index(ptr) + +Returns the circuit-local value-table index; `UINT32_MAX` for NULL. + +### classical_value_ty(ptr, tag, width) + +Writes the value type to the out-parameters. + +Returns: 0 on success (`*tag` is one of `CQLIB_CLASSICAL_TYPE_*`, `*width` is the type width); -1 for NULL pointers. + +### classical_value_expr(ptr) + +Creates an expression that reads this immutable runtime value. + +Returns: a new `CClassicalExpr*` (free with `classical_expr_free`); NULL on NULL input. + +--- + +## Sampling Outcome Lists + +The `*_sample_shots` entry points (see [Statevector](1_statevector.md), [Stabilizer](4_stabilizer.md), etc.) return a `COutcomeList*`: each entry is a big-endian binary bitstring whose leftmost character corresponds to qubit N-1 and rightmost character to qubit 0. + +### outcome_list_free(ptr) + +Frees an outcome list; NULL is allowed. + +### outcome_list_len(ptr) + +Returns the number of bitstrings; 0 for NULL. + +### outcome_list_get(ptr, index) + +Returns the bitstring at `index`. + +Returns: a heap-allocated C string; free with `cqlib_string_free`. NULL on out-of-bounds. + +--- + +## Example + +Full flow of declaring a measurement, sampling, and traversing the OutcomeList: + +```c +#include +#include "cqlib_c.h" + +int main(void) { + /* 1. Build the Bell circuit and declare a measurement */ + struct CCircuit *qc = circuit_new(2); + circuit_h(qc, 0); + circuit_cx(qc, 0, 1); + if (circuit_measure(qc, 0) != 0) { + circuit_free(qc); + return 1; + } + + /* 2. Exact simulation, then 100 samples */ + struct CStatevector *sv = statevector_from_circuit(qc); + struct COutcomeList *shots = statevector_sample_shots(sv, 100); + + /* 3. Traverse the OutcomeList (big-endian: leftmost char is qubit N-1) */ + uintptr_t n = outcome_list_len(shots); /* 100 */ + for (uintptr_t i = 0; i < n; i++) { + char *bs = outcome_list_get(shots, i); /* "00" or "11" */ + printf("%s\n", bs); + cqlib_string_free(bs); + } + + outcome_list_free(shots); + statevector_free(sv); + circuit_free(qc); + + /* 4. Standalone measurement: qubits[0] is result bit 0, full-register projection */ + uint32_t qubits[2] = {1, 0}; + struct CMeasurement *m = + measurement_new(0, CQLIB_CLASSICAL_TYPE_BIT_VEC, 2, qubits, 2); + char *proj = measurement_project(m, "01"); /* "10" */ + cqlib_string_free(proj); + measurement_free(m); + return 0; +} +``` diff --git a/docs/api/en/c/3_qis/6_pauli.md b/docs/api/en/c/3_qis/6_pauli.md new file mode 100644 index 00000000..d42ac6f3 --- /dev/null +++ b/docs/api/en/c/3_qis/6_pauli.md @@ -0,0 +1,206 @@ +# Pauli (C) + +`CPauliString` is an opaque handle to a Pauli string: each of N qubits carries an `I`/`X`/`Y`/`Z` operator plus an overall phase. Single-qubit operators use the `PAULI_*` constants, and the text format of a string is `[+|-][i|j]`. Error codes, string and handle ownership conventions follow the [Overview](../0_overview.md). + +--- + +## Constants and Format Conventions + +Single-qubit Pauli operator tags: + +| Constant | Value | Operator | +| --- | --- | --- | +| `PAULI_I` | 0 | Identity I. | +| `PAULI_X` | 1 | Pauli X. | +| `PAULI_Y` | 2 | Pauli Y. | +| `PAULI_Z` | 3 | Pauli Z. | + +Text format and bit order: + +- The format is `[+|-][i|j]` with operators `I`, `X`, `Y`, `Z`; the phase is one of `+`, `-`, `+i` (also written `+j`), `-i` (also written `-j`), defaulting to `+` when omitted. +- The bit order is reversed with respect to reading order: **the first character of the string corresponds to the highest qubit index**. For example, after parsing `"XZI"`, qubit 2 carries `X`, qubit 1 carries `Z`, and qubit 0 carries `I`; `pauli_string_to_string` outputs with the same convention (e.g. `"+XZI"`). + +--- + +## Single-Qubit Operator Multiplication + +### pauli_mul_with_phase(left, right, out_pauli, out_phase) + +```c +int32_t pauli_mul_with_phase(uint8_t left, uint8_t right, uint8_t *out_pauli, Complex64 *out_phase); +``` + +Multiplies two single-qubit Pauli operators as `left * right` and writes the resulting operator tag (one of `PAULI_I/X/Y/Z`) to `*out_pauli` and the Pauli-group phase factor (one of `1`, `i`, `-1`, `-i`) to `*out_phase`; for example `X * Z = -iY` and `Z * X = iY`. + +Returns: `0` on success; `-1` when an output pointer is NULL; `-8` when an input tag is not one of `PAULI_I/X/Y/Z`. + +--- + +## Construction and Release + +### pauli_string_new(num_qubits) + +Creates an all-identity Pauli string. + +Parameters: + +- `num_qubits` (`uintptr_t`): number of qubits. + +Returns: a new `CPauliString*` on success; NULL when `num_qubits` is 0. + +### pauli_string_free(ptr) + +Frees a Pauli string handle; NULL is allowed. + +### pauli_string_parse(source) + +Parses a C string in the `[+|-][i|j]` format (e.g. `"XYZ"`, `"+ZZI"`, `"-iZII"`). + +Returns: a new `CPauliString*` on success; NULL on NULL input, invalid UTF-8, or parse failure. + +--- + +## Operator Access + +### pauli_string_set_pauli(ptr, idx, pauli) + +Sets the operator at qubit `idx` to `pauli` (one of `PAULI_*`). + +Returns: 0 on success; -1 for NULL pointers; -2 for an out-of-bounds `idx`; -8 for an invalid operator tag. + +### pauli_string_get_pauli(ptr, idx) + +Reads the operator at qubit `idx`. + +Returns: one of `PAULI_I`/`PAULI_X`/`PAULI_Y`/`PAULI_Z`; 255 (`0xFF`) for NULL input or out-of-bounds. + +### pauli_string_try_get_pauli(ptr, idx, out) + +Non-panicking operator read: writes the operator tag to `*out`. + +Returns: 0 on success; -1 for NULL pointers; -2 for out-of-bounds. + +### pauli_string_try_set_pauli(ptr, idx, pauli) + +Non-panicking operator write: `pauli` must be one of `PAULI_*`. + +Returns: 0 on success; -1 for NULL pointers; -2 for out-of-bounds; -8 for an invalid operator tag. + +--- + +## Attribute Queries + +### pauli_string_num_qubits(ptr) + +Returns the number of qubits the string acts on; 0 for NULL. + +### pauli_string_to_string(ptr) + +Formats the Pauli string as a `"+XYZ"`-style C string (first character is the highest qubit). + +Returns: a heap-allocated string; free with `cqlib_string_free`. NULL input returns NULL. + +### pauli_string_x_mask(ptr) + +Returns the X-component bitmask: bit `i` is set when qubit `i` carries `X` or `Y`; 0 for NULL. + +### pauli_string_z_mask(ptr) + +Returns the Z-component bitmask: bit `i` is set when qubit `i` carries `Z` or `Y`; 0 for NULL. + +### pauli_string_y_phase(ptr, out) + +Computes the phase factor `i^n` contributed by the n `Y` operators of the string. + +Returns: 0 on success (complex value written to `*out`); -1 for NULL pointers. + +### pauli_string_support_len(ptr) / pauli_string_support(ptr, buffer, len) + +Two-step read of the support: `*_len` returns the number of non-identity qubits (0 for NULL), and the fill function copies the support indices (ascending) into `buffer` as `uint32_t` values. + +Returns (fill function): 0 on success; -1 for NULL pointers; -8 when `len` differs from `pauli_string_support_len`. + +--- + +## Commutation and Matrices + +### pauli_string_commutes_with(ptr, other) + +Tests whether two Pauli strings commute. + +Returns: 1 when they commute; 0 otherwise; -1 for NULL handles; -8 when the qubit counts differ. + +### pauli_string_matrix_len(ptr) + +Returns the side length of the dense matrix (`2^N`); 0 for NULL. + +### pauli_string_matrix(ptr, buffer, len) + +Copies the `2^N × 2^N` dense matrix into `buffer` in row-major order (`Complex64` values). `len` must equal `pauli_string_matrix_len(ptr) * pauli_string_matrix_len(ptr)`. + +Returns: 0 on success; -1 for NULL pointers; -8 when `len` does not match the matrix element count. + +--- + +## Expectation Values + +### pauli_string_expectation(ptr, bitstrings, probs, len, out) + +Computes the expectation value of the Pauli string over a probability distribution on the computational basis, `⟨P⟩ = Σ_s p(s)·⟨s|P|s⟩`, given as parallel arrays: `bitstrings[i]` pairs with `probs[i]`. + +Conventions: + +- Each bitstring uses the little-endian convention (the rightmost character is qubit 0) and must be exactly `num_qubits` characters of `'0'`/`'1'`. +- Strings containing `X` or `Y` operators yield `0.0`. +- Strings containing only `I` and `Z` evaluate `phase × Σ_s p(s) × (-1)^(Σ_i z[i]·s[i])`, where `phase` is the string's global phase and `z[i]` is the Z component of qubit `i`. + +Parameters: + +- `ptr` (`const struct CPauliString*`): Pauli string handle. +- `bitstrings` (`const char* const*`): array of NUL-terminated bitstrings; may be NULL when `len` is 0. +- `probs` (`const double*`): probability array parallel to `bitstrings`; may be NULL when `len` is 0. +- `len` (`uintptr_t`): number of distribution entries. +- `out` (`double*`): output pointer. + +Returns: 0 on success; -1 when `ptr` or `out` is NULL, or when either array is NULL while `len > 0`; -4 when a bitstring is not valid UTF-8; -7 when a bitstring has the wrong length or contains characters other than `'0'`/`'1'`; -8 when a bitstring array entry is NULL. + +--- + +## Example + +```c +#include +#include +#include "cqlib_c.h" + +int main(void) { + /* 1. Parsing: first character is the highest qubit, "XZI" -> qubit2=X, qubit1=Z, qubit0=I */ + struct CPauliString *ps = pauli_string_parse("XZI"); + uintptr_t n = pauli_string_num_qubits(ps); /* 3 */ + uint8_t p2 = pauli_string_get_pauli(ps, 2); /* PAULI_X (1) */ + uint8_t p0 = pauli_string_get_pauli(ps, 0); /* PAULI_I (0) */ + + /* 2. Masks and support */ + uintptr_t x_mask = pauli_string_x_mask(ps); /* bit 2 -> 0b100 */ + uintptr_t z_mask = pauli_string_z_mask(ps); /* bit 1 -> 0b010 */ + uintptr_t support_len = pauli_string_support_len(ps); /* 2 */ + uint32_t support[2]; + pauli_string_support(ps, support, support_len); /* {1, 2} */ + + /* 3. Text output and commutation */ + char *text = pauli_string_to_string(ps); /* "+XZI" */ + struct CPauliString *zz = pauli_string_parse("ZZI"); + int32_t commutes = pauli_string_commutes_with(ps, zz); /* 0 (X and Z anticommute on qubit 2) */ + + /* 4. Two-step read of the 8x8 dense matrix */ + uintptr_t dim = pauli_string_matrix_len(ps); /* 8 */ + Complex64 *matrix = malloc(dim * dim * sizeof(Complex64)); + pauli_string_matrix(ps, matrix, dim * dim); + free(matrix); + + cqlib_string_free(text); + pauli_string_free(zz); + pauli_string_free(ps); + return 0; +} +``` diff --git a/docs/api/en/c/3_qis/7_hamiltonian.md b/docs/api/en/c/3_qis/7_hamiltonian.md new file mode 100644 index 00000000..08f1b73b --- /dev/null +++ b/docs/api/en/c/3_qis/7_hamiltonian.md @@ -0,0 +1,282 @@ +# Hamiltonian (C) + +`CHamiltonian` is an opaque handle to an observable (Hamiltonian): a sum of Pauli-string terms with complex coefficients, supporting simplification, scaling, dense matrix output, and time-evolution circuit construction. Error codes, string and handle ownership conventions follow the [Overview](../0_overview.md). + +--- + +## Constants + +Trotter-Suzuki decomposition mode tags (the `mode` parameter of `hamiltonian_to_trotter_circuit` and `hamiltonian_to_evolution_circuit`): + +| Constant | Value | Mode | +| --- | --- | --- | +| `TROTTER_MODE_FIRST_ORDER` | 0 | First-order Lie-Trotter. | +| `TROTTER_MODE_SECOND_ORDER` | 1 | Second-order Strang splitting. | + +--- + +## Construction and Release + +### hamiltonian_new(num_qubits) + +Creates an empty Hamiltonian (no Pauli terms) acting on `num_qubits` qubits. + +Parameters: + +- `num_qubits` (`uintptr_t`): number of qubits. + +Returns: a new `CHamiltonian*`. + +### hamiltonian_free(ptr) + +Frees a Hamiltonian handle; NULL is allowed. + +### hamiltonian_from_pauli(pauli) + +Constructs a Hamiltonian from a single Pauli string (coefficient 1.0). + +**Ownership**: this function takes ownership of the input `CPauliString*` and frees it; after the call, do not use or free the original handle again. + +Parameters: + +- `pauli` (`CPauliString*`): Pauli string handle whose ownership is taken. + +Returns: a new `CHamiltonian*` on success; NULL on NULL input. + +### hamiltonian_add_term(ptr, pauli, coeff_re, coeff_im) + +Adds a Pauli term with a complex coefficient. + +**Ownership**: the Pauli string is cloned; the input handle is not consumed and remains owned by the caller. + +Parameters: + +- `ptr` (`CHamiltonian*`): Hamiltonian handle. +- `pauli` (`const CPauliString*`): Pauli string handle (cloned). +- `coeff_re` (`double`): real part of the coefficient. +- `coeff_im` (`double`): imaginary part of the coefficient. + +Returns: 0 on success; -1 for NULL pointers; -8 when the Pauli string width differs from the Hamiltonian. + +### hamiltonian_simplify(ptr) + +Simplifies in place: combines terms with the same Pauli string and removes near-zero coefficients. + +Returns: 0 on success; -1 for NULL pointers. + +### hamiltonian_scale(ptr, factor_re, factor_im) + +Scales all coefficients by a complex factor in place. + +Parameters: + +- `factor_re` (`double`): real part of the factor. +- `factor_im` (`double`): imaginary part of the factor. + +Returns: 0 on success; -1 for NULL pointers; -8 for a non-finite factor (NaN or infinity). + +--- + +## Queries + +### hamiltonian_num_qubits(ptr) + +Returns the number of qubits; 0 for NULL. + +### hamiltonian_num_terms(ptr) + +Returns the number of Pauli terms; 0 for NULL. + +### hamiltonian_all_terms_commute(ptr) + +Tests whether all Pauli terms mutually commute. When they do, the time evolution decomposes exactly into per-term Pauli rotations. + +Returns: 1 when all terms commute; 0 otherwise; -1 for NULL. + +--- + +## Dense Matrix Output + +### hamiltonian_to_matrix_len(ptr) / hamiltonian_to_matrix(ptr, buffer, len) + +Two-step read of the dense matrix: `*_len` returns the side length `2^N` (0 for NULL), and the fill function copies the `2^N × 2^N` matrix into `buffer` in row-major order (`Complex64` values). `len` must equal the square of the side length. + +Returns (fill function): 0 on success; -1 for NULL pointers; -8 when `len` does not match the matrix element count. + +--- + +## Expectation Values and Variance + +### CProbMeasurement + +Descriptor of one measured basis and its observed outcome distribution, used by `hamiltonian_expectation_probs`: + +```c +typedef struct CProbMeasurement { + const struct CPauliString *basis; /* borrowed, not consumed */ + const char *const *states; /* len computational-basis bitstrings */ + const double *probs; /* len probabilities, paired with states */ + uintptr_t len; /* number of entries in states and probs */ +} CProbMeasurement; +``` + +- `basis` (`const CPauliString*`): measurement basis as a Pauli string; borrowed, not consumed, and freed by the caller. +- `states` (`const char* const*`): array of `len` computational-basis bitstrings, one `'0'`/`'1'` character per qubit, big-endian (qubit 0 is the last character). +- `probs` (`const double*`): array of `len` observed probabilities, paired with `states` by index. +- `len` (`uintptr_t`): number of entries in `states` and `probs`. + +### hamiltonian_expectation_probs(ptr, measurements, measurements_len, out) + +Computes the expectation value of the Hamiltonian from measurement outcome probabilities. `measurements` lists one entry per measured basis; each term of the Hamiltonian is evaluated in the first compatible measured basis — a basis is compatible with a term when it agrees with the term on every non-identity factor. The result is written to `out`. The distributions only need to cover the observed outcomes; unlisted outcomes contribute zero. + +```c +int32_t hamiltonian_expectation_probs(const struct CHamiltonian *ptr, + const struct CProbMeasurement *measurements, + uintptr_t measurements_len, + double *out); +``` + +Parameters: + +- `ptr` (`const CHamiltonian*`): Hamiltonian handle. +- `measurements` (`const CProbMeasurement*`): array of `measurements_len` measurement descriptors. +- `measurements_len` (`uintptr_t`): number of entries in `measurements`; `0` supplies no measured basis. +- `out` (`double*`): receives the expectation value. + +Error codes: + +| Value | Scenario | +| --- | --- | +| `0` | Success. | +| `-1` | `ptr` or `out` is NULL; `measurements` is NULL with a non-zero count; or an entry has a NULL `basis`, or NULL `states`/`probs`/bitstring with a non-zero `len`. | +| `-4` | A bitstring in `states` is not valid UTF-8. | +| `-7` | A term has no compatible measurement basis, a bitstring is malformed or its length differs from the Hamiltonian's qubit count, or another core failure. | + +### hamiltonian_variance_statevector(ptr, sv, out) + +Computes the variance of the Hamiltonian for a statevector, `Var(H) = - ^2`, and writes it to `out`. + +```c +int32_t hamiltonian_variance_statevector(const struct CHamiltonian *ptr, + const struct CStatevector *sv, + double *out); +``` + +Parameters: + +- `ptr` (`const CHamiltonian*`): Hamiltonian handle. +- `sv` (`const CStatevector*`): input statevector; its qubit count must match the Hamiltonian's. +- `out` (`double*`): receives the variance. + +Error codes: + +| Value | Scenario | +| --- | --- | +| `0` | Success. | +| `-1` | `ptr`, `sv`, or `out` is NULL. | +| `-7` | The Hamiltonian has non-Hermitian terms, or another simulation error. | +| `-8` | The statevector's qubit count does not match the Hamiltonian, or another invalid parameter. | + +--- + +## Time-Evolution Circuits + +### hamiltonian_to_trotter_circuit(ptr, time, steps, mode) + +Converts the Hamiltonian to a Trotterized time-evolution circuit approximating `U(t) = exp(-i H t)`. + +Parameters: + +- `ptr` (`const CHamiltonian*`): Hamiltonian handle. +- `time` (`double`): evolution time; must be finite. +- `steps` (`uintptr_t`): number of Trotter steps. +- `mode` (`uint32_t`): `TROTTER_MODE_FIRST_ORDER` or `TROTTER_MODE_SECOND_ORDER`. + +Returns: a new `CCircuit*` on success (free with `circuit_free`); NULL on NULL input, a non-finite time, an invalid mode, or construction failure. + +### hamiltonian_to_evolution_circuit(ptr, time, steps, mode) + +Converts the Hamiltonian to a time-evolution circuit: uses the exact single-pass decomposition when all terms commute, and falls back to the Trotter approximation (`mode`, `steps`) otherwise. + +Parameters: + +- `ptr` (`const CHamiltonian*`): Hamiltonian handle. +- `time` (`double`): evolution time; must be finite. +- `steps` (`uintptr_t`): number of Trotter steps. +- `mode` (`uint32_t`): `TROTTER_MODE_FIRST_ORDER` or `TROTTER_MODE_SECOND_ORDER`. + +Returns: a new `CCircuit*` on success (free with `circuit_free`); NULL on NULL input, a non-finite time, an invalid mode, or construction failure. + +--- + +## Example + +```c +#include +#include +#include "cqlib_c.h" + +int main(void) { + /* 1. Build H = 0.5 * ZI + 0.5 * IZ ("ZI": qubit1=Z, qubit0=I) */ + struct CHamiltonian *ham = hamiltonian_new(2); + struct CPauliString *zi = pauli_string_parse("ZI"); + struct CPauliString *iz = pauli_string_parse("IZ"); + hamiltonian_add_term(ham, zi, 0.5, 0.0); /* cloned; handle stays valid */ + hamiltonian_add_term(ham, iz, 0.5, 0.0); + pauli_string_free(zi); + pauli_string_free(iz); + + /* 2. Queries and simplification */ + uintptr_t nq = hamiltonian_num_qubits(ham); /* 2 */ + uintptr_t nt = hamiltonian_num_terms(ham); /* 2 */ + int32_t commute = hamiltonian_all_terms_commute(ham); /* 1 (single-qubit terms commute) */ + hamiltonian_simplify(ham); + + /* 3. Two-step read of the 4x4 dense matrix */ + uintptr_t dim = hamiltonian_to_matrix_len(ham); /* 4 */ + Complex64 *matrix = malloc(dim * dim * sizeof(Complex64)); + hamiltonian_to_matrix(ham, matrix, dim * dim); + free(matrix); + + /* 4. Time-evolution circuit (exact when terms commute) */ + struct CCircuit *evo = + hamiltonian_to_evolution_circuit(ham, 0.1, 1, TROTTER_MODE_FIRST_ORDER); + if (evo != NULL) { + circuit_free(evo); + } + + /* 5. Construct from a single Pauli string (ownership taken) */ + struct CPauliString *zz = pauli_string_parse("ZZ"); + struct CHamiltonian *from_pauli = hamiltonian_from_pauli(zz); /* zz is consumed */ + + /* 6. Expectation value from measured outcome probabilities, and the + statevector variance (H = Z⊗Z on the Bell state, P(00) = P(11) = 0.5) */ + struct CStatevector *bell = statevector_new(2); + statevector_apply_h(bell, 0); + statevector_apply_cx(bell, 0, 1); + + struct CPauliString *basis = pauli_string_parse("ZZ"); + const char *states[4] = {"00", "01", "10", "11"}; + double probs[4] = {0.5, 0.0, 0.0, 0.5}; + struct CProbMeasurement measurement; + measurement.basis = basis; /* borrowed, not consumed */ + measurement.states = states; + measurement.probs = probs; + measurement.len = 4; + + double expectation = 0.0; + hamiltonian_expectation_probs(from_pauli, &measurement, 1, &expectation); /* 1.0 */ + + double variance = 0.0; + hamiltonian_variance_statevector(from_pauli, bell, &variance); /* 0.0 (eigenstate of Z⊗Z) */ + + pauli_string_free(basis); + statevector_free(bell); + + hamiltonian_free(from_pauli); + hamiltonian_free(ham); + return 0; +} +``` + +Hamiltonians serve as observables in the error-mitigation workflows (see [Zero-Noise Extrapolation](../6_error_mitigation/1_zne.md) and [Virtual Distillation](../6_error_mitigation/2_virtual_distillation.md)); their expectation values can be computed directly with the statevector simulator (see [Statevector](1_statevector.md)). diff --git a/docs/api/en/c/3_qis/8_evolution.md b/docs/api/en/c/3_qis/8_evolution.md new file mode 100644 index 00000000..f65584fc --- /dev/null +++ b/docs/api/en/c/3_qis/8_evolution.md @@ -0,0 +1,94 @@ +# Pauli Evolution (C) + +This page covers exponentiating a single Pauli string `P` into the evolution operator `e^(-i·θ/2·P)` and appending it to a circuit. Time-evolution circuits for a full Hamiltonian (Trotter decomposition, and the automatic choice between the exact decomposition and the Trotter approximation) are provided by `hamiltonian_to_trotter_circuit` and `hamiltonian_to_evolution_circuit` on the [Hamiltonian](7_hamiltonian.md) page. Error codes, handle and string ownership conventions follow the [Overview](../0_overview.md). + +--- + +## circuit_pauli_evolution(circuit, pauli, angle, qubits, qubits_len) + +Appends the evolution operator `e^(-i·θ/2·P)` to the end of the circuit, where `θ` is `angle`. + +```c +int32_t circuit_pauli_evolution(struct CCircuit *circuit, + const struct CPauliString *pauli, + double angle, + const uint32_t *qubits, + uintptr_t qubits_len); +``` + +Generated gate sequence: + +1. **Phase check**: the Pauli string phase must be ±1 (Hermitian); a phase of ±i cannot generate a unitary evolution and returns `-3`. +2. **Phase absorption**: the ±1 phase is absorbed into the rotation angle, giving the effective angle `θ_eff = θ · phase`. +3. **Basis change**: each non-identity qubit receives gates that rotate its component into the Z basis — `H` for `X`, `S†` then `H` for `Y`; `Z` needs no change. +4. **CNOT ladder**: `CX` gates are applied in increasing order of the non-identity qubit indices, accumulating parity level by level. +5. **Core rotation**: `RZ(θ_eff)` is applied on the last non-identity qubit. +6. **Reverse CNOT ladder and inverse basis changes** restore the qubits to their original basis. + +When the Pauli string consists entirely of identity operators, the evolution reduces to the global phase `e^(-i·θ/2)` and produces no gates; the phase is recorded on the circuit's global phase. + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `circuit` | `CCircuit*` | Target circuit handle; the evolution gates are appended to its end. | +| `pauli` | `const CPauliString*` | Pauli string `P` to exponentiate; construction is covered in [Pauli](6_pauli.md). | +| `angle` | `double` | Rotation angle `θ` (fixed floating-point value). | +| `qubits` | `const uint32_t*` | Array of circuit positions, ordered like the Pauli string positions. | +| `qubits_len` | `uintptr_t` | Number of positions; must equal `pauli_string_num_qubits(pauli)`. | + +Return value (error codes): + +| Value | Scenario | +| --- | --- | +| `0` | Success. | +| `-1` | `circuit` or `pauli` is NULL; or `qubits_len > 0` while `qubits` is NULL. | +| `-2` | A qubit index in `qubits` is out of bounds. | +| `-3` | The position count does not match the Pauli string qubit count, or the Pauli string phase is ±i. | +| `-8` | `angle` is not finite (NaN or infinity). | + +--- + +## Example + +```c +#include +#include +#include "cqlib_c.h" + +int main(void) { + /* Append e^(-i * pi/4 * XZ): basis changes, a CNOT ladder, + one RZ, and the inverse ladder. */ + CCircuit* circuit = circuit_new(2); + CPauliString* pauli = pauli_string_parse("XZ"); + assert(pauli != NULL); + + const uint32_t qubits[2] = {0, 1}; + assert(circuit_pauli_evolution(circuit, pauli, 1.5707963267948966, qubits, 2) == 0); + assert(circuit_num_operations(circuit) > 0); + + /* NULL arguments are rejected. */ + assert(circuit_pauli_evolution(NULL, pauli, 1.0, qubits, 2) == -1); + + /* Out-of-bounds qubit index. */ + const uint32_t bad[2] = {0, 5}; + assert(circuit_pauli_evolution(circuit, pauli, 1.0, bad, 2) == -2); + + /* Position-count mismatch: a 3-qubit Pauli string on 2 positions. */ + CPauliString* big = pauli_string_parse("XXX"); + assert(circuit_pauli_evolution(circuit, big, 1.0, qubits, 2) == -3); + + pauli_string_free(big); + pauli_string_free(pauli); + circuit_free(circuit); + return 0; +} +``` + +--- + +## Related pages + +- [QIS Overview](0_overview.md): module overview and shared conventions. +- [Pauli](6_pauli.md): Pauli string parsing from text and phase values. +- [Hamiltonian](7_hamiltonian.md): Trotter and exact evolution entries for a full Hamiltonian. diff --git a/docs/api/en/c/3_qis/9_metrics_entropy.md b/docs/api/en/c/3_qis/9_metrics_entropy.md new file mode 100644 index 00000000..d6cfd9c4 --- /dev/null +++ b/docs/api/en/c/3_qis/9_metrics_entropy.md @@ -0,0 +1,190 @@ +# Metrics and Entropy (C) + +This page covers quantum information metrics (purity, fidelity, trace distance) and entropy/entanglement measures (Von Neumann entropy, Renyi entropy, entanglement entropy, negativity, concurrence, entanglement of formation), taking `CStatevector` or `CDensityMatrix` handles as input. Error codes, string and handle ownership conventions follow the [Overview](../0_overview.md). + +--- + +## Shared Conventions + +- Every function returns `int32_t`: 0 on success (value written to `out`), -1 for NULL pointers, other negative values for simulator errors (typically -8: dimension mismatch, non-normalized input, non-2-qubit input). +- Subsystem parameters (`qubits`/`subsys_a`/`sys_a` + `len`): `len` 0 denotes an empty subsystem and is valid; a NULL pointer with `len > 0` returns -1. +- `density_matrix_partial_transpose` returns a new handle; all other functions write values through out pointers. + +--- + +## Purity and Fidelity + +### statevector_purity(sv, out) + +Computes the purity norm of a statevector (1.0 for a normalized pure state). + +Returns: 0 on success; -1 for NULL pointers. + +### density_matrix_purity(dm, out) + +Computes the purity `Tr(rho^2)` of a density matrix. + +Returns: 0 on success; -1 for NULL pointers. + +### statevector_fidelity(sv1, sv2, out) + +Computes the state fidelity `||^2` between two statevectors. + +Returns: 0 on success; -1 for NULL pointers. + +### statevector_fidelity_pure_mixed(sv, dm, out) + +Computes the fidelity `` between a pure statevector and a density matrix. + +Returns: 0 on success; -1 for NULL pointers. + +### density_matrix_fidelity(dm1, dm2, out) + +Computes the state fidelity `(Tr sqrt(sqrt(rho) sigma sqrt(rho)))^2` between two density matrices. + +Returns: 0 on success; -1 for NULL pointers. + +--- + +## Trace Distance + +### statevector_trace_distance(sv1, sv2, out) + +Computes the trace distance `sqrt(1 - ||^2)` between two pure states. + +Returns: 0 on success; -1 for NULL pointers. + +### density_matrix_trace_distance(dm1, dm2, out) + +Computes the trace distance `1/2 Tr|rho - sigma|` between two density matrices. + +Returns: 0 on success; -1 for NULL pointers. + +--- + +## Entropy + +### density_matrix_entropy(dm, out) + +Computes the Von Neumann entropy `-Tr(rho log2 rho)` of a density matrix (in bits). + +Returns: 0 on success; -1 for NULL pointers. + +### density_matrix_linear_entropy(dm, out) + +Computes the linear entropy `S_L = 1 - Tr(rho^2)`. + +Returns: 0 on success; -1 for NULL pointers. + +### density_matrix_renyi_entropy(dm, alpha, out) + +Computes the Renyi entropy of order `alpha` (base-2 logarithm). `alpha` must be a positive finite value; values close to 1 fall back to the Von Neumann entropy. + +Parameters: + +- `alpha` (`double`): Renyi order. + +Returns: 0 on success; -1 for NULL pointers; -8 for a non-finite or non-positive `alpha`. + +--- + +## Entanglement Measures + +### statevector_entanglement_entropy(sv, subsys_a, len, out) + +Computes the bipartite entanglement entropy (in bits) of a pure state with respect to subsystem A: `subsys_a` holds `len` qubit indices. + +Returns: 0 on success; -1 for NULL pointers; -8 for invalid qubit indices or dimensions. + +### statevector_entanglement_entropy_pure(sv, subsys_a, len, out) + +Computes the pure-state bipartite entanglement entropy (in bits, log2 base) of a statevector with respect to subsystem A: `subsys_a` holds `len` qubit indices; for a Bell state with subsystem `{0}` the value is `1.0`. + +Returns: 0 on success; -1 for NULL pointers; -8 for invalid qubit indices or dimensions. + +### density_matrix_negativity(dm, subsys_a, len, out) + +Computes the negativity entanglement measure of a bipartite density matrix: transposes subsystem A and evaluates the trace norm. + +Returns: 0 on success; -1 for NULL pointers; -8 for invalid qubit indices or dimensions. + +### density_matrix_logarithmic_negativity(dm, sys_a, len, out) + +Computes the logarithmic negativity `log2 ||rho^{T_A}||_1` with respect to subsystem A (`sys_a` holds `len` qubit indices). + +Returns: 0 on success; -1 for NULL pointers; -8 for invalid qubit indices or dimensions. + +### density_matrix_concurrence(dm, out) + +Computes the concurrence of a 2-qubit density matrix: 0 for separable states, 1 for maximally entangled states. + +Returns: 0 on success; -1 for NULL pointers; -8 when the input is not a 2-qubit system. + +### density_matrix_entanglement_of_formation(dm, out) + +Computes the entanglement of formation (in bits) of a 2-qubit density matrix, derived from the concurrence. + +Returns: 0 on success; -1 for NULL pointers; -8 when the input is not a 2-qubit system. + +--- + +## Partial Transpose + +### density_matrix_partial_transpose(dm, qubits, len) + +Performs the partial transpose on `qubits` (`len` indices) and returns a new density matrix. + +Returns: a new `CDensityMatrix*` on success (free with `density_matrix_free`); NULL on NULL pointers (including a NULL array with `len > 0`) or failure. + +--- + +## Example + +```c +#include +#include "cqlib_c.h" + +int main(void) { + /* 1. Bell state and its density matrix */ + struct CCircuit *qc = circuit_new(2); + circuit_h(qc, 0); + circuit_cx(qc, 0, 1); + struct CStatevector *sv = statevector_from_circuit(qc); + struct CDensityMatrix *dm = density_matrix_from_circuit(qc); + + /* 2. Purity and fidelity */ + double purity = 0.0; + density_matrix_purity(dm, &purity); /* 1.0: pure state */ + + struct CCircuit *qc2 = circuit_new(2); + circuit_h(qc2, 0); + circuit_cx(qc2, 0, 1); + struct CStatevector *sv2 = statevector_from_circuit(qc2); + double fid = 0.0; + statevector_fidelity(sv, sv2, &fid); /* 1.0: same state */ + + /* 3. Von Neumann entropy and entanglement entropy */ + double entropy = 0.0; + density_matrix_entropy(dm, &entropy); /* 0.0: pure state */ + + uint32_t subsystem[1] = {0}; + double entanglement = 0.0; + statevector_entanglement_entropy(sv, subsystem, 1, &entanglement); /* 1.0 bit */ + + /* 4. Partial transpose and negativity */ + double negativity = 0.0; + density_matrix_negativity(dm, subsystem, 1, &negativity); /* 0.5 */ + + struct CDensityMatrix *pt = density_matrix_partial_transpose(dm, subsystem, 1); + if (pt != NULL) { + density_matrix_free(pt); + } + + statevector_free(sv2); + circuit_free(qc2); + density_matrix_free(dm); + statevector_free(sv); + circuit_free(qc); + return 0; +} +``` diff --git a/docs/api/en/c/4_compile/0_overview.md b/docs/api/en/c/4_compile/0_overview.md new file mode 100644 index 00000000..0c1b9830 --- /dev/null +++ b/docs/api/en/c/4_compile/0_overview.md @@ -0,0 +1,111 @@ +# Compile (C) + +The compile module provides optimization workflows through free functions plus an opaque handle (`CCompileResult`): run logical compilation driven by a `CompileConfigC`, or run routing and native-gate lowering against a `CDevice` target, recording the optimized circuit, change flags, and per-step reports in the compile result. Error codes, string and handle ownership conventions follow the [Overview](../0_overview.md). + +--- + +## Concepts + +- `compile` takes `CompileConfigC` by value: `mode` selects the Normal or Enhanced workflow, and `target` selects the compilation target; non-`Logical` target values are handled as logical compilation. +- `compile_with_device` is the device-target entry point: the circuit is routed on the device topology and lowered to the device's ordered native capabilities, so the resulting circuit is always accepted by the device; an optional initial layout and seed control the mapping process. +- `CCompileResult` carries the optimized circuit (retrieved as a deep clone), the overall change flag, the compile mode, and per-step reports; device-target compilations additionally record the initial and final layouts (`CLayout`, see [Layout](../2_device/3_layout.md)). + +--- + +## Page Navigation + +| Page | Content | +| --- | --- | +| [Compiler](1_compiler.md) | `compile` / `compile_with_device` and `CCompileResult`: construction, result reading, step reports, and layout access. | +| [Transform](2_transform.md) | Standalone transform passes: structural analysis with `circuit_analyze`, canonicalization with `canonicalize_circuit`, knowledge rewriting with `rewrite_circuit`, and the `transform_*` wrappers. | +| [Initial Layout](3_layout.md) | The layout entry points `trivial_layout` / `greedy_layout` / `vf2_perfect_layout` / `sabre_layout`, circuit interaction analysis, the physical layout graph, prepared SABRE objects, and layout result reading. | +| [Routing](4_routing.md) | The routing entry points `route_with_layout` / `route_sabre`, the `CRoutedCircuit` routing result, and layout reachability validation. | +| [SABRE Configuration](5_sabre.md) | The `SabreConfigC` configuration family and `sabre_route` routing from a known initial layout. | +| [Decompose / Resynthesis](6_decompose_resynthesis.md) | Definition expansion, unitary synthesis, multi-controlled-gate decomposition, and two-qubit-block resynthesis. | +| [Knowledge](7_knowledge.md) | Construction, validation, and structural matching of knowledge rules. | +| [Resource](8_resource.md) | Ancilla resource policies, limits, and lease management. | +| [Commutation](9_commutation.md) | Conservative commutation checks between operations. | + +--- + +## CompileConfigC + +The input configuration of `compile`, passed by value: + +```c +typedef struct CompileConfigC { + uint8_t mode; /* One of COMPILE_MODE_*. */ + uint8_t target; /* One of COMPILE_TARGET_*. */ + uint8_t allow_dirty_ancilla; /* Reserved for future use. */ + uint8_t allow_clean_ancilla; /* Reserved for future use. */ + uint8_t _reserved[4]; /* Reserved padding. */ +} CompileConfigC; +``` + +| Field | Type | Meaning | +| --- | --- | --- | +| `mode` | `uint8_t` | Compile mode, one of `COMPILE_MODE_*`. | +| `target` | `uint8_t` | Compile target, one of `COMPILE_TARGET_*`; only `mode` is consulted for `Logical` targets. | +| `allow_dirty_ancilla` | `uint8_t` | Reserved field (resource policy flags); not used by the workflow today. | +| `allow_clean_ancilla` | `uint8_t` | Reserved field (resource policy flags); not used by the workflow today. | +| `_reserved[4]` | `uint8_t[4]` | Padding that keeps the struct layout stable; do not write non-zero values. | + +For `Logical` targets only `mode` is consulted; other target values are handled as logical compilation. All fields are kept in the struct so it can be extended without breaking the ABI. + +--- + +## Constants + +Compile mode tags (the `CompileConfigC.mode` field and the `mode` parameter of `compile_with_device`): + +| Constant | Value | Mode | +| --- | --- | --- | +| `COMPILE_MODE_NORMAL` | 0 | Normal compilation mode. | +| `COMPILE_MODE_ENHANCED` | 1 | Enhanced compilation mode. | + +Compile target tags (the `CompileConfigC.target` field): + +| Constant | Value | Target | +| --- | --- | --- | +| `COMPILE_TARGET_LOGICAL` | 0 | Logical-qubit target. | +| `COMPILE_TARGET_BASIS` | 1 | Basis-gate-set target. | +| `COMPILE_TARGET_DEVICE` | 2 | Device target. | +| `COMPILE_TARGET_TOPOLOGY_BASIS` | 3 | Topology basis-gate-set target. | + +--- + +## Example + +```c +#include +#include "cqlib_c.h" + +int main(void) { + struct CCircuit *qc = circuit_new(2); + circuit_h(qc, 0); + circuit_cx(qc, 0, 1); + + /* Enhanced-mode logical compilation */ + struct CompileConfigC config = {0}; + config.mode = COMPILE_MODE_ENHANCED; + config.target = COMPILE_TARGET_LOGICAL; + + struct CCompileResult *result = compile(qc, config); + if (result == NULL) { + circuit_free(qc); + return 1; + } + + int32_t changed = compile_result_changed(result); /* 1: the workflow changed the circuit */ + struct CCircuit *optimized = compile_result_circuit(result); + if (optimized != NULL) { + circuit_free(optimized); /* deep clone; independent of result */ + } + + compile_result_free(result); + circuit_free(qc); + return 0; +} +``` + +See [Compiler](1_compiler.md) for the full device-target flow, including routing and layout access. diff --git a/docs/api/en/c/4_compile/1_compiler.md b/docs/api/en/c/4_compile/1_compiler.md new file mode 100644 index 00000000..4700c440 --- /dev/null +++ b/docs/api/en/c/4_compile/1_compiler.md @@ -0,0 +1,344 @@ +# Compiler (C) + +This page covers the compilation entry points `compile` / `compile_with_device` and every read accessor of the compile-result handle `CCompileResult`: the optimized circuit, change flags, the compile mode, step reports, and device layouts. Error codes, string and handle ownership conventions follow the [Overview](../0_overview.md); the configuration struct and `COMPILE_*` constants are documented in [Overview](0_overview.md). + +--- + +## Compilation Entry Points + +### compile(circuit, config) + +Runs the compilation workflow with a `CompileConfigC`: `config.mode` selects the Normal or Enhanced workflow; `config.target` selects the `Logical` target, and other target values are handled as logical compilation. + +Parameters: + +- `circuit` (`const CCircuit*`): input circuit. +- `config` (`struct CompileConfigC`): compile configuration (passed by value). + +Returns: a new `CCompileResult*` on success; NULL on NULL input or compilation failure. + +### compile_with_device(circuit, mode, device, initial_layout, seed) + +Runs the compilation workflow for a concrete device target: the circuit is routed on the device topology and lowered to the device's ordered native capabilities, so the resulting circuit is always accepted by the device. + +Parameters: + +- `circuit` (`const CCircuit*`): input circuit. +- `mode` (`uint8_t`): compile mode, one of `COMPILE_MODE_*`. +- `device` (`const CDevice*`): target device (construction in [Device / Properties](../2_device/2_properties_device.md)). +- `initial_layout` (`const CLayout*`): optional initial logical-to-physical mapping; NULL lets the workflow choose one. +- `seed` (`int64_t`): deterministic device layout/routing seed; a negative value means "no seed" (heuristic default). + +Returns: a new `CCompileResult*` on success; NULL on NULL input or compilation failure. + +### compile_result_free(ptr) + +Frees a compile result handle; NULL is allowed. + +--- + +## Result Reading + +### compile_result_circuit(ptr) + +Retrieves the optimized circuit stored in the result. The returned circuit is a deep clone and can be freed independently of `result`. + +Returns: a new `CCircuit*` on success (free with `circuit_free`); NULL on NULL input or failure. + +### compile_result_changed(ptr) + +Tests whether the workflow changed the circuit. + +Returns: 1 when changed; 0 otherwise; -1 for NULL. + +### compile_result_mode(ptr) + +Returns the compile mode. + +Returns: `0` (Normal), `1` (Enhanced), or -1 for NULL. + +### compile_result_initial_layout(ptr) + +Reads the initial logical-to-physical layout recorded by a device-target compilation. + +Returns: a new `CLayout*` on success (free with `layout_free`); NULL when the result carries no device metadata (for example after a logical-target compile) or on NULL input. + +### compile_result_final_layout(ptr) + +Reads the final logical-to-physical layout recorded by a device-target compilation. + +Returns: a new `CLayout*` on success (free with `layout_free`); NULL when the result carries no device metadata (for example after a logical-target compile) or on NULL input. + +--- + +## Step Reports + +### compile_result_num_steps(ptr) + +Returns the number of step reports in the result; 0 for NULL. + +### compile_result_step_name(ptr, index) + +Returns the name of step `index`. + +Returns: a heap-allocated C string; free with `cqlib_string_free`. NULL on out-of-bounds. + +### compile_result_step_changed(ptr, index) + +Tests whether step `index` changed the circuit. + +Returns: 1 when changed; 0 otherwise; -1 on error. + +### compile_result_step_reason(ptr, index) + +Returns the optional skip or configuration note of step `index`. + +Returns: a caller-owned heap-allocated C string; free with `cqlib_string_free`. NULL when the step record carries no reason, when `ptr` is NULL, or when `index` is out of bounds. + +### compile_result_step(ptr, name, out_changed, out_skipped) + +Finds the first workflow step report named `name` and writes the step's raw `changed` and `skipped` flags to the out pointers. + +Parameters: + +- `name` (`const char*`): step name. +- `out_changed` (`int32_t*`): changed-flag output. +- `out_skipped` (`int32_t*`): skipped-flag output. + +Returns: 1 when a matching step exists; 0 when no step matches; -1 on NULL arguments or invalid UTF-8. + +--- + +## Reusable Workflow (CompilerWorkflow) + +`CCompilerWorkflow` is a reusable compiler-workflow handle: built once from a `CompileConfigC`, it can then run over any number of circuits, and the input circuit is never modified. Workflow results are read with the same accessors as `compile` results (see "Result Reading" and "Step Reports" above). + +### compiler_workflow_new(config) + +```c +struct CCompilerWorkflow *compiler_workflow_new(struct CompileConfigC config); +``` + +Creates a reusable compiler workflow from a C configuration. `config.mode` selects the Normal or Enhanced workflow; `config.target` only supports `COMPILE_TARGET_LOGICAL`, and other target values return NULL. Target preparation is validated eagerly at construction, so an invalid configuration returns NULL. + +- `config` (`struct CompileConfigC`): compile configuration, passed by value (field reference in [Overview](0_overview.md)). + +Returns: a new `CCompilerWorkflow*` on success (free with `compiler_workflow_free`); NULL when `config.mode` is invalid, `config.target` is not `COMPILE_TARGET_LOGICAL`, or configuration validation fails. + +### compiler_workflow_free(ptr) + +```c +void compiler_workflow_free(struct CCompilerWorkflow *ptr); +``` + +Frees a workflow handle; NULL is allowed. + +### compiler_workflow_run(workflow, circuit) + +```c +struct CCompileResult *compiler_workflow_run(const struct CCompilerWorkflow *workflow, + const struct CCircuit *circuit); +``` + +Runs the workflow over one circuit. The workflow can be run repeatedly over different circuits; the input circuit is never modified. + +- `workflow` (`const CCompilerWorkflow*`): workflow handle. +- `circuit` (`const CCircuit*`): input circuit. + +Returns: a new `CCompileResult*` on success (free with `compile_result_free`); NULL when either argument is NULL or compilation fails. + +--- + +## Device Compilation Metadata (DeviceCompilationMetadata) + +A device-target compilation (`compile_with_device`) records device metadata in the result: the initial and final layouts (the same data read by `compile_result_initial_layout` / `compile_result_final_layout`) plus the virtual output permutation — the mapping from original output qubits to rewritten output qubits, used to map measurement results back to the original qubit order. A logical-target compile carries no device metadata. + +### compile_result_has_device_metadata(ptr) + +```c +int32_t compile_result_has_device_metadata(const struct CCompileResult *ptr); +``` + +Tests whether the result carries device compilation metadata (a device-target compile). + +Returns: `1` when device metadata is present; `0` when it is not (for example after a logical-target compile); `-1` for NULL. + +### compile_result_device_metadata_initial_layout(ptr) + +```c +struct CLayout *compile_result_device_metadata_initial_layout(const struct CCompileResult *ptr); +``` + +Reads the initial logical-to-physical layout recorded by a device-target compilation (before routing). + +Returns: a new `CLayout*` on success (free with `layout_free`); NULL when the result carries no device metadata or on NULL input. + +### compile_result_device_metadata_final_layout(ptr) + +```c +struct CLayout *compile_result_device_metadata_final_layout(const struct CCompileResult *ptr); +``` + +Reads the final logical-to-physical layout recorded by a device-target compilation (after all routing SWAPs). + +Returns: a new `CLayout*` on success (free with `layout_free`); NULL when the result carries no device metadata or on NULL input. + +### compile_result_device_metadata_permutation_len(ptr) + +```c +uintptr_t compile_result_device_metadata_permutation_len(const struct CCompileResult *ptr); +``` + +Returns the number of entries in the virtual output permutation, used to size the buffers for `compile_result_device_metadata_permutation`. + +Returns: the entry count; 0 for NULL input or a result without device metadata. + +### compile_result_device_metadata_permutation(ptr, original, rewritten, len) + +```c +int32_t compile_result_device_metadata_permutation(const struct CCompileResult *ptr, + uint32_t *original, + uint32_t *rewritten, + uintptr_t len); +``` + +Two-step read of the virtual output permutation: copies qubit-ID pairs into `original` and `rewritten` in ascending original-output order. Index `i` records that the state of original output `original[i]` is carried by rewritten output `rewritten[i]`; `len` must equal `compile_result_device_metadata_permutation_len(ptr)`. + +- `ptr` (`const CCompileResult*`): compile result handle. +- `original` (`uint32_t*`): buffer for the original output qubit IDs. +- `rewritten` (`uint32_t*`): buffer for the rewritten output qubit IDs. +- `len` (`uintptr_t`): entry count. + +Returns: `0` on success (including a no-metadata result with `len = 0`); `-1` on NULL pointers (`ptr` is NULL, or `len > 0` with either buffer NULL); `-8` when `len` does not match the permutation length (including a no-metadata result with `len > 0`). + +--- + +## Example: Reusable Workflow and Device Metadata + +```c +#include +#include +#include +#include "cqlib_c.h" + +int main(void) { + struct CCircuit *qc = circuit_new(2); + circuit_h(qc, 0); + circuit_cx(qc, 0, 1); + + /* Enhanced-mode logical workflow, built once */ + struct CompileConfigC config; + memset(&config, 0, sizeof(config)); + config.mode = COMPILE_MODE_ENHANCED; + config.target = COMPILE_TARGET_LOGICAL; + struct CCompilerWorkflow *workflow = compiler_workflow_new(config); + if (workflow == NULL) { + circuit_free(qc); + return 1; + } + + /* Run the workflow; it is reusable across circuits */ + struct CCompileResult *result = compiler_workflow_run(workflow, qc); + if (result == NULL) { + compiler_workflow_free(workflow); + circuit_free(qc); + return 1; + } + struct CCompileResult *second = compiler_workflow_run(workflow, qc); + if (second != NULL) { + compile_result_free(second); /* independent result handle */ + } + + /* A logical-target compile carries no device metadata */ + int32_t has_meta = compile_result_has_device_metadata(result); /* 0 */ + struct CLayout *initial = compile_result_device_metadata_initial_layout(result); + if (initial != NULL) { + layout_free(initial); + } + uintptr_t perm_len = compile_result_device_metadata_permutation_len(result); + uint32_t *original = malloc(perm_len * sizeof(uint32_t)); + uint32_t *rewritten = malloc(perm_len * sizeof(uint32_t)); + int32_t rc = compile_result_device_metadata_permutation( + result, original, rewritten, perm_len); /* 0: len == 0 */ + free(original); + free(rewritten); + + compile_result_free(result); + compiler_workflow_free(workflow); + circuit_free(qc); + return 0; +} +``` + +After a device-target compile, `compile_result_has_device_metadata` returns 1 and the layouts and output permutation become available; that flow is the `compile_with_device` usage shown in the "Example" section below. + +--- + +## Example + +Compiling a circuit and reading the step reports (device-target flow): + +```c +#include +#include "cqlib_c.h" + +int main(void) { + /* 1. Input circuit and a line-topology device 0 - 1 - 2 */ + struct CCircuit *qc = circuit_new(3); + circuit_h(qc, 0); + circuit_cx(qc, 0, 2); /* non-adjacent coupling: requires routing */ + + struct CDevice *dev = device_line("demo", 3); + if (dev == NULL) { + circuit_free(qc); + return 1; + } + + /* 2. Device-target compilation: no initial layout, no seed */ + struct CCompileResult *result = + compile_with_device(qc, COMPILE_MODE_ENHANCED, dev, NULL, -1); + if (result == NULL) { + device_free(dev); + circuit_free(qc); + return 1; + } + + /* 3. Optimized circuit and overall change flag */ + int32_t changed = compile_result_changed(result); /* 1: routing changed the circuit */ + struct CCircuit *optimized = compile_result_circuit(result); + if (optimized != NULL) { + circuit_free(optimized); + } + + /* 4. Traverse the step reports */ + uintptr_t steps = compile_result_num_steps(result); + for (uintptr_t i = 0; i < steps; i++) { + char *name = compile_result_step_name(result, i); + int32_t step_changed = compile_result_step_changed(result, i); + printf("step %s: changed=%d\n", name, step_changed); + cqlib_string_free(name); + } + + /* 5. Look up a step by name (names look like "route.sabre", "validate.device") */ + int32_t flag_changed = 0, flag_skipped = 0; + int32_t found = compile_result_step(result, "route.sabre", + &flag_changed, &flag_skipped); + + /* 6. Device compilation metadata: initial and final layouts */ + struct CLayout *initial = compile_result_initial_layout(result); + struct CLayout *final = compile_result_final_layout(result); + if (initial != NULL) { + layout_free(initial); + } + if (final != NULL) { + layout_free(final); + } + + compile_result_free(result); + device_free(dev); + circuit_free(qc); + return 0; +} +``` + +The logical-target flow (with `CompileConfigC` passed by value) is shown in [Overview](0_overview.md); layout handle accessors are documented in [Layout](../2_device/3_layout.md). diff --git a/docs/api/en/c/4_compile/2_transform.md b/docs/api/en/c/4_compile/2_transform.md new file mode 100644 index 00000000..a2b8a29e --- /dev/null +++ b/docs/api/en/c/4_compile/2_transform.md @@ -0,0 +1,856 @@ +# Transform (C) + +This page covers the C ABI of the compile transform stage: the structural-analysis snapshot `CCircuitAnalysis`, the handle-based configurations `CTransformConfig` and `CCanonicalizeConfig`, canonicalization with `canonicalize_circuit` / `canonicalize_circuit_with_config`, knowledge-rule rewriting with `rewrite_circuit`, and the five standalone pass wrappers `transform_*`. No entry point modifies its input circuit: analysis snapshots are written into caller-provided structs, and transforms return new handles or new circuits. Error codes, string and handle ownership conventions follow the [Overview](../0_overview.md). + +--- + +## Constants + +Rewrite mode tags (the `CRewriteConfig.mode` field): + +| Constant | Value | Mode | +| --- | --- | --- | +| `REWRITE_MODE_OPTIMIZE` | 0 | Conservative optimization (production default): accepted rewrites must strictly improve the local cost. | +| `REWRITE_MODE_LOWERING` | 1 | Explicit lowering: decomposition and hardware-native rules may expand locally. | + +Rewrite rule-category tags (used by `transform_config_with_enabled_kinds` and `transform_config_enabled_kinds`): + +| Constant | Value | Kind | +| --- | --- | --- | +| `REWRITE_KIND_SIMPLIFY` | 0 | Algebraic simplification. | +| `REWRITE_KIND_CANCEL` | 1 | Inverse / repeat cancellation. | +| `REWRITE_KIND_MERGE` | 2 | Neighbor-gate merge. | +| `REWRITE_KIND_COMMUTE` | 3 | Explicit commutation. | +| `REWRITE_KIND_DECOMPOSE` | 4 | Decomposition / lowering. | +| `REWRITE_KIND_CANONICALIZE` | 5 | Canonical representation. | +| `REWRITE_KIND_HARDWARE_NATIVE` | 6 | Hardware-native rewriting. | +| `REWRITE_KIND_OTHER` | 7 | Other. | + +--- + +## Rewrite Configuration + +`CRewriteConfig` is the input configuration of the knowledge rewriter, passed by value: + +```c +typedef struct CRewriteConfig { + uint8_t mode; /* One of REWRITE_MODE_*. */ + uint8_t max_rounds; /* Fixpoint round limit. */ + uint8_t recurse_control_flow; /* 1 recurses into control-flow bodies. */ + uint8_t skip_labeled_ops; /* 1 skips labeled operations. */ + uint8_t preserve_two_qubit_connectivity; /* 1 preserves undirected 2q connectivity. */ + uint8_t _pad[3]; /* Reserved padding. */ + uintptr_t max_window_ops; /* Matching-window operation limit. */ + uintptr_t max_pattern_len; /* Maximum matched pattern length. */ + const char *const *target_gate_names; /* Borrowed target-basis gate names (NULL = none). */ + uintptr_t target_gate_names_len; /* Number of entries in target_gate_names. */ +} CRewriteConfig; +``` + +| Field | Type | Meaning | +| --- | --- | --- | +| `mode` | `uint8_t` | Rule-set baseline, one of `REWRITE_MODE_*`: `OPTIMIZE` uses the production defaults, `LOWERING` adds decomposition and hardware-native rules. | +| `max_rounds` | `uint8_t` | Fixpoint round limit. | +| `recurse_control_flow` | `uint8_t` | 1 recurses into control-flow bodies, 0 does not. | +| `skip_labeled_ops` | `uint8_t` | 1 skips labeled operations, 0 does not. | +| `preserve_two_qubit_connectivity` | `uint8_t` | 1 preserves undirected two-qubit connectivity (rewrites may not split adjacent two-qubit pairs), 0 does not require it. | +| `max_window_ops` | `uintptr_t` | Operation limit of the rule matching window. | +| `max_pattern_len` | `uintptr_t` | Maximum length of a single matched pattern. | +| `target_gate_names` | `const char *const *` | Borrowed target-basis gate names (each a NUL-terminated string); NULL when no target basis is configured. Set with `rewrite_config_with_target_instructions`. | +| `target_gate_names_len` | `uintptr_t` | Number of entries in `target_gate_names`. | +| `_pad[3]` | — | Padding that keeps the struct layout stable; do not write non-zero values. | + +Every field except `mode` overrides the baseline: start from `rewrite_config_default()` / `rewrite_config_lowering()` and adjust the fields you need. The `target_gate_names` array and its strings are borrowed, not copied: they must stay valid until the `rewrite_circuit` / `transform_knowledge_rewrite` call that consumes the configuration. + +### rewrite_config_default() + +```c +struct CRewriteConfig rewrite_config_default(void); +``` + +Returns the production optimization configuration by value (`REWRITE_MODE_OPTIMIZE`): `max_rounds = 8`, `recurse_control_flow = 1`, `skip_labeled_ops = 1`, `preserve_two_qubit_connectivity = 0`, `max_window_ops = 16`, `max_pattern_len = 8`. + +### rewrite_config_lowering() + +```c +struct CRewriteConfig rewrite_config_lowering(void); +``` + +Returns the explicit lowering configuration by value: identical field values to `rewrite_config_default()` except `mode = REWRITE_MODE_LOWERING`, which adds decomposition and hardware-native rules to the rule set. + +### rewrite_config_with_target_instructions(config, gate_names, len) + +```c +int32_t rewrite_config_with_target_instructions(struct CRewriteConfig *config, + const char *const *gate_names, + uintptr_t len); +``` + +Restricts a by-value rewrite configuration to an explicit standard-gate target-instruction basis, parsed from `len` gate names (e.g. `"H"`, `"CX"`). The name array is borrowed (see the field table above) and validated eagerly; an empty basis is rejected and the configuration is left unchanged. The target basis only takes effect when `config` uses `REWRITE_MODE_LOWERING`. + +- `config` (`CRewriteConfig*`): rewrite configuration, modified in place. +- `gate_names` (`const char *const *`): array of `len` NUL-terminated standard-gate names. +- `len` (`uintptr_t`): number of entries in `gate_names`. + +Returns: `0` on success. + +| Error code | Name | Scenario | +| --- | --- | --- | +| `-1` | NullPtr | `config` is NULL, or `len` is greater than 0 but `gate_names` is NULL or contains a NULL entry. | +| `-4` | ParseError | An entry is invalid UTF-8 or an unknown gate name. | +| `-6` | CompilerError | The core rejected the basis. | +| `-8` | InvalidParam | The basis is empty (`len` is 0); the configuration is left unchanged. | + +--- + +## Transform Configuration Handle + +`CTransformConfig` is an opaque handle around the same core rewrite / transform configuration whose by-value C form is `CRewriteConfig`. Unlike the by-value struct it lives on the heap and is modified in place: every `transform_config_with_*` setter rewrites the configuration inside the handle and returns an `int32_t` status code — setters are not chainable. Rule-category fields use the `REWRITE_KIND_*` tags from the [Constants](#constants) section. + +### transform_config_default() + +```c +struct CTransformConfig *transform_config_default(void); +``` + +Creates a transform configuration with production (conservative optimization) defaults: `mode = REWRITE_MODE_OPTIMIZE`, `max_rounds = 8`, `max_window_ops = 16`, `max_pattern_len = 8`, control-flow recursion and labeled-operation skipping enabled, the enabled rule categories `SIMPLIFY`, `CANCEL`, `MERGE`, `CANONICALIZE`, and no target-instruction basis. + +Returns: a new `CTransformConfig*` on success (free with `transform_config_free`); NULL on allocation failure. + +### transform_config_lowering() + +```c +struct CTransformConfig *transform_config_lowering(void); +``` + +Creates a transform configuration with explicit knowledge-based lowering defaults: identical to `transform_config_default()` except `mode = REWRITE_MODE_LOWERING`, which widens the enabled rule set to six categories (adding `DECOMPOSE` and `HARDWARE_NATIVE`). + +Returns: a new `CTransformConfig*` on success (free with `transform_config_free`); NULL on allocation failure. + +### transform_config_free(ptr) + +```c +void transform_config_free(struct CTransformConfig *ptr); +``` + +Frees a transform configuration; NULL is allowed. + +### transform_config_with_mode(ptr, mode) + +```c +int32_t transform_config_with_mode(struct CTransformConfig *ptr, uint8_t mode); +``` + +Sets the rewrite mode of `config` in place; `mode` must be one of `REWRITE_MODE_*`. + +Returns: `0` on success. + +| Error code | Name | Scenario | +| --- | --- | --- | +| `-1` | NullPtr | `ptr` is NULL. | +| `-8` | InvalidParam | `mode` is not a valid `REWRITE_MODE_*` tag. | + +### transform_config_with_max_rounds(ptr, max_rounds) + +```c +int32_t transform_config_with_max_rounds(struct CTransformConfig *ptr, uint8_t max_rounds); +``` + +Sets the fixpoint round limit of the configuration in place. + +Returns: `0` on success; `-1` (`NullPtr`) when `ptr` is NULL. + +### transform_config_with_max_window_ops(ptr, max_window_ops) + +```c +int32_t transform_config_with_max_window_ops(struct CTransformConfig *ptr, + uintptr_t max_window_ops); +``` + +Sets the matching-window operation limit of the configuration in place. + +Returns: `0` on success; `-1` (`NullPtr`) when `ptr` is NULL. + +### transform_config_with_max_pattern_len(ptr, max_pattern_len) + +```c +int32_t transform_config_with_max_pattern_len(struct CTransformConfig *ptr, + uintptr_t max_pattern_len); +``` + +Sets the maximum matched pattern length of the configuration in place. + +Returns: `0` on success; `-1` (`NullPtr`) when `ptr` is NULL. + +### transform_config_with_enabled_kinds(ptr, kinds, len) + +```c +int32_t transform_config_with_enabled_kinds(struct CTransformConfig *ptr, + const uint8_t *kinds, + uintptr_t len); +``` + +Replaces the enabled rule categories of the configuration in place: `kinds` points at `len` `REWRITE_KIND_*` tags (NULL when `len` is 0, which disables every rule category). When the call is rejected the configuration is left unchanged. + +Returns: `0` on success. + +| Error code | Name | Scenario | +| --- | --- | --- | +| `-1` | NullPtr | `ptr` is NULL, or `len` is greater than 0 but `kinds` is NULL. | +| `-8` | InvalidParam | A tag in `kinds` is not a valid `REWRITE_KIND_*` tag. | + +### transform_config_with_target_instructions(ptr, gate_names, len) + +```c +int32_t transform_config_with_target_instructions(struct CTransformConfig *ptr, + const char *const *gate_names, + uintptr_t len); +``` + +Restricts the configuration to an explicit standard-gate target-instruction basis, parsed from `len` gate names (e.g. `"H"`, `"CX"`). An empty basis is rejected; when the call is rejected the configuration is left unchanged. + +Returns: `0` on success. + +| Error code | Name | Scenario | +| --- | --- | --- | +| `-1` | NullPtr | `ptr` is NULL, or `len` is greater than 0 but `gate_names` is NULL or contains a NULL entry. | +| `-4` | ParseError | An entry is invalid UTF-8 or an unknown gate name. | +| `-6` | CompilerError | The core rejected the basis. | +| `-8` | InvalidParam | The basis is empty (`len` is 0). | + +### transform_config_mode(ptr) + +```c +int32_t transform_config_mode(const struct CTransformConfig *ptr); +``` + +Returns the `REWRITE_MODE_*` mode tag of the configuration. + +Returns: the mode tag; `-1` for NULL input. + +### transform_config_max_rounds(ptr) + +```c +uint8_t transform_config_max_rounds(const struct CTransformConfig *ptr); +``` + +Returns the fixpoint round limit of the configuration. + +Returns: the round limit; 0 for NULL input. + +### transform_config_max_window_ops(ptr) + +```c +uintptr_t transform_config_max_window_ops(const struct CTransformConfig *ptr); +``` + +Returns the matching-window operation limit of the configuration. + +Returns: the operation limit; 0 for NULL input. + +### transform_config_max_pattern_len(ptr) + +```c +uintptr_t transform_config_max_pattern_len(const struct CTransformConfig *ptr); +``` + +Returns the maximum matched pattern length of the configuration. + +Returns: the pattern-length limit; 0 for NULL input. + +### transform_config_recurse_control_flow(ptr) + +```c +int32_t transform_config_recurse_control_flow(const struct CTransformConfig *ptr); +``` + +Tests whether the configuration recurses into control-flow bodies. + +Returns: `1` when enabled; `0` otherwise; `-1` for NULL input. + +### transform_config_skip_labeled_ops(ptr) + +```c +int32_t transform_config_skip_labeled_ops(const struct CTransformConfig *ptr); +``` + +Tests whether the configuration skips labeled operations. + +Returns: `1` when enabled; `0` otherwise; `-1` for NULL input. + +### transform_config_enabled_kinds_len(ptr) / transform_config_enabled_kinds(ptr, out, len) + +```c +uintptr_t transform_config_enabled_kinds_len(const struct CTransformConfig *ptr); +uintptr_t transform_config_enabled_kinds(const struct CTransformConfig *ptr, + uint8_t *out, + uintptr_t len); +``` + +Two-step read of the enabled rule categories (step one: `*_len` returns the total; step two: the fill function copies `REWRITE_KIND_*` tags into `out`, writing `min(total, len)` entries). When `out` is NULL the fill function only returns the total without copying. + +Returns (both functions): the total number of enabled categories; 0 for NULL input. + +### transform_config_target_instruction_basis_len(ptr) / transform_config_target_instruction_basis(ptr, out, len) + +```c +uintptr_t transform_config_target_instruction_basis_len(const struct CTransformConfig *ptr); +uintptr_t transform_config_target_instruction_basis(const struct CTransformConfig *ptr, + char **out, + uintptr_t len); +``` + +Two-step read of the configured target-basis instruction names (0 when no target basis is set). The fill function copies the gate names into `out`, writing `min(total, len)` entries; each written entry is a freshly allocated C string freed with `cqlib_string_free`. When `out` is NULL it only returns the total without copying. + +Returns (both functions): the total number of basis instructions; 0 for NULL input or when no target basis is set. + +--- + +## Structural Analysis + +`CCircuitAnalysis` is a snapshot of circuit facts; every flag is 0 or 1: + +```c +typedef struct CCircuitAnalysis { + uint8_t has_classical_data; + uint8_t has_classical_control; + uint8_t has_measurement; + uint8_t has_classical_values; + uint8_t has_classical_vars; + uint8_t has_runtime_classical; + uint8_t needs_classical_handle_preservation; + uint8_t has_circuit_gate_definitions; + uint8_t has_unitary_circuit_definitions; + uint8_t has_unitary_gates; + uint8_t has_mc_gates; +} CCircuitAnalysis; +``` + +| Field | Meaning | +| --- | --- | +| `has_classical_data` | The circuit contains classical-data operations. | +| `has_classical_control` | The circuit contains classical control flow. | +| `has_measurement` | The circuit contains measurement operations. | +| `has_classical_values` | The circuit registers classical values. | +| `has_classical_vars` | The circuit registers classical variables. | +| `has_runtime_classical` | The circuit uses any runtime classical construct. | +| `needs_classical_handle_preservation` | Rewrites must preserve classical handles for this circuit. | +| `has_circuit_gate_definitions` | The circuit contains unexpanded circuit-backed gate definitions. | +| `has_unitary_circuit_definitions` | The circuit contains unitary definitions with circuit bodies. | +| `has_unitary_gates` | The circuit contains matrix-backed unitary gates. | +| `has_mc_gates` | The circuit contains multi-controlled gates. | + +### circuit_analyze(circuit, out) + +```c +int32_t circuit_analyze(const struct CCircuit *circuit, struct CCircuitAnalysis *out); +``` + +Analyzes the circuit and writes the structural snapshot to `*out`. + +- `circuit` (`const CCircuit*`): input circuit. +- `out` (`CCircuitAnalysis*`): snapshot output. + +Returns: `0` on success; `-1` when either argument is NULL. + +--- + +## Canonicalization + +The canonicalizer unifies instruction forms, folds global phases, and removes no-ops; its outcome is carried by a `CCanonicalizeResult` handle. The pass runs with production defaults under `canonicalize_circuit`, or with a caller-built `CCanonicalizeConfig` handle under `canonicalize_circuit_with_config`. + +### canonicalize_circuit(circuit) + +```c +struct CCanonicalizeResult *canonicalize_circuit(const struct CCircuit *circuit); +``` + +Canonicalizes the circuit with production defaults; the input circuit is not modified. + +- `circuit` (`const CCircuit*`): input circuit. + +Returns: a new `CCanonicalizeResult*` on success (free with `canonicalize_result_free`); NULL on NULL input or execution failure. + +### canonicalize_circuit_with_config(circuit, config) + +```c +struct CCanonicalizeResult *canonicalize_circuit_with_config(const struct CCircuit *circuit, + const struct CCanonicalizeConfig *config); +``` + +Canonicalizes the circuit using the supplied configuration; the input circuit is not modified and the config is not consumed — it remains owned by the caller and can be reused. + +- `circuit` (`const CCircuit*`): input circuit. +- `config` (`const CCanonicalizeConfig*`): canonicalize configuration handle (see below). + +Returns: a new `CCanonicalizeResult*` on success (free with `canonicalize_result_free`); NULL on NULL input or execution failure. + +### canonicalize_result_free(ptr) + +```c +void canonicalize_result_free(struct CCanonicalizeResult *ptr); +``` + +Frees a canonicalize result handle; NULL is allowed. + +### canonicalize_result_circuit(ptr) + +```c +struct CCircuit *canonicalize_result_circuit(const struct CCanonicalizeResult *ptr); +``` + +Retrieves the canonicalized circuit (a deep clone, independent of `ptr`). + +Returns: a new `CCircuit*` on success (free with `circuit_free`); NULL on NULL input or failure. + +### canonicalize_result_changed(ptr) + +```c +int32_t canonicalize_result_changed(const struct CCanonicalizeResult *ptr); +``` + +Tests whether canonicalization changed the circuit representation. + +Returns: `1` when changed; `0` otherwise; `-1` for NULL. + +### canonicalize_result_rounds(ptr) + +```c +uint8_t canonicalize_result_rounds(const struct CCanonicalizeResult *ptr); +``` + +Returns the number of canonicalization rounds executed. + +Returns: the round count; 0 for NULL input. + +### canonicalize_config_default() + +```c +struct CCanonicalizeConfig *canonicalize_config_default(void); +``` + +Creates a canonicalize configuration with production defaults: round limit 8 and every optional behavior enabled (control-flow recursion, `GPhase` folding, instruction-form canonicalization, strict no-op removal, barrier canonicalization). + +Returns: a new `CCanonicalizeConfig*` on success (free with `canonicalize_config_free`); NULL on allocation failure. + +### canonicalize_config_free(ptr) + +```c +void canonicalize_config_free(struct CCanonicalizeConfig *ptr); +``` + +Frees a canonicalize configuration; NULL is allowed. + +### canonicalize_config_with_round_limit(ptr, round_limit) + +```c +int32_t canonicalize_config_with_round_limit(struct CCanonicalizeConfig *ptr, uint8_t round_limit); +``` + +Sets the maximum number of canonicalization rounds in place. + +Returns: `0` on success; `-1` (`NullPtr`) when `ptr` is NULL. + +### canonicalize_config_with_recurse_control_flow(ptr, enabled) + +```c +int32_t canonicalize_config_with_recurse_control_flow(struct CCanonicalizeConfig *ptr, + int32_t enabled); +``` + +Sets whether control-flow bodies are recursively canonicalized (non-zero `enabled`) or not (`0`) in place. + +Returns: `0` on success; `-1` (`NullPtr`) when `ptr` is NULL. + +### canonicalize_config_with_fold_gphase(ptr, enabled) + +```c +int32_t canonicalize_config_with_fold_gphase(struct CCanonicalizeConfig *ptr, int32_t enabled); +``` + +Sets whether `GPhase` operations are folded into scope-local phase, in place. + +Returns: `0` on success; `-1` (`NullPtr`) when `ptr` is NULL. + +### canonicalize_config_with_canonicalize_instruction_form(ptr, enabled) + +```c +int32_t canonicalize_config_with_canonicalize_instruction_form(struct CCanonicalizeConfig *ptr, + int32_t enabled); +``` + +Sets whether `McGate` forms are collapsed into existing standard gates, in place. + +Returns: `0` on success; `-1` (`NullPtr`) when `ptr` is NULL. + +### canonicalize_config_with_drop_noops(ptr, enabled) + +```c +int32_t canonicalize_config_with_drop_noops(struct CCanonicalizeConfig *ptr, int32_t enabled); +``` + +Sets whether strict no-op removal is performed, in place. + +Returns: `0` on success; `-1` (`NullPtr`) when `ptr` is NULL. + +### canonicalize_config_with_canonicalize_barriers(ptr, enabled) + +```c +int32_t canonicalize_config_with_canonicalize_barriers(struct CCanonicalizeConfig *ptr, + int32_t enabled); +``` + +Sets whether barrier scopes are canonicalized and adjacent barriers merged, in place. + +Returns: `0` on success; `-1` (`NullPtr`) when `ptr` is NULL. + +### canonicalize_config_round_limit(ptr) + +```c +uint8_t canonicalize_config_round_limit(const struct CCanonicalizeConfig *ptr); +``` + +Returns the configured round limit. + +Returns: the round limit; 0 for NULL input. + +### canonicalize_config_recurse_control_flow(ptr) + +```c +int32_t canonicalize_config_recurse_control_flow(const struct CCanonicalizeConfig *ptr); +``` + +Tests whether control-flow recursion is enabled. + +Returns: `1` when enabled; `0` otherwise; `-1` for NULL input. + +### canonicalize_config_fold_gphase(ptr) + +```c +int32_t canonicalize_config_fold_gphase(const struct CCanonicalizeConfig *ptr); +``` + +Tests whether `GPhase` folding is enabled. + +Returns: `1` when enabled; `0` otherwise; `-1` for NULL input. + +### canonicalize_config_canonicalize_instruction_form(ptr) + +```c +int32_t canonicalize_config_canonicalize_instruction_form(const struct CCanonicalizeConfig *ptr); +``` + +Tests whether instruction-form canonicalization is enabled. + +Returns: `1` when enabled; `0` otherwise; `-1` for NULL input. + +### canonicalize_config_drop_noops(ptr) + +```c +int32_t canonicalize_config_drop_noops(const struct CCanonicalizeConfig *ptr); +``` + +Tests whether strict no-op removal is enabled. + +Returns: `1` when enabled; `0` otherwise; `-1` for NULL input. + +### canonicalize_config_canonicalize_barriers(ptr) + +```c +int32_t canonicalize_config_canonicalize_barriers(const struct CCanonicalizeConfig *ptr); +``` + +Tests whether barrier canonicalization is enabled. + +Returns: `1` when enabled; `0` otherwise; `-1` for NULL input. + +--- + +## Knowledge Rewrite + +The knowledge rewriter performs pattern-based simplification and cancellation through knowledge rules; it is the core of the pipeline's logical optimization. Its outcome is carried by a `CKnowledgeRewriteResult` handle. + +### rewrite_circuit(circuit, config) + +```c +struct CKnowledgeRewriteResult *rewrite_circuit(const struct CCircuit *circuit, + struct CRewriteConfig config); +``` + +Applies the knowledge rewriter to the circuit with the supplied configuration; the input circuit is not modified. + +- `circuit` (`const CCircuit*`): input circuit. +- `config` (`struct CRewriteConfig`): rewrite configuration, passed by value. + +Returns: a new `CKnowledgeRewriteResult*` on success (free with `knowledge_rewrite_result_free`); NULL on NULL input, an invalid `config.mode` or target-instruction basis, or execution failure. + +### knowledge_rewrite_result_free(ptr) + +```c +void knowledge_rewrite_result_free(struct CKnowledgeRewriteResult *ptr); +``` + +Frees a knowledge-rewrite result handle; NULL is allowed. + +### knowledge_rewrite_result_circuit(ptr) + +```c +struct CCircuit *knowledge_rewrite_result_circuit(const struct CKnowledgeRewriteResult *ptr); +``` + +Retrieves the rewritten circuit (a deep clone, independent of `ptr`). + +Returns: a new `CCircuit*` on success (free with `circuit_free`); NULL on NULL input or failure. + +### knowledge_rewrite_result_changed(ptr) + +```c +int32_t knowledge_rewrite_result_changed(const struct CKnowledgeRewriteResult *ptr); +``` + +Tests whether rewriting changed the circuit representation. + +Returns: `1` when changed; `0` otherwise; `-1` for NULL. + +### knowledge_rewrite_result_stats(ptr, out) + +```c +int32_t knowledge_rewrite_result_stats(const struct CKnowledgeRewriteResult *ptr, + struct CKnowledgeRewriteStats *out); +``` + +Writes the rewrite run-statistics snapshot to `*out`. The `CKnowledgeRewriteStats` layout: + +| Field | Type | Meaning | +| --- | --- | --- | +| `rounds_executed` | `uint8_t` | Fixpoint rounds actually executed. | +| `reached_fixpoint` | `uint8_t` | 1 when a stable round was observed before `max_rounds`. | +| `rules_applied` | `uintptr_t` | Rule patches emitted into rebuilt sequences. | +| `changed_sequences` | `uintptr_t` | Operation sequences whose patch set was non-empty. | + +- `ptr` (`const CKnowledgeRewriteResult*`): rewrite result handle. +- `out` (`CKnowledgeRewriteStats*`): statistics snapshot output. + +Returns: `0` on success; `-1` when either argument is NULL. + +### knowledge_rewrite_result_diagnostics(ptr, out) + +```c +int32_t knowledge_rewrite_result_diagnostics(const struct CKnowledgeRewriteResult *ptr, + struct CKnowledgeRewriteDiagnostics *out); +``` + +Writes the rewrite performance-diagnostics snapshot to `*out`. The `CKnowledgeRewriteDiagnostics` layout: + +| Field | Type | Meaning | +| --- | --- | --- | +| `direct_reuses` | `uintptr_t` | Rewrite phases skipped by a reusable session fixpoint proof. | +| `dirty_anchors` | `uintptr_t` | Anchors presented by verified dirty ranges before the density fallback. | +| `full_scan_fallbacks` | `uintptr_t` | Incremental executions conservatively promoted to full scans. | +| `condition_cache_hits` | `uintptr_t` | Cached rule-condition results reused. | +| `condition_cache_misses` | `uintptr_t` | Cacheable rule-condition bindings not found in the local cache. | +| `symbolic_fallbacks` | `uintptr_t` | Conditions evaluated by the conservative symbolic path. | + +- `ptr` (`const CKnowledgeRewriteResult*`): rewrite result handle. +- `out` (`CKnowledgeRewriteDiagnostics*`): diagnostics snapshot output. + +Returns: `0` on success; `-1` when either argument is NULL. + +--- + +## Standalone Pass Wrappers + +The `transform_*` family wraps a single transform pass: it returns the rebuilt circuit directly and skips the intermediate result handle. None of them modifies the input circuit; free outputs with `circuit_free`. + +### transform_canonicalize(circuit) + +```c +struct CCircuit *transform_canonicalize(const struct CCircuit *circuit); +``` + +Canonicalizes the circuit with production defaults and returns the rebuilt circuit (equivalent to running `canonicalize_circuit` and then retrieving the circuit). + +Returns: a new `CCircuit*` on success; NULL on NULL input or execution failure. + +### transform_knowledge_rewrite(circuit, config) + +```c +struct CCircuit *transform_knowledge_rewrite(const struct CCircuit *circuit, + struct CRewriteConfig config); +``` + +Applies the knowledge rewriter with the supplied configuration and returns the rebuilt circuit. + +- `circuit` (`const CCircuit*`): input circuit. +- `config` (`struct CRewriteConfig`): rewrite configuration, passed by value. + +Returns: a new `CCircuit*` on success; NULL on NULL input, an invalid `config.mode` or target-instruction basis, or execution failure. + +### transform_optimize_one_qubit_runs(circuit) + +```c +struct CCircuit *transform_optimize_one_qubit_runs(const struct CCircuit *circuit); +``` + +Fuses fixed numeric one-qubit runs through exact synthesis (target-neutral logical cost); the output never has more operations than the input. + +Returns: a new `CCircuit*` on success; NULL on NULL input or execution failure. + +### transform_commutative_cancellation(circuit) + +```c +struct CCircuit *transform_commutative_cancellation(const struct CCircuit *circuit); +``` + +Cancels self-inverse gate pairs (such as adjacent `H·H`, `CX·CX`) through global commutation analysis. + +Returns: a new `CCircuit*` on success; NULL on NULL input or execution failure. + +### transform_lower_to_routing_basis(circuit) + +```c +struct CCircuit *transform_lower_to_routing_basis(const struct CCircuit *circuit); +``` + +Lowers the circuit to the routing basis: gates exceeding the SABRE gate-arity constraints (such as `CCX`) are expanded into operations of at most two qubits, preparing the circuit for routing. + +Returns: a new `CCircuit*` on success; NULL on NULL input or execution failure. + +--- + +## Example + +Structural analysis, canonicalization, and knowledge rewriting: + +```c +#include +#include +#include "cqlib_c.h" + +int main(void) { + /* 1. Structural analysis of a Bell-pair circuit */ + struct CCircuit *bell = circuit_new(2); + circuit_h(bell, 0); + circuit_cx(bell, 0, 1); + + struct CCircuitAnalysis analysis; + memset(&analysis, 0, sizeof(analysis)); + if (circuit_analyze(bell, &analysis) != 0) { + circuit_free(bell); + return 1; + } + /* analysis.has_classical_control == 0, analysis.has_unitary_gates == 0 */ + + /* 2. Canonicalize and read the result handle */ + struct CCanonicalizeResult *canon = canonicalize_circuit(bell); + if (canon == NULL) { + circuit_free(bell); + return 1; + } + uint8_t rounds = canonicalize_result_rounds(canon); /* >= 1 */ + struct CCircuit *canonical = canonicalize_result_circuit(canon); + if (canonical != NULL) { + circuit_free(canonical); /* deep clone, independent of canon */ + } + canonicalize_result_free(canon); + + /* 3. Knowledge rewrite with the production configuration */ + struct CRewriteConfig config = rewrite_config_default(); + struct CKnowledgeRewriteResult *rewritten = rewrite_circuit(bell, config); + if (rewritten != NULL) { + int32_t changed = knowledge_rewrite_result_changed(rewritten); + struct CKnowledgeRewriteStats stats; + struct CKnowledgeRewriteDiagnostics diagnostics; + memset(&stats, 0, sizeof(stats)); + memset(&diagnostics, 0, sizeof(diagnostics)); + if (knowledge_rewrite_result_stats(rewritten, &stats) == 0) { + printf("rewrite: rounds=%u rules=%llu\n", + stats.rounds_executed, (unsigned long long)stats.rules_applied); + } + knowledge_rewrite_result_diagnostics(rewritten, &diagnostics); + knowledge_rewrite_result_free(rewritten); + } + circuit_free(bell); + + /* 4. Standalone pass wrappers return rebuilt circuits directly */ + struct CCircuit *runs = circuit_new(1); + circuit_h(runs, 0); + circuit_t(runs, 0); + circuit_tdg(runs, 0); + struct CCircuit *fused = transform_optimize_one_qubit_runs(runs); + if (fused != NULL) { + circuit_free(fused); + } + + struct CCircuit *lowered = transform_lower_to_routing_basis(runs); + if (lowered != NULL) { + circuit_free(lowered); + } + circuit_free(runs); + return 0; +} +``` + +Configured canonicalization and the transform configuration handle: + +```c +#include +#include +#include "cqlib_c.h" + +int main(void) { + /* 1. Build a canonicalize config and tighten the round limit */ + struct CCanonicalizeConfig *config = canonicalize_config_default(); + if (config == NULL) { + return 1; + } + /* canonicalize_config_round_limit(config) == 8, all optional behaviors on */ + canonicalize_config_with_round_limit(config, 2); + + struct CCircuit *circuit = circuit_new(2); + circuit_h(circuit, 0); + circuit_cx(circuit, 0, 1); + circuit_t(circuit, 1); + + /* 2. Canonicalize under the config; the config stays owned by the caller */ + struct CCanonicalizeResult *result = canonicalize_circuit_with_config(circuit, config); + if (result != NULL) { + struct CCircuit *canonical = canonicalize_result_circuit(result); + /* canonicalize_result_rounds(result) <= 2 */ + circuit_free(canonical); + canonicalize_result_free(result); + } + + /* 3. Reuse the same config with a tighter limit */ + canonicalize_config_with_round_limit(config, 1); + struct CCanonicalizeResult *limited = canonicalize_circuit_with_config(circuit, config); + /* canonicalize_result_rounds(limited) <= 1 */ + canonicalize_result_free(limited); + canonicalize_config_free(config); + circuit_free(circuit); + + /* 4. Transform config: inspect defaults, then restrict in place */ + struct CTransformConfig *tconfig = transform_config_default(); + if (tconfig == NULL) { + return 1; + } + /* transform_config_mode(tconfig) == REWRITE_MODE_OPTIMIZE, + transform_config_enabled_kinds_len(tconfig) == 4 */ + const uint8_t kinds[2] = {REWRITE_KIND_CANCEL, REWRITE_KIND_DECOMPOSE}; + transform_config_with_enabled_kinds(tconfig, kinds, 2); + + const char *const gates[2] = {"H", "CX"}; + if (transform_config_with_target_instructions(tconfig, gates, 2) == 0) { + char *basis[2] = {NULL, NULL}; + uintptr_t total = transform_config_target_instruction_basis(tconfig, basis, 2); + for (uintptr_t i = 0; i < total && i < 2; i++) { + cqlib_string_free(basis[i]); /* each entry: newly allocated */ + } + } + transform_config_with_mode(tconfig, 99); /* -8, config unchanged */ + transform_config_free(tconfig); + return 0; +} +``` + +The fused one-qubit run never grows after `transform_optimize_one_qubit_runs`; the compile entry point that orchestrates these passes end-to-end is documented in [Compiler](1_compiler.md), and layout/routing in [Initial Layout](3_layout.md) and [Routing](4_routing.md). diff --git a/docs/api/en/c/4_compile/3_layout.md b/docs/api/en/c/4_compile/3_layout.md new file mode 100644 index 00000000..4d848dbe --- /dev/null +++ b/docs/api/en/c/4_compile/3_layout.md @@ -0,0 +1,773 @@ +# Initial Layout (C) + +This page covers the C ABI of the initial-layout algorithms and layout analysis tools: the four layout entry points (`trivial_layout` / `greedy_layout` / `vf2_perfect_layout` / `sabre_layout`), circuit interaction analysis and physical-layout-graph queries, the prepared SABRE objects and the prepared layout pipeline, and the read accessors of the layout result `CLayoutResult`. A layout maps logical qubits onto physical qubits and provides the starting point of the routing stage. Error codes, string and handle ownership conventions follow the [Overview](../0_overview.md); construction and access of the mapping handle `CLayout` are documented in [Layout](../2_device/3_layout.md). + +--- + +## Constants + +Layout objective tags (the `objective` parameter of every layout entry point and of `score_layout`): + +| Constant | Value | Objective | +| --- | --- | --- | +| `LAYOUT_OBJECTIVE_TOPOLOGY_ONLY` | 0 | Topology only: weighted distance plus direction mismatch. | +| `LAYOUT_OBJECTIVE_FIDELITY_AWARE` | 1 | Fidelity aware: adds error-rate terms on top of the topology terms. | +| `LAYOUT_OBJECTIVE_AUTO` | 2 | Auto-selected from the device's calibration data: fidelity-aware when the physical graph carries calibration data, otherwise topology only. | +| `LAYOUT_OBJECTIVE_FIDELITY_REQUIRED` | 3 | Fidelity required: like fidelity-aware, but errors when the physical graph carries no usable calibration data. | + +VF2 edge-requirement tags (the `CVf2LayoutConfig.edge_requirement` field): + +| Constant | Value | Requirement | +| --- | --- | --- | +| `VF2_EDGE_REQUIREMENT_POSITIVE_INTERACTIONS` | 0 | Only positive-weight interaction edges must match. | +| `VF2_EDGE_REQUIREMENT_ALL_INTERACTIONS` | 1 | All interaction edges must match. | + +--- + +## Data Structures + +### CLayoutScore + +Layout score snapshot, written by `score_layout` and `layout_result_score`: + +```c +typedef struct CLayoutScore { + double total; /* Weighted total according to the objective. */ + double distance; /* Raw weighted-distance component. */ + double direction; /* Raw direction-mismatch component. */ + double two_qubit_error; /* Unweighted effective two-qubit placement cost. */ + double readout_error; /* Raw readout error component. */ + uint8_t used_fidelity; /* 1 when the objective used fidelity terms. */ + uint8_t _reserved[7]; /* Reserved padding. */ +} CLayoutScore; +``` + +| Field | Meaning | +| --- | --- | +| `total` | Weighted total according to the objective; lower is better. | +| `distance` | Raw weighted-distance component. | +| `direction` | Raw direction-mismatch component. | +| `two_qubit_error` | Unweighted effective two-qubit placement cost. | +| `readout_error` | Raw readout-error component. | +| `used_fidelity` | 1 when the objective used fidelity terms. | + +### CInteraction + +Snapshot of one logical-qubit interaction, written by `circuit_layout_analysis_interaction`: + +| Field | Type | Meaning | +| --- | --- | --- | +| `left` | `uint32_t` | Lower-sorted logical qubit endpoint. | +| `right` | `uint32_t` | Higher-sorted logical qubit endpoint. | +| `weight` | `double` | Total interaction weight of this unordered pair. | +| `directed_weight_left_to_right` | `double` | Weight observed in operation order `left -> right`. | +| `directed_weight_right_to_left` | `double` | Weight observed in operation order `right -> left`. | +| `first_seen_order` | `uintptr_t` | First appearance index in operation order. | + +### CVf2LayoutConfig + +VF2 perfect-layout configuration: + +```c +typedef struct CVf2LayoutConfig { + uintptr_t candidate_limit; /* Max complete perfect candidates to score. */ + uintptr_t call_limit; /* Max partial mapping extensions ((uintptr_t)-1 = none). */ + uint8_t edge_requirement; /* One of VF2_EDGE_REQUIREMENT_*. */ + uint8_t _reserved[7]; /* Reserved padding. */ +} CVf2LayoutConfig; +``` + +| Field | Type | Meaning | +| --- | --- | --- | +| `candidate_limit` | `uintptr_t` | Maximum number of complete perfect candidates to score. | +| `call_limit` | `uintptr_t` | Maximum number of partial mapping extensions attempted; `(uintptr_t)-1` means no explicit limit. | +| `edge_requirement` | `uint8_t` | One of `VF2_EDGE_REQUIREMENT_*`. | +| `_reserved[7]` | `uint8_t[7]` | Padding that keeps the struct layout stable; do not write non-zero values. | + +### vf2_layout_config_default() + +```c +struct CVf2LayoutConfig vf2_layout_config_default(void); +``` + +Returns the default VF2 configuration by value: `candidate_limit = 10`, `call_limit = (uintptr_t)-1` (no limit), `edge_requirement = VF2_EDGE_REQUIREMENT_POSITIVE_INTERACTIONS`. + +--- + +## Layout Entry Points + +None of the four entry points modifies the input circuit; free the returned `CLayoutResult*` with `layout_result_free`. `objective` is one of the `LAYOUT_OBJECTIVE_*` tags; `LAYOUT_OBJECTIVE_AUTO` resolves against the device's calibration data (the physical graph is derived from the device), and `LAYOUT_OBJECTIVE_FIDELITY_REQUIRED` makes every entry point return NULL when the target carries no usable calibration data. + +### trivial_layout(circuit, device, objective) + +```c +struct CLayoutResult *trivial_layout(const struct CCircuit *circuit, + const struct CDevice *device, + uint8_t objective); +``` + +Trivial layout: maps logical qubits onto usable physical qubits in their existing order (logical i goes to the i-th usable physical qubit). + +- `circuit` (`const CCircuit*`): input circuit. +- `device` (`const CDevice*`): target device (construction in [Device / Properties](../2_device/2_properties_device.md)). +- `objective` (`uint8_t`): layout objective tag. + +Returns: a new `CLayoutResult*` on success; NULL when either argument is NULL, the `objective` tag is unknown, or the device has too few usable physical qubits. + +### greedy_layout(circuit, device, objective) + +```c +struct CLayoutResult *greedy_layout(const struct CCircuit *circuit, + const struct CDevice *device, + uint8_t objective); +``` + +Greedy layout: places logical qubits in interaction-strength order, prioritizing strongly interacting pairs. + +Parameters and return value as for `trivial_layout`: a new `CLayoutResult*` on success; NULL on NULL arguments, an unknown `objective` tag, or execution failure. + +### vf2_perfect_layout(circuit, device, objective, config) + +```c +struct CLayoutResult *vf2_perfect_layout(const struct CCircuit *circuit, + const struct CDevice *device, + uint8_t objective, + const struct CVf2LayoutConfig *config); +``` + +Searches for a perfect initial layout using non-induced VF2++ matching: when the circuit's interaction graph embeds exactly into the device topology, every positive-weight interaction lands on an adjacent hardware edge. + +- `circuit` (`const CCircuit*`): input circuit. +- `device` (`const CDevice*`): target device. +- `objective` (`uint8_t`): layout objective tag. +- `config` (`const CVf2LayoutConfig*`): VF2 configuration; NULL selects the default. + +Returns: a new `CLayoutResult*` on success; NULL when no perfect mapping exists, when a pointer argument is NULL, when the `objective` tag is unknown, or when `config->edge_requirement` is invalid. + +### sabre_layout(circuit, device, objective, config) + +```c +struct CLayoutResult *sabre_layout(const struct CCircuit *circuit, + const struct CDevice *device, + uint8_t objective, + const struct SabreConfigC *config); +``` + +Selects an initial layout using SABRE layout refinement: several candidate layouts are refined forward/backward, scored, and the best one is returned. + +- `circuit` (`const CCircuit*`): input circuit. +- `device` (`const CDevice*`): target device. +- `objective` (`uint8_t`): layout objective tag. +- `config` (`const SabreConfigC*`): SABRE configuration; NULL selects the default, field reference in [SABRE Configuration](5_sabre.md). + +Returns: a new `CLayoutResult*` on success; NULL on NULL arguments, an unknown `objective` tag, or an invalid configuration. + +--- + +## Circuit Layout Analysis + +`analyze_circuit_for_layout` precomputes the weighted logical interactions of a circuit for reuse by the prepared layout entry points. + +### analyze_circuit_for_layout(circuit) + +```c +struct CCircuitLayoutAnalysis *analyze_circuit_for_layout(const struct CCircuit *circuit); +``` + +Analyzes the circuit for layout planning (the logical-qubit table and the weighted interaction graph). + +- `circuit` (`const CCircuit*`): input circuit. + +Returns: a new `CCircuitLayoutAnalysis*` on success (free with `circuit_layout_analysis_free`); NULL on NULL input or failure. + +### circuit_layout_analysis_free(ptr) + +```c +void circuit_layout_analysis_free(struct CCircuitLayoutAnalysis *ptr); +``` + +Frees a circuit layout analysis handle; NULL is allowed. + +### circuit_layout_analysis_num_logical(ptr) + +```c +uintptr_t circuit_layout_analysis_num_logical(const struct CCircuitLayoutAnalysis *ptr); +``` + +Returns the number of logical qubits in the analysis. + +Returns: the logical-qubit count; 0 for NULL input. + +### circuit_layout_analysis_logical_qubits(ptr, out, len) + +```c +uintptr_t circuit_layout_analysis_logical_qubits(const struct CCircuitLayoutAnalysis *ptr, + uint32_t *out, + uintptr_t len); +``` + +Two-step read: copies the logical qubit IDs in source-circuit order into `out`. First call with `len = 0` and `out = NULL` to query the total, then allocate a buffer and call again with `len` equal to the total. + +- `ptr` (`const CCircuitLayoutAnalysis*`): analysis handle. +- `out` (`uint32_t*`): output buffer. +- `len` (`uintptr_t`): buffer capacity. + +Returns: the total count; 0 for NULL input. + +### circuit_layout_analysis_interactions_len(ptr) + +```c +uintptr_t circuit_layout_analysis_interactions_len(const struct CCircuitLayoutAnalysis *ptr); +``` + +Returns the number of weighted logical interactions. + +Returns: the interaction count; 0 for NULL input. + +### circuit_layout_analysis_interaction(ptr, index, out) + +```c +int32_t circuit_layout_analysis_interaction(const struct CCircuitLayoutAnalysis *ptr, + uintptr_t index, + struct CInteraction *out); +``` + +Writes the interaction at `index` to `*out` (fields in the `CInteraction` table above). + +- `ptr` (`const CCircuitLayoutAnalysis*`): analysis handle. +- `index` (`uintptr_t`): interaction index, must be smaller than `circuit_layout_analysis_interactions_len`. +- `out` (`CInteraction*`): interaction snapshot output. + +Returns: `0` on success; `-1` when either argument is NULL; `-8` when `index` is out of bounds. + +--- + +## Physical Layout Graph + +`CPhysicalLayoutGraph` is the compiler-local physical topology and calibration view of a device: usable physical qubits, shortest distances, coupling directions, and calibrated error rates. + +### physical_layout_graph_from_device(device) + +```c +struct CPhysicalLayoutGraph *physical_layout_graph_from_device(const struct CDevice *device); +``` + +Builds the physical layout graph of a device. + +- `device` (`const CDevice*`): target device. + +Returns: a new `CPhysicalLayoutGraph*` on success (free with `physical_layout_graph_free`); NULL on NULL input or failure. + +### physical_layout_graph_free(ptr) + +```c +void physical_layout_graph_free(struct CPhysicalLayoutGraph *ptr); +``` + +Frees a physical layout graph handle; NULL is allowed. + +### physical_layout_graph_num_physical(ptr) + +```c +uintptr_t physical_layout_graph_num_physical(const struct CPhysicalLayoutGraph *ptr); +``` + +Returns the number of usable physical qubits. + +Returns: the usable-qubit count; 0 for NULL input. + +### physical_layout_graph_physical_qubits(ptr, out, len) + +```c +uintptr_t physical_layout_graph_physical_qubits(const struct CPhysicalLayoutGraph *ptr, + uint32_t *out, + uintptr_t len); +``` + +Two-step read: copies the usable physical qubit IDs in ascending order into `out`. Query the total first (`physical_layout_graph_num_physical`), then allocate and fill. + +- `ptr` (`const CPhysicalLayoutGraph*`): graph handle. +- `out` (`uint32_t*`): output buffer. +- `len` (`uintptr_t`): buffer capacity. + +Returns: the total count; 0 for NULL input. + +### physical_layout_graph_distance(ptr, a, b) + +```c +uint32_t physical_layout_graph_distance(const struct CPhysicalLayoutGraph *ptr, + uint32_t a, + uint32_t b); +``` + +Returns the undirected shortest-path distance between two physical qubits. + +- `ptr` (`const CPhysicalLayoutGraph*`): graph handle. +- `a`, `b` (`uint32_t`): physical qubit IDs. + +Returns: the shortest distance; `UINT32_MAX` when the pair is disconnected, unknown, or the handle is NULL. + +### physical_layout_graph_is_adjacent_undirected(ptr, a, b) + +```c +int32_t physical_layout_graph_is_adjacent_undirected(const struct CPhysicalLayoutGraph *ptr, + uint32_t a, + uint32_t b); +``` + +Tests whether two physical qubits are adjacent (directly coupled, undirected). + +Returns: `1` when adjacent; `0` otherwise; `-1` for NULL. + +### physical_layout_graph_readout_error(ptr, qubit, out) + +```c +int32_t physical_layout_graph_readout_error(const struct CPhysicalLayoutGraph *ptr, + uint32_t qubit, + double *out); +``` + +Reads the readout error rate of one physical qubit. + +- `ptr` (`const CPhysicalLayoutGraph*`): graph handle. +- `qubit` (`uint32_t`): physical qubit ID. +- `out` (`double*`): error-rate output. + +Returns: `0` on success (value written to `*out`); `-1` when either argument is NULL; `-8` when no readout data is recorded for the qubit. + +### physical_layout_graph_supports_directed_coupling(ptr, control, target) + +```c +int32_t physical_layout_graph_supports_directed_coupling(const struct CPhysicalLayoutGraph *ptr, + uint32_t control, + uint32_t target); +``` + +Tests whether the directed coupling `control -> target` exists. + +- `control`, `target` (`uint32_t`): physical qubit IDs. + +Returns: `1` when the directed coupling exists; `0` when it does not; `-1` for NULL. + +### physical_layout_graph_supports_two_qubit_gate_directed(ptr, gate_name, control, target) + +```c +int32_t physical_layout_graph_supports_two_qubit_gate_directed(const struct CPhysicalLayoutGraph *ptr, + const char *gate_name, + uint32_t control, + uint32_t target); +``` + +Tests whether `gate_name` is a native capability on the directed edge `control -> target`. + +- `gate_name` (`const char*`): standard gate name (for example `"CX"`, `"SWAP"`). +- `control`, `target` (`uint32_t`): physical qubit IDs. + +Returns: `1` when supported; `0` when not; `-1` for NULL; `-4` for an unknown gate name. + +### physical_layout_graph_two_qubit_gate_error_directed(ptr, gate_name, control, target, out) + +```c +int32_t physical_layout_graph_two_qubit_gate_error_directed(const struct CPhysicalLayoutGraph *ptr, + const char *gate_name, + uint32_t control, + uint32_t target, + double *out); +``` + +Reads the calibrated error rate of `gate_name` on the directed edge `control -> target`. + +- `gate_name` (`const char*`): standard gate name. +- `control`, `target` (`uint32_t`): physical qubit IDs. +- `out` (`double*`): error-rate output. + +Returns: `0` on success (value written to `*out`); `-1` for NULL; `-4` for an unknown gate name; `-8` when the gate is unsupported or uncalibrated on the edge. + +### physical_layout_graph_has_fidelity_data(ptr) / has_readout_error_data / has_two_qubit_error_data + +```c +int32_t physical_layout_graph_has_fidelity_data(const struct CPhysicalLayoutGraph *ptr); +int32_t physical_layout_graph_has_readout_error_data(const struct CPhysicalLayoutGraph *ptr); +int32_t physical_layout_graph_has_two_qubit_error_data(const struct CPhysicalLayoutGraph *ptr); +``` + +Query calibration-data availability: any readout or two-qubit calibration data / readout-error data / two-qubit error data. `LAYOUT_OBJECTIVE_AUTO` uses these flags to pick the fidelity-aware or topology-only objective. + +Returns: `1` when the data is available; `0` otherwise; `-1` for NULL. + +--- + +## Prepared SABRE Objects + +The prepared pipeline builds the circuit-side and device-side SABRE data once and reuses it: `prepare_sabre_circuit` prepares the circuit analysis, `prepare_sabre_device_target` the exact device-side data. + +### prepare_sabre_circuit(circuit) + +```c +struct CPreparedSabreCircuit *prepare_sabre_circuit(const struct CCircuit *circuit); +``` + +Prepares the circuit-side analysis and dependency models used by SABRE. + +- `circuit` (`const CCircuit*`): input circuit. + +Returns: a new `CPreparedSabreCircuit*` on success (free with `prepared_sabre_circuit_free`); NULL on NULL input or failure. + +### prepared_sabre_circuit_free(ptr) + +```c +void prepared_sabre_circuit_free(struct CPreparedSabreCircuit *ptr); +``` + +Frees a prepared SABRE circuit handle; NULL is allowed. + +### prepared_sabre_circuit_logical_qubits_len(ptr) + +```c +uintptr_t prepared_sabre_circuit_logical_qubits_len(const struct CPreparedSabreCircuit *ptr); +``` + +Returns the number of logical qubits of the prepared circuit. + +Returns: the logical-qubit count; 0 for NULL input. + +### prepared_sabre_circuit_logical_qubits(ptr, out, len) + +```c +uintptr_t prepared_sabre_circuit_logical_qubits(const struct CPreparedSabreCircuit *ptr, + uint32_t *out, + uintptr_t len); +``` + +Two-step read: copies the prepared circuit's logical qubit IDs in source-circuit order into `out`. Query the total first, then allocate and fill. + +Returns: the total count; 0 for NULL input. + +### prepare_sabre_device_target(prepared, device) + +```c +struct CPreparedSabreTarget *prepare_sabre_device_target(const struct CPreparedSabreCircuit *prepared, + const struct CDevice *device); +``` + +Prepares exact device-native SABRE data for a prepared circuit: native operation feasibility, direction, and implementation costs. + +**Prerequisite**: the device must declare routing capability — one of the following must hold: the device-level native gate set is non-empty (for example set via `device_with_native_gates`, see [Device / Properties](../2_device/2_properties_device.md)); at least one usable physical qubit records native instructions in its properties; or at least one usable coupling edge records native instructions in its properties. A device without declared routing capability makes this call return NULL. + +- `prepared` (`const CPreparedSabreCircuit*`): prepared circuit handle (result of `prepare_sabre_circuit`). +- `device` (`const CDevice*`): target device. + +Returns: a new `CPreparedSabreTarget*` on success (free with `prepared_sabre_target_free`); NULL when either argument is NULL, when the device declares no routing capability, or on failure. + +### prepared_sabre_target_free(ptr) + +```c +void prepared_sabre_target_free(struct CPreparedSabreTarget *ptr); +``` + +Frees a prepared SABRE target handle; NULL is allowed. + +### prepared_sabre_target_physical(ptr) + +```c +struct CPhysicalLayoutGraph *prepared_sabre_target_physical(const struct CPreparedSabreTarget *ptr); +``` + +Retrieves the physical layout graph used by layout objective scoring (a deep clone). + +Returns: a new `CPhysicalLayoutGraph*` on success (free with `physical_layout_graph_free`); NULL on NULL input. + +--- + +## Prepared Layout Algorithms + +The prepared entry points accept the analysis from `analyze_circuit_for_layout` and a `CPhysicalLayoutGraph` (or the SABRE prepared objects), skipping the repeated analysis. + +### trivial_layout_prepared(analysis, physical, objective) + +```c +struct CLayoutResult *trivial_layout_prepared(const struct CCircuitLayoutAnalysis *analysis, + const struct CPhysicalLayoutGraph *physical, + uint8_t objective); +``` + +Trivial layout over a prepared analysis and physical graph. `LAYOUT_OBJECTIVE_AUTO` resolves against the passed physical graph's calibration data. + +- `analysis` (`const CCircuitLayoutAnalysis*`): circuit layout analysis. +- `physical` (`const CPhysicalLayoutGraph*`): physical layout graph. +- `objective` (`uint8_t`): layout objective tag. + +Returns: a new `CLayoutResult*` on success; NULL on NULL arguments, an unknown `objective` tag, or execution failure. + +### greedy_layout_prepared(analysis, physical, objective) + +```c +struct CLayoutResult *greedy_layout_prepared(const struct CCircuitLayoutAnalysis *analysis, + const struct CPhysicalLayoutGraph *physical, + uint8_t objective); +``` + +Greedy layout over a prepared analysis and physical graph. Parameters and return value as for `trivial_layout_prepared`. + +### vf2_perfect_layout_prepared(analysis, physical, objective, config) + +```c +struct CLayoutResult *vf2_perfect_layout_prepared(const struct CCircuitLayoutAnalysis *analysis, + const struct CPhysicalLayoutGraph *physical, + uint8_t objective, + const struct CVf2LayoutConfig *config); +``` + +VF2 perfect layout over a prepared analysis and physical graph; `config` NULL selects the default. + +Returns: a new `CLayoutResult*` on success; NULL when no perfect mapping exists, on invalid arguments, or on execution failure. + +### sabre_layout_prepared(prepared, prepared_target, objective, config) + +```c +struct CLayoutResult *sabre_layout_prepared(const struct CPreparedSabreCircuit *prepared, + const struct CPreparedSabreTarget *prepared_target, + uint8_t objective, + const struct SabreConfigC *config); +``` + +SABRE layout over a prepared circuit and prepared target; `LAYOUT_OBJECTIVE_AUTO` resolves against the prepared target's physical graph, and `config` NULL selects the default. + +- `prepared` (`const CPreparedSabreCircuit*`): prepared circuit. +- `prepared_target` (`const CPreparedSabreTarget*`): prepared target (physical graph plus native costs). +- `objective` (`uint8_t`): layout objective tag. +- `config` (`const SabreConfigC*`): SABRE configuration, see [SABRE Configuration](5_sabre.md). + +Returns: a new `CLayoutResult*` on success; NULL on NULL pointer arguments, an unknown `objective` tag, or an invalid configuration. + +### score_layout(objective, analysis, physical, layout, out) + +```c +int32_t score_layout(uint8_t objective, + const struct CCircuitLayoutAnalysis *analysis, + const struct CPhysicalLayoutGraph *physical, + const struct CLayout *layout, + struct CLayoutScore *out); +``` + +Scores a candidate layout against the layout objective and writes the snapshot to `*out` (fields in the `CLayoutScore` table above). `layout` must map the logical qubits of `analysis` onto the physical qubits of `physical`. + +- `objective` (`uint8_t`): layout objective tag. +- `analysis` (`const CCircuitLayoutAnalysis*`): circuit layout analysis. +- `physical` (`const CPhysicalLayoutGraph*`): physical layout graph. +- `layout` (`const CLayout*`): candidate layout, see [Layout](../2_device/3_layout.md). +- `out` (`CLayoutScore*`): score snapshot output. + +Returns: `0` on success; `-1` when any argument is NULL; `-8` for an unknown `objective` tag or an unresolvable objective (for example `LAYOUT_OBJECTIVE_FIDELITY_REQUIRED` without calibration data); `-6` when the core scoring fails. + +--- + +## Layout Result Accessors + +`CLayoutResult` carries the selected mapping, an optional score, and diagnostics. + +### layout_result_free(ptr) + +```c +void layout_result_free(struct CLayoutResult *ptr); +``` + +Frees a layout result handle; NULL is allowed. + +### layout_result_layout(ptr) + +```c +struct CLayout *layout_result_layout(const struct CLayoutResult *ptr); +``` + +Retrieves the selected logical-to-physical mapping (a deep clone). + +Returns: a new `CLayout*` on success (free with `layout_free`); NULL on NULL input. + +### layout_result_has_score(ptr) + +```c +int32_t layout_result_has_score(const struct CLayoutResult *ptr); +``` + +Tests whether the result carries an observed score. + +Returns: `1` when a score is present; `0` otherwise; `-1` for NULL. + +### layout_result_score(ptr, out) + +```c +int32_t layout_result_score(const struct CLayoutResult *ptr, struct CLayoutScore *out); +``` + +Writes the score snapshot to `*out`. + +Returns: `0` on success; `-1` when either argument is NULL; `-8` when the result has no score. + +### layout_result_is_perfect(ptr) + +```c +int32_t layout_result_is_perfect(const struct CLayoutResult *ptr); +``` + +Tests whether the layout is perfect: every positive-weight interaction lands on an adjacent hardware edge. + +Returns: `1` when perfect; `0` otherwise; `-1` for NULL. + +### layout_result_candidates_evaluated(ptr) + +```c +uintptr_t layout_result_candidates_evaluated(const struct CLayoutResult *ptr); +``` + +Returns the number of candidate layouts considered by the method. + +Returns: the candidate count; 0 for NULL input. + +### layout_result_used_fidelity(ptr) + +```c +int32_t layout_result_used_fidelity(const struct CLayoutResult *ptr); +``` + +Tests whether fidelity data contributed to the selected score. + +Returns: `1` when it contributed; `0` otherwise; `-1` for NULL. + +### layout_result_notes_len(ptr) / layout_result_note(ptr, index) + +```c +uintptr_t layout_result_notes_len(const struct CLayoutResult *ptr); +char *layout_result_note(const struct CLayoutResult *ptr, uintptr_t index); +``` + +Two-step read of diagnostic notes: `notes_len` returns the note count (0 for NULL), and `note` returns the note at `index` as a heap-allocated C string (free with `cqlib_string_free`); NULL on out-of-bounds. + +--- + +## Example + +The four layout entry points with result reading: + +```c +#include +#include "cqlib_c.h" + +int main(void) { + /* Directed line 0 -> 1 -> 2 built from an edge list */ + uint32_t edges[4] = {0, 1, 1, 2}; + struct CDevice *device = device_from_edges("line-3", 3, edges, 2); + struct CCircuit *circuit = circuit_new(3); + circuit_cx(circuit, 0, 2); /* non-adjacent pair on the line */ + if (device == NULL || circuit == NULL) { + return 1; + } + + /* Trivial layout maps logical i to physical i */ + struct CLayoutResult *trivial = + trivial_layout(circuit, device, LAYOUT_OBJECTIVE_TOPOLOGY_ONLY); + if (trivial != NULL) { + struct CLayout *mapping = layout_result_layout(trivial); + uintptr_t n = layout_num_logical(mapping); /* 3 */ + layout_free(mapping); + layout_result_free(trivial); + } + + /* Greedy and SABRE layouts run on the same circuit */ + struct CLayoutResult *greedy = + greedy_layout(circuit, device, LAYOUT_OBJECTIVE_TOPOLOGY_ONLY); + if (greedy != NULL) { + layout_result_free(greedy); + } + + struct CLayoutResult *sabre = + sabre_layout(circuit, device, LAYOUT_OBJECTIVE_TOPOLOGY_ONLY, NULL); + if (sabre != NULL) { + uintptr_t evaluated = layout_result_candidates_evaluated(sabre); /* >= 1 */ + layout_result_free(sabre); + } + + circuit_free(circuit); + device_free(device); + return 0; +} +``` + +The prepared pipeline: circuit analysis, physical graph, prepared SABRE objects, and scoring (the device declares its native gate set first): + +```c +#include +#include +#include "cqlib_c.h" + +int main(void) { + uint32_t edges[4] = {0, 1, 1, 2}; + struct CDevice *device = device_from_edges("line-3", 3, edges, 2); + struct CCircuit *circuit = circuit_new(3); + circuit_cx(circuit, 0, 2); + if (device == NULL || circuit == NULL) { + return 1; + } + + /* Exact device-native SABRE requires declared routing capability */ + if (device_with_native_gates(device, "H,CX,SWAP") != 0) { + circuit_free(circuit); + device_free(device); + return 1; + } + + /* Circuit-side analysis: one cx(0, 2) collapses to one interaction pair */ + struct CCircuitLayoutAnalysis *analysis = analyze_circuit_for_layout(circuit); + uintptr_t num_interactions = circuit_layout_analysis_interactions_len(analysis); + for (uintptr_t i = 0; i < num_interactions; i++) { + struct CInteraction interaction; + if (circuit_layout_analysis_interaction(analysis, i, &interaction) == 0) { + printf("interaction %u-%u weight %f\n", + interaction.left, interaction.right, interaction.weight); + } + } + + /* Physical graph view of the device */ + struct CPhysicalLayoutGraph *graph = physical_layout_graph_from_device(device); + uintptr_t num_physical = physical_layout_graph_num_physical(graph); /* 3 */ + uint32_t *physical_ids = malloc(num_physical * sizeof(uint32_t)); + physical_layout_graph_physical_qubits(graph, physical_ids, num_physical); + uint32_t dist = physical_layout_graph_distance(graph, 0, 2); /* 2 */ + int32_t adjacent = + physical_layout_graph_is_adjacent_undirected(graph, 0, 2); /* 0 */ + free(physical_ids); + + /* Prepared SABRE pipeline */ + struct CPreparedSabreCircuit *prepared = prepare_sabre_circuit(circuit); + struct CPreparedSabreTarget *target = prepare_sabre_device_target(prepared, device); + if (prepared != NULL && target != NULL) { + struct CPhysicalLayoutGraph *target_graph = prepared_sabre_target_physical(target); + physical_layout_graph_free(target_graph); + + struct CLayoutResult *result = sabre_layout_prepared( + prepared, target, LAYOUT_OBJECTIVE_TOPOLOGY_ONLY, NULL); + if (result != NULL) { + struct CLayout *mapping = layout_result_layout(result); + + /* Score the selected mapping against the prepared analysis */ + struct CLayoutScore score; + if (score_layout(LAYOUT_OBJECTIVE_TOPOLOGY_ONLY, + analysis, graph, mapping, &score) == 0) { + printf("total=%f distance=%f\n", score.total, score.distance); + } + layout_free(mapping); + layout_result_free(result); + } + prepared_sabre_target_free(target); + prepared_sabre_circuit_free(prepared); + } + + physical_layout_graph_free(graph); + circuit_layout_analysis_free(analysis); + circuit_free(circuit); + device_free(device); + return 0; +} +``` + +Handing the selected layout to the routing stage for SWAP insertion is covered in [Routing](4_routing.md); the device-target compile entry point that performs layout and routing in one call is documented in [Compiler](1_compiler.md). diff --git a/docs/api/en/c/4_compile/4_routing.md b/docs/api/en/c/4_compile/4_routing.md new file mode 100644 index 00000000..21db40fb --- /dev/null +++ b/docs/api/en/c/4_compile/4_routing.md @@ -0,0 +1,535 @@ +# Routing (C) + +This page covers the transform-level routing entry points `route_with_layout` / `route_sabre` and every read accessor of the routing-result handles `CRoutedCircuit` / `CSabreRouteResult`: the routed circuit, initial and final layouts, the SWAP count, routing diagnostics, and layout metadata. Routing inserts SWAP operations on the device topology so that every two-qubit interaction lands on adjacent physical qubits; the routed circuit uses physical qubit identifiers. Error codes, string and handle ownership conventions follow the [Overview](../0_overview.md); the `SabreConfigC` configuration struct, the `CSabreRoutingDiagnostics` struct, and the low-level `sabre_route` are documented in [SABRE](5_sabre.md). + +--- + +## Constants + +Layout-objective tags (the `objective` parameter of `route_sabre`): + +| Constant | Value | Meaning | +| --- | --- | --- | +| `LAYOUT_OBJECTIVE_TOPOLOGY_ONLY` | 0 | Topology-only scoring: distance and direction terms. | +| `LAYOUT_OBJECTIVE_FIDELITY_AWARE` | 1 | Adds fidelity terms on top of the topology score (two-qubit error weight 10.0, readout error weight 1.0). | +| `LAYOUT_OBJECTIVE_AUTO` | 2 | Chooses automatically from device data: enables the fidelity terms the device carries (two-qubit gate / readout error data), otherwise falls back to topology-only. | +| `LAYOUT_OBJECTIVE_FIDELITY_REQUIRED` | 3 | Requires fidelity terms: like fidelity-aware, but fails when the device carries no usable calibration data. | + +--- + +## Routing Entry Points + +### route_with_layout(circuit, device, initial_layout, config) + +Deterministic routing from a caller-supplied initial layout: no layout selection, refinement, or scoring is performed. Intended for callers that already hold a layout (for example from a layout search or a previous SABRE run). + +```c +struct CRoutedCircuit *route_with_layout(const struct CCircuit *circuit, + const struct CDevice *device, + const struct CLayout *initial_layout, + const struct SabreConfigC *config); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `circuit` | `const CCircuit*` | Input circuit. | +| `device` | `const CDevice*` | Target device (construction in [Device / Properties](../2_device/2_properties_device.md)). | +| `initial_layout` | `const CLayout*` | Initial logical-to-physical mapping (construction in [Layout](../2_device/3_layout.md)). | +| `config` | `const SabreConfigC*` | SABRE configuration; NULL selects the default (see [SABRE](5_sabre.md)). | + +Returns: a new `CRoutedCircuit*` on success (free with `routed_circuit_free`); NULL when any handle is NULL, the configuration cannot be converted (for example `num_lookahead_weights` exceeds 8), or routing fails (invalid configuration, insufficient usable physical qubits, unreachable interactions, ...). + +### route_sabre(circuit, device, objective, config) + +SABRE routing with layout selection: first selects an initial layout under `objective`, then routes the original forward circuit from that layout. The selected layout is returned alongside the routing result. + +```c +struct CSabreRouteResult *route_sabre(const struct CCircuit *circuit, + const struct CDevice *device, + uint8_t objective, + const struct SabreConfigC *config); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `circuit` | `const CCircuit*` | Input circuit. | +| `device` | `const CDevice*` | Target device. | +| `objective` | `uint8_t` | Layout objective, one of `LAYOUT_OBJECTIVE_*`. | +| `config` | `const SabreConfigC*` | SABRE configuration; NULL selects the default. Equal seeds produce equal routing results for the same circuit and device. | + +Returns: a new `CSabreRouteResult*` on success (free with `sabre_route_result_free`); NULL when any handle is NULL, `objective` is not a valid tag, the configuration cannot be converted, or layout selection / routing fails. + +--- + +## RoutedCircuit Accessors + +Result handle of `route_with_layout`. + +### routed_circuit_free(ptr) + +Frees a routing-result handle; NULL is allowed. + +```c +void routed_circuit_free(struct CRoutedCircuit *ptr); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `ptr` | `struct CRoutedCircuit*` | Handle to free; may be NULL. | + +### routed_circuit_circuit(ptr) + +Retrieves the routed physical circuit (a deep clone; the circuit qubits are physical qubit identifiers and every two-qubit operation is adjacent in the usable physical topology). + +```c +struct CCircuit *routed_circuit_circuit(const struct CRoutedCircuit *ptr); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `ptr` | `const CRoutedCircuit*` | Routing-result handle. | + +Returns: a new `CCircuit*` on success (free with `circuit_free`); NULL on NULL input. + +### routed_circuit_initial_layout(ptr) + +Retrieves the initial logical-to-physical layout used before routing (deep clone). + +```c +struct CLayout *routed_circuit_initial_layout(const struct CRoutedCircuit *ptr); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `ptr` | `const CRoutedCircuit*` | Routing-result handle. | + +Returns: a new `CLayout*` on success (free with `layout_free`); NULL on NULL input. + +### routed_circuit_final_layout(ptr) + +Retrieves the final logical-to-physical layout after all routed operations (deep clone). + +```c +struct CLayout *routed_circuit_final_layout(const struct CRoutedCircuit *ptr); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `ptr` | `const CRoutedCircuit*` | Routing-result handle. | + +Returns: a new `CLayout*` on success (free with `layout_free`); NULL on NULL input. + +### routed_circuit_swap_count(ptr) + +Returns the number of inserted SWAP operations. + +```c +uintptr_t routed_circuit_swap_count(const struct CRoutedCircuit *ptr); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `ptr` | `const CRoutedCircuit*` | Routing-result handle. | + +Returns: the SWAP count; 0 for NULL. + +### routed_circuit_diagnostics(ptr, out) + +Writes the routing-diagnostics snapshot to `*out` (field meanings in [SABRE](5_sabre.md), `CSabreRoutingDiagnostics`). + +```c +int32_t routed_circuit_diagnostics(const struct CRoutedCircuit *ptr, + struct CSabreRoutingDiagnostics *out); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `ptr` | `const CRoutedCircuit*` | Routing-result handle. | +| `out` | `struct CSabreRoutingDiagnostics*` | Diagnostics snapshot output. | + +Returns: 0 on success. + +| Error code | Name | Scenario | +| --- | --- | --- | +| `-1` | NullPtr | `ptr` or `out` is NULL. | + +### routed_circuit_changed(ptr, original) + +Tests whether routing observably changed `original`: changed when SWAP operations were inserted, the physical qubit set differs from the input, the global phase changed, the initial layout is not the identity mapping, or the operation stream differs structurally. + +```c +int32_t routed_circuit_changed(const struct CRoutedCircuit *ptr, const struct CCircuit *original); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `ptr` | `const CRoutedCircuit*` | Routing-result handle. | +| `original` | `const CCircuit*` | The original circuit before routing. | + +Returns: 1 when changed; 0 otherwise. + +| Error code | Name | Scenario | +| --- | --- | --- | +| `-1` | NullPtr | `ptr` or `original` is NULL. | + +--- + +## SabreRouteResult Accessors + +Result handle of `route_sabre`; carries the routed data plus metadata about the selected layout. + +### sabre_route_result_free(ptr) + +Frees a routing-result handle; NULL is allowed. + +```c +void sabre_route_result_free(struct CSabreRouteResult *ptr); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `ptr` | `struct CSabreRouteResult*` | Handle to free; may be NULL. | + +### sabre_route_result_routed(ptr) + +Retrieves the embedded routed circuit and routing metadata (deep clone). + +```c +struct CRoutedCircuit *sabre_route_result_routed(const struct CSabreRouteResult *ptr); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `ptr` | `const CSabreRouteResult*` | Routing-result handle. | + +Returns: a new `CRoutedCircuit*` on success (free with `routed_circuit_free`); NULL on NULL input. + +### sabre_route_result_circuit(ptr) + +Retrieves the routed physical circuit (deep clone); equivalent to reading the circuit of `routed`. + +```c +struct CCircuit *sabre_route_result_circuit(const struct CSabreRouteResult *ptr); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `ptr` | `const CSabreRouteResult*` | Routing-result handle. | + +Returns: a new `CCircuit*` on success (free with `circuit_free`); NULL on NULL input. + +### sabre_route_result_initial_layout(ptr) + +Retrieves the initial logical-to-physical layout used for routing (deep clone). + +```c +struct CLayout *sabre_route_result_initial_layout(const struct CSabreRouteResult *ptr); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `ptr` | `const CSabreRouteResult*` | Routing-result handle. | + +Returns: a new `CLayout*` on success (free with `layout_free`); NULL on NULL input. + +### sabre_route_result_final_layout(ptr) + +Retrieves the final logical-to-physical layout after routing (deep clone). + +```c +struct CLayout *sabre_route_result_final_layout(const struct CSabreRouteResult *ptr); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `ptr` | `const CSabreRouteResult*` | Routing-result handle. | + +Returns: a new `CLayout*` on success (free with `layout_free`); NULL on NULL input. + +### sabre_route_result_swap_count(ptr) + +Returns the number of inserted SWAP operations. + +```c +uintptr_t sabre_route_result_swap_count(const struct CSabreRouteResult *ptr); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `ptr` | `const CSabreRouteResult*` | Routing-result handle. | + +Returns: the SWAP count; 0 for NULL. + +### sabre_route_result_diagnostics(ptr, out) + +Writes the routing-diagnostics snapshot to `*out` (field meanings in [SABRE](5_sabre.md), `CSabreRoutingDiagnostics`). + +```c +int32_t sabre_route_result_diagnostics(const struct CSabreRouteResult *ptr, + struct CSabreRoutingDiagnostics *out); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `ptr` | `const CSabreRouteResult*` | Routing-result handle. | +| `out` | `struct CSabreRoutingDiagnostics*` | Diagnostics snapshot output. | + +Returns: 0 on success. + +| Error code | Name | Scenario | +| --- | --- | --- | +| `-1` | NullPtr | `ptr` or `out` is NULL. | + +### sabre_route_result_changed(ptr, original) + +Tests whether routing observably changed `original`; the decision rule matches `routed_circuit_changed`. + +```c +int32_t sabre_route_result_changed(const struct CSabreRouteResult *ptr, + const struct CCircuit *original); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `ptr` | `const CSabreRouteResult*` | Routing-result handle. | +| `original` | `const CCircuit*` | The original circuit before routing. | + +Returns: 1 when changed; 0 otherwise. + +| Error code | Name | Scenario | +| --- | --- | --- | +| `-1` | NullPtr | `ptr` or `original` is NULL. | + +--- + +## Layout Metadata + +Additional information about the layout selected by `route_sabre`. + +### sabre_route_result_layout_score(ptr, out) + +Writes the layout-score snapshot of the selected initial layout to `*out`. The score is diagnostic only; SABRE selects the winner by predicted native routing quality. + +```c +int32_t sabre_route_result_layout_score(const struct CSabreRouteResult *ptr, + struct CLayoutScore *out); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `ptr` | `const CSabreRouteResult*` | Routing-result handle. | +| `out` | `struct CLayoutScore*` | Layout-score snapshot output. | + +Returns: 0 on success. + +| Error code | Name | Scenario | +| --- | --- | --- | +| `-1` | NullPtr | `ptr` or `out` is NULL. | +| `-8` | InvalidParam | The result carries no layout score. | + +`CLayoutScore` fields: + +| Field | Type | Meaning | +| --- | --- | --- | +| `total` | `double` | Weighted total according to the objective. | +| `distance` | `double` | Raw weighted-distance component. | +| `direction` | `double` | Raw direction-mismatch component. | +| `two_qubit_error` | `double` | Unweighted effective two-qubit placement cost. | +| `readout_error` | `double` | Raw readout error component. | +| `used_fidelity` | `uint8_t` | 1 when the objective used fidelity terms. | +| `_reserved[7]` | `uint8_t[7]` | Reserved padding that keeps the struct layout stable. | + +### sabre_route_result_layout_is_perfect(ptr) + +Tests whether the selected layout places every positive-weight interaction directly on an adjacent hardware edge (no SWAP needed at all). + +```c +int32_t sabre_route_result_layout_is_perfect(const struct CSabreRouteResult *ptr); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `ptr` | `const CSabreRouteResult*` | Routing-result handle. | + +Returns: 1 when perfect; 0 otherwise. + +| Error code | Name | Scenario | +| --- | --- | --- | +| `-1` | NullPtr | `ptr` is NULL. | + +### sabre_route_result_layout_candidates_evaluated(ptr) + +Returns the number of candidate layouts evaluated while selecting the initial layout. + +```c +uintptr_t sabre_route_result_layout_candidates_evaluated(const struct CSabreRouteResult *ptr); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `ptr` | `const CSabreRouteResult*` | Routing-result handle. | + +Returns: the candidate count; 0 for NULL. + +### sabre_route_result_layout_used_fidelity(ptr) + +Tests whether fidelity data contributed to the selected layout score. + +```c +int32_t sabre_route_result_layout_used_fidelity(const struct CSabreRouteResult *ptr); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `ptr` | `const CSabreRouteResult*` | Routing-result handle. | + +Returns: 1 when fidelity data contributed; 0 otherwise. + +| Error code | Name | Scenario | +| --- | --- | --- | +| `-1` | NullPtr | `ptr` is NULL. | + +### sabre_route_result_layout_notes_len(ptr) / sabre_route_result_layout_note(ptr, index) + +Two-step read of the layout diagnostic notes: `*_len` returns the number of notes (0 for NULL), and `layout_note` returns note `index`. + +```c +uintptr_t sabre_route_result_layout_notes_len(const struct CSabreRouteResult *ptr); +``` + +```c +char *sabre_route_result_layout_note(const struct CSabreRouteResult *ptr, uintptr_t index); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `ptr` | `const CSabreRouteResult*` | Routing-result handle. | +| `index` | `uintptr_t` | Note index (for `layout_note`). | + +Returns: `layout_note` returns a heap-allocated C string; free with `cqlib_string_free`. NULL on NULL input or out-of-bounds index. + +--- + +## Example + +Routing `cx(0, 2)` on a line topology 0–1–2 and reading all metadata of both result kinds: + +```c +#include +#include "cqlib_c.h" + +int main(void) { + /* 1. Line topology 0 - 1 - 2 and a non-adjacent cx(0, 2) */ + uint32_t edges[4] = {0, 1, 1, 2}; + struct CDevice *device = device_from_edges("line-3", 3, edges, 2); + struct CCircuit *circuit = circuit_new(3); + circuit_cx(circuit, 0, 2); + if (device == NULL || circuit == NULL) { + return 1; + } + + /* 2. Route from an explicit identity layout (logical i -> physical i) */ + uint32_t logical[3] = {0, 1, 2}; + uint32_t physical[3] = {0, 1, 2}; + struct CLayout *layout = layout_new(logical, 3, physical, 3); + + struct CRoutedCircuit *routed = route_with_layout(circuit, device, layout, NULL); + if (routed != NULL) { + /* 3. Read the routed circuit, layouts, and diagnostics */ + uintptr_t swaps = routed_circuit_swap_count(routed); /* >= 1 */ + int32_t changed = routed_circuit_changed(routed, circuit); /* 1 */ + struct CCircuit *physical_circuit = routed_circuit_circuit(routed); + struct CLayout *initial = routed_circuit_initial_layout(routed); + struct CLayout *final_layout = routed_circuit_final_layout(routed); + struct CSabreRoutingDiagnostics diagnostics; + if (routed_circuit_diagnostics(routed, &diagnostics) == 0) { + printf("trials=%lu swaps=%lu changed=%d\n", + (unsigned long)diagnostics.trials_evaluated, + (unsigned long)swaps, changed); + } + circuit_free(physical_circuit); + layout_free(initial); + layout_free(final_layout); + routed_circuit_free(routed); + } + layout_free(layout); + + /* 4. route_sabre: select a layout and route in one call */ + struct CSabreRouteResult *result = + route_sabre(circuit, device, LAYOUT_OBJECTIVE_TOPOLOGY_ONLY, NULL); + if (result != NULL) { + struct CLayoutScore score; + if (sabre_route_result_layout_score(result, &score) == 0) { + printf("layout score total=%f perfect=%d\n", + score.total, sabre_route_result_layout_is_perfect(result)); + } + printf("candidates=%lu\n", + (unsigned long)sabre_route_result_layout_candidates_evaluated(result)); + + /* 5. Walk the layout diagnostic notes (two-step pattern) */ + uintptr_t notes = sabre_route_result_layout_notes_len(result); + for (uintptr_t i = 0; i < notes; i++) { + char *note = sabre_route_result_layout_note(result, i); + if (note != NULL) { + cqlib_string_free(note); + } + } + + struct CRoutedCircuit *inner = sabre_route_result_routed(result); + if (inner != NULL) { + routed_circuit_free(inner); + } + sabre_route_result_free(result); + } + + circuit_free(circuit); + device_free(device); + return 0; +} +``` + +Lowering a routed result to the device native gate set is done with `decompose_lower_to_device` in [Decompose / Resynthesis](6_decompose_resynthesis.md); the full compilation workflow (routing and lowering included) is shown in [Compiler](1_compiler.md). diff --git a/docs/api/en/c/4_compile/5_sabre.md b/docs/api/en/c/4_compile/5_sabre.md new file mode 100644 index 00000000..54294381 --- /dev/null +++ b/docs/api/en/c/4_compile/5_sabre.md @@ -0,0 +1,417 @@ +# SABRE (C) + +`CSabreRoutingResult` is the result handle of the low-level SABRE routing entry `sabre_route`. This page covers construction and validation of the SABRE configuration struct `SabreConfigC`, the routing entry with a known initial layout, the routing-result readers, plus the layout normalizer `normalize_initial_layout` and the reachability checker `validate_reachable_interactions`. A fixed random seed makes the tie-breaking and choices of the randomized trials reproducible. Error codes, string and handle ownership conventions follow the [Overview](../0_overview.md); the layout-selecting routing entries `route_sabre` / `route_with_layout` are documented in [Routing](4_routing.md). + +--- + +## Constants + +| Constant | Value | Meaning | +| --- | --- | --- | +| `SABRE_MAX_LOOKAHEAD_WEIGHTS` | 8 | Length of the `SabreHeuristicConfigC.lookahead_weights` array; the configuration cannot be converted (-8) when `num_lookahead_weights` exceeds this value. | + +--- + +## Configuration Construction and Validation + +### sabre_config_default() + +Returns the core default SABRE configuration (by value): `layout_trials=10`, `layout_assignment_budget=1000000`, VF2 prepass enabled (`candidate_limit=10`, `call_limit=1000000`), `refinement_iterations=1`, `routing_trials=1`, no seed; the heuristic uses `basic_weight=1.0`, `lookahead_weights=[0.5, 0.25, 0.125, 0.0625, 0.03125]`, congestion control enabled (`decay_increment=0.002`, `decay_reset=10`), `attempt_limit=1000`, and `best_epsilon=1e-10`. + +```c +struct SabreConfigC sabre_config_default(void); +``` + +Returns: the default configuration struct (by value, nothing to free). + +### sabre_config_deterministic_seeded(seed) + +Returns a compact deterministic configuration suited to reproducible runs: shrunken trial sizes (`layout_trials=2`, `layout_assignment_budget=100000`, VF2 prepass `candidate_limit=10`, `call_limit=100000`, `refinement_iterations=1`, `routing_trials=1`), the random seed fixed to `seed` (`has_seed=1`), and `attempt_limit` tightened to 20; the remaining fields match the default configuration. + +```c +struct SabreConfigC sabre_config_deterministic_seeded(uint64_t seed); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `seed` | `uint64_t` | Deterministic random seed. The same seed, circuit, and device produce the same routing result. | + +Returns: the deterministic configuration struct (by value, nothing to free). + +### sabre_config_validate(config) + +Validates the routing-relevant fields of a configuration: `routing_trials` must be greater than 0; the weights (`basic_weight`, each `lookahead_weights` entry, `decay_increment`) must be finite and non-negative; when congestion control is enabled, `decay_reset` must be greater than 0; `best_epsilon` must be finite and non-negative. Layout-refinement fields (such as `layout_trials`) are not validated. + +```c +int32_t sabre_config_validate(const struct SabreConfigC *config); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `config` | `const SabreConfigC*` | Configuration to validate. | + +Returns: 0 when valid. + +| Error code | Name | Scenario | +| --- | --- | --- | +| `-1` | NullPtr | `config` is NULL. | +| `-6` | CompilerError | Core validation rejects: `routing_trials` is 0, a weight is invalid, congestion control is enabled but `decay_reset` is 0, or `best_epsilon` is invalid. | +| `-8` | InvalidParam | `num_lookahead_weights` exceeds `SABRE_MAX_LOOKAHEAD_WEIGHTS`, so the configuration cannot be converted. | + +--- + +## Routing Entry + +### sabre_route(circuit, device, initial_layout, config) + +Low-level SABRE routing with a known initial layout: inserts SWAPs so that every two-qubit interaction becomes adjacent on the device's available physical topology; control-flow blocks are routed recursively and restore their entry layout before leaving the block. The returned circuit uses physical qubit identifiers. + +```c +struct CSabreRoutingResult *sabre_route(const struct CCircuit *circuit, + const struct CDevice *device, + const struct CLayout *initial_layout, + const struct SabreConfigC *config); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `circuit` | `const CCircuit*` | Input circuit. | +| `device` | `const CDevice*` | Target device (construction in [Device / Properties](../2_device/2_properties_device.md)). | +| `initial_layout` | `const CLayout*` | Initial logical-to-physical mapping (construction in [Layout](../2_device/3_layout.md)). | +| `config` | `const SabreConfigC*` | SABRE configuration; NULL selects the default. | + +Returns: a new `CSabreRoutingResult*` on success (free with `sabre_routing_result_free`); NULL when any handle is NULL, the configuration cannot be converted or fails validation (for example `routing_trials` is 0), or routing fails. + +--- + +## Result Readers + +### sabre_routing_result_free(ptr) + +Frees a routing-result handle; NULL is allowed. + +```c +void sabre_routing_result_free(struct CSabreRoutingResult *ptr); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `ptr` | `struct CSabreRoutingResult*` | Handle to free; may be NULL. | + +### sabre_routing_result_circuit(ptr) + +Extracts the physical circuit with SWAPs inserted (deep copy; the circuit's qubits are physical qubit identifiers). + +```c +struct CCircuit *sabre_routing_result_circuit(const struct CSabreRoutingResult *ptr); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `ptr` | `const CSabreRoutingResult*` | Routing-result handle. | + +Returns: a new `CCircuit*` on success (free with `circuit_free`); NULL on NULL input. + +### sabre_routing_result_initial_layout(ptr) + +Extracts the initial logical-to-physical layout used by the selected trial (deep copy). + +```c +struct CLayout *sabre_routing_result_initial_layout(const struct CSabreRoutingResult *ptr); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `ptr` | `const CSabreRoutingResult*` | Routing-result handle. | + +Returns: a new `CLayout*` on success (free with `layout_free`); NULL on NULL input. + +### sabre_routing_result_final_layout(ptr) + +Extracts the final logical-to-physical layout after all routing operations (including control-flow epilogues) have executed (deep copy). + +```c +struct CLayout *sabre_routing_result_final_layout(const struct CSabreRoutingResult *ptr); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `ptr` | `const CSabreRoutingResult*` | Routing-result handle. | + +Returns: a new `CLayout*` on success (free with `layout_free`); NULL on NULL input. + +### sabre_routing_result_swap_count(ptr) + +Returns the number of inserted SWAP operations (including control-flow epilogues). + +```c +uintptr_t sabre_routing_result_swap_count(const struct CSabreRoutingResult *ptr); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `ptr` | `const CSabreRoutingResult*` | Routing-result handle. | + +Returns: the SWAP count; 0 on NULL input. + +### sabre_routing_result_diagnostics(ptr, out) + +Writes a diagnostics snapshot of the routing run into `*out`. + +```c +int32_t sabre_routing_result_diagnostics(const struct CSabreRoutingResult *ptr, + struct CSabreRoutingDiagnostics *out); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `ptr` | `const CSabreRoutingResult*` | Routing-result handle. | +| `out` | `struct CSabreRoutingDiagnostics*` | Diagnostics snapshot output. | + +Returns: 0 on success. + +| Error code | Name | Scenario | +| --- | --- | --- | +| `-1` | NullPtr | `ptr` or `out` is NULL. | + +--- + +## Layout Utilities + +### normalize_initial_layout(logical_qubits, num_logical, device, initial_layout) + +Normalizes a complete logical-to-physical layout onto the device's usable physical qubits: every logical qubit in `logical_qubits` must already be mapped by `initial_layout`, and each mapping target must be a usable physical qubit of the device; the returned layout's physical-qubit set is the full set of the device's usable physical qubits. + +```c +struct CLayout *normalize_initial_layout(const uint32_t *logical_qubits, + uintptr_t num_logical, + const struct CDevice *device, + const struct CLayout *initial_layout); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `logical_qubits` | `const uint32_t*` | Array of logical qubit IDs; may be NULL when `num_logical` is 0. | +| `num_logical` | `uintptr_t` | Number of logical qubits. | +| `device` | `const CDevice*` | Target device. | +| `initial_layout` | `const CLayout*` | Layout to normalize. | + +Returns: a new `CLayout*` on success (free with `layout_free`); NULL when `device` or `initial_layout` is NULL, `num_logical` is greater than 0 but `logical_qubits` is NULL, or normalization fails (a logical qubit is unmapped, or mapped to an unusable physical qubit). + +### validate_reachable_interactions(circuit, device, initial_layout) + +Without performing routing, validates that starting from `initial_layout` every interaction in the circuit is reachable (both sides of the interaction lie in the same connected component of the device topology, so SWAPs can bring them together). The layout is first normalized onto the device's usable physical qubits before validation. + +```c +int32_t validate_reachable_interactions(const struct CCircuit *circuit, + const struct CDevice *device, + const struct CLayout *initial_layout); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `circuit` | `const CCircuit*` | Input circuit. | +| `device` | `const CDevice*` | Target device. | +| `initial_layout` | `const CLayout*` | Initial logical-to-physical mapping. | + +Returns: 0 when every interaction is reachable. + +| Error code | Name | Scenario | +| --- | --- | --- | +| `-1` | NullPtr | Any handle is NULL. | +| `-6` | CompilerError | An unreachable interaction exists (for example one end of an interaction lands on an uncoupled physical qubit), or the layout cannot be normalized. | + +--- + +## SabreConfigC + +Configuration shared by SABRE layout refinement and routing, passed by value: + +```c +typedef struct SabreConfigC { + uintptr_t layout_trials; /* Randomized starting layouts. */ + uintptr_t layout_assignment_budget; /* Max layout assignment states. */ + uint8_t vf2_prepass_enabled; /* 1 enables the bounded VF2 prepass. */ + struct SabreVf2PrepassConfigC vf2_prepass; + uintptr_t refinement_iterations; /* Refinement rounds per layout trial. */ + uintptr_t routing_trials; /* Complete routing trials per layout. */ + uint64_t seed; /* Used when has_seed != 0. */ + uint8_t has_seed; /* 1 uses seed. */ + struct SabreHeuristicConfigC heuristic; + uint64_t _reserved[4]; /* Reserved padding. */ +} SabreConfigC; +``` + +| Field | Type | Meaning | +| --- | --- | --- | +| `layout_trials` | `uintptr_t` | Number of randomized starting layouts added on top of the deterministic candidates. | +| `layout_assignment_budget` | `uintptr_t` | Maximum number of layout-component assignment states to explore; when exhausted, a budget exhaustion is reported, distinct from proven infeasibility. | +| `vf2_prepass_enabled` | `uint8_t` | 1 enables the bounded VF2 prepass, 0 disables it (`vf2_prepass` is then ignored). | +| `vf2_prepass` | `struct SabreVf2PrepassConfigC` | VF2 prepass limits (used when `vf2_prepass_enabled != 0`). | +| `refinement_iterations` | `uintptr_t` | Forward/backward refinement rounds per layout trial. | +| `routing_trials` | `uintptr_t` | Complete routing trials executed per fully refined layout; must be greater than 0. | +| `seed` | `uint64_t` | Deterministic seed (used when `has_seed != 0`). | +| `has_seed` | `uint8_t` | 1 uses `seed`, 0 leaves the seed unset. | +| `heuristic` | `struct SabreHeuristicConfigC` | Swap-selection heuristic configuration. | +| `_reserved[4]` | `uint64_t[4]` | Padding that keeps the struct layout stable; do not write non-zero values. | + +### SabreVf2PrepassConfigC + +```c +typedef struct SabreVf2PrepassConfigC { + uintptr_t candidate_limit; /* Complete perfect mappings scored. */ + uintptr_t call_limit; /* Partial mapping extensions attempted. */ + uint64_t _reserved[2]; /* Reserved padding. */ +} SabreVf2PrepassConfigC; +``` + +| Field | Type | Meaning | +| --- | --- | --- | +| `candidate_limit` | `uintptr_t` | Upper bound on complete perfect mappings scored by VF2. | +| `call_limit` | `uintptr_t` | Upper bound on partial mapping extensions attempted by VF2. | +| `_reserved[2]` | `uint64_t[2]` | Padding that keeps the struct layout stable. | + +### SabreHeuristicConfigC + +SABRE swap-selection heuristic configuration: the main score combines the current front-layer total distance, the lookahead layers scaled by active layer, and the multiplicative congestion; when native solutions exist, candidates are adjudicated within a narrow structural window by the exact native two-qubit cost. + +```c +typedef struct SabreHeuristicConfigC { + double basic_weight; /* Front-layer total distance weight. */ + double lookahead_weights[SABRE_MAX_LOOKAHEAD_WEIGHTS]; + uintptr_t num_lookahead_weights; /* Leading entries in use. */ + double decay_increment; /* Congestion multiplier increment. */ + uint8_t decay_enabled; /* 1 enables congestion control. */ + uintptr_t decay_reset; /* Attempts before decay resets. */ + uintptr_t attempt_limit; /* Heuristic SWAPs before fallback. */ + double best_epsilon; /* Tie tolerance for candidate scores. */ + uint8_t _reserved[8]; /* Reserved padding. */ +} SabreHeuristicConfigC; +``` + +| Field | Type | Meaning | +| --- | --- | --- | +| `basic_weight` | `double` | Weight of the current front-layer total distance; must be finite and non-negative. | +| `lookahead_weights` | `double[8]` | Weights scaling the lookahead-layer totals per active layer; only the leading `num_lookahead_weights` entries take effect, the rest are padding. | +| `num_lookahead_weights` | `uintptr_t` | Number of leading valid entries in `lookahead_weights`; must not exceed `SABRE_MAX_LOOKAHEAD_WEIGHTS`. | +| `decay_increment` | `double` | Increment applied to a physical qubit's congestion multiplier after a heuristic SWAP uses it. | +| `decay_enabled` | `uint8_t` | 1 enables congestion control (uses `decay_increment`), 0 disables it. | +| `decay_reset` | `uintptr_t` | Number of heuristic SWAP attempts before the decay values reset; must be greater than 0 when congestion control is enabled. | +| `attempt_limit` | `uintptr_t` | Number of consecutive heuristic SWAPs allowed without routing a front-layer node before falling back to shortest-path escape. | +| `best_epsilon` | `double` | Floating-point tolerance within which candidate SWAP scores count as tied; must be finite and non-negative. | +| `_reserved[8]` | `uint8_t[8]` | Padding that keeps the struct layout stable. | + +--- + +## CSabreRoutingDiagnostics + +Diagnostics snapshot of a routing run (the output of `sabre_routing_result_diagnostics` and the diagnostics interfaces of [Routing](4_routing.md)): + +| Field | Type | Meaning | +| --- | --- | --- | +| `trials_evaluated` | `uintptr_t` | Number of routing trials evaluated. | +| `selected_trial_index` | `uintptr_t` | Zero-based index of the selected routing trial. | +| `fallback_count` | `uintptr_t` | Number of shortest-path fallbacks used. | +| `control_flow_blocks_routed` | `uintptr_t` | Number of control-flow blocks routed recursively. | +| `two_qubit_depth` | `uintptr_t` | ASAP two-qubit depth of the selected routed operation stream. | +| `operation_count` | `uintptr_t` | Total number of operations in the selected routed stream. | +| `native_two_qubit_count` | `uintptr_t` | Predicted native two-qubit operation count after device lowering. | +| `native_two_qubit_depth` | `uintptr_t` | ASAP two-qubit depth using the selected exact-qargs native solutions. | +| `native_total_depth` | `uintptr_t` | ASAP total depth using the selected exact-qargs native solutions. | +| `native_operation_count` | `uintptr_t` | Predicted total native operation count after device lowering. | +| `unknown_loop_count` | `uintptr_t` | Number of dynamic `for`/`while` loops whose total execution cost is unknown. | +| `requirement_signature_count` | `uintptr_t` | Number of distinct single-qubit / qubit-pair requirement signatures prepared. | +| `eager_pair_state_count` | `uintptr_t` | Number of qubit-pair placement lower-bound states the target eagerly retains. | +| `lazy_pair_l1_lookup_count` | `uintptr_t` | Number of lazy qubit-pair state lower-bound probes issued by the selected trial. | +| `lazy_pair_l1_hit_count` | `uintptr_t` | Number of probes served from the selected trial's local L1 cache. | +| `lazy_pair_l1_cached_count` | `uintptr_t` | Number of qubit-pair state entries retained by the selected trial's bounded L1 cache. | + +--- + +## Example + +On the line topology 0-1-2, run low-level SABRE routing from the identity layout, then normalize the layout and validate reachability: + +```c +#include +#include "cqlib_c.h" + +int main(void) { + /* 1. Line topology 0 - 1 - 2 with a non-adjacent cx(0, 2) */ + uint32_t edges[4] = {0, 1, 1, 2}; + struct CDevice *device = device_from_edges("line-3", 3, edges, 2); + struct CCircuit *circuit = circuit_new(3); + circuit_cx(circuit, 0, 2); + if (device == NULL || circuit == NULL) { + return 1; + } + + /* 2. Deterministic seeded config: same seed -> same routing result */ + struct SabreConfigC config = sabre_config_deterministic_seeded(42); + if (sabre_config_validate(&config) != 0) { + circuit_free(circuit); + device_free(device); + return 1; + } + + /* 3. Low-level SABRE route from the identity layout */ + uint32_t logical[3] = {0, 1, 2}; + uint32_t physical[3] = {0, 1, 2}; + struct CLayout *layout = layout_new(logical, 3, physical, 3); + + struct CSabreRoutingResult *result = sabre_route(circuit, device, layout, &config); + if (result != NULL) { + /* 4. Routed circuit, swap count, layouts, and diagnostics */ + struct CCircuit *routed = sabre_routing_result_circuit(result); + uintptr_t swaps = sabre_routing_result_swap_count(result); /* >= 1 */ + struct CLayout *final_layout = sabre_routing_result_final_layout(result); + struct CSabreRoutingDiagnostics diagnostics; + if (sabre_routing_result_diagnostics(result, &diagnostics) == 0) { + printf("trials=%lu swaps=%lu\n", + (unsigned long)diagnostics.trials_evaluated, + (unsigned long)swaps); + } + circuit_free(routed); + layout_free(final_layout); + sabre_routing_result_free(result); + } + + /* 5. Normalize the layout onto usable qubits */ + struct CLayout *normalized = normalize_initial_layout(logical, 3, device, layout); + if (normalized != NULL) { + layout_free(normalized); + } + + /* 6. Reachability check: qubits 0 and 2 share one connected component */ + int32_t reachable = validate_reachable_interactions(circuit, device, layout); /* 0 */ + + layout_free(layout); + circuit_free(circuit); + device_free(device); + return 0; +} +``` + +Use `route_sabre` when the initial layout should be selected automatically; see [Routing](4_routing.md). diff --git a/docs/api/en/c/4_compile/6_decompose_resynthesis.md b/docs/api/en/c/4_compile/6_decompose_resynthesis.md new file mode 100644 index 00000000..fcffb315 --- /dev/null +++ b/docs/api/en/c/4_compile/6_decompose_resynthesis.md @@ -0,0 +1,1091 @@ +# Decompose / Resynthesis (C) + +This page covers the circuit-level decomposition passes (circuit-gate definition expansion `decompose_expand_definitions`, matrix-backed unitary synthesis `decompose_unitaries`, multi-controlled gate rewriting `decompose_mc_gates`), two-qubit block resynthesis `resynthesize_two_qubit_blocks`, the numeric unitary-matrix synthesis primitives (one-qubit / two-qubit), unitary synthesis targets `decompose_unitary_target_*`, KAK (Weyl) decomposition, target-basis lowering `target_basis_lowerer_*`, and device lowering `decompose_lower_to_device`. Decomposition is layered by the representation each operation carries: definition expansion runs before numeric synthesis, so unitary gates that ship their own implementation circuit are expanded before entering matrix synthesis. Matrix inputs use `Complex64` (`{double re; double im}` interleaved pairs, row-major); the layout is described in [Unitary Gates](../0_circuit/6_gate_unitary.md). Error codes, string and handle ownership conventions follow the [Overview](../0_overview.md). + +--- + +## Constants + +Two-qubit synthesis basis tags (the `basis` parameter of `synthesize_numeric_2q_unitary`): + +| Constant | Value | Meaning | +| --- | --- | --- | +| `TWO_QUBIT_BASIS_PAULI_ROTATIONS` | 0 | Local `U` + `RXX`/`RYY`/`RZZ` Pauli rotations. | +| `TWO_QUBIT_BASIS_CX` | 1 | Local `U` + `CX` templates. | +| `TWO_QUBIT_BASIS_CY` | 2 | Local `U` + `CY` templates. | +| `TWO_QUBIT_BASIS_CZ` | 3 | Local `U` + `CZ` templates. | +| `TWO_QUBIT_BASIS_RZZ` | 4 | Local `U` + `RZZ`. | + +KAK local-factor tags (the `factor` parameter of `kak_decomposition_local_factor`): + +| Constant | Value | Meaning | +| --- | --- | --- | +| `KAK_FACTOR_K1L` | 0 | `k1l` (left local factor, applied after the interaction). | +| `KAK_FACTOR_K1R` | 1 | `k1r` (right local factor, applied after the interaction). | +| `KAK_FACTOR_K2L` | 2 | `k2l` (left local factor, applied before the interaction). | +| `KAK_FACTOR_K2R` | 3 | `k2r` (right local factor, applied before the interaction). | + +--- + +## Configuration Construction + +### decompose_unitary_config_default() + +Returns the default unitary-decompose configuration by value: `recurse_control_flow=1` (recurse into control-flow bodies). + +```c +struct CUnitaryDecomposeConfig decompose_unitary_config_default(void); +``` + +Returns: the default configuration struct (by value, nothing to free). + +### mc_gate_config_default() + +Returns the default MC-gate decompose configuration by value: no pre-layout clean ancillas (`max_pre_layout_clean_ancillas=0`), no dirty borrowing (`allow_dirty_borrowing=0`), no hard limit on total qubits (`has_max_total_qubits=0`). + +```c +struct CMcGateDecomposeConfig mc_gate_config_default(void); +``` + +Returns: the default configuration struct (by value, nothing to free). + +### resynthesis_config_normal() + +Returns the normal-budget two-qubit block resynthesis configuration by value: `max_block_ops=16`, `max_crossed_ops=4`, `max_scan_span=32`, `skip_labeled_ops=1`, `recurse_control_flow=1`, knowledge-rule commutation oracle enabled (`enable_rule_oracle=1`), matrix fallback disabled (`enable_matrix_fallback=0`), `max_matrix_qubits=4`. + +```c +struct CResynthesisConfig resynthesis_config_normal(void); +``` + +Returns: the normal-budget configuration struct (by value, nothing to free). + +### resynthesis_config_enhanced() + +Returns the enhanced-budget two-qubit block resynthesis configuration by value: widens the block-collection budgets on top of the normal budget (`max_block_ops=32`, `max_crossed_ops=8`, `max_scan_span=64`); the remaining fields match the normal budget. + +```c +struct CResynthesisConfig resynthesis_config_enhanced(void); +``` + +Returns: the enhanced-budget configuration struct (by value, nothing to free). + +--- + +## Circuit-Level Decomposition Passes + +### decompose_expand_definitions(circuit) + +Expands the circuit-gate definitions of the circuit (gates that ship their own implementation circuit), replacing gate calls with the plain operations of their implementation circuits, and returns the rebuilt circuit. + +```c +struct CCircuit *decompose_expand_definitions(const struct CCircuit *circuit); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `circuit` | `const CCircuit*` | Input circuit. | + +Returns: a new `CCircuit*` on success (free with `circuit_free`); NULL when `circuit` is NULL or expansion fails. + +Construction of circuit gates is covered in [Circuit Gates](../0_circuit/8_gate_circuit_gate.md). + +### decompose_unitaries(circuit, config) + +Synthesizes the matrix-backed unitary gates of the circuit (gates added by `circuit_unitary`), replacing each matrix with a sequence of standard-gate operations, and returns the rebuilt circuit. Equivalent to `decompose_unitaries_with_rule_stats` with a NULL `stats`. + +```c +struct CCircuit *decompose_unitaries(const struct CCircuit *circuit, + struct CUnitaryDecomposeConfig config); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `circuit` | `const CCircuit*` | Input circuit. | +| `config` | `struct CUnitaryDecomposeConfig` | Decompose configuration (by value). | + +Returns: a new `CCircuit*` on success (free with `circuit_free`); NULL when `circuit` is NULL or synthesis fails. + +Construction of matrix-backed unitary gates is covered in [Unitary Gates](../0_circuit/6_gate_unitary.md). + +### decompose_unitaries_with_rule_stats(circuit, config, stats) + +Diagnostic form of `decompose_unitaries`: when `stats` is non-NULL, writes a snapshot of the pass-local decomposition-rule cache statistics of this run into `*stats`. + +```c +struct CCircuit *decompose_unitaries_with_rule_stats(const struct CCircuit *circuit, + struct CUnitaryDecomposeConfig config, + struct CDecompositionRuleStats *stats); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `circuit` | `const CCircuit*` | Input circuit. | +| `config` | `struct CUnitaryDecomposeConfig` | Decompose configuration (by value). | +| `stats` | `struct CDecompositionRuleStats*` | Rule-cache statistics output; NULL skips statistics. | + +Returns: a new `CCircuit*` on success (free with `circuit_free`); NULL when `circuit` is NULL or synthesis fails (`*stats` is then left untouched). + +### decompose_mc_gates(circuit, config) + +Rewrites the multi-controlled gates of the circuit (gates added by `circuit_multi_control`), replacing each with a sequence of singly-controlled / uncontrolled operations, and returns the rebuilt circuit. Equivalent to `decompose_mc_gates_with_rule_stats` with a NULL `stats`. + +```c +struct CCircuit *decompose_mc_gates(const struct CCircuit *circuit, + struct CMcGateDecomposeConfig config); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `circuit` | `const CCircuit*` | Input circuit. | +| `config` | `struct CMcGateDecomposeConfig` | Resource and limit configuration (by value). | + +Returns: a new `CCircuit*` on success (free with `circuit_free`); NULL when `circuit` is NULL or rewriting fails. + +Construction of multi-controlled gates is covered in [Multi-Controlled Gates](../0_circuit/7_gate_mc_gate.md). + +### decompose_mc_gates_with_rule_stats(circuit, config, stats) + +Diagnostic form of `decompose_mc_gates`: when `stats` is non-NULL, writes a snapshot of the pass-local decomposition-rule cache statistics of this run into `*stats`. + +```c +struct CCircuit *decompose_mc_gates_with_rule_stats(const struct CCircuit *circuit, + struct CMcGateDecomposeConfig config, + struct CDecompositionRuleStats *stats); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `circuit` | `const CCircuit*` | Input circuit. | +| `config` | `struct CMcGateDecomposeConfig` | Resource and limit configuration (by value). | +| `stats` | `struct CDecompositionRuleStats*` | Rule-cache statistics output; NULL skips statistics. | + +Returns: a new `CCircuit*` on success (free with `circuit_free`); NULL when `circuit` is NULL or rewriting fails (`*stats` is then left untouched). + +### decompose_mc_gates_for_device(circuit, device, policy) + +Rewrites the multi-controlled gates of the circuit within the usable-qubit capacity of the device (a pre-layout logical transform): the rewriting plan is constrained by the device's usable physical qubit count. `policy` may be NULL to select the default resource policy. + +```c +struct CCircuit *decompose_mc_gates_for_device(const struct CCircuit *circuit, + const struct CDevice *device, + const struct CResourcePolicy *policy); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `circuit` | `const CCircuit*` | Input circuit. | +| `device` | `const CDevice*` | Target device (construction in [Device / Properties](../2_device/2_properties_device.md)). | +| `policy` | `const CResourcePolicy*` | Resource policy; NULL selects the default (see [Resource Policy](8_resource.md)). | + +Returns: a new `CCircuit*` on success (free with `circuit_free`); NULL when any handle is NULL or rewriting fails. + +--- + +## Two-Qubit Block Resynthesis + +### resynthesize_two_qubit_blocks(circuit, config) + +Resynthesizes the two-qubit blocks of the circuit: candidate blocks are collected within bounded budgets around two-qubit anchors, reordered through the commutation engine, and re-synthesized; returns the rebuilt circuit. + +```c +struct CCircuit *resynthesize_two_qubit_blocks(const struct CCircuit *circuit, + struct CResynthesisConfig config); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `circuit` | `const CCircuit*` | Input circuit. | +| `config` | `struct CResynthesisConfig` | Resynthesis budget configuration (by value). | + +Returns: a new `CCircuit*` on success (free with `circuit_free`); NULL when `circuit` is NULL or resynthesis fails. + +--- + +## Numeric Unitary Synthesis Primitives + +### synthesize_numeric_1q_unitary(matrix, out) + +Synthesizes a 2x2 unitary matrix into Cqlib's `U` convention: the represented matrix is `exp(i * global_phase) * U(theta, phi, lambda)`. `matrix` points at 4 row-major `Complex64` elements. + +```c +int32_t synthesize_numeric_1q_unitary(const Complex64 *matrix, + struct COneQubitUnitaryDecomposition *out); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `matrix` | `const Complex64*` | 4 row-major complex elements (2x2 matrix). | +| `out` | `struct COneQubitUnitaryDecomposition*` | Synthesis result output. | + +Returns: 0 on success. + +| Error code | Name | Scenario | +| --- | --- | --- | +| `-1` | NullPtr | `matrix` or `out` is NULL. | +| `-6` | CompilerError | The matrix is non-finite, non-unitary, or otherwise rejected by the synthesizer. | + +### synthesize_numeric_2q_unitary(matrix, first, second, basis) + +Synthesizes a 4x4 unitary matrix into a sequence of standard-gate operations, delivered as a `CTwoQubitUnitarySynthesis` handle. `matrix` points at 16 row-major `Complex64` elements; `first` and `second` are the target qubits; `basis` is one of `TWO_QUBIT_BASIS_*`. + +```c +struct CTwoQubitUnitarySynthesis *synthesize_numeric_2q_unitary(const Complex64 *matrix, + uint32_t first, + uint32_t second, + uint8_t basis); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `matrix` | `const Complex64*` | 16 row-major complex elements (4x4 matrix). | +| `first` | `uint32_t` | First target qubit ID. | +| `second` | `uint32_t` | Second target qubit ID. | +| `basis` | `uint8_t` | Synthesis basis, one of `TWO_QUBIT_BASIS_*`. | + +Returns: a new `CTwoQubitUnitarySynthesis*` on success (free with `two_qubit_synthesis_free`); NULL when `matrix` is NULL, `basis` is not a valid tag, or synthesis fails (for example the matrix is non-finite or non-unitary). + +--- + +## Unitary Synthesis Targets + +`CDecomposeUnitaryTarget` describes the native gate set a unitary synthesis step may emit: a native one-qubit basis, a native two-qubit basis, and a Pauli-rotation fallback flag. It also evaluates the exact target-basis cost of a fixed operation sequence. + +### decompose_unitary_target_unconstrained() + +```c +struct CDecomposeUnitaryTarget *decompose_unitary_target_unconstrained(void); +``` + +Creates an unconstrained synthesis target: no native-basis restrictions and the neutral exact Pauli-rotation fallback enabled. + +Returns: a new `CDecomposeUnitaryTarget*` (free with `decompose_unitary_target_free`). + +### decompose_unitary_target_from_instructions(gate_names, len) + +Creates a synthesis target from a workflow-style target-basis list of standard-gate names (e.g. `"H"`, `"RZ"`, `"CX"`): one-qubit names become the native one-qubit basis, two-qubit names the native two-qubit basis, and the exact Pauli-rotation fallback stays enabled. + +```c +struct CDecomposeUnitaryTarget *decompose_unitary_target_from_instructions( + const char *const *gate_names, + uintptr_t len); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `gate_names` | `const char* const*` | Array of standard-gate names; may be NULL when `len` is 0. | +| `len` | `uintptr_t` | Number of gate names. | + +Returns: a new `CDecomposeUnitaryTarget*` on success (free with `decompose_unitary_target_free`); NULL when `len` is greater than 0 but `gate_names` is NULL, an entry is invalid UTF-8 or an unknown gate name, or the core rejects the basis (for example an empty basis). + +### decompose_unitary_target_from_standard_gates(native_1q_names, num_1q, native_2q_names, num_2q, fallback_pauli) + +Creates a synthesis target from explicit native one-qubit and two-qubit standard-gate name lists and a Pauli-rotation fallback flag. Every name in `native_1q_names` must be a one-qubit standard gate and every name in `native_2q_names` a two-qubit standard gate. + +```c +struct CDecomposeUnitaryTarget *decompose_unitary_target_from_standard_gates( + const char *const *native_1q_names, + uintptr_t num_1q, + const char *const *native_2q_names, + uintptr_t num_2q, + uint8_t fallback_pauli); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `native_1q_names` | `const char* const*` | Native one-qubit standard-gate names; may be NULL when `num_1q` is 0. | +| `num_1q` | `uintptr_t` | Number of one-qubit gate names. | +| `native_2q_names` | `const char* const*` | Native two-qubit standard-gate names; may be NULL when `num_2q` is 0. | +| `num_2q` | `uintptr_t` | Number of two-qubit gate names. | +| `fallback_pauli` | `uint8_t` | Non-zero enables the exact Pauli-rotation fallback. | + +Returns: a new `CDecomposeUnitaryTarget*` on success (free with `decompose_unitary_target_free`); NULL on NULL input, invalid UTF-8, an unknown gate name, a wrong-arity gate, or an empty combined basis. + +### decompose_unitary_target_free(ptr) + +Frees a unitary-decompose synthesis target; NULL is allowed. + +```c +void decompose_unitary_target_free(struct CDecomposeUnitaryTarget *ptr); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `ptr` | `struct CDecomposeUnitaryTarget*` | Handle to free; may be NULL. | + +### decompose_unitary_target_fallback_pauli(ptr) + +Tests whether the exact Pauli-rotation fallback is permitted for the target. + +```c +int32_t decompose_unitary_target_fallback_pauli(const struct CDecomposeUnitaryTarget *ptr); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `ptr` | `const CDecomposeUnitaryTarget*` | Synthesis target handle. | + +Returns: `1` when the fallback is permitted; `0` otherwise; `-1` for NULL input. + +### decompose_unitary_target_set_fallback_pauli(ptr, value) + +Sets whether the Pauli-rotation fallback is permitted for the target. The target is rebuilt around its native gate lists, which requires a non-empty basis; when the call is rejected the target is left unchanged. + +```c +int32_t decompose_unitary_target_set_fallback_pauli(struct CDecomposeUnitaryTarget *ptr, + uint8_t value); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `ptr` | `struct CDecomposeUnitaryTarget*` | Synthesis target handle. | +| `value` | `uint8_t` | Non-zero permits the fallback. | + +Returns: `0` on success. + +| Error code | Name | Scenario | +| --- | --- | --- | +| `-1` | NullPtr | `ptr` is NULL. | +| `-8` | InvalidParam | The target is unconstrained (no native gates). | + +### decompose_unitary_target_native_1q_len(ptr) / decompose_unitary_target_native_1q(ptr, out, len) + +```c +uintptr_t decompose_unitary_target_native_1q_len(const struct CDecomposeUnitaryTarget *ptr); +uintptr_t decompose_unitary_target_native_1q(const struct CDecomposeUnitaryTarget *ptr, + char **out, + uintptr_t len); +``` + +Two-step read of the native one-qubit gate names (step one: `*_len` returns the total; step two: the fill function copies the names into `out`, writing `min(total, len)` entries). Each written entry is a freshly allocated C string freed with `cqlib_string_free`; when `out` is NULL the fill function only returns the total without copying. + +Returns (both functions): the total number of native one-qubit gates; 0 for NULL input. + +### decompose_unitary_target_native_2q_len(ptr) / decompose_unitary_target_native_2q(ptr, out, len) + +```c +uintptr_t decompose_unitary_target_native_2q_len(const struct CDecomposeUnitaryTarget *ptr); +uintptr_t decompose_unitary_target_native_2q(const struct CDecomposeUnitaryTarget *ptr, + char **out, + uintptr_t len); +``` + +Two-step read of the native two-qubit gate names (same buffer-copy pattern as the one-qubit pair; each written entry is freed with `cqlib_string_free`). + +Returns (both functions): the total number of native two-qubit gates; 0 for NULL input. + +### decompose_unitary_target_basis_len(ptr) / decompose_unitary_target_basis(ptr, out, len) + +```c +uintptr_t decompose_unitary_target_basis_len(const struct CDecomposeUnitaryTarget *ptr); +uintptr_t decompose_unitary_target_basis(const struct CDecomposeUnitaryTarget *ptr, + char **out, + uintptr_t len); +``` + +Two-step read of the combined native basis (one-qubit gates followed by two-qubit gates; same buffer-copy pattern; each written entry is freed with `cqlib_string_free`). + +Returns (both functions): the total number of basis gates; 0 for NULL input. + +### decompose_unitary_target_cost_of_fixed_operations(ptr, gate_names, qubit_ids, qubit_counts, num_ops, out) + +Computes the exact target-basis cost of a fixed standard-gate operation sequence under the native basis of the target. The sequence is given as `num_ops` gate names, with each operation's qubit IDs flattened into `qubit_ids` and per-operation qubit counts in `qubit_counts`. Parameters are treated as fixed zeros. Requires a constrained target (an unconstrained target has no basis to cost against). The cost snapshot is written to `*out` (the `CTargetBasisCost` layout is documented in [Configuration and Result Structs](#configuration-and-result-structs)). + +```c +int32_t decompose_unitary_target_cost_of_fixed_operations( + const struct CDecomposeUnitaryTarget *ptr, + const char *const *gate_names, + const uint32_t *qubit_ids, + const uint32_t *qubit_counts, + uintptr_t num_ops, + struct CTargetBasisCost *out); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `ptr` | `const CDecomposeUnitaryTarget*` | Synthesis target handle. | +| `gate_names` | `const char* const*` | Gate name of each operation in the sequence. | +| `qubit_ids` | `const uint32_t*` | Qubit IDs of all operations, flattened in sequence order. | +| `qubit_counts` | `const uint32_t*` | Number of qubits each operation acts on. | +| `num_ops` | `uintptr_t` | Number of operations in the sequence. | +| `out` | `struct CTargetBasisCost*` | Cost snapshot output. | + +Returns: `0` on success. + +| Error code | Name | Scenario | +| --- | --- | --- | +| `-1` | NullPtr | `ptr` or `out` is NULL. | +| `-4` | ParseError | A gate name is invalid UTF-8 or an unknown gate name. | +| `-6` | CompilerError | The core rejected the sequence. | +| `-8` | InvalidParam | The target is unconstrained (no native basis). | + +--- + +## CTwoQubitUnitarySynthesis Readers + +Result handle of `synthesize_numeric_2q_unitary`: the emitted operation sequence and the global phase. + +### two_qubit_synthesis_free(ptr) + +Frees a two-qubit synthesis result handle; NULL is allowed. + +```c +void two_qubit_synthesis_free(struct CTwoQubitUnitarySynthesis *ptr); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `ptr` | `struct CTwoQubitUnitarySynthesis*` | Handle to free; may be NULL. | + +### two_qubit_synthesis_num_operations(ptr) + +Returns the number of emitted operations. + +```c +uintptr_t two_qubit_synthesis_num_operations(const struct CTwoQubitUnitarySynthesis *ptr); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `ptr` | `const CTwoQubitUnitarySynthesis*` | Synthesis result handle. | + +Returns: the operation count; 0 on NULL input. + +### two_qubit_synthesis_global_phase(ptr) + +Returns the global phase multiplying the emitted operation sequence. + +```c +double two_qubit_synthesis_global_phase(const struct CTwoQubitUnitarySynthesis *ptr); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `ptr` | `const CTwoQubitUnitarySynthesis*` | Synthesis result handle. | + +Returns: the global phase (radians); 0.0 on NULL input. + +### two_qubit_synthesis_operation_name(ptr, index) + +Returns the instruction name of operation `index`. + +```c +char *two_qubit_synthesis_operation_name(const struct CTwoQubitUnitarySynthesis *ptr, + uintptr_t index); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `ptr` | `const CTwoQubitUnitarySynthesis*` | Synthesis result handle. | +| `index` | `uintptr_t` | Zero-based operation index. | + +Returns: a newly allocated C string on success (free with `cqlib_string_free`); NULL on NULL input or an out-of-bounds index. + +### two_qubit_synthesis_operation_qubits_len(ptr, index) + +Returns the number of qubits operation `index` acts on (step one of the two-step read pattern). + +```c +uintptr_t two_qubit_synthesis_operation_qubits_len(const struct CTwoQubitUnitarySynthesis *ptr, + uintptr_t index); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `ptr` | `const CTwoQubitUnitarySynthesis*` | Synthesis result handle. | +| `index` | `uintptr_t` | Zero-based operation index. | + +Returns: the qubit count; 0 on NULL input or an out-of-bounds index. + +### two_qubit_synthesis_operation_qubits(ptr, index, out, len) + +Copies the qubit IDs of operation `index` into `out` (step two of the two-step read pattern; call `two_qubit_synthesis_operation_qubits_len` first). When `len` is smaller than the total, only the first `len` entries are copied. + +```c +uintptr_t two_qubit_synthesis_operation_qubits(const struct CTwoQubitUnitarySynthesis *ptr, + uintptr_t index, + uint32_t *out, + uintptr_t len); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `ptr` | `const CTwoQubitUnitarySynthesis*` | Synthesis result handle. | +| `index` | `uintptr_t` | Zero-based operation index. | +| `out` | `uint32_t*` | Qubit ID output array. | +| `len` | `uintptr_t` | Capacity of `out`. | + +Returns: the total number of qubits the operation acts on; 0 on NULL input or an out-of-bounds index. + +--- + +## KAK Decomposition + +### kak_decompose(matrix) + +Decomposes a 4x4 unitary matrix into its KAK (Weyl) coordinates and local factors. `matrix` points at 16 row-major `Complex64` elements. + +```c +struct CKakDecomposition *kak_decompose(const Complex64 *matrix); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `matrix` | `const Complex64*` | 16 row-major complex elements (4x4 matrix). | + +Returns: a new `CKakDecomposition*` on success (free with `kak_decomposition_free`); NULL when `matrix` is NULL or decomposition fails (for example the matrix is non-finite or non-unitary). + +### kak_decomposition_free(ptr) + +Frees a KAK decomposition handle; NULL is allowed. + +```c +void kak_decomposition_free(struct CKakDecomposition *ptr); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `ptr` | `struct CKakDecomposition*` | Handle to free; may be NULL. | + +### kak_decomposition_global_phase(ptr) + +Returns the scalar phase multiplying the complete decomposition. + +```c +double kak_decomposition_global_phase(const struct CKakDecomposition *ptr); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `ptr` | `const CKakDecomposition*` | KAK decomposition handle. | + +Returns: the global phase (radians); 0.0 on NULL input. + +### kak_decomposition_coordinates(ptr, a, b, c) + +Writes the canonical Pauli XX/YY/ZZ interaction coordinates `a`, `b`, `c` to the given out pointers. + +```c +int32_t kak_decomposition_coordinates(const struct CKakDecomposition *ptr, + double *a, + double *b, + double *c); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `ptr` | `const CKakDecomposition*` | KAK decomposition handle. | +| `a` | `double*` | XX interaction coordinate output. | +| `b` | `double*` | YY interaction coordinate output. | +| `c` | `double*` | ZZ interaction coordinate output. | + +Returns: 0 on success. + +| Error code | Name | Scenario | +| --- | --- | --- | +| `-1` | NullPtr | Any pointer is NULL. | + +### kak_decomposition_local_factor(ptr, factor, out) + +Copies one 2x2 local factor (selected by a `KAK_FACTOR_*` tag) into `out` as 4 row-major `Complex64` elements. + +```c +int32_t kak_decomposition_local_factor(const struct CKakDecomposition *ptr, + uint8_t factor, + Complex64 *out); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `ptr` | `const CKakDecomposition*` | KAK decomposition handle. | +| `factor` | `uint8_t` | Local-factor tag, one of `KAK_FACTOR_*`. | +| `out` | `Complex64*` | 4 row-major complex elements output. | + +Returns: 0 on success. + +| Error code | Name | Scenario | +| --- | --- | --- | +| `-1` | NullPtr | `ptr` or `out` is NULL. | +| `-8` | InvalidParam | `factor` is not a valid `KAK_FACTOR_*` tag. | + +--- + +## Target-Basis Lowering + +### target_basis_lowerer_new(gate_names, len) + +Creates a target-basis lowerer from an array of standard-gate names: each entry of `gate_names` must be a standard-gate name accepted elsewhere by the C ABI (for example `"H"`, `"RZ"`, `"CX"`); the names become the target basis. + +```c +struct CTargetBasisLowerer *target_basis_lowerer_new(const char *const *gate_names, uintptr_t len); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `gate_names` | `const char* const*` | Array of standard-gate names; may be NULL when `len` is 0. | +| `len` | `uintptr_t` | Number of gate names. | + +Returns: a new `CTargetBasisLowerer*` on success (free with `target_basis_lowerer_free`); NULL when `len` is greater than 0 but `gate_names` is NULL, an entry is NULL, an entry is invalid UTF-8, an entry is an unknown gate name, or the basis is empty (`len` is 0). + +### target_basis_lowerer_free(ptr) + +Frees a target-basis lowerer handle; NULL is allowed. + +```c +void target_basis_lowerer_free(struct CTargetBasisLowerer *ptr); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `ptr` | `struct CTargetBasisLowerer*` | Handle to free; may be NULL. | + +### target_basis_lowerer_num_gates(ptr) + +Returns the number of gates in the target basis. + +```c +uintptr_t target_basis_lowerer_num_gates(const struct CTargetBasisLowerer *ptr); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `ptr` | `const CTargetBasisLowerer*` | Lowerer handle. | + +Returns: the gate count; 0 on NULL input. + +### target_basis_lowerer_requires_lowering(ptr, circuit) + +Reports whether translating `circuit` to the target basis can change or reject its gate-like operations. + +```c +int32_t target_basis_lowerer_requires_lowering(const struct CTargetBasisLowerer *ptr, + const struct CCircuit *circuit); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `ptr` | `const CTargetBasisLowerer*` | Lowerer handle. | +| `circuit` | `const CCircuit*` | Input circuit. | + +Returns: 1 when lowering is needed (the circuit contains gate-like operations outside the basis); 0 when it is not. + +| Error code | Name | Scenario | +| --- | --- | --- | +| `-1` | NullPtr | `ptr` or `circuit` is NULL. | + +### target_basis_lowerer_apply(ptr, circuit) + +Lowers `circuit` to the target basis and returns the rebuilt circuit. + +```c +struct CCircuit *target_basis_lowerer_apply(const struct CTargetBasisLowerer *ptr, + const struct CCircuit *circuit); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `ptr` | `const CTargetBasisLowerer*` | Lowerer handle. | +| `circuit` | `const CCircuit*` | Input circuit. | + +Returns: a new `CCircuit*` on success (free with `circuit_free`); NULL when any handle is NULL or lowering fails. + +--- + +## Device Lowering + +### decompose_lower_to_device(circuit, device) + +Lowers `circuit` to the native instruction set of the device and returns the rebuilt circuit. + +```c +struct CCircuit *decompose_lower_to_device(const struct CCircuit *circuit, + const struct CDevice *device); +``` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| `circuit` | `const CCircuit*` | Input circuit. | +| `device` | `const CDevice*` | Target device; its native gate set (set by `device_with_native_gates`, see [Device / Properties](../2_device/2_properties_device.md)) is the lowering target. | + +Returns: a new `CCircuit*` on success (free with `circuit_free`); NULL when any handle is NULL or lowering fails. + +--- + +## Configuration and Result Structs + +### CUnitaryDecomposeConfig + +```c +typedef struct CUnitaryDecomposeConfig { + uint8_t recurse_control_flow; + uint8_t _reserved[7]; +} CUnitaryDecomposeConfig; +``` + +| Field | Type | Meaning | +| --- | --- | --- | +| `recurse_control_flow` | `uint8_t` | 1 recurses into control-flow bodies, 0 does not. | +| `_reserved[7]` | `uint8_t[7]` | Padding that keeps the struct layout stable. | + +### CMcGateDecomposeConfig + +```c +typedef struct CMcGateDecomposeConfig { + uintptr_t max_pre_layout_clean_ancillas; + uint8_t allow_dirty_borrowing; + uint8_t has_max_total_qubits; + uint8_t _pad[6]; + uintptr_t max_total_qubits; + uint64_t _reserved[2]; +} CMcGateDecomposeConfig; +``` + +| Field | Type | Meaning | +| --- | --- | --- | +| `max_pre_layout_clean_ancillas` | `uintptr_t` | Upper bound on total clean logical ancillas the pass may create before layout. | +| `allow_dirty_borrowing` | `uint8_t` | 1 allows borrowing input qubits under the dirty contract. | +| `has_max_total_qubits` | `uint8_t` | 1 enforces the `max_total_qubits` hard limit, 0 leaves it unset. | +| `_pad[6]` | `uint8_t[6]` | Padding that keeps the struct layout stable. | +| `max_total_qubits` | `uintptr_t` | Hard limit on total logical qubits (used when `has_max_total_qubits != 0`). | +| `_reserved[2]` | `uint64_t[2]` | Padding that keeps the struct layout stable. | + +### CResynthesisConfig + +```c +typedef struct CResynthesisConfig { + uintptr_t max_block_ops; + uintptr_t max_crossed_ops; + uintptr_t max_scan_span; + uint8_t skip_labeled_ops; + uint8_t recurse_control_flow; + uint8_t enable_rule_oracle; + uint8_t enable_matrix_fallback; + uint8_t _pad[4]; + uintptr_t max_matrix_qubits; + uint64_t _reserved[2]; +} CResynthesisConfig; +``` + +| Field | Type | Meaning | +| --- | --- | --- | +| `max_block_ops` | `uintptr_t` | Maximum source operations in one bounded candidate block. | +| `max_crossed_ops` | `uintptr_t` | Maximum non-block operations crossed while collecting a block. | +| `max_scan_span` | `uintptr_t` | Collection budget per side of a two-qubit anchor. | +| `skip_labeled_ops` | `uint8_t` | 1 treats labeled operations as hard boundaries. | +| `recurse_control_flow` | `uint8_t` | 1 recurses into structured classical-control bodies. | +| `enable_rule_oracle` | `uint8_t` | 1 enables the knowledge-rule commutation oracle. | +| `enable_matrix_fallback` | `uint8_t` | 1 enables the local matrix fallback in the commutation engine. | +| `_pad[4]` | `uint8_t[4]` | Padding that keeps the struct layout stable. | +| `max_matrix_qubits` | `uintptr_t` | Maximum union-support size for the matrix fallback. | +| `_reserved[2]` | `uint64_t[2]` | Padding that keeps the struct layout stable. | + +### CDecompositionRuleStats + +Snapshot of the pass-local decomposition-rule cache statistics (the output of `decompose_unitaries_with_rule_stats` / `decompose_mc_gates_with_rule_stats`): + +| Field | Type | Meaning | +| --- | --- | --- | +| `hits` | `uintptr_t` | Cache hits during the run. | +| `misses` | `uintptr_t` | Cache misses during the run. | +| `inserts` | `uintptr_t` | Cache writes during the run. | + +### CTargetBasisCost + +Snapshot of a target-basis lowering cost (the output of `decompose_unitary_target_cost_of_fixed_operations`): + +| Field | Type | Meaning | +| --- | --- | --- | +| `two_qubit_ops` | `uintptr_t` | Two-qubit operations in the lowered sequence. | +| `depth` | `uintptr_t` | Critical-path depth of the lowered sequence. | +| `total_ops` | `uintptr_t` | Total operation count of the lowered sequence (global phase excluded). | +| `parameterized_ops` | `uintptr_t` | Operations carrying parameters in the lowered sequence. | + +### COneQubitUnitaryDecomposition + +Snapshot of a one-qubit numeric synthesis result (the output of `synthesize_numeric_1q_unitary`); the represented matrix is `exp(i * global_phase) * U(theta, phi, lambda)`: + +| Field | Type | Meaning | +| --- | --- | --- | +| `theta` | `double` | Polar rotation angle. | +| `phi` | `double` | First azimuthal angle. | +| `lambda` | `double` | Second azimuthal angle. | +| `global_phase` | `double` | Scalar phase multiplying the synthesized gate. | + +--- + +## Examples + +First a chain of circuit-level decomposition passes (definition expansion -> unitary synthesis -> MC-gate rewriting -> two-qubit block resynthesis), following the call order of the test suite: + +```c +#include +#include +#include "cqlib_c.h" + +int main(void) { + /* 1. A frozen circuit-gate is expanded back into plain operations */ + struct CCircuit *inner = circuit_new(1); + circuit_h(inner, 0); + struct CCircuitGate *gate = circuit_to_gate(inner, "my_h"); + struct CCircuit *outer = circuit_new(2); + uint32_t gate_qubits[1] = {0}; + circuit_circuit_gate(outer, gate, gate_qubits, 1, NULL, 0); + struct CCircuit *expanded = decompose_expand_definitions(outer); /* 1 op */ + + /* 2. A matrix-backed H is synthesized into standard operations */ + struct CCircuit *unitary_circuit = circuit_new(2); + Complex64 h_matrix[4] = { + {1.0 / sqrt(2.0), 0.0}, {1.0 / sqrt(2.0), 0.0}, + {1.0 / sqrt(2.0), 0.0}, {-1.0 / sqrt(2.0), 0.0}, + }; + uint32_t unitary_targets[1] = {0}; + circuit_unitary(unitary_circuit, "my_u", 1, h_matrix, unitary_targets, 1); + struct CDecompositionRuleStats stats; + struct CCircuit *synthesized = decompose_unitaries_with_rule_stats( + unitary_circuit, decompose_unitary_config_default(), &stats); + /* stats.hits / stats.misses / stats.inserts reflect the rule cache */ + + /* 3. A doubly-controlled X is rewritten within device capacity */ + struct CCircuit *mc_circuit = circuit_new(3); + uint32_t controls[2] = {0, 1}; + uint32_t targets[1] = {2}; + circuit_multi_control(mc_circuit, "X", controls, 2, targets, 1, NULL, 0); + uint32_t edges[4] = {0, 1, 1, 2}; + struct CDevice *device = device_from_edges("line-3", 3, edges, 2); + struct CCircuit *rewritten = decompose_mc_gates_for_device(mc_circuit, device, NULL); + + /* 4. Two-qubit blocks are resynthesized under the enhanced budget */ + struct CCircuit *block_circuit = circuit_new(2); + circuit_h(block_circuit, 0); + circuit_cx(block_circuit, 0, 1); + struct CCircuit *resynthesized = + resynthesize_two_qubit_blocks(block_circuit, resynthesis_config_enhanced()); + + circuit_free(resynthesized); + circuit_free(block_circuit); + circuit_free(rewritten); + device_free(device); + circuit_free(mc_circuit); + circuit_free(synthesized); + circuit_free(unitary_circuit); + circuit_free(expanded); + circuit_gate_free(gate); + circuit_free(outer); + circuit_free(inner); + return 0; +} +``` + +Numeric synthesis, KAK decomposition, and lowering (the 2q synthesis and the KAK coordinates of the identity are all zero): + +```c +#include +#include +#include +#include "cqlib_c.h" + +int main(void) { + /* 1. 1q synthesis of the identity yields U(0, 0, 0), zero phase */ + Complex64 identity2[4] = {{1.0, 0.0}, {0.0, 0.0}, {0.0, 0.0}, {1.0, 0.0}}; + struct COneQubitUnitaryDecomposition one_q; + memset(&one_q, 0, sizeof(one_q)); + if (synthesize_numeric_1q_unitary(identity2, &one_q) != 0) { + return 1; + } + + /* 2. 2q synthesis of the identity in the CX basis */ + Complex64 identity4[16]; + memset(identity4, 0, sizeof(identity4)); + for (int i = 0; i < 16; i += 5) { + identity4[i].re = 1.0; + } + struct CTwoQubitUnitarySynthesis *synthesis = + synthesize_numeric_2q_unitary(identity4, 0, 1, TWO_QUBIT_BASIS_CX); + if (synthesis != NULL) { + for (uintptr_t i = 0; i < two_qubit_synthesis_num_operations(synthesis); i++) { + char *name = two_qubit_synthesis_operation_name(synthesis, i); + uint32_t qubits[8]; + uintptr_t len = two_qubit_synthesis_operation_qubits_len(synthesis, i); + uintptr_t copied = two_qubit_synthesis_operation_qubits(synthesis, i, qubits, len); + printf("op=%s qubits=%lu\n", name, (unsigned long)copied); + cqlib_string_free(name); + } + two_qubit_synthesis_free(synthesis); + } + + /* 3. KAK decomposition of the identity: zero interaction coordinates */ + struct CKakDecomposition *kak = kak_decompose(identity4); + if (kak != NULL) { + double a = 0.0, b = 0.0, c = 0.0; + Complex64 k1l[4]; + if (kak_decomposition_coordinates(kak, &a, &b, &c) == 0) { + printf("a=%f b=%f c=%f\n", a, b, c); /* ~0 */ + } + kak_decomposition_local_factor(kak, KAK_FACTOR_K1L, k1l); /* 0 */ + kak_decomposition_free(kak); + } + + /* 4. Target-basis lowering: T is outside {H, RZ, CX} */ + const char *basis_names[3] = {"H", "RZ", "CX"}; + struct CTargetBasisLowerer *lowerer = target_basis_lowerer_new(basis_names, 3); + if (lowerer != NULL) { + struct CCircuit *t_circuit = circuit_new(1); + circuit_t(t_circuit, 0); + if (target_basis_lowerer_requires_lowering(lowerer, t_circuit) == 1) { + struct CCircuit *lowered = target_basis_lowerer_apply(lowerer, t_circuit); + circuit_free(lowered); + } + circuit_free(t_circuit); + target_basis_lowerer_free(lowerer); + } + + /* 5. Device lowering: a native circuit passes through unchanged */ + uint32_t edges[2] = {0, 1}; + struct CDevice *device = device_from_edges("line-2", 2, edges, 1); + device_with_native_gates(device, "H,RZ,CX"); + struct CCircuit *native_circuit = circuit_new(2); + circuit_h(native_circuit, 0); + circuit_cx(native_circuit, 0, 1); + struct CCircuit *device_lowered = decompose_lower_to_device(native_circuit, device); /* 2 ops */ + + circuit_free(device_lowered); + circuit_free(native_circuit); + device_free(device); + return 0; +} +``` + +A unitary synthesis target: native-basis read-back and cost evaluation: + +```c +#include +#include +#include "cqlib_c.h" + +static void sort_names(char **names, uintptr_t len) { + for (uintptr_t i = 0; i + 1 < len; i++) { + for (uintptr_t j = i + 1; j < len; j++) { + if (strcmp(names[i], names[j]) > 0) { + char *tmp = names[i]; + names[i] = names[j]; + names[j] = tmp; + } + } + } +} + +int main(void) { + /* 1. Build a target from a workflow-style basis list */ + const char *const basis_gates[3] = {"H", "RZ", "CX"}; + struct CDecomposeUnitaryTarget *target = + decompose_unitary_target_from_instructions(basis_gates, 3); + if (target == NULL) { + return 1; + } + /* decompose_unitary_target_fallback_pauli(target) == 1, + native_1q_len == 2 (H, RZ), native_2q_len == 1 (CX), basis_len == 3 */ + + /* 2. Two-step read of the combined basis (1q gates first) */ + char *basis[3] = {NULL, NULL, NULL}; + uintptr_t total = decompose_unitary_target_basis(target, basis, 3); + sort_names(basis, total); + for (uintptr_t i = 0; i < total && i < 3; i++) { + printf("basis[%llu]=%s\n", (unsigned long long)i, basis[i]); + cqlib_string_free(basis[i]); + } + + /* 3. Cost of a fixed operation sequence under the native basis */ + const char *const ops[3] = {"H", "CX", "H"}; + const uint32_t qubit_ids[4] = {0, 0, 1, 1}; + const uint32_t op_counts[3] = {1, 2, 1}; + struct CTargetBasisCost cost; + memset(&cost, 0, sizeof(cost)); + if (decompose_unitary_target_cost_of_fixed_operations(target, ops, qubit_ids, + op_counts, 3, &cost) == 0) { + /* cost.total_ops == 3, cost.two_qubit_ops == 1, + cost.parameterized_ops == 0, cost.depth == 3 */ + printf("ops=%llu two_qubit=%llu depth=%llu\n", + (unsigned long long)cost.total_ops, + (unsigned long long)cost.two_qubit_ops, + (unsigned long long)cost.depth); + } + + /* 4. A parameterized gate is counted as such */ + const char *const rz_ops[1] = {"RZ"}; + const uint32_t rz_counts[1] = {1}; + decompose_unitary_target_cost_of_fixed_operations(target, rz_ops, qubit_ids, + rz_counts, 1, &cost); + /* cost.total_ops == 1, cost.parameterized_ops == 1 */ + + /* 5. Toggling the fallback rebuilds the target around its native lists */ + decompose_unitary_target_set_fallback_pauli(target, 0); + /* decompose_unitary_target_fallback_pauli(target) == 0 */ + decompose_unitary_target_set_fallback_pauli(target, 1); + + decompose_unitary_target_free(target); + return 0; +} +``` diff --git a/docs/api/en/c/4_compile/7_knowledge.md b/docs/api/en/c/4_compile/7_knowledge.md new file mode 100644 index 00000000..b250d601 --- /dev/null +++ b/docs/api/en/c/4_compile/7_knowledge.md @@ -0,0 +1,1272 @@ +# Knowledge (C) + +`CKnowledgeLibrary` is an opaque handle to a validated compiler rule library: a set of knowledge rules registered by kind and indexed by name. `CKnowledgeRule` is a standalone handle to a single rule that can be parsed, validated, and inserted into any library. A knowledge rule describes an adjacent operation pattern (match), optional symbolic parameter conditions, and a replacement sequence (rewrite); rules are the source for knowledge rewriting and gate decomposition. A match produces a `CKnowledgeBindings*` that records the bound qubits and parameters; the rewrite target instantiated under those bindings is returned as a `CKnowledgeReplacements*`. Error codes, string and handle ownership conventions follow the [Overview](../0_overview.md). + +--- + +## Constants + +Rule-kind tags (the `kind` parameter and the return value of `knowledge_library_rule_kind`): + +| Constant | Value | Kind | +| --- | --- | --- | +| `RULE_KIND_SIMPLIFY` | 0 | Simplification rules. | +| `RULE_KIND_CANCEL` | 1 | Cancellation rules (adjacent inverse operations removed). | +| `RULE_KIND_MERGE` | 2 | Merge rules. | +| `RULE_KIND_COMMUTE` | 3 | Explicit commutation rules (`A; B -> B; A`). | +| `RULE_KIND_DECOMPOSE` | 4 | Decomposition or lowering rules. | +| `RULE_KIND_CANONICALIZE` | 5 | Canonicalization rules. | +| `RULE_KIND_HARDWARE_NATIVE` | 6 | Hardware-native rules. | +| `RULE_KIND_OTHER` | 7 | Other rules. | + +--- + +## DSL Rules + +`knowledge_library_from_dsl_str` / `knowledge_library_from_dsl_file` / `knowledge_rule_from_dsl` accept `.rule` DSL text: + +```text +rule merge_rz { + match { + RZ(a) 0 + RZ(b) 0 + } + rewrite { + RZ(a + b) 0 + } +} +``` + +- `rule `: the rule name, unique within a library. +- `match { ... }`: the adjacent operation pattern to match, at least one item. +- `rewrite { ... }`: the replacement sequence; may be empty (an empty rewrite cancels and deletes). +- Each operation is `gate_name (params) qubit_labels...`; the parentheses are omitted for parameterless gates (for example `H 0`). +- Qubit labels are non-negative integer placeholders that must start at zero and be contiguous; matching binds them to concrete qubits. +- Parameters may be numeric values, symbols (`a`, `b`), or expressions (`a + b`). + +--- + +## Library Construction and Release + +### knowledge_library_new() + +```c +struct CKnowledgeLibrary *knowledge_library_new(void); +``` + +Creates an empty rule library. + +Return value: + +| Return | Scenario | +| --- | --- | +| Non-NULL | A new empty-library handle; free with `knowledge_library_free`. | + +### knowledge_library_builtin() + +```c +struct CKnowledgeLibrary *knowledge_library_builtin(void); +``` + +Creates a library containing the builtin compiler rules. The rules are registered under their own compiler kinds and can be queried per kind with `knowledge_library_rules_by_kind_len`. + +Return value: + +| Return | Scenario | +| --- | --- | +| Non-NULL | A new builtin-library handle; free with `knowledge_library_free`. | +| NULL | The builtin rule sources failed to validate. | + +### knowledge_library_from_dsl_str(source, kind, out) + +```c +int32_t knowledge_library_from_dsl_str(const char *source, + uint8_t kind, + struct CKnowledgeLibrary **out); +``` + +Parses and validates a DSL source string to build a rule library; `kind` (one of `RULE_KIND_*`) classifies every parsed rule. + +Parameters: + +- `source` (`const char*`): DSL source text. +- `kind` (`uint8_t`): rule-kind tag, one of `RULE_KIND_*`. +- `out` (`struct CKnowledgeLibrary**`): output pointer receiving the new library on success. + +Returns: 0 on success with a new `CKnowledgeLibrary*` (free with `knowledge_library_free`) written to `*out`. + +Error codes: + +| Value | Scenario | +| --- | --- | +| 0 | Success. | +| -1 | `source` or `out` is NULL. | +| -4 | `source` is invalid UTF-8, or DSL parsing, lowering, or validation failed. | +| -8 | `kind` is not a valid `RULE_KIND_*` tag. | + +### knowledge_library_from_dsl_file(path, kind, out) + +```c +int32_t knowledge_library_from_dsl_file(const char *path, + uint8_t kind, + struct CKnowledgeLibrary **out); +``` + +Loads, parses, and validates rules from a DSL file to build a rule library; `kind` classifies every rule. + +Parameters: + +- `path` (`const char*`): path to the DSL file. +- `kind` (`uint8_t`): rule-kind tag, one of `RULE_KIND_*`. +- `out` (`struct CKnowledgeLibrary**`): output pointer receiving the new library on success. + +Returns: 0 on success with a new `CKnowledgeLibrary*` written to `*out`. + +Error codes: + +| Value | Scenario | +| --- | --- | +| 0 | Success. | +| -1 | `path` or `out` is NULL. | +| -4 | `path` is invalid UTF-8, or DSL parsing, lowering, or validation failed. | +| -5 | The file could not be read. | +| -8 | `kind` is not a valid `RULE_KIND_*` tag. | + +### knowledge_library_free(ptr) + +```c +void knowledge_library_free(struct CKnowledgeLibrary *ptr); +``` + +Frees a rule library handle; NULL is allowed. + +--- + +## Library Queries + +### knowledge_library_len(ptr) + +```c +uintptr_t knowledge_library_len(const struct CKnowledgeLibrary *ptr); +``` + +Returns the number of rules in the library. + +Return value: + +| Return | Scenario | +| --- | --- | +| Non-negative | The rule count. | +| 0 | `ptr` is NULL or the library is empty. | + +### knowledge_library_is_empty(ptr) + +```c +int32_t knowledge_library_is_empty(const struct CKnowledgeLibrary *ptr); +``` + +Tests whether the library contains no rules. + +Return value: + +| Value | Scenario | +| --- | --- | +| 1 | The library is empty. | +| 0 | The library is not empty. | +| -1 | `ptr` is NULL. | + +### knowledge_library_contains(ptr, name) + +```c +int32_t knowledge_library_contains(const struct CKnowledgeLibrary *ptr, const char *name); +``` + +Tests whether a rule named `name` exists in the library. + +Parameters: + +- `ptr` (`const CKnowledgeLibrary*`): rule library handle. +- `name` (`const char*`): rule name. + +Return value: + +| Value | Scenario | +| --- | --- | +| 1 | A rule with that name exists. | +| 0 | No rule with that name exists. | +| -1 | `ptr` or `name` is NULL. | +| -4 | `name` is invalid UTF-8. | + +### knowledge_library_id_by_name(ptr, name) + +```c +int64_t knowledge_library_id_by_name(const struct CKnowledgeLibrary *ptr, const char *name); +``` + +Looks up the rule id of `name` (the insertion index; a rule's id equals the library length at insertion time). + +Parameters: + +- `ptr` (`const CKnowledgeLibrary*`): rule library handle. +- `name` (`const char*`): rule name. + +Return value: + +| Return | Scenario | +| --- | --- | +| Non-negative | The rule id. | +| -1 | `ptr` or `name` is NULL, or no rule with that name exists. | +| -4 | `name` is invalid UTF-8. | + +### knowledge_library_rules_by_kind_len(ptr, kind) / knowledge_library_rules_by_kind(ptr, kind, out, len) + +```c +uintptr_t knowledge_library_rules_by_kind_len(const struct CKnowledgeLibrary *ptr, uint8_t kind); +uintptr_t knowledge_library_rules_by_kind(const struct CKnowledgeLibrary *ptr, + uint8_t kind, + uint32_t *out, + uintptr_t len); +``` + +Two-step read of the rule ids registered under a kind: `*_len` returns the total number of rules of that kind; the fill function copies the ids into `out` (writing `min(total, len)` entries) and returns the total. When `out` is NULL it only returns the total without copying. + +Parameters: + +- `ptr` (`const CKnowledgeLibrary*`): rule library handle. +- `kind` (`uint8_t`): rule-kind tag, one of `RULE_KIND_*`. +- `out` (`uint32_t*`): buffer receiving the ids. +- `len` (`uintptr_t`): buffer length. + +Return value (both functions): + +| Return | Scenario | +| --- | --- | +| Non-negative | The total number of rules of that kind. | +| 0 | `ptr` is NULL, or `kind` is not a valid `RULE_KIND_*` tag. | + +### knowledge_rules_len(ptr) / knowledge_rules(ptr, out, len) + +```c +uintptr_t knowledge_rules_len(const struct CKnowledgeLibrary *ptr); +uintptr_t knowledge_rules(const struct CKnowledgeLibrary *ptr, uint32_t *out, uintptr_t len); +``` + +Two-step read of the rule ids (insertion indices) in the library: `*_len` returns the total number of rules; the fill function copies the ids into `out` (writing `min(total, len)` entries) and returns the total. When `out` is NULL it only returns the total without copying. + +Parameters: + +- `ptr` (`const CKnowledgeLibrary*`): rule library handle. +- `out` (`uint32_t*`): buffer receiving the ids. +- `len` (`uintptr_t`): buffer length. + +Return value (both functions): + +| Return | Scenario | +| --- | --- | +| Non-negative | The total number of rules. | +| 0 | `ptr` is NULL. | + +### knowledge_get_by_name(ptr, name) + +```c +struct CKnowledgeRule *knowledge_get_by_name(const struct CKnowledgeLibrary *ptr, const char *name); +``` + +Looks up a rule by name and returns a cloned handle. + +Parameters: + +- `ptr` (`const CKnowledgeLibrary*`): rule library handle. +- `name` (`const char*`): rule name. + +Return value: + +| Return | Scenario | +| --- | --- | +| Non-NULL | A new `CKnowledgeRule*` clone; free with `knowledge_rule_free`. | +| NULL | `ptr` or `name` is NULL, or no rule with that name exists. | + +--- + +## Rule Metadata + +The following functions read the precomputed metadata of the rule at insertion index `index`; valid indices span `[0, knowledge_library_len(ptr))`. + +### knowledge_library_rule_name(ptr, index) + +```c +char *knowledge_library_rule_name(const struct CKnowledgeLibrary *ptr, uintptr_t index); +``` + +Returns the name of the rule at `index`. + +Return value: + +| Return | Scenario | +| --- | --- | +| Non-NULL | A heap-allocated C string; free with `cqlib_string_free`. | +| NULL | `ptr` is NULL or `index` is out of bounds. | + +### knowledge_library_rule_kind(ptr, index) + +```c +int32_t knowledge_library_rule_kind(const struct CKnowledgeLibrary *ptr, uintptr_t index); +``` + +Returns the kind tag of the rule at `index`. + +Return value: + +| Value | Scenario | +| --- | --- | +| 0–7 | A `RULE_KIND_*` kind tag. | +| -1 | `ptr` is NULL. | +| -8 | `index` is out of bounds. | + +### knowledge_library_rule_first_instruction(ptr, index) + +```c +char *knowledge_library_rule_first_instruction(const struct CKnowledgeLibrary *ptr, + uintptr_t index); +``` + +Returns the name of the instruction starting the rule's match pattern (candidate rules can be filtered by their first instruction). + +Return value: + +| Return | Scenario | +| --- | --- | +| Non-NULL | A heap-allocated C string; free with `cqlib_string_free`. | +| NULL | `ptr` is NULL or `index` is out of bounds. | + +### knowledge_library_rule_pattern_len(ptr, index) + +```c +uintptr_t knowledge_library_rule_pattern_len(const struct CKnowledgeLibrary *ptr, uintptr_t index); +``` + +Returns the number of operations in the rule's match pattern. + +Return value: + +| Return | Scenario | +| --- | --- | +| Non-negative | The pattern length. | +| 0 | `ptr` is NULL or `index` is out of bounds. | + +### knowledge_library_rule_rewrite_len(ptr, index) + +```c +uintptr_t knowledge_library_rule_rewrite_len(const struct CKnowledgeLibrary *ptr, uintptr_t index); +``` + +Returns the number of operations emitted by the rule's rewrite target. + +Return value: + +| Return | Scenario | +| --- | --- | +| Non-negative | The rewrite length. | +| 0 | `ptr` is NULL or `index` is out of bounds. | + +### knowledge_library_rule_qubit_count(ptr, index) + +```c +uintptr_t knowledge_library_rule_qubit_count(const struct CKnowledgeLibrary *ptr, uintptr_t index); +``` + +Returns the number of distinct rule-local qubit labels used by the rule. + +Return value: + +| Return | Scenario | +| --- | --- | +| Non-negative | The qubit-label count. | +| 0 | `ptr` is NULL or `index` is out of bounds. | + +### knowledge_library_rule_cost_delta(ptr, index) + +```c +int64_t knowledge_library_rule_cost_delta(const struct CKnowledgeLibrary *ptr, uintptr_t index); +``` + +Returns the rule's static operation-count delta `rewrite_len - pattern_len` (may be negative when the rule removes operations). + +Return value: + +| Return | Scenario | +| --- | --- | +| Integer | `rewrite_len - pattern_len`. | +| 0 | `ptr` is NULL or `index` is out of bounds. | + +### knowledge_library_rule_has_conditions(ptr, index) + +```c +int32_t knowledge_library_rule_has_conditions(const struct CKnowledgeLibrary *ptr, uintptr_t index); +``` + +Tests whether the rule carries non-empty symbolic parameter conditions. + +Return value: + +| Value | Scenario | +| --- | --- | +| 1 | The rule carries non-empty conditions. | +| 0 | The rule has no conditions. | +| -1 | `ptr` is NULL. | +| -8 | `index` is out of bounds. | + +--- + +## Single-Rule Parsing and Insertion + +### knowledge_rule_from_dsl(source, out) + +```c +int32_t knowledge_rule_from_dsl(const char *source, struct CKnowledgeRule **out); +``` + +Parses a DSL source string that must define exactly one rule and creates a single-rule handle (to be inserted with `knowledge_library_add_rule`). + +Parameters: + +- `source` (`const char*`): DSL source text (exactly one `rule` definition). +- `out` (`struct CKnowledgeRule**`): output pointer receiving the new handle on success. + +Returns: 0 on success with a new `CKnowledgeRule*` (free with `knowledge_rule_free`) written to `*out`. + +Error codes: + +| Value | Scenario | +| --- | --- | +| 0 | Success. | +| -1 | `source` or `out` is NULL. | +| -4 | `source` is invalid UTF-8, or DSL parsing, lowering, or validation failed. | +| -8 | The source does not define exactly one rule. | + +### knowledge_rule_free(ptr) + +```c +void knowledge_rule_free(struct CKnowledgeRule *ptr); +``` + +Frees a single-rule handle; NULL is allowed. + +### knowledge_library_add_rule(ptr, rule, kind) + +```c +int64_t knowledge_library_add_rule(struct CKnowledgeLibrary *ptr, + const struct CKnowledgeRule *rule, + uint8_t kind); +``` + +Inserts a parsed rule into the library under kind `kind` and returns its assigned rule id. Structural validation runs before insertion; rule names must be unique within the library. + +Parameters: + +- `ptr` (`CKnowledgeLibrary*`): target rule library. +- `rule` (`const CKnowledgeRule*`): the rule to insert (cloned; the handle is not consumed and remains owned by the caller). +- `kind` (`uint8_t`): rule-kind tag, one of `RULE_KIND_*`. + +Return value: + +| Return | Scenario | +| --- | --- | +| Non-negative | The assigned rule id. | +| -1 | `ptr` or `rule` is NULL. | +| -4 | Structural validation failed or the rule name duplicates an existing one. | +| -6 | The rule's first instruction cannot be indexed. | +| -8 | `kind` is not a valid `RULE_KIND_*` tag. | + +--- + +## Single-Rule Accessors + +### knowledge_rule_name(ptr) + +```c +char *knowledge_rule_name(const struct CKnowledgeRule *ptr); +``` + +Returns the rule's name. + +Return value: + +| Return | Scenario | +| --- | --- | +| Non-NULL | A heap-allocated C string; free with `cqlib_string_free`. | +| NULL | `ptr` is NULL. | + +### knowledge_rule_num_qubits(ptr) + +```c +uintptr_t knowledge_rule_num_qubits(const struct CKnowledgeRule *ptr); +``` + +Returns the number of qubit labels the rule involves. + +Return value: + +| Return | Scenario | +| --- | --- | +| Non-negative | The qubit-label count. | +| 0 | `ptr` is NULL. | + +### knowledge_rule_pattern_len(ptr) + +```c +uintptr_t knowledge_rule_pattern_len(const struct CKnowledgeRule *ptr); +``` + +Returns the number of operations in the rule's match pattern. + +Return value: + +| Return | Scenario | +| --- | --- | +| Non-negative | The pattern length. | +| 0 | `ptr` is NULL. | + +### knowledge_rule_rewrite_len(ptr) + +```c +uintptr_t knowledge_rule_rewrite_len(const struct CKnowledgeRule *ptr); +``` + +Returns the number of operations emitted by the rule's rewrite target. + +Return value: + +| Return | Scenario | +| --- | --- | +| Non-negative | The rewrite length. | +| 0 | `ptr` is NULL. | + +### knowledge_rule_operation_symbols_len(ptr) / knowledge_rule_operation_symbols(ptr, out, len) + +```c +uintptr_t knowledge_rule_operation_symbols_len(const struct CKnowledgeRule *ptr); +int32_t knowledge_rule_operation_symbols(const struct CKnowledgeRule *ptr, + char **out, + uintptr_t len); +``` + +Two-step read of the distinct symbols bound by the rule's match block: `*_len` returns the total number of symbols; the fill function copies the symbols (sorted, deduplicated) into `out` as newly allocated C strings and returns 0. Each string must be released with `cqlib_string_free`. + +Parameters: + +- `ptr` (`const CKnowledgeRule*`): rule handle. +- `out` (`char**`): buffer receiving the symbol strings. +- `len` (`uintptr_t`): buffer length, which must equal the count returned by `knowledge_rule_operation_symbols_len`. + +Return value (`*_len`): + +| Return | Scenario | +| --- | --- | +| Non-negative | The total number of symbols. | +| 0 | `ptr` is NULL. | + +Error codes (fill function): + +| Value | Scenario | +| --- | --- | +| 0 | Success; `out` holds `len` newly allocated strings. | +| -1 | `ptr` is NULL, or `out` is NULL while `len > 0`. | +| -8 | `len` does not match the symbol count. | + +### knowledge_rule_target_symbols_len(ptr) / knowledge_rule_target_symbols(ptr, out, len) + +```c +uintptr_t knowledge_rule_target_symbols_len(const struct CKnowledgeRule *ptr); +int32_t knowledge_rule_target_symbols(const struct CKnowledgeRule *ptr, + char **out, + uintptr_t len); +``` + +Two-step read of the distinct symbols referenced by the rule's rewrite target: `*_len` returns the total number of symbols; the fill function copies the symbols (sorted, deduplicated) into `out` as newly allocated C strings and returns 0. Each string must be released with `cqlib_string_free`. + +Parameters: + +- `ptr` (`const CKnowledgeRule*`): rule handle. +- `out` (`char**`): buffer receiving the symbol strings. +- `len` (`uintptr_t`): buffer length, which must equal the count returned by `knowledge_rule_target_symbols_len`. + +Return value (`*_len`): + +| Return | Scenario | +| --- | --- | +| Non-negative | The total number of symbols. | +| 0 | `ptr` is NULL. | + +Error codes (fill function): + +| Value | Scenario | +| --- | --- | +| 0 | Success; `out` holds `len` newly allocated strings. | +| -1 | `ptr` is NULL, or `out` is NULL while `len > 0`. | +| -8 | `len` does not match the symbol count. | + +### knowledge_rule_condition_symbols_len(ptr) / knowledge_rule_condition_symbols(ptr, out, len) + +```c +uintptr_t knowledge_rule_condition_symbols_len(const struct CKnowledgeRule *ptr); +int32_t knowledge_rule_condition_symbols(const struct CKnowledgeRule *ptr, + char **out, + uintptr_t len); +``` + +Two-step read of the distinct symbols referenced by the rule's parameter conditions: `*_len` returns the total number of symbols; the fill function copies the symbols (sorted, deduplicated) into `out` as newly allocated C strings and returns 0. Each string must be released with `cqlib_string_free`. + +Parameters: + +- `ptr` (`const CKnowledgeRule*`): rule handle. +- `out` (`char**`): buffer receiving the symbol strings. +- `len` (`uintptr_t`): buffer length, which must equal the count returned by `knowledge_rule_condition_symbols_len`. + +Return value (`*_len`): + +| Return | Scenario | +| --- | --- | +| Non-negative | The total number of symbols. | +| 0 | `ptr` is NULL. | + +Error codes (fill function): + +| Value | Scenario | +| --- | --- | +| 0 | Success; `out` holds `len` newly allocated strings. | +| -1 | `ptr` is NULL, or `out` is NULL while `len > 0`. | +| -8 | `len` does not match the symbol count. | + +### knowledge_rule_has_conditions(ptr) + +```c +int32_t knowledge_rule_has_conditions(const struct CKnowledgeRule *ptr); +``` + +Tests whether the rule carries non-empty symbolic parameter conditions. + +Return value: + +| Value | Scenario | +| --- | --- | +| 1 | The rule carries non-empty conditions. | +| 0 | The rule has no conditions. | +| -1 | `ptr` is NULL. | + +### knowledge_rule_validate(ptr) + +```c +int32_t knowledge_rule_validate(const struct CKnowledgeRule *ptr); +``` + +Runs the rule's structural validation. + +Error codes: + +| Value | Scenario | +| --- | --- | +| 0 | The rule is well formed. | +| -1 | `ptr` is NULL. | +| -4 | Validation failed. | + +--- + +## Matching Engine + +The matching engine drives a rule manually: match the pattern against concrete operations to obtain `CKnowledgeBindings*`, evaluate conditions on the bindings, and instantiate the rewrite target into `CKnowledgeReplacements*`. Operations are passed as `COperation*` handles created with `operation_new` (see [Operation / Instruction](../0_circuit/4_operation_instruction.md)). + +### knowledge_bindings_new() + +```c +struct CKnowledgeBindings *knowledge_bindings_new(void); +``` + +Creates an empty match-bindings record for use with `knowledge_match_rule_item`. + +Return value: + +| Return | Scenario | +| --- | --- | +| Non-NULL | A new bindings handle; free with `knowledge_bindings_free`. | + +### knowledge_bindings_free(ptr) + +```c +void knowledge_bindings_free(struct CKnowledgeBindings *ptr); +``` + +Frees match bindings; NULL is allowed. + +### knowledge_match_rule_item(ptr, item_index, op, bindings) + +```c +int32_t knowledge_match_rule_item(const struct CKnowledgeRule *ptr, + uintptr_t item_index, + const struct COperation *op, + struct CKnowledgeBindings *bindings); +``` + +Matches the rule's match-pattern item at `item_index` against a concrete operation and records the bound qubits and parameters into `bindings`. The match is transactional: when the item does not match, the bindings are left unchanged, so a failed attempt does not corrupt earlier bindings. + +Parameters: + +- `ptr` (`const CKnowledgeRule*`): rule handle. +- `item_index` (`uintptr_t`): index into the rule's match pattern. +- `op` (`const COperation*`): the operation to match. +- `bindings` (`CKnowledgeBindings*`): bindings record updated on success. + +Error codes: + +| Value | Scenario | +| --- | --- | +| 1 | The item matches; `bindings` now records the bound qubits and parameters. | +| 0 | The item does not match; `bindings` unchanged. | +| -1 | `ptr`, `op`, or `bindings` is NULL. | +| -2 | `item_index` is out of bounds. | +| -8 | The operation carries indexed (table-resolved) parameters. | +| -6 | The rule item uses an instruction the matcher cannot represent. | + +### knowledge_rule_matches_operations(ptr, operations, operations_len, out) + +```c +int32_t knowledge_rule_matches_operations(const struct CKnowledgeRule *ptr, + const struct COperation *const *operations, + uintptr_t operations_len, + struct CKnowledgeBindings **out); +``` + +Matches the rule's full match pattern against `operations_len` adjacent operations. On success writes a new `CKnowledgeBindings*` to `*out` and returns 1. Parameter conditions are not evaluated here; check them separately with `knowledge_conditions_hold` on the returned bindings. + +Parameters: + +- `ptr` (`const CKnowledgeRule*`): rule handle. +- `operations` (`const COperation* const*`): array of adjacent operations. +- `operations_len` (`uintptr_t`): number of operations. +- `out` (`struct CKnowledgeBindings**`): output pointer receiving the bindings on success. + +Error codes: + +| Value | Scenario | +| --- | --- | +| 1 | The pattern matches; a new `CKnowledgeBindings*` (free with `knowledge_bindings_free`) was written to `*out`. | +| 0 | The pattern does not match; `out` left untouched. | +| -1 | `ptr`, `operations`, or `out` is NULL. | +| -8 | `operations_len` differs from the pattern length, or an operation carries indexed parameters. | + +### knowledge_conditions_hold(ptr, bindings) + +```c +int32_t knowledge_conditions_hold(const struct CKnowledgeRule *ptr, + const struct CKnowledgeBindings *bindings); +``` + +Evaluates the rule's symbolic parameter conditions under the bindings. + +Return value: + +| Value | Scenario | +| --- | --- | +| 1 | All parameter conditions hold. | +| 0 | At least one condition fails. | +| -1 | `ptr` or `bindings` is NULL. | + +### knowledge_equivalent_to(lhs, lhs_item, rhs, rhs_item) + +```c +int32_t knowledge_equivalent_to(const struct CKnowledgeRule *lhs, + uintptr_t lhs_item, + const struct CKnowledgeRule *rhs, + uintptr_t rhs_item); +``` + +Tests whether the match-pattern item at `lhs_item` of `lhs` is equivalent to the item at `rhs_item` of `rhs`. + +Parameters: + +- `lhs` (`const CKnowledgeRule*`): first rule handle. +- `lhs_item` (`uintptr_t`): item index in the first rule's pattern. +- `rhs` (`const CKnowledgeRule*`): second rule handle. +- `rhs_item` (`uintptr_t`): item index in the second rule's pattern. + +Return value: + +| Value | Scenario | +| --- | --- | +| 1 | The two items are equivalent. | +| 0 | The items are not equivalent. | +| -1 | `lhs` or `rhs` is NULL. | +| -2 | An item index is out of bounds. | + +### knowledge_candidates_for_first_instruction_len(ptr, name) / knowledge_candidates_for_first_instruction(ptr, name, out, len) + +```c +uintptr_t knowledge_candidates_for_first_instruction_len(const struct CKnowledgeLibrary *ptr, + const char *name); +uintptr_t knowledge_candidates_for_first_instruction(const struct CKnowledgeLibrary *ptr, + const char *name, + uint32_t *out, + uintptr_t len); +``` + +Two-step read of the rule ids whose first match instruction has the same matcher key as the gate `name`: `*_len` returns the total number of candidates; the fill function copies the ids into `out` (writing `min(total, len)` entries) and returns the total. When `out` is NULL it only returns the total without copying. + +Parameters: + +- `ptr` (`const CKnowledgeLibrary*`): rule library handle. +- `name` (`const char*`): gate name. +- `out` (`uint32_t*`): buffer receiving the ids. +- `len` (`uintptr_t`): buffer length. + +Return value (both functions): + +| Return | Scenario | +| --- | --- | +| Non-negative | The total number of candidate rules. | +| 0 | `ptr` or `name` is NULL, or `name` is an unknown gate name. | + +### knowledge_collect_free_symbols_len(ptr) / knowledge_collect_free_symbols(ptr, out, len) + +```c +uintptr_t knowledge_collect_free_symbols_len(const struct CKnowledgeRule *ptr); +uintptr_t knowledge_collect_free_symbols(const struct CKnowledgeRule *ptr, + char **out, + uintptr_t len); +``` + +Two-step read of the rule's free symbols (sorted): `*_len` returns the total number of symbols; the fill function copies the symbols into `out` as newly allocated C strings (writing `min(total, len)` entries) and returns the total. Each string must be released with `cqlib_string_free`. + +Parameters: + +- `ptr` (`const CKnowledgeRule*`): rule handle. +- `out` (`char**`): buffer receiving the symbol strings. +- `len` (`uintptr_t`): buffer length. + +Return value (both functions): + +| Return | Scenario | +| --- | --- | +| Non-negative | The total number of free symbols. | +| 0 | `ptr` is NULL. | + +### knowledge_filter_rule_ids_by_instruction_keys_len(ptr, ops, ops_len, targets, targets_len) / knowledge_filter_rule_ids_by_instruction_keys(ptr, ops, ops_len, targets, targets_len, out, len) + +```c +uintptr_t knowledge_filter_rule_ids_by_instruction_keys_len(const struct CKnowledgeLibrary *ptr, + const char *const *ops, + uintptr_t ops_len, + const char *const *targets, + uintptr_t targets_len); +uintptr_t knowledge_filter_rule_ids_by_instruction_keys(const struct CKnowledgeLibrary *ptr, + const char *const *ops, + uintptr_t ops_len, + const char *const *targets, + uintptr_t targets_len, + uint32_t *out, + uintptr_t len); +``` + +Two-step read of the rule ids whose match pattern covers every gate in `ops` and whose rewrite target covers every gate in `targets` (both as arrays of gate names): `*_len` returns the total number of matching rules; the fill function copies the ids into `out` (writing `min(total, len)` entries) and returns the total. When `out` is NULL it only returns the total without copying. + +Parameters: + +- `ptr` (`const CKnowledgeLibrary*`): rule library handle. +- `ops` (`const char* const*`): match-side gate names. +- `ops_len` (`uintptr_t`): number of match-side names. +- `targets` (`const char* const*`): rewrite-side gate names. +- `targets_len` (`uintptr_t`): number of rewrite-side names. +- `out` (`uint32_t*`): buffer receiving the ids. +- `len` (`uintptr_t`): buffer length. + +Return value (both functions): + +| Return | Scenario | +| --- | --- | +| Non-negative | The total number of matching rules. | +| 0 | `ptr` is NULL, or a gate name is NULL or unknown. | + +### knowledge_extend_rules(ptr, rules, rules_len, kind, out, out_len) + +```c +int32_t knowledge_extend_rules(struct CKnowledgeLibrary *ptr, + const struct CKnowledgeRule *const *rules, + uintptr_t rules_len, + uint8_t kind, + uint32_t *out, + uintptr_t out_len); +``` + +Adds several parsed rules to the library under kind `kind` atomically and writes the assigned rule ids into `out` (which must have room for `rules_len` entries). If any rule fails, the library is left unchanged. + +Parameters: + +- `ptr` (`CKnowledgeLibrary*`): target rule library. +- `rules` (`const CKnowledgeRule* const*`): array of parsed rule handles (cloned; the handles are not consumed). +- `rules_len` (`uintptr_t`): number of rules. +- `kind` (`uint8_t`): rule-kind tag, one of `RULE_KIND_*`. +- `out` (`uint32_t*`): buffer receiving the assigned rule ids. +- `out_len` (`uintptr_t`): buffer length, at least `rules_len`. + +Error codes: + +| Value | Scenario | +| --- | --- | +| 0 | Success; `out` holds the assigned ids. | +| -1 | `ptr`, `rules`, or `out` is NULL. | +| -8 | `kind` is not a valid `RULE_KIND_*` tag, or `out_len` is smaller than `rules_len`. | +| -4 | A validation failure or duplicate rule name; the library is unchanged. | + +### knowledge_target_qubits_len(ptr) / knowledge_target_qubits(ptr, out, len) + +```c +uintptr_t knowledge_target_qubits_len(const struct CKnowledgeRule *ptr); +uintptr_t knowledge_target_qubits(const struct CKnowledgeRule *ptr, uint32_t *out, uintptr_t len); +``` + +Two-step read of the rule-local qubit labels (sorted) used by the rule's rewrite target: `*_len` returns the total number of labels; the fill function copies the labels into `out` (writing `min(total, len)` entries) and returns the total. When `out` is NULL it only returns the total without copying. + +Parameters: + +- `ptr` (`const CKnowledgeRule*`): rule handle. +- `out` (`uint32_t*`): buffer receiving the labels. +- `len` (`uintptr_t`): buffer length. + +Return value (both functions): + +| Return | Scenario | +| --- | --- | +| Non-negative | The total number of labels. | +| 0 | `ptr` is NULL. | + +### knowledge_instantiate_target(ptr, bindings, out) + +```c +int32_t knowledge_instantiate_target(const struct CKnowledgeRule *ptr, + const struct CKnowledgeBindings *bindings, + struct CKnowledgeReplacements **out); +``` + +Instantiates the rule's rewrite target under the match bindings, evaluating each target parameter expression with the bound values. + +Parameters: + +- `ptr` (`const CKnowledgeRule*`): rule handle. +- `bindings` (`const CKnowledgeBindings*`): bindings produced by a match. +- `out` (`struct CKnowledgeReplacements**`): output pointer receiving the instantiation result on success. + +Error codes: + +| Value | Scenario | +| --- | --- | +| 0 | Success; a new `CKnowledgeReplacements*` (free with `knowledge_replacements_free`) was written to `*out`. | +| -1 | `ptr`, `bindings`, or `out` is NULL. | +| -2 | The target references a qubit not bound by the match. | +| -8 | The target references a symbol not bound by the match. | +| -6 | The target uses an instruction the matcher cannot represent. | + +### knowledge_replacements_len(ptr) + +```c +uintptr_t knowledge_replacements_len(const struct CKnowledgeReplacements *ptr); +``` + +Returns the number of instantiated replacement items. + +Return value: + +| Return | Scenario | +| --- | --- | +| Non-negative | The replacement item count. | +| 0 | `ptr` is NULL. | + +### knowledge_replacement_instruction(ptr, index) + +```c +char *knowledge_replacement_instruction(const struct CKnowledgeReplacements *ptr, uintptr_t index); +``` + +Returns the instruction name (e.g. `"RZ"`) of the replacement item at `index`. + +Parameters: + +- `ptr` (`const CKnowledgeReplacements*`): instantiation result handle. +- `index` (`uintptr_t`): replacement item index. + +Return value: + +| Return | Scenario | +| --- | --- | +| Non-NULL | A heap-allocated C string; free with `cqlib_string_free`. | +| NULL | `ptr` is NULL or `index` is out of bounds. | + +### knowledge_replacement_qubits_len(ptr, index) / knowledge_replacement_qubits(ptr, index, out, len) + +```c +uintptr_t knowledge_replacement_qubits_len(const struct CKnowledgeReplacements *ptr, + uintptr_t index); +uintptr_t knowledge_replacement_qubits(const struct CKnowledgeReplacements *ptr, + uintptr_t index, + uint32_t *out, + uintptr_t len); +``` + +Two-step read of the concrete qubits of the replacement item at `index`: `*_len` returns the total number of qubits; the fill function copies the qubits into `out` (writing `min(total, len)` entries) and returns the total. When `out` is NULL it only returns the total without copying. + +Parameters: + +- `ptr` (`const CKnowledgeReplacements*`): instantiation result handle. +- `index` (`uintptr_t`): replacement item index. +- `out` (`uint32_t*`): buffer receiving the qubits. +- `len` (`uintptr_t`): buffer length. + +Return value (both functions): + +| Return | Scenario | +| --- | --- | +| Non-negative | The total number of qubits. | +| 0 | `ptr` is NULL or `index` is out of bounds. | + +### knowledge_replacement_params_len(ptr, index) / knowledge_replacement_params(ptr, index, out, len) + +```c +uintptr_t knowledge_replacement_params_len(const struct CKnowledgeReplacements *ptr, + uintptr_t index); +uintptr_t knowledge_replacement_params(const struct CKnowledgeReplacements *ptr, + uintptr_t index, + double *out, + uintptr_t len); +``` + +Two-step read of the evaluated double parameters of the replacement item at `index`: `*_len` returns the total number of parameters; the fill function copies the parameters into `out` (writing `min(total, len)` entries) and returns the total. When `out` is NULL it only returns the total without copying. + +Parameters: + +- `ptr` (`const CKnowledgeReplacements*`): instantiation result handle. +- `index` (`uintptr_t`): replacement item index. +- `out` (`double*`): buffer receiving the evaluated parameters. +- `len` (`uintptr_t`): buffer length. + +Return value (both functions): + +| Return | Scenario | +| --- | --- | +| Non-negative | The total number of parameters. | +| 0 | `ptr` is NULL or `index` is out of bounds. | + +### knowledge_replacements_free(ptr) + +```c +void knowledge_replacements_free(struct CKnowledgeReplacements *ptr); +``` + +Frees a rewrite-target instantiation result; NULL is allowed. + +--- + +## Examples + +Walking the builtin library and querying by kind: + +```c +#include +#include +#include "cqlib_c.h" + +int main(void) { + /* 1. Load the builtin rule library. */ + struct CKnowledgeLibrary *lib = knowledge_library_builtin(); + if (lib == NULL) { + return 1; + } + + /* 2. Walk the rule metadata by insertion index. */ + for (uintptr_t i = 0; i < knowledge_library_len(lib); i++) { + char *name = knowledge_library_rule_name(lib, i); + int32_t kind = knowledge_library_rule_kind(lib, i); + uintptr_t pattern = knowledge_library_rule_pattern_len(lib, i); + if (name != NULL) { + printf("rule %s: kind=%d pattern_len=%llu\n", + name, kind, (unsigned long long)pattern); + cqlib_string_free(name); + } + } + + /* 3. Two-step read of the decompose-rule ids. */ + uintptr_t n = knowledge_library_rules_by_kind_len(lib, RULE_KIND_DECOMPOSE); + uint32_t *ids = malloc(n * sizeof(uint32_t)); + if (ids != NULL && n > 0) { + uintptr_t written = knowledge_library_rules_by_kind(lib, RULE_KIND_DECOMPOSE, ids, n); + printf("%llu decompose rules (first id %u)\n", + (unsigned long long)written, ids[0]); + free(ids); + } + + knowledge_library_free(lib); + return 0; +} +``` + +Building a library from DSL text and querying it: + +```c +#include +#include "cqlib_c.h" + +int main(void) { + /* 1. Build a library from DSL text (merge two adjacent RZ rotations). */ + const char *source = + "rule merge_rz {\n" + " match {\n" + " RZ(a) 0\n" + " RZ(b) 0\n" + " }\n" + " rewrite {\n" + " RZ(a + b) 0\n" + " }\n" + "}"; + struct CKnowledgeLibrary *lib = NULL; + if (knowledge_library_from_dsl_str(source, RULE_KIND_MERGE, &lib) != 0) { + return 1; + } + + /* 2. Query by name. */ + if (knowledge_library_contains(lib, "merge_rz") == 1) { + printf("id=%lld\n", (long long)knowledge_library_id_by_name(lib, "merge_rz")); + } + + /* 3. Read the metadata of rule 0. */ + char *first = knowledge_library_rule_first_instruction(lib, 0); /* "RZ" */ + printf("first=%s pattern=%llu rewrite=%llu cost_delta=%lld\n", + first, + (unsigned long long)knowledge_library_rule_pattern_len(lib, 0), + (unsigned long long)knowledge_library_rule_rewrite_len(lib, 0), + (long long)knowledge_library_rule_cost_delta(lib, 0)); + cqlib_string_free(first); + + knowledge_library_free(lib); + return 0; +} +``` + +Parsing and inserting a single rule: + +```c +#include +#include "cqlib_c.h" + +int main(void) { + /* 1. Parse exactly one rule: cancel two adjacent H gates. */ + const char *source = + "rule cancel_h {\n" + " match {\n" + " H 0\n" + " H 0\n" + " }\n" + " rewrite {}\n" + "}"; + struct CKnowledgeRule *rule = NULL; + if (knowledge_rule_from_dsl(source, &rule) != 0) { + return 1; + } + + /* 2. Inspect and validate the standalone rule. */ + if (knowledge_rule_validate(rule) != 0) { + knowledge_rule_free(rule); + return 1; + } + char *name = knowledge_rule_name(rule); /* "cancel_h" */ + printf("rule %s: qubits=%llu pattern=%llu rewrite=%llu conditions=%d\n", + name, + (unsigned long long)knowledge_rule_num_qubits(rule), + (unsigned long long)knowledge_rule_pattern_len(rule), + (unsigned long long)knowledge_rule_rewrite_len(rule), + knowledge_rule_has_conditions(rule)); + cqlib_string_free(name); + + /* 3. Insert into a fresh library (assigns id 0), then reject duplicates. */ + struct CKnowledgeLibrary *lib = knowledge_library_new(); + if (knowledge_library_add_rule(lib, rule, RULE_KIND_CANCEL) != 0) { + knowledge_rule_free(rule); + knowledge_library_free(lib); + return 1; + } + if (knowledge_library_add_rule(lib, rule, RULE_KIND_CANCEL) == -4) { + printf("duplicate name rejected\n"); + } + + knowledge_rule_free(rule); + knowledge_library_free(lib); + return 0; +} +``` + +Matching a rule against concrete operations and instantiating its rewrite target: + +```c +#include +#include "cqlib_c.h" + +int main(void) { + /* 1. Build the merge_rz rule. */ + const char *source = + "rule merge_rz {\n" + " match {\n" + " RZ(a) 0\n" + " RZ(b) 0\n" + " }\n" + " rewrite {\n" + " RZ(a + b) 0\n" + " }\n" + "}"; + struct CKnowledgeRule *rule = NULL; + if (knowledge_rule_from_dsl(source, &rule) != 0) { + return 1; + } + + /* 2. Build two adjacent RZ operations. */ + uint32_t q0[1] = {7}; + double p1[1] = {0.25}; + double p2[1] = {0.5}; + struct COperation *a = operation_new("RZ", q0, 1, p1, 1); + struct COperation *b = operation_new("RZ", q0, 1, p2, 1); + const struct COperation *ops[2] = {a, b}; + + /* 3. Match the full pattern (conditions are checked separately). */ + struct CKnowledgeBindings *bindings = NULL; + if (knowledge_rule_matches_operations(rule, ops, 2, &bindings) == 1) { + /* 4. Instantiate the rewrite target: RZ(a + b) with a + b == 0.75. */ + struct CKnowledgeReplacements *repl = NULL; + if (knowledge_instantiate_target(rule, bindings, &repl) == 0) { + for (uintptr_t i = 0; i < knowledge_replacements_len(repl); i++) { + char *name = knowledge_replacement_instruction(repl, i); /* "RZ" */ + uint32_t qubits[1]; + double params[1]; + knowledge_replacement_qubits(repl, i, qubits, 1); /* {7} */ + knowledge_replacement_params(repl, i, params, 1); /* {0.75} */ + printf("repl %s q=%u p=%f\n", name, qubits[0], params[0]); + cqlib_string_free(name); + } + knowledge_replacements_free(repl); + } + knowledge_bindings_free(bindings); + } + + /* 5. Inspect rule internals: free symbols and target qubit labels. */ + uintptr_t nsym = knowledge_collect_free_symbols_len(rule); + char **symbols = malloc(nsym * sizeof(char *)); + if (symbols != NULL && knowledge_collect_free_symbols(rule, symbols, nsym) == nsym) { + for (uintptr_t i = 0; i < nsym; i++) { + printf("symbol %s\n", symbols[i]); + cqlib_string_free(symbols[i]); + } + free(symbols); + } + + operation_free(a); + operation_free(b); + knowledge_rule_free(rule); + return 0; +} +``` + +Checkers that draw their commutation rules from a rule library are covered in [Commutation](9_commutation.md). diff --git a/docs/api/en/c/4_compile/8_resource.md b/docs/api/en/c/4_compile/8_resource.md new file mode 100644 index 00000000..c86d6699 --- /dev/null +++ b/docs/api/en/c/4_compile/8_resource.md @@ -0,0 +1,415 @@ +# Resource (C) + +`CResourceManager` manages the request, planning, leasing, and recycling of ancilla qubits: previewing produces a side-effect-free plan snapshot, committing actually reserves the qubits and extends the manager's working circuit, a lease holds the reserved qubits, and releasing returns them to the resource pool. Clean and dirty ancillas are restoration contracts: a clean ancilla must be in `|0>` when entering and leaving the consuming transform; a dirty ancilla may be in an unknown or entangled state, but the transform must fully restore its input state before the lease is released. Error codes, string and handle ownership conventions follow the [Overview](../0_overview.md); circuit handle construction is covered in [Circuit](../0_circuit/1_circuit.md). + +--- + +## Constants + +Ancilla-requirement tags (the `requirement` parameter of `resource_manager_preview` and the return value of `resource_plan_requirement` / `resource_lease_requirement`): + +| Constant | Value | Requirement | +| --- | --- | --- | +| `RESOURCE_REQUIREMENT_CLEAN_ZERO` | 0 | Clean ancilla: enters and leaves the transform in `\|0>`. | +| `RESOURCE_REQUIREMENT_DIRTY` | 1 | Dirty ancilla: may enter in an unknown state, must be restored exactly. | + +--- + +## Configuration Structs + +### CResourcePolicy / resource_policy_default() + +```c +typedef struct CResourcePolicy { + uintptr_t max_pre_layout_clean_ancillas; + uint8_t allow_dirty_borrowing; + uint8_t _reserved[7]; +} CResourcePolicy; + +struct CResourcePolicy resource_policy_default(void); +``` + +`resource_policy_default` returns the default policy by value: no clean ancillas, no dirty borrowing. + +| Field | Type | Meaning | +| --- | --- | --- | +| `max_pre_layout_clean_ancillas` | `uintptr_t` | Total clean logical ancillas the compiler may create before layout; released qubits still count toward the total and are reusable. | +| `allow_dirty_borrowing` | `uint8_t` | 1 allows borrowing input qubits under the dirty contract; 0 forbids it. | +| `_reserved[7]` | `uint8_t[7]` | Reserved padding to keep the struct layout stable; do not write non-zero values. | + +### CResourceLimits / resource_limits_default() + +```c +typedef struct CResourceLimits { + uint8_t has_max_total_qubits; + uint8_t _pad[7]; + uintptr_t max_total_qubits; + uint64_t _reserved[2]; +} CResourceLimits; + +struct CResourceLimits resource_limits_default(void); +``` + +`resource_limits_default` returns the default limits by value: no total-qubit bound. + +| Field | Type | Meaning | +| --- | --- | --- | +| `has_max_total_qubits` | `uint8_t` | 1 enforces the `max_total_qubits` limit; 0 leaves it unset. | +| `_pad[7]` | `uint8_t[7]` | Reserved padding to keep the struct layout stable; do not write non-zero values. | +| `max_total_qubits` | `uintptr_t` | Hard limit on total logical qubits of the complete circuit (input qubits plus compiler-allocated clean ancillas); used only when `has_max_total_qubits != 0`. | +| `_reserved[2]` | `uint64_t[2]` | Reserved padding; do not write non-zero values. | + +--- + +## Manager Lifecycle + +### resource_manager_from_circuit(circuit, policy, limits) + +```c +struct CResourceManager *resource_manager_from_circuit(const struct CCircuit *circuit, + const struct CResourcePolicy *policy, + const struct CResourceLimits *limits); +``` + +Creates a resource manager synchronized with a circuit. The manager owns a private working copy of the circuit (extended when plans are committed) and does not take over the input circuit. `policy` and `limits` may be NULL to select the defaults. + +Parameters: + +- `circuit` (`const CCircuit*`): source circuit. +- `policy` (`const CResourcePolicy*`): resource policy; NULL selects the default. +- `limits` (`const CResourceLimits*`): resource limits; NULL selects the default. + +Return value: + +| Return | Scenario | +| --- | --- | +| Non-NULL | A new manager handle; free with `resource_manager_free`. | +| NULL | `circuit` is NULL, or the circuit already exceeds the configured limits. | + +### resource_manager_free(ptr) + +```c +void resource_manager_free(struct CResourceManager *ptr); +``` + +Frees a manager handle; NULL is allowed. The manager's internal working circuit is released with it; copies taken out by `resource_manager_circuit` are unaffected. + +### resource_manager_circuit(ptr) + +```c +struct CCircuit *resource_manager_circuit(const struct CResourceManager *ptr); +``` + +Takes out an owned copy of the manager's current working circuit (committing a plan extends it). + +Return value: + +| Return | Scenario | +| --- | --- | +| Non-NULL | A new `CCircuit*`; free with `circuit_free`. Independent of `ptr`. | +| NULL | `ptr` is NULL. | + +### resource_manager_enter_post_layout(ptr) + +```c +int32_t resource_manager_enter_post_layout(struct CResourceManager *ptr); +``` + +Moves the manager into the post-layout phase: no fresh logical clean ancillas are allocated afterwards. + +Error codes: + +| Value | Scenario | +| --- | --- | +| 0 | Success. | +| -1 | `ptr` is NULL. | +| -6 | A consistency check between the manager and its working circuit failed. | + +--- + +## Planning (Preview) + +### resource_manager_preview(ptr, requirement, count, excluded, excluded_len) + +```c +struct CResourcePlan *resource_manager_preview(const struct CResourceManager *ptr, + uint8_t requirement, + uintptr_t count, + const uint32_t *excluded, + uintptr_t excluded_len); +``` + +Previews an ancilla request against the manager without reserving resources or mutating the working circuit. `excluded` lists qubits that must not be consumed as ancillary resources (typically data, control, and target qubits); it may be NULL when `excluded_len` is 0. A plan is a manager-private snapshot: any successful change to the resource pool (for example committing another plan) invalidates older plans. + +Parameters: + +- `ptr` (`const CResourceManager*`): resource manager. +- `requirement` (`uint8_t`): ancilla-requirement tag, one of `RESOURCE_REQUIREMENT_*`. +- `count` (`uintptr_t`): number of ancillas requested. +- `excluded` (`const uint32_t*`): array of excluded qubit ids. +- `excluded_len` (`uintptr_t`): length of the excluded array. + +Return value: + +| Return | Scenario | +| --- | --- | +| Non-NULL | A new plan handle; free with `resource_plan_free`. | +| NULL | `ptr` is NULL, `requirement` is invalid, `excluded_len` is greater than 0 while `excluded` is NULL, or the request cannot be satisfied. | + +### resource_plan_free(ptr) + +```c +void resource_plan_free(struct CResourcePlan *ptr); +``` + +Frees a plan handle; NULL is allowed. When the plan is committed by `resource_manager_commit`, the handle is consumed by that call and must not be freed again. + +### resource_plan_qubits_len(ptr) / resource_plan_qubits(ptr, out, len) + +```c +uintptr_t resource_plan_qubits_len(const struct CResourcePlan *ptr); +uintptr_t resource_plan_qubits(const struct CResourcePlan *ptr, uint32_t *out, uintptr_t len); +``` + +Two-step read of the planned qubits: `*_len` returns the number of planned qubits; the fill function copies the qubit ids into `out` (writing `min(total, len)` entries) and returns the total. When `out` is NULL it only returns the total without copying. + +Parameters: + +- `ptr` (`const CResourcePlan*`): plan handle. +- `out` (`uint32_t*`): buffer receiving the qubit ids. +- `len` (`uintptr_t`): buffer length. + +Return value (both functions): + +| Return | Scenario | +| --- | --- | +| Non-negative | The total number of planned qubits. | +| 0 | `ptr` is NULL. | + +### resource_plan_num_new_qubits(ptr) + +```c +uintptr_t resource_plan_num_new_qubits(const struct CResourcePlan *ptr); +``` + +Returns the number of new logical qubits the plan introduces when committed (the part beyond the input circuit width). + +Return value: + +| Return | Scenario | +| --- | --- | +| Non-negative | The number of new qubits. | +| 0 | `ptr` is NULL. | + +### resource_plan_requirement(ptr) + +```c +int32_t resource_plan_requirement(const struct CResourcePlan *ptr); +``` + +Returns the plan's ancilla-requirement tag. + +Return value: + +| Value | Scenario | +| --- | --- | +| 0 / 1 | A `RESOURCE_REQUIREMENT_*` tag. | +| -1 | `ptr` is NULL. | + +--- + +## Commit and Lease + +### resource_manager_commit(ptr, plan) + +```c +struct CResourceLease *resource_manager_commit(struct CResourceManager *ptr, + struct CResourcePlan *plan); +``` + +Commits a previewed plan: reserves its qubits, extends the manager's working circuit, and returns the active lease. + +**Ownership**: the `plan` handle is consumed (freed) by this call, whether the commit succeeds or not; after the call, do not use or free the original handle again. + +Parameters: + +- `ptr` (`CResourceManager*`): resource manager. +- `plan` (`CResourcePlan*`): plan to commit whose ownership is taken. + +Return value: + +| Return | Scenario | +| --- | --- | +| Non-NULL | A new lease handle; return the qubits with `resource_manager_release` first, then free it with `resource_lease_free`. | +| NULL | `ptr` or `plan` is NULL, or the plan is stale. | + +### resource_lease_free(ptr) + +```c +void resource_lease_free(struct CResourceLease *ptr); +``` + +Frees a lease handle; NULL is allowed. `resource_manager_release` should have been called first to return the qubits. + +### resource_lease_qubits_len(ptr) / resource_lease_qubits(ptr, out, len) + +```c +uintptr_t resource_lease_qubits_len(const struct CResourceLease *ptr); +uintptr_t resource_lease_qubits(const struct CResourceLease *ptr, uint32_t *out, uintptr_t len); +``` + +Two-step read of the leased qubits: `*_len` returns the number of leased qubits; the fill function copies the qubit ids into `out` (writing `min(total, len)` entries) and returns the total. When `out` is NULL it only returns the total without copying. + +Parameters: + +- `ptr` (`const CResourceLease*`): lease handle. +- `out` (`uint32_t*`): buffer receiving the qubit ids. +- `len` (`uintptr_t`): buffer length. + +Return value (both functions): + +| Return | Scenario | +| --- | --- | +| Non-negative | The total number of leased qubits. | +| 0 | `ptr` is NULL. | + +### resource_lease_requirement(ptr) + +```c +int32_t resource_lease_requirement(const struct CResourceLease *ptr); +``` + +Returns the lease's ancilla-requirement tag. + +Return value: + +| Value | Scenario | +| --- | --- | +| 0 / 1 | A `RESOURCE_REQUIREMENT_*` tag. | +| -1 | `ptr` is NULL. | + +### resource_manager_release(ptr, lease) + +```c +int32_t resource_manager_release(struct CResourceManager *ptr, const struct CResourceLease *lease); +``` + +Releases an active lease, returning its qubits to the resource pool. The consuming transform must have restored the state contract before this call: clean ancillas back to `|0>`, dirty ancillas restored to their input state. The lease handle stays valid for inspection until freed with `resource_lease_free`. + +Error codes: + +| Value | Scenario | +| --- | --- | +| 0 | Success. | +| -1 | `ptr` or `lease` is NULL. | +| -6 | The lease is unknown or already released. | + +--- + +## Consistency Checks + +### resource_manager_verify_consistency(ptr) + +```c +int32_t resource_manager_verify_consistency(const struct CResourceManager *ptr); +``` + +Checks that the manager still agrees with its working circuit. + +Error codes: + +| Value | Scenario | +| --- | --- | +| 0 | Consistent. | +| -1 | `ptr` is NULL. | +| -6 | Mismatch. | + +### resource_manager_verify_idle(ptr) + +```c +int32_t resource_manager_verify_idle(const struct CResourceManager *ptr); +``` + +Checks that every lease has been released and the pool is idle. + +Error codes: + +| Value | Scenario | +| --- | --- | +| 0 | Idle. | +| -1 | `ptr` is NULL. | +| -6 | Leases are still active. | + +--- + +## Example + +Full lifecycle of requesting, committing, and releasing one clean ancilla: + +```c +#include +#include "cqlib_c.h" + +int main(void) { + /* 1. Input circuit and a policy allowing two clean ancillas. */ + struct CCircuit *qc = circuit_new(2); + circuit_h(qc, 0); + + struct CResourcePolicy policy = resource_policy_default(); + policy.max_pre_layout_clean_ancillas = 2; + + struct CResourceManager *manager = resource_manager_from_circuit(qc, &policy, NULL); + if (manager == NULL) { + circuit_free(qc); + return 1; + } + + /* 2. Preview one clean ancilla without touching the circuit. */ + struct CResourcePlan *plan = + resource_manager_preview(manager, RESOURCE_REQUIREMENT_CLEAN_ZERO, 1, NULL, 0); + if (plan == NULL) { + resource_manager_free(manager); + circuit_free(qc); + return 1; + } + uint32_t planned[1]; + resource_plan_qubits(plan, planned, 1); /* planned[0] == 2 */ + uintptr_t new_qubits = resource_plan_num_new_qubits(plan); /* 1 */ + + /* 3. Commit: the plan handle is consumed and the lease becomes active. */ + struct CResourceLease *lease = resource_manager_commit(manager, plan); + if (lease == NULL) { + resource_manager_free(manager); + circuit_free(qc); + return 1; + } + uint32_t leased[1]; + resource_lease_qubits(lease, leased, 1); /* leased[0] == planned[0] */ + printf("leased qubit %u, %llu new qubits\n", + leased[0], (unsigned long long)new_qubits); + + /* 4. The working circuit now spans the borrowed qubit. */ + struct CCircuit *working = resource_manager_circuit(manager); + printf("working qubits: %llu\n", + (unsigned long long)circuit_num_qubits(working)); /* 3 */ + circuit_free(working); + + /* 5. Restore the ancilla to |0>, then release the lease. */ + if (resource_manager_release(manager, lease) != 0) { + resource_lease_free(lease); + resource_manager_free(manager); + circuit_free(qc); + return 1; + } + if (resource_manager_verify_idle(manager) == 0) { + printf("all leases released\n"); + } + + resource_lease_free(lease); + resource_manager_free(manager); + circuit_free(qc); + return 0; +} +``` + +Common reasons for a NULL preview result: an invalid requirement tag, a count exceeding the clean-ancilla budget of the policy, or an exclusion list that makes the request unsatisfiable. diff --git a/docs/api/en/c/4_compile/9_commutation.md b/docs/api/en/c/4_compile/9_commutation.md new file mode 100644 index 00000000..dd792ac5 --- /dev/null +++ b/docs/api/en/c/4_compile/9_commutation.md @@ -0,0 +1,268 @@ +# Commutation (C) + +Checks whether two concrete gate applications commute. Each gate application is described by a standard-gate name (for example `"H"`, `"RZ"`, `"CX"`), a qubit-id array, and a numeric parameter array; when the proof is up to a global phase, the phase is written to the `phase` out pointer. Proof results are conservative: `COMMUTATION_RESULT_UNPROVEN` only means the configured proof sources could not establish commutation, not that the two operations fail to commute. Error codes, string and handle ownership conventions follow the [Overview](../0_overview.md). + +--- + +## Constants + +Commutation proof tags (the return value of the `commutation_check` family): + +| Constant | Value | Proof | +| --- | --- | --- | +| `COMMUTATION_RESULT_UNPROVEN` | 0 | Commutation could not be established with the configured proof sources. | +| `COMMUTATION_RESULT_EXACT` | 1 | Exact commutation. | +| `COMMUTATION_RESULT_UP_TO_GLOBAL_PHASE` | 2 | Commutation up to a global phase (`lhs·rhs = exp(i·phase)·rhs·lhs`); the phase is written to `phase`. | + +--- + +## Configuration Struct + +### CCommutationConfig / commutation_config_default() + +```c +typedef struct CCommutationConfig { + uint8_t enable_rule_oracle; + uint8_t enable_matrix_fallback; + uint8_t _pad[6]; + uintptr_t max_matrix_qubits; + uint64_t _reserved[2]; +} CCommutationConfig; + +struct CCommutationConfig commutation_config_default(void); +``` + +`commutation_config_default` returns the default configuration by value: the rule oracle and the matrix fallback are enabled and `max_matrix_qubits` is 4. + +| Field | Type | Meaning | +| --- | --- | --- | +| `enable_rule_oracle` | `uint8_t` | 1 enables matching explicit knowledge-base commutation rules (`A; B -> B; A`). | +| `enable_matrix_fallback` | `uint8_t` | 1 enables small local matrix comparison as a fallback. | +| `_pad[6]` | `uint8_t[6]` | Reserved padding to keep the struct layout stable; do not write non-zero values. | +| `max_matrix_qubits` | `uintptr_t` | Maximum union-support size for the matrix fallback (computed on the sorted union of both operations' supports). | +| `_reserved[2]` | `uint64_t[2]` | Reserved padding; do not write non-zero values. | + +--- + +## One-Shot Checks + +### commutation_check(lhs_gate, lhs_qubits, lhs_qubits_len, lhs_params, lhs_params_len, rhs_gate, rhs_qubits, rhs_qubits_len, rhs_params, rhs_params_len, phase) + +```c +int32_t commutation_check(const char *lhs_gate, + const uint32_t *lhs_qubits, + uintptr_t lhs_qubits_len, + const double *lhs_params, + uintptr_t lhs_params_len, + const char *rhs_gate, + const uint32_t *rhs_qubits, + uintptr_t rhs_qubits_len, + const double *rhs_params, + uintptr_t rhs_params_len, + double *phase); +``` + +Checks whether two gate applications commute using the shared builtin checker (knowledge rules and matrix fallback enabled). When the proof is up to a global phase, the phase is written to `phase` (a symbolic phase that cannot be evaluated numerically surfaces as NaN); exact proofs write 0.0; nothing is written when commutation cannot be proven. `phase` may be NULL. + +Parameters: + +- `lhs_gate` (`const char*`): left-hand gate name. +- `lhs_qubits` (`const uint32_t*`): left-hand qubit-id array. +- `lhs_qubits_len` (`uintptr_t`): left-hand qubit count. +- `lhs_params` (`const double*`): left-hand numeric parameter array; may be NULL when its length is 0. +- `lhs_params_len` (`uintptr_t`): left-hand parameter count. +- `rhs_gate` (`const char*`): right-hand gate name. +- `rhs_qubits` (`const uint32_t*`): right-hand qubit-id array. +- `rhs_qubits_len` (`uintptr_t`): right-hand qubit count. +- `rhs_params` (`const double*`): right-hand numeric parameter array; may be NULL when its length is 0. +- `rhs_params_len` (`uintptr_t`): right-hand parameter count. +- `phase` (`double*`): global-phase output pointer; may be NULL. + +Return value: + +| Value | Scenario | +| --- | --- | +| 0 / 1 / 2 | A `COMMUTATION_RESULT_*` proof tag. | +| -1 | A gate name is NULL, or a qubit/parameter array with length greater than 0 is NULL. | +| -4 | A gate name is invalid UTF-8. | +| -8 | A gate name is not a known standard gate. | + +### commutation_check_algebraic(lhs_gate, lhs_qubits, lhs_qubits_len, lhs_params, lhs_params_len, rhs_gate, rhs_qubits, rhs_qubits_len, rhs_params, rhs_params_len, phase) + +```c +int32_t commutation_check_algebraic(const char *lhs_gate, + const uint32_t *lhs_qubits, + uintptr_t lhs_qubits_len, + const double *lhs_params, + uintptr_t lhs_params_len, + const char *rhs_gate, + const uint32_t *rhs_qubits, + uintptr_t rhs_qubits_len, + const double *rhs_params, + uintptr_t rhs_params_len, + double *phase); +``` + +Checks commutation using only the symbolic algebraic proof sources (structural facts, diagonal/axis algebra, and the controlled-axis and symmetric two-qubit families). Parameters and return values match `commutation_check`; when commutation cannot be proven it returns `COMMUTATION_RESULT_UNPROVEN` (0), keeping the result conservative. + +--- + +## Reusable Checkers + +### commutation_checker_new(config) + +```c +struct CCommutationChecker *commutation_checker_new(struct CCommutationConfig config); +``` + +Creates a reusable checker with builtin commutation rules and an explicit configuration; free with `commutation_checker_free`. + +Parameters: + +- `config` (`struct CCommutationConfig`): checker configuration, passed by value. + +Return value: + +| Return | Scenario | +| --- | --- | +| Non-NULL | A new checker handle; free with `commutation_checker_free`. | + +### commutation_checker_from_library(library, config) + +```c +struct CCommutationChecker *commutation_checker_from_library(const struct CKnowledgeLibrary *library, + struct CCommutationConfig config); +``` + +Creates a checker that draws its commutation rules from an already loaded rule library: the library's commutation-kind rules participate in proofs. The library handle (construction in [Knowledge](7_knowledge.md)) is not taken over and remains owned by the caller. + +Parameters: + +- `library` (`const CKnowledgeLibrary*`): rule library handle. +- `config` (`struct CCommutationConfig`): checker configuration, passed by value. + +Return value: + +| Return | Scenario | +| --- | --- | +| Non-NULL | A new checker handle; free with `commutation_checker_free`. | +| NULL | `library` is NULL. | + +### commutation_checker_free(ptr) + +```c +void commutation_checker_free(struct CCommutationChecker *ptr); +``` + +Frees a checker handle; NULL is allowed. + +### commutation_checker_check(ptr, lhs_gate, lhs_qubits, lhs_qubits_len, lhs_params, lhs_params_len, rhs_gate, rhs_qubits, rhs_qubits_len, rhs_params, rhs_params_len, phase) + +```c +int32_t commutation_checker_check(const struct CCommutationChecker *ptr, + const char *lhs_gate, + const uint32_t *lhs_qubits, + uintptr_t lhs_qubits_len, + const double *lhs_params, + uintptr_t lhs_params_len, + const char *rhs_gate, + const uint32_t *rhs_qubits, + uintptr_t rhs_qubits_len, + const double *rhs_params, + uintptr_t rhs_params_len, + double *phase); +``` + +Checks whether two gate applications commute using this checker; the checker's configuration selects which proof sources participate. Apart from the leading handle parameter, parameters and return values match `commutation_check`, with the -1 scenario additionally covering a NULL `ptr`. + +Parameters: + +- `ptr` (`const CCommutationChecker*`): checker handle. +- Remaining parameters as in `commutation_check`. + +Return value: + +| Value | Scenario | +| --- | --- | +| 0 / 1 / 2 | A `COMMUTATION_RESULT_*` proof tag. | +| -1 | `ptr` is NULL, or one of the -1 scenarios of `commutation_check`. | +| -4 | A gate name is invalid UTF-8. | +| -8 | A gate name is not a known standard gate. | + +--- + +## Examples + +One-shot checks: + +```c +#include +#include +#include "cqlib_c.h" + +int main(void) { + uint32_t q0[1] = {0}; + uint32_t q1[1] = {1}; + double angle_a[1] = {0.3}; + double angle_b[1] = {0.5}; + double phase = 0.0; + + /* 1. Disjoint supports commute exactly. */ + int32_t r = commutation_check("X", q0, 1, NULL, 0, "X", q1, 1, NULL, 0, &phase); + printf("disjoint: %d (phase %f)\n", r, phase); /* 1, 0.0 */ + + /* 2. X and Z on the same qubit commute up to a global phase of pi. */ + phase = 0.0; + r = commutation_check("X", q0, 1, NULL, 0, "Z", q0, 1, NULL, 0, &phase); + printf("x/z: %d (phase %f)\n", r, phase); /* 2, pi */ + + /* 3. Diagonal rotations with numeric parameters commute exactly. */ + r = commutation_check("RZ", q0, 1, angle_a, 1, "RZ", q0, 1, angle_b, 1, &phase); + printf("rz/rz: %d\n", r); /* 1 */ + + /* 4. The algebraic-only variant proves the same structural facts. */ + r = commutation_check_algebraic("X", q0, 1, NULL, 0, "X", q1, 1, NULL, 0, &phase); + printf("algebraic disjoint: %d\n", r); /* 1 */ + return 0; +} +``` + +Reusable checkers: + +```c +#include +#include "cqlib_c.h" + +int main(void) { + uint32_t q0[1] = {0}; + uint32_t q1[1] = {1}; + double phase = 0.0; + + /* 1. A reusable checker with the default configuration. */ + struct CCommutationConfig config = commutation_config_default(); + struct CCommutationChecker *checker = commutation_checker_new(config); + if (checker == NULL) { + return 1; + } + int32_t r = commutation_checker_check(checker, "X", q0, 1, NULL, 0, + "X", q1, 1, NULL, 0, &phase); + printf("checker disjoint: %d\n", r); /* 1 */ + commutation_checker_free(checker); + + /* 2. A checker drawing commutation rules from a knowledge library. */ + struct CKnowledgeLibrary *lib = knowledge_library_builtin(); + if (lib != NULL) { + struct CCommutationChecker *from_lib = commutation_checker_from_library(lib, config); + if (from_lib != NULL) { + r = commutation_checker_check(from_lib, "X", q0, 1, NULL, 0, + "X", q1, 1, NULL, 0, &phase); + printf("library checker disjoint: %d\n", r); /* 1 */ + commutation_checker_free(from_lib); + } + knowledge_library_free(lib); + } + return 0; +} +``` + +A checker proves commutation in the order structural facts → symbolic algebra for supported gate families → explicit knowledge-base commutation rules (when enabled) → concrete local matrix comparison (when enabled and small enough); parameter comparison is symbolic and tolerant, so provably equal expressions are treated as the same application. diff --git a/docs/api/en/c/5_visualization/0_overview.md b/docs/api/en/c/5_visualization/0_overview.md new file mode 100644 index 00000000..bb843c0f --- /dev/null +++ b/docs/api/en/c/5_visualization/0_overview.md @@ -0,0 +1,25 @@ +# Visualization (C) + +The visualization module provides four groups of output: `circuit_to_text` outputs a box-drawing-character UTF-8 text diagram, `circuit_to_figure` outputs an SVG markup string, and `render_figure_to_file` renders a circuit directly to a `.svg` / `.png` file; state plots (Bloch, multi-Bloch, city, paulivec) and result plots (histogram, distribution) generate SVG from simulator states and execution results, and the Visual IR compiles a circuit into a backend-agnostic drawing intermediate representation. Error codes, string and handle ownership conventions follow the [Overview](../0_overview.md). + +--- + +## Page Navigation + +| Page | Content | +| --- | --- | +| [Text Drawing](1_draw_text.md) | `circuit_to_text` and the `TextDrawerOptionsC` option table. | +| [SVG Figure](2_draw_figure.md) | `circuit_to_figure` and the `FigureDrawerOptionsC` option table. | +| [State Plots](3_state_plots.md) | `plot_bloch_vector`, `plot_bloch_multivector`, `plot_state_city`, `plot_state_paulivec`, and the `CStatePlotOptionsC` option table. | +| [Result Plots](4_result_plots.md) | `plot_histogram`, `plot_distribution`, and the `CResultPlotOptionsC` option table. | +| [File Output](5_render_to_file.md) | `render_figure_to_file`, `render_state_plot_to_file`, `render_result_plot_to_file`: renders to a `.svg` or `.png` file by extension. | +| [Visual IR](6_visual_ir.md) | `build_visual_circuit`, the `CVisualCircuit` handle family, and `draw_text_from_visual`, `draw_figure_from_visual`. | + +--- + +## Shared Conventions + +- The `options` parameter of every drawing entry accepts NULL, in which case library defaults apply; for explicit configuration, fill the corresponding options struct (`TextDrawerOptionsC` / `FigureDrawerOptionsC` / `CStatePlotOptionsC` / `CResultPlotOptionsC` / `CVisualBuildOptionsC`) by value — the struct memory stays owned by the caller. +- Functions returning `char*` (`circuit_to_text`, `circuit_to_figure`, the `plot_*` and `draw_*_from_visual` entries) are freed with `cqlib_string_free`. +- `CVisualCircuit*` handles are freed with `visual_circuit_free` (NULL is allowed); the accessor family is covered in [Visual IR](6_visual_ir.md). +- `render_figure_to_file` and the two plot-to-file entries return an `int32_t` error code: 0 on success; -5 on I/O or rendering failure; -1 for NULL pointers (the plot-to-file entries additionally return -4 for strings that are not valid UTF-8). diff --git a/docs/api/en/c/5_visualization/1_draw_text.md b/docs/api/en/c/5_visualization/1_draw_text.md new file mode 100644 index 00000000..42527499 --- /dev/null +++ b/docs/api/en/c/5_visualization/1_draw_text.md @@ -0,0 +1,68 @@ +# Text Drawing (C) + +`circuit_to_text` draws a circuit as a UTF-8 text diagram (box-drawing characters). Error codes, string and handle ownership conventions follow the [Overview](../0_overview.md). + +--- + +## circuit_to_text(circuit, options) + +Draws the circuit as UTF-8 text. + +Parameters: + +- `circuit` (`const CCircuit*`): circuit to draw. +- `options` (`const TextDrawerOptionsC*`): drawing options; NULL uses library defaults. + +Returns: a heap-allocated C string; free with `cqlib_string_free`. NULL on error. + +--- + +## TextDrawerOptionsC + +Text drawing options, filled by value; unset fields use library defaults: + +| Field | Type | Meaning | Value behavior | +| --- | --- | --- | --- | +| `show_params` | `uint8_t` | Whether to append parameter text to gate labels. | `0` off, `1` on. | +| `decompose_circuit_gates` | `uint8_t` | Whether to decompose circuit-gates before drawing. | `0` no decomposition, `1` decompose. | +| `line_width` | `intptr_t` | Maximum width per wrapped segment. | `<= 0` disables wrapping. | +| `initial_state` | `uint8_t` | Whether to show `\|0>` at the start of each qubit wire. | `0` hidden, `1` shown. | +| `reverse_bits` | `uint8_t` | Whether to reverse the displayed qubit order. | `0` original order, `1` reversed. | + +--- + +## Example + +```c +#include +#include "cqlib_c.h" + +int main(void) { + struct CCircuit *qc = circuit_new(2); + circuit_h(qc, 0); + circuit_cx(qc, 0, 1); + + /* 1. Default options */ + char *text = circuit_to_text(qc, NULL); + if (text != NULL) { + printf("%s\n", text); + cqlib_string_free(text); + } + + /* 2. Explicit options: show initial state, disable wrapping */ + struct TextDrawerOptionsC options = {0}; + options.show_params = 1; + options.initial_state = 1; + options.line_width = 0; /* <= 0 disables wrapping */ + char *text2 = circuit_to_text(qc, &options); + if (text2 != NULL) { + printf("%s\n", text2); + cqlib_string_free(text2); + } + + circuit_free(qc); + return 0; +} +``` + +For SVG figures and file output see [SVG Figure](2_draw_figure.md) and [File Output](5_render_to_file.md). diff --git a/docs/api/en/c/5_visualization/2_draw_figure.md b/docs/api/en/c/5_visualization/2_draw_figure.md new file mode 100644 index 00000000..b4c9fd32 --- /dev/null +++ b/docs/api/en/c/5_visualization/2_draw_figure.md @@ -0,0 +1,72 @@ +# SVG Figure (C) + +`circuit_to_figure` draws a circuit as an SVG figure and returns the SVG markup string. Error codes, string and handle ownership conventions follow the [Overview](../0_overview.md). + +--- + +## circuit_to_figure(circuit, options) + +Draws the circuit as an SVG figure. + +Parameters: + +- `circuit` (`const CCircuit*`): circuit to draw. +- `options` (`const FigureDrawerOptionsC*`): drawing options; NULL uses library defaults. + +Returns: a heap-allocated C string containing SVG markup; free with `cqlib_string_free`. NULL on error. + +--- + +## FigureDrawerOptionsC + +Figure drawing options, filled by value; unset fields use library defaults: + +| Field | Type | Meaning | Value behavior | +| --- | --- | --- | --- | +| `show_params` | `uint8_t` | Whether to append parameter text to gate labels. | `0` off, `1` on. | +| `decompose_circuit_gates` | `uint8_t` | Whether to decompose circuit-gates before drawing. | `0` no decomposition, `1` decompose. | +| `width_per_column` | `double` | Figure width scale per logical column. | Positive values apply. | +| `height_per_qubit` | `double` | Figure height scale per qubit. | Positive values apply. | +| `dpi` | `uint32_t` | Rasterization DPI when exporting PNG. | Must be `> 0`. | +| `fold` | `int32_t` | Maximum columns per row (excess columns fold onto new rows). | `<= 0` disables folding. | +| `initial_state` | `uint8_t` | Whether to show `\|0>` in qubit labels. | `0` hidden, `1` shown. | +| `reverse_bits` | `uint8_t` | Whether to reverse display order of qubits. | `0` original order, `1` reversed. | + +`dpi` participates only in PNG rasterization (see [File Output](5_render_to_file.md)); `fold` controls folding of wide circuits onto multiple rows, with `<= 0` keeping a single unfolded row. + +--- + +## Example + +```c +#include +#include "cqlib_c.h" + +int main(void) { + struct CCircuit *qc = circuit_new(2); + circuit_h(qc, 0); + circuit_cx(qc, 0, 1); + + /* 1. Default options: returns the SVG markup string */ + char *svg = circuit_to_figure(qc, NULL); + if (svg != NULL) { + printf("%s\n", svg); /* "..." */ + cqlib_string_free(svg); + } + + /* 2. Explicit options: show initial state, fold at 8 columns, 300 DPI for PNG export */ + struct FigureDrawerOptionsC options = {0}; + options.initial_state = 1; + options.fold = 8; /* <= 0 disables folding */ + options.dpi = 300; + char *svg2 = circuit_to_figure(qc, &options); + if (svg2 != NULL) { + cqlib_string_free(svg2); + } + + circuit_free(qc); + return 0; +} +``` + +For text drawing and file output see [Text Drawing](1_draw_text.md) and [File Output](5_render_to_file.md). diff --git a/docs/api/en/c/5_visualization/3_state_plots.md b/docs/api/en/c/5_visualization/3_state_plots.md new file mode 100644 index 00000000..8757ef43 --- /dev/null +++ b/docs/api/en/c/5_visualization/3_state_plots.md @@ -0,0 +1,248 @@ +# Quantum State Visualization (C) + +This page covers quantum-state visualization: the Bloch-sphere plot `plot_bloch_vector`, the reduced-Bloch-vector plot `plot_bloch_multivector`, the density-matrix plot `plot_state_city`, the Pauli-basis expectation plot `plot_state_paulivec`, the options struct `CStatePlotOptions`, and the numeric state-inspection helpers `local_bloch_vectors` and `state_to_density_matrix`. The plot entries return a heap-allocated SVG markup string freed with `cqlib_string_free`; the numeric helpers write into caller-allocated buffers following the two-step array pattern. Error codes and the conventions for freeing strings and handles follow the [Overview](../0_overview.md). + +State-based entries come in two variants: the base name accepts a `CStatevector` (a pure state is expanded to a density matrix via `ρ = |ψ⟩⟨ψ|`), while the `_density_matrix` suffix accepts a `CDensityMatrix` (its matrix data is used directly); both share the same `CStatePlotOptions`. For constructing the state handles see [Statevector](../3_qis/1_statevector.md) and [DensityMatrix](../3_qis/2_density_matrix.md). + +--- + +## plot_bloch_vector(x, y, z, options) + +Plots a single Bloch vector. + +```c +char *plot_bloch_vector(double x, double y, double z, + const struct CStatePlotOptions *options); +``` + +Parameters: + +- `x`, `y`, `z` (`double`): the Bloch coordinates `(x, y, z)`, typically within `[-1, 1]`. Vectors longer than `1` are projected back onto the unit sphere; vectors inside the sphere are kept unchanged. +- `options` (`const CStatePlotOptions*`): plot options; NULL uses library defaults. + +Returns: a heap-allocated C string (SVG markup), freed with `cqlib_string_free`; NULL on error. The vector label is fixed to `q0`. + +--- + +## plot_bloch_multivector(state, options) / plot_bloch_multivector_density_matrix(state, options) + +Plots one reduced Bloch vector per qubit. Each vector is computed from the qubit's reduced density matrix, and the labels follow the qubit index as `q0`, `q1`, and so on; the spheres are arranged in a grid with at most 4 per row. + +```c +char *plot_bloch_multivector(const struct CStatevector *state, + const struct CStatePlotOptions *options); +char *plot_bloch_multivector_density_matrix(const struct CDensityMatrix *state, + const struct CStatePlotOptions *options); +``` + +Parameters: + +- `state`: the input state handle (`const CStatevector*` for the first, `const CDensityMatrix*` for the second). +- `options` (`const CStatePlotOptions*`): plot options; NULL uses library defaults. + +Returns: a heap-allocated C string (SVG markup), freed with `cqlib_string_free`; NULL on error. + +Failure scenarios: + +- `state` is NULL. +- `options` contains a string that is not valid UTF-8, or the `color` array contains a NULL entry. +- The state buffer length does not match the qubit count, or the qubit count is large enough to overflow the matrix dimension. + +--- + +## plot_state_city(state, options) / plot_state_city_density_matrix(state, options) + +Plots the real and imaginary parts of the density matrix, one panel each, titled `Re[rho]` and `Im[rho]`. When every element has zero imaginary part, no imaginary panel is generated. Each cell's area scales with `sqrt(|value| / max_abs)` so that small-magnitude elements stay visible; `max_abs` is the largest element magnitude, lower-bounded by `1e-12`. The bar fill opacity is controlled by `CStatePlotOptions.alpha`. + +```c +char *plot_state_city(const struct CStatevector *state, + const struct CStatePlotOptions *options); +char *plot_state_city_density_matrix(const struct CDensityMatrix *state, + const struct CStatePlotOptions *options); +``` + +Parameters: + +- `state`: the input state handle (`const CStatevector*` for the first, `const CDensityMatrix*` for the second). +- `options` (`const CStatePlotOptions*`): plot options; NULL uses library defaults. + +Returns: a heap-allocated C string (SVG markup), freed with `cqlib_string_free`; NULL on error. + +Failure scenarios: same as [plot_bloch_multivector](#plot_bloch_multivectorstate-options--plot_bloch_multivector_density_matrixstate-options). + +--- + +## plot_state_paulivec(state, options) / plot_state_paulivec_density_matrix(state, options) + +Plots Pauli-basis expectation values as a bar chart. Labels are tensor products of `I`, `X`, `Y`, `Z`, for `4^n` bars in total where `n` is the qubit count; non-negative and negative bars use different colors. Rotated x-axis labels are drawn only when the bar count does not exceed `64`. + +```c +char *plot_state_paulivec(const struct CStatevector *state, + const struct CStatePlotOptions *options); +char *plot_state_paulivec_density_matrix(const struct CDensityMatrix *state, + const struct CStatePlotOptions *options); +``` + +Parameters: + +- `state`: the input state handle (`const CStatevector*` for the first, `const CDensityMatrix*` for the second). +- `options` (`const CStatePlotOptions*`): plot options; NULL uses library defaults. + +Returns: a heap-allocated C string (SVG markup), freed with `cqlib_string_free`; NULL on error. + +Failure scenarios: same as [plot_bloch_multivector](#plot_bloch_multivectorstate-options--plot_bloch_multivector_density_matrixstate-options). + +--- + +## local_bloch_vectors_len(state) / local_bloch_vectors(state, out, len) + +Computes one reduced single-qubit Bloch vector per qubit of a statevector — the numeric counterpart of the [plot_bloch_multivector](#plot_bloch_multivectorstate-options--plot_bloch_multivector_density_matrixstate-options) plot, returned as data instead of drawn. + +```c +uintptr_t local_bloch_vectors_len(const struct CStatevector *state); +int32_t local_bloch_vectors(const struct CStatevector *state, double *out, uintptr_t len); +``` + +Two-step array output: `local_bloch_vectors_len` returns the required buffer length `3 * num_qubits` (0 for NULL); allocate that many `double` values and pass the buffer together with the same `len` to `local_bloch_vectors`. The output is laid out flat as `[x0, y0, z0, x1, y1, z1, ...]`: entries `q * 3 .. q * 3 + 3` hold the ``, ``, `` expectation values of qubit `q`'s reduced state. + +Parameters: + +- `state` (`const CStatevector*`): input statevector. +- `out` (`double*`): output buffer of `len` doubles. +- `len` (`uintptr_t`): buffer length; must equal `local_bloch_vectors_len(state)`. + +Error codes (fill function): + +| Value | Scenario | +| --- | --- | +| `0` | Success. | +| `-1` | `state` or `out` is NULL. | +| `-8` | `len` does not match `local_bloch_vectors_len(state)`, or the state payload is invalid. | + +For the Bell state every local Bloch vector vanishes, so all six values are `0`; for `|+>` on qubit 0 and `|0>` on qubit 1 the buffer is `[1, 0, 0, 0, 0, 1]`. + +--- + +## state_to_density_matrix_len(state) / state_to_density_matrix(state, out, len) + +Converts a statevector into its density matrix `ρ = |ψ⟩⟨ψ|` — the numeric counterpart of the [plot_state_city](#plot_state_citystate-options--plot_state_city_density_matrixstate-options) plot, returned as data instead of drawn. + +```c +uintptr_t state_to_density_matrix_len(const struct CStatevector *state); +int32_t state_to_density_matrix(const struct CStatevector *state, Complex64 *out, uintptr_t len); +``` + +Two-step array output: `state_to_density_matrix_len` returns the required buffer length `4^num_qubits` (0 for NULL); `state_to_density_matrix` copies that many `Complex64` values into `out`, laid out row-major as an `N × N` matrix with `N = 2^num_qubits`: element `(row, col)` is stored at index `row * N + col`. + +Parameters: + +- `state` (`const CStatevector*`): input statevector. +- `out` (`Complex64*`): output buffer of `len` values. +- `len` (`uintptr_t`): buffer length; must equal `state_to_density_matrix_len(state)`. + +Error codes (fill function): + +| Value | Scenario | +| --- | --- | +| `0` | Success. | +| `-1` | `state` or `out` is NULL. | +| `-8` | `len` does not match `state_to_density_matrix_len(state)`, or the state payload is invalid. | + +For the two-qubit Bell state the `4 × 4` matrix carries `0.5` on the diagonal corners and on the `(0,3)`/`(3,0)` coherences; every other element is `0`. + +--- + +## CStatePlotOptions + +Quantum-state plot options, filled by value; passing NULL for `options` uses library defaults. Strings inside the options must be valid UTF-8, otherwise the plot entry returns NULL. + +| Field | Type | Meaning | Value behavior | +| --- | --- | --- | --- | +| `title` | `const char*` | Chart title. | NULL draws no title. | +| `color` | `const char* const*` | Color array; length given by `color_len`. | NULL or `color_len == 0` uses the plot-family defaults; a NULL entry inside the array makes the entry return NULL. | +| `color_len` | `uintptr_t` | Number of entries in `color`. | `0` is treated as no colors provided. | +| `alpha` | `double` | Fill opacity of `plot_state_city` bars. | Takes the given value as-is; the library default is `1.0` (applied only when `options` is NULL — an explicitly passed struct with zero makes the bars fully transparent). | +| `reverse_bits` | `uint8_t` | Whether to reverse the displayed computational-basis bit order. | `0` original order, `1` reversed. When set to `1`, the density-matrix plot's row/column labels are ordered in reverse and matrix elements are mapped back to storage positions by bit order; Bloch and Pauli plots reverse their label order accordingly. | +| `has_figsize` | `uint8_t` | Whether `fig_width` / `fig_height` carry a custom figure size. | `0` ignores width/height (each plot family sizes the canvas by its own layout); non-zero enables them. | +| `fig_width` | `double` | Figure width in inch-like units, converted to SVG pixels at `100` pixels per unit. | Ignored when `has_figsize == 0`. When given, the density-matrix and Pauli plots lower-bound width/height at `4`/`3`, and the Bloch plots lower-bound both at `3`. | +| `fig_height` | `double` | Figure height, same units and conversion as `fig_width`. | Ignored when `has_figsize == 0`; same lower bounds as `fig_width`. | + +How `color` is consumed depends on the plot family: `plot_state_paulivec` takes the first entry as the non-negative bar color (default `#4569d4`) and the second entry as the negative-bar color (default `#d64b5f`); the other plot families use fixed colors. + +--- + +## Example + +```c +#include +#include +#include +#include "cqlib_c.h" + +int main(void) { + /* 1. Build a Bell state */ + double inv = 0.7071067811865476; + Complex64 amplitudes[4] = { + {inv, 0.0}, {0.0, 0.0}, {0.0, 0.0}, {inv, 0.0}, + }; + struct CStatevector *state = statevector_from_state(2, amplitudes, 4); + if (state == NULL) { + return 1; + } + + /* 2. Plot with default options; every entry returns SVG markup */ + char *svg = plot_bloch_multivector(state, NULL); + if (svg != NULL) { + printf("%s\n", svg); + cqlib_string_free(svg); + } + + /* 3. Plot with explicit options */ + struct CStatePlotOptions options; + memset(&options, 0, sizeof(options)); + options.title = "Bell state"; + options.alpha = 0.75; + options.reverse_bits = 1; + options.has_figsize = 1; + options.fig_width = 4.0; + options.fig_height = 3.0; + svg = plot_state_city(state, &options); + if (svg != NULL) { + printf("%s\n", svg); + cqlib_string_free(svg); + } + + /* 4. Density-matrix input works through the _density_matrix variants */ + struct CDensityMatrix *rho = density_matrix_from_state(2, amplitudes, 4); + if (rho != NULL) { + svg = plot_state_paulivec_density_matrix(rho, NULL); + if (svg != NULL) { + cqlib_string_free(svg); + } + density_matrix_free(rho); + } + + /* 5. Single Bloch vector: vectors longer than the unit + sphere are clamped, not rejected */ + svg = plot_bloch_vector(0.0, 0.0, 1.0, NULL); + if (svg != NULL) { + cqlib_string_free(svg); + } + + /* 6. Numeric state inspection via the two-step array pattern */ + uintptr_t bloch_len = local_bloch_vectors_len(state); /* 6 = 3 * 2 */ + double *bloch = malloc(bloch_len * sizeof(double)); + local_bloch_vectors(state, bloch, bloch_len); /* all zeros for the Bell state */ + free(bloch); + + uintptr_t rho_len = state_to_density_matrix_len(state); /* 16 = 4^2 */ + Complex64 *rho_buf = malloc(rho_len * sizeof(Complex64)); + state_to_density_matrix(state, rho_buf, rho_len); /* 4x4 matrix, corners 0.5 */ + free(rho_buf); + + statevector_free(state); + return 0; +} +``` + +Plotting the same pure state through `plot_state_paulivec` (statevector variant) and `plot_state_paulivec_density_matrix` (density-matrix variant) yields identical SVG markup. To write the plots out to files, see [File Output](5_render_to_file.md). diff --git a/docs/api/en/c/5_visualization/4_result_plots.md b/docs/api/en/c/5_visualization/4_result_plots.md new file mode 100644 index 00000000..e8bde56b --- /dev/null +++ b/docs/api/en/c/5_visualization/4_result_plots.md @@ -0,0 +1,133 @@ +# Measurement Result Visualization (C) + +This page covers measurement-result visualization: the histogram `plot_histogram` and the probability distribution `plot_distribution`, plus the options struct `CResultPlotOptions`. Both entries accept a `CExecutionResult` handle and return a heap-allocated SVG markup string freed with `cqlib_string_free`. Error codes and the conventions for freeing strings and handles follow the [Overview](../0_overview.md). + +The `CExecutionResult` handle is created with `execution_result_new` and populated with measurement counts via `execution_result_finish`; see [Result](../2_device/5_result.md) for construction. + +--- + +## plot_histogram(result, options) + +Plots an execution result's measured counts as a histogram. Counts are formatted into bitstring labels sized to the result's qubit count, serving as x-axis ticks; the y-axis starts at zero and no negative range is drawn. + +```c +char *plot_histogram(const struct CExecutionResult *result, + const struct CResultPlotOptions *options); +``` + +Parameters: + +- `result` (`const CExecutionResult*`): the execution result holding the measurement counts. +- `options` (`const CResultPlotOptions*`): plot options; NULL uses library defaults. + +Returns: a heap-allocated C string (SVG markup), freed with `cqlib_string_free`; NULL on error. + +Failure scenarios: + +- `result` is NULL, or the execution result carries no counts. +- `options` contains a string that is not valid UTF-8, a NULL entry inside the `color` / `legend` arrays, or inconsistent plot options. + +--- + +## plot_distribution(result, options) + +Plots an execution result's measured counts as a normalized probability distribution. Each count is divided by the total to obtain a probability; bar heights and y-axis ticks are labeled by probability. + +```c +char *plot_distribution(const struct CExecutionResult *result, + const struct CResultPlotOptions *options); +``` + +Parameters: + +- `result` (`const CExecutionResult*`): the execution result holding the measurement counts. +- `options` (`const CResultPlotOptions*`): plot options; NULL uses library defaults. + +Returns: a heap-allocated C string (SVG markup), freed with `cqlib_string_free`; NULL on error. + +Failure scenarios: same as [plot_histogram](#plothistogramresult-options), plus counts summing to zero. + +--- + +## CResultPlotOptions + +Result-plot options, filled by value; passing NULL for `options` uses library defaults. Strings inside the options must be valid UTF-8, otherwise the plot entry returns NULL. + +| Field | Type | Meaning | Value behavior | +| --- | --- | --- | --- | +| `has_figsize` | `uint8_t` | Whether `fig_width` / `fig_height` carry a custom figure size. | `0` ignores width/height; non-zero enables them. | +| `fig_width` | `double` | Figure width in inch-like units, converted to SVG pixels at `100` pixels per unit. | Ignored when `has_figsize == 0`. When given, width/height are lower-bounded at `2`. | +| `fig_height` | `double` | Figure height, same units and conversion as `fig_width`. | Ignored when `has_figsize == 0`; same lower bounds as `fig_width`. | +| `color` | `const char* const*` | Per-dataset colors; length given by `color_len`. | NULL or `color_len == 0` uses the built-in qualitative palette; a NULL entry inside the array makes the entry return NULL. | +| `color_len` | `uintptr_t` | Number of entries in `color`. | `0` is treated as no colors provided. | +| `number_to_keep` | `intptr_t` | Keep the largest `k` bars and aggregate the rest into an entry labeled `rest`. | Negative disables aggregation; `k` of `0` or at least the entry count performs no aggregation. Ties are broken by key label so the output is consistent across platforms. | +| `sort` | `const char*` | Sort policy. | NULL defaults to `asc`; values listed below. Invalid UTF-8 makes the entry return NULL. | +| `target_string` | `const char*` | Target bitstring used by `sort = "hamming"`. | NULL means unset. | +| `legend` | `const char* const*` | Legend entries, one per dataset; length given by `legend_len`. | NULL draws no legend. The result-plot entries produce a single dataset, so a provided legend must have length `1`. | +| `legend_len` | `uintptr_t` | Number of entries in `legend`. | Paired with `legend`. | +| `bar_labels` | `uint8_t` | Whether to draw numeric labels above bars. | `0` off, `1` on; the library default draws them (applied only when `options` is NULL). | +| `title` | `const char*` | Chart title. | NULL draws no title. | + +### Sort policies + +| `sort` value | Description | +| --- | --- | +| `asc` | Ascending lexicographic order by bitstring label. | +| `desc` | Descending lexicographic order by bitstring label. | +| `value` | Ascending by each label's peak across datasets; ties broken by ascending label. | +| `value_desc` | Descending by each label's peak across datasets; ties broken by ascending label. | +| `hamming` | Ascending by Hamming distance to `target_string`; ties broken by ascending label. The `rest` label always sorts last. | + +--- + +## Example + +```c +#include +#include +#include "cqlib_c.h" + +int main(void) { + /* 1. Build an execution result with measurement counts */ + const char *task_id = "task-visualization"; + uint32_t qubits[2] = {0, 1}; + struct CExecutionResult *result = + execution_result_new(task_id, qubits, 2, 7, NULL); + if (result == NULL) { + return 1; + } + const char *bitstrings[2] = {"00", "11"}; + uint64_t counts[2] = {2, 5}; + if (execution_result_finish(result, bitstrings, counts, 2) != 0) { + execution_result_free(result); + return 1; + } + + /* 2. Histogram with default options */ + char *svg = plot_histogram(result, NULL); + if (svg != NULL) { + printf("%s\n", svg); + cqlib_string_free(svg); + } + + /* 3. Distribution with explicit options: custom canvas and title */ + struct CResultPlotOptions options; + memset(&options, 0, sizeof(options)); + options.has_figsize = 1; + options.fig_width = 3.2; + options.fig_height = 2.4; + options.number_to_keep = -1; /* negative disables aggregation */ + options.bar_labels = 1; + options.title = "Measurement counts"; + svg = plot_distribution(result, &options); + if (svg != NULL) { + printf("%s\n", svg); + cqlib_string_free(svg); + } + + execution_result_free(result); + return 0; +} +``` + +To sort by Hamming distance to a target bitstring and aggregate the rest, set `sort` to `"hamming"`, `target_string` to the target bitstring, and `number_to_keep` to the number of bars to keep; the remaining entries are aggregated into `rest`. To write the plots out to files, see [File Output](5_render_to_file.md). diff --git a/docs/api/en/c/5_visualization/5_render_to_file.md b/docs/api/en/c/5_visualization/5_render_to_file.md new file mode 100644 index 00000000..398755f7 --- /dev/null +++ b/docs/api/en/c/5_visualization/5_render_to_file.md @@ -0,0 +1,86 @@ +# File Output (C) + +`render_figure_to_file` renders a circuit directly to an output file, selecting the format by extension: `.svg` writes a vector figure, `.png` rasterizes with `FigureDrawerOptionsC.dpi`. `render_state_plot_to_file` and `render_result_plot_to_file` write state-plot and result-plot SVG markup strings to files, with the same two extensions. Error codes, string and handle ownership conventions follow the [Overview](../0_overview.md). + +--- + +## render_figure_to_file(circuit, output_path, options) + +Renders the circuit to an output file. + +Parameters: + +- `circuit` (`const CCircuit*`): circuit to draw. +- `output_path` (`const char*`): output file path; the `.svg` or `.png` extension selects the format. +- `options` (`const FigureDrawerOptionsC*`): drawing options (field table in [SVG Figure](2_draw_figure.md)); NULL uses library defaults. For `.png` output, `dpi` must be greater than 0. + +Returns: 0 on success; -5 on I/O or rendering failure; -1 for NULL pointers. + +--- + +## render_state_plot_to_file(svg, output_path) + +Writes state-plot SVG markup to an output file: `.svg` writes vector output, `.png` writes raster output. The markup comes from the `plot_*` entries on the [State Plots](3_state_plots.md) page. + +Parameters: + +- `svg` (`const char*`): SVG markup string returned by `plot_bloch_vector`, `plot_state_city`, and their siblings (the string itself is still released by the caller with `cqlib_string_free`). +- `output_path` (`const char*`): output file path; the `.svg` or `.png` extension selects the format. + +Returns: 0 on success; -1 for NULL pointers; -4 for strings that are not valid UTF-8; -5 on I/O or rendering failure. + +--- + +## render_result_plot_to_file(svg, output_path) + +Writes result-plot SVG markup to an output file: `.svg` writes vector output, `.png` writes raster output. The markup comes from `plot_histogram` and `plot_distribution` on the [Result Plots](4_result_plots.md) page. + +Parameters: + +- `svg` (`const char*`): SVG markup string returned by the result-plot entries (the string itself is still released by the caller with `cqlib_string_free`). +- `output_path` (`const char*`): output file path; the `.svg` or `.png` extension selects the format. + +Returns: 0 on success; -1 for NULL pointers; -4 for strings that are not valid UTF-8; -5 on I/O or rendering failure. + +--- + +## Example + +```c +#include +#include "cqlib_c.h" + +int main(void) { + struct CCircuit *qc = circuit_new(2); + circuit_h(qc, 0); + circuit_cx(qc, 0, 1); + + /* 1. SVG output with default options */ + int32_t rc = render_figure_to_file(qc, "bell.svg", NULL); + if (rc != 0) { + printf("render failed: %d\n", rc); /* -5: I/O or rendering failure */ + } + + /* 2. PNG output with an explicit DPI */ + struct FigureDrawerOptionsC options = {0}; + options.dpi = 300; + options.fold = 8; + rc = render_figure_to_file(qc, "bell.png", &options); + + /* 3. Write a state-plot SVG markup string to a file */ + struct CStatevector *sv = statevector_new(2); + statevector_apply_h(sv, 0); + statevector_apply_cx(sv, 0, 1); + char *svg = plot_bloch_vector(sv, NULL); + if (svg != NULL) { + render_state_plot_to_file(svg, "bell_bloch.svg"); + cqlib_string_free(svg); + } + + statevector_free(sv); + circuit_free(qc); + return 0; +} +``` + +To obtain the SVG markup string in memory, see [SVG Figure](2_draw_figure.md). diff --git a/docs/api/en/c/5_visualization/6_visual_ir.md b/docs/api/en/c/5_visualization/6_visual_ir.md new file mode 100644 index 00000000..1daa533d --- /dev/null +++ b/docs/api/en/c/5_visualization/6_visual_ir.md @@ -0,0 +1,379 @@ +# Visual IR (C) + +This page covers the visualization intermediate representation: the build entry point `build_visual_circuit`, the build options `CVisualBuildOptions` / `CParameterFormatOptions`, and the query and drawing functions of the IR handle `CVisualCircuit`. Error codes and the conventions for freeing strings and handles follow the [Overview](../0_overview.md). + +Circuit drawing is a two-step pipeline: first build the backend-agnostic visualization IR from the circuit, then render it with a concrete backend: + +```text +CCircuit ──build_visual_circuit──▶ CVisualCircuit ──┬── draw_text_from_visual ──▶ text diagram + └── draw_figure_from_visual ──▶ SVG figure +``` + +The build stage handles the circuit-semantic work: mapping qubits to lane indices, formatting parameters into label text, classifying instructions into drawing styles, and scheduling operations into non-overlapping columns. The drawing stages only consume these results and never read the circuit back. Consequently both backends agree on gate order, column layout, and labels for the same circuit; build options can also be tuned independently of drawing options. + +--- + +## Constants + +### Parameter display modes (`CParameterFormatOptions.mode`) + +| Constant | Value | Mode | +| --- | --- | --- | +| `PARAM_MODE_NUMERIC` | 0 | Prefer numeric display when evaluable. | +| `PARAM_MODE_SYMBOLIC` | 1 | Prefer symbolic expression display. | +| `PARAM_MODE_SYMBOLIC_WITH_VALUE` | 2 | Symbolic expression with numeric value. | +| `PARAM_MODE_PI_FRACTION_PREFERRED` | 3 | Prefer `k*pi/n` fraction display. | + +Unknown values fall back to numeric mode (`PARAM_MODE_NUMERIC`). + +### Operation drawing styles (returned by `visual_circuit_operation_style`) + +| Constant | Value | Style | +| --- | --- | --- | +| `VISUAL_OP_STYLE_GATE` | 0 | Generic gate-like box. | +| `VISUAL_OP_STYLE_CONTROLLED` | 1 | Controlled operation; query the control count with `visual_circuit_operation_num_controls`. | +| `VISUAL_OP_STYLE_CZ` | 2 | Controlled-Z marker, drawn as two dots joined by a vertical line. | +| `VISUAL_OP_STYLE_SWAP` | 3 | Swap marker. | +| `VISUAL_OP_STYLE_BARRIER` | 4 | Barrier marker. | +| `VISUAL_OP_STYLE_MEASURE` | 5 | Measurement marker. | +| `VISUAL_OP_STYLE_RESET` | 6 | Reset marker. | +| `VISUAL_OP_STYLE_DELAY` | 7 | Delay marker. | +| `VISUAL_OP_STYLE_CONTROL_FLOW` | 8 | Control-flow marker; query the kind with `visual_circuit_operation_control_flow_kind`. | + +### Control-flow kinds (returned by `visual_circuit_operation_control_flow_kind`) + +| Constant | Value | Kind | +| --- | --- | --- | +| `VISUAL_CF_KIND_IF_ELSE_BLOCK` | 0 | If/else block. | +| `VISUAL_CF_KIND_WHILE_BLOCK` | 1 | `while` loop block. | +| `VISUAL_CF_KIND_FOR_BLOCK` | 2 | `for` loop block. | +| `VISUAL_CF_KIND_SWITCH_BLOCK` | 3 | Switch block. | +| `VISUAL_CF_KIND_BREAK` | 4 | Structured `break` marker. | +| `VISUAL_CF_KIND_CONTINUE` | 5 | Structured `continue` marker. | +| `VISUAL_CF_KIND_IF_START` | 6 | Flattened if-start marker. | +| `VISUAL_CF_KIND_ELSE_START` | 7 | Flattened else-start marker. | +| `VISUAL_CF_KIND_WHILE_START` | 8 | Flattened while-start marker. | +| `VISUAL_CF_KIND_FOR_START` | 9 | Flattened for-start marker. | +| `VISUAL_CF_KIND_SWITCH_START` | 10 | Flattened switch-start marker. | +| `VISUAL_CF_KIND_CASE_START` | 11 | Flattened case-start marker. | +| `VISUAL_CF_KIND_DEFAULT_START` | 12 | Flattened default-start marker. | +| `VISUAL_CF_KIND_END` | 13 | Flattened block-end marker. | + +Block kinds (`*_BLOCK`) are produced by the build stage; before drawing, the backends flatten blocks into per-marker kinds (`*_START`, `VISUAL_CF_KIND_END`). Marker labels look like `If-0 true`, `End-0`, with an index that increases within a figure. + +--- + +## Build Options + +### CParameterFormatOptions + +Formatting options for parameter label text: + +| Field | Type | Meaning | Value behavior | +| --- | --- | --- | --- | +| `mode` | `uint32_t` | Parameter display mode. | One of the `PARAM_MODE_*` constants; unknown values fall back to numeric mode. | +| `decimal_precision` | `uintptr_t` | Decimal precision for fixed-point/scientific formatting. | Takes the given value as-is. | +| `scientific_lower_bound` | `double` | Scientific-notation lower bound. | Values in `(0, scientific_lower_bound)` use scientific notation. | +| `scientific_upper_bound` | `double` | Scientific-notation upper bound. | Values `>= scientific_upper_bound` use scientific notation. | +| `pi_tolerance` | `double` | Tolerance for pi-fraction matching. | When `value / pi` is within this tolerance of a rational fraction, the fraction display applies. | +| `pi_max_denominator` | `int64_t` | Maximum denominator for pi-fraction matching. | Fractions with a larger denominator do not participate in matching. | + +### CVisualBuildOptions + +Visualization IR build options; they only affect the build stage and are shared by the text and figure backends. Passing NULL for `options` uses library defaults: + +| Field | Type | Meaning | Value behavior | +| --- | --- | --- | --- | +| `decompose_circuit_gates` | `uint8_t` | Whether to expand circuit-gate definitions before building. | `0` no expansion (default), `1` expand. A failed expansion makes the build return NULL. | +| `reserve_full_span_for_multi_qubit` | `uint8_t` | Whether to reserve the full lane span (`min..=max`) for multi-qubit operations. | `0` keeps only the operation's own lanes, `1` keeps the full span (default). | +| `parameter_format` | `struct CParameterFormatOptions` | Parameter label formatting options, embedded by value. | Field table above. | + +--- + +## Build and Free + +### build_visual_circuit(circuit, options) + +Builds the visualization IR from a circuit. + +```c +struct CVisualCircuit *build_visual_circuit(const struct CCircuit *circuit, + const struct CVisualBuildOptions *options); +``` + +Parameters: + +- `circuit` (`const CCircuit*`): the circuit to build. +- `options` (`const CVisualBuildOptions*`): build options; NULL uses library defaults. + +Returns: a newly allocated `CVisualCircuit*` on success (free with `visual_circuit_free`); NULL on NULL input or build failure (circuit-gate expansion failure, an operation referencing an unknown qubit, or a symbolic-parameter index out of range). The IR's qubit count equals the circuit's qubit count. + +### visual_circuit_free(ptr) + +Frees the `CVisualCircuit` handle; NULL is allowed. + +--- + +## Circuit-Level Queries + +### visual_circuit_num_qubits(ptr) / visual_circuit_num_operations(ptr) / visual_circuit_num_columns(ptr) + +Return the qubit count, the operation count, and the number of occupied columns, respectively; a NULL handle returns 0. + +### visual_circuit_qubits_len(ptr) / visual_circuit_qubits(ptr, buffer, len) + +Two-step read of the qubit ids in display order: `*_len` returns the entry count (0 for NULL), and the fill function copies the ids into `buffer`. + +```c +uintptr_t visual_circuit_qubits_len(const struct CVisualCircuit *ptr); +int32_t visual_circuit_qubits(const struct CVisualCircuit *ptr, + uint32_t *buffer, + uintptr_t len); +``` + +Returns (fill function): 0 on success; -1 for a NULL handle (or a NULL `buffer` with a non-zero entry count); -8 when `len` is smaller than the entry count. + +--- + +## Operation-Level Queries + +The following functions access the operation list (in circuit order at build time) by index. Out-of-range `index` or NULL-handle return values are described per function. + +### visual_circuit_operation_label(ptr, index) + +Returns the operation's primary display label as a heap-allocated C string, freed with `cqlib_string_free`; NULL input or out-of-bounds returns NULL. + +```c +char *visual_circuit_operation_label(const struct CVisualCircuit *ptr, + uintptr_t index); +``` + +### visual_circuit_operation_column(ptr, index) + +Returns the column the operation was scheduled into; NULL input or out-of-bounds returns `UINTPTR_MAX`. + +### visual_circuit_operation_span_cols(ptr, index) + +Returns the number of logical columns the operation reserves; NULL input or out-of-bounds returns `UINTPTR_MAX`. Ordinary operations take `1`; control-flow blocks are computed from the child circuit's column count plus separators and margins. + +### visual_circuit_operation_is_span_box(ptr, index) + +Returns whether the operation is drawn as a single box spanning multiple qubit wires: `1` yes, `0` no; NULL input or out-of-bounds returns `-1`. Custom unitary gates, circuit gates, and store instructions take `1`. + +### visual_circuit_operation_lanes_len(ptr, index) / visual_circuit_operation_lanes(ptr, index, buffer, len) + +Two-step read of the operation's operand lanes: the display-order indices of the qubits involved, arranged in operand order. Operations sharing a qubit must occupy adjacent columns; operations on disjoint qubits may share a column. + +```c +uintptr_t visual_circuit_operation_lanes_len(const struct CVisualCircuit *ptr, + uintptr_t index); +int32_t visual_circuit_operation_lanes(const struct CVisualCircuit *ptr, + uintptr_t index, + uintptr_t *buffer, + uintptr_t len); +``` + +Returns (fill function): 0 on success; -1 for a NULL handle (or a NULL `buffer` with a non-zero entry count); -8 when `len` is smaller than the entry count. + +### visual_circuit_operation_covered_lanes_len(ptr, index) / visual_circuit_operation_covered_lanes(ptr, index, buffer, len) + +Two-step read of the lanes the operation reserves in its column: qubit indices kept free of overlap. Control-flow blocks reserve all lanes; a barrier with empty `lanes` reserves all lanes; other operations with empty `lanes` reserve `[0]`. When more than one lane is involved and `reserve_full_span_for_multi_qubit` is `1`, the full `min..=max` span of `lanes` is reserved; otherwise `lanes` itself is reserved. + +```c +uintptr_t visual_circuit_operation_covered_lanes_len(const struct CVisualCircuit *ptr, + uintptr_t index); +int32_t visual_circuit_operation_covered_lanes(const struct CVisualCircuit *ptr, + uintptr_t index, + uintptr_t *buffer, + uintptr_t len); +``` + +Returns (fill function): 0 on success; -1 for a NULL handle (or a NULL `buffer` with a non-zero entry count); -8 when `len` is smaller than the entry count. + +### visual_circuit_operation_params_len(ptr, index) / visual_circuit_operation_params(ptr, index, out, len) + +Two-step read of the operation's formatted parameter labels: `*_len` returns the entry count (0 for NULL input or out-of-bounds), and the fill function writes each label as a freshly allocated C string, each freed individually with `cqlib_string_free`. + +```c +uintptr_t visual_circuit_operation_params_len(const struct CVisualCircuit *ptr, + uintptr_t index); +int32_t visual_circuit_operation_params(const struct CVisualCircuit *ptr, + uintptr_t index, + char **out, + uintptr_t len); +``` + +Returns (fill function): 0 on success; -1 for a NULL handle (or a NULL `out` with a non-zero entry count); -8 when `len` is smaller than the entry count; -3 when a label contains an embedded NUL and cannot form a C string. + +### visual_circuit_operation_style(ptr, index) + +Returns the operation's drawing style as one of the `VISUAL_OP_STYLE_*` constants; NULL input or out-of-bounds returns `UINT32_MAX`. + +```c +uint32_t visual_circuit_operation_style(const struct CVisualCircuit *ptr, + uintptr_t index); +``` + +### visual_circuit_operation_num_controls(ptr, index) + +Returns the number of control qubits; meaningful only when the style is `VISUAL_OP_STYLE_CONTROLLED`. NULL input, out-of-bounds, or non-controlled styles return 0. + +### visual_circuit_operation_control_flow_kind(ptr, index) + +Returns the control-flow kind as one of the `VISUAL_CF_KIND_*` constants; meaningful only when the style is `VISUAL_OP_STYLE_CONTROL_FLOW`. NULL input, out-of-bounds, or non-control-flow styles return `UINT32_MAX`. + +```c +uint32_t visual_circuit_operation_control_flow_kind(const struct CVisualCircuit *ptr, + uintptr_t index); +``` + +### Style and label classification rules + +The build stage classifies instructions into styles and labels per the following table: + +| Instruction | Style | Label | +| --- | --- | --- | +| Standard gate `SWAP` | `VISUAL_OP_STYLE_SWAP` | `SWAP` | +| Standard gate `CZ` | `VISUAL_OP_STYLE_CZ` | `CZ` | +| Standard gate with control count > 0 | `VISUAL_OP_STYLE_CONTROLLED` | Target gate name | +| Other standard gates | `VISUAL_OP_STYLE_GATE` | Gate name | +| Multi-controlled gate with control value `1` and base gate `Z` | `VISUAL_OP_STYLE_CZ` | `CZ` | +| Other multi-controlled gates | `VISUAL_OP_STYLE_CONTROLLED` | Base gate name | +| Custom unitary gate | `VISUAL_OP_STYLE_GATE`, span box | Unitary label, or `Unitary` when empty | +| Circuit gate | `VISUAL_OP_STYLE_GATE`, span box | Circuit-gate name, or `Gate` when empty | +| Barrier | `VISUAL_OP_STYLE_BARRIER` | `B` | +| Measurement | `VISUAL_OP_STYLE_MEASURE` | `M` | +| Reset | `VISUAL_OP_STYLE_RESET` | `R` | +| Delay | `VISUAL_OP_STYLE_DELAY` | `D` | +| Classical control flow | `VISUAL_OP_STYLE_CONTROL_FLOW` | `IF`, `WH`, `FOR`, `SW`, `Break`, or `Continue` | +| Measure-into-bit instruction | `VISUAL_OP_STYLE_MEASURE` | `M` | +| Store instruction | `VISUAL_OP_STYLE_GATE`, span box | `STORE` | + +Gate names are abbreviated by the standard-gate convention: `SDG` displays as `SD`, `TDG` as `TD`, `Phase` and `GPhase` as `P`. Controlled-gate labels take the target gate name: `CX` and `CCX` become `X`, `CY` becomes `Y`, `CRX`/`CRY`/`CRZ` become `RX`/`RY`/`RZ`; otherwise the leading control prefix is dropped from the gate name. + +--- + +## Drawing + +### draw_text_from_visual(visual, options) + +Draws pre-built visualization IR as a UTF-8 text diagram (box-drawing characters). + +```c +char *draw_text_from_visual(const struct CVisualCircuit *visual, + const struct TextDrawerOptionsC *options); +``` + +Parameters: + +- `visual` (`const CVisualCircuit*`): the visualization IR handle. +- `options` (`const TextDrawerOptionsC*`): drawing options (field table in [Text Drawing](1_draw_text.md)); NULL uses library defaults. + +Returns: a heap-allocated C string, freed with `cqlib_string_free`; NULL on error. + +### draw_figure_from_visual(visual, options) + +Draws pre-built visualization IR as an SVG figure. + +```c +char *draw_figure_from_visual(const struct CVisualCircuit *visual, + const struct FigureDrawerOptionsC *options); +``` + +Parameters: + +- `visual` (`const CVisualCircuit*`): the visualization IR handle. +- `options` (`const FigureDrawerOptionsC*`): drawing options (field table in [SVG Figure](2_draw_figure.md)); NULL uses library defaults. + +Returns: a heap-allocated C string (SVG markup), freed with `cqlib_string_free`; NULL on error. + +--- + +## Example + +```c +#include +#include +#include +#include "cqlib_c.h" + +int main(void) { + struct CCircuit *circuit = circuit_new(2); + circuit_h(circuit, 0); + circuit_cx(circuit, 0, 1); + + /* 1. Build the IR with default options */ + struct CVisualCircuit *visual = build_visual_circuit(circuit, NULL); + if (visual == NULL) { + circuit_free(circuit); + return 1; + } + + /* 2. Circuit-level queries */ + uintptr_t num_qubits = visual_circuit_num_qubits(visual); /* 2 */ + uintptr_t num_ops = visual_circuit_num_operations(visual); /* 2 */ + uintptr_t num_cols = visual_circuit_num_columns(visual); /* 2 */ + + /* 3. Two-step read of the qubit ids in display order */ + uintptr_t qubits_len = visual_circuit_qubits_len(visual); + uint32_t *qubits = malloc(qubits_len * sizeof(uint32_t)); + if (visual_circuit_qubits(visual, qubits, qubits_len) == 0) { + /* qubits[0] == 0, qubits[1] == 1 */ + } + free(qubits); + + /* 4. Operation-level queries */ + char *label = visual_circuit_operation_label(visual, 0); + if (label != NULL) { + printf("label: %s\n", label); /* "H" */ + cqlib_string_free(label); + } + uintptr_t column = visual_circuit_operation_column(visual, 1); /* 1 */ + uint32_t style = visual_circuit_operation_style(visual, 1); /* CONTROLLED */ + uintptr_t num_controls = visual_circuit_operation_num_controls(visual, 1); /* 1 */ + + uintptr_t lanes_len = visual_circuit_operation_lanes_len(visual, 1); + uintptr_t *lanes = malloc(lanes_len * sizeof(uintptr_t)); + if (visual_circuit_operation_lanes(visual, 1, lanes, lanes_len) == 0) { + /* lanes[0] == 0, lanes[1] == 1 */ + } + free(lanes); + + /* 5. Out-of-bounds access fails gracefully */ + if (visual_circuit_operation_label(visual, 2) == NULL) { + printf("index 2 out of range\n"); + } + + /* 6. Draw the pre-built IR with both backends */ + char *text = draw_text_from_visual(visual, NULL); + if (text != NULL) { + printf("%s\n", text); + cqlib_string_free(text); + } + char *svg = draw_figure_from_visual(visual, NULL); + if (svg != NULL) { + cqlib_string_free(svg); + } + + /* 7. Explicit build options: pi-fraction parameter labels */ + struct CVisualBuildOptions options; + memset(&options, 0, sizeof(options)); + options.reserve_full_span_for_multi_qubit = 1; + options.parameter_format.mode = PARAM_MODE_PI_FRACTION_PREFERRED; + options.parameter_format.decimal_precision = 2; + options.parameter_format.scientific_lower_bound = 1e-3; + options.parameter_format.scientific_upper_bound = 1e4; + options.parameter_format.pi_tolerance = 1e-3; + options.parameter_format.pi_max_denominator = 16; + struct CVisualCircuit *visual2 = build_visual_circuit(circuit, &options); + if (visual2 != NULL) { + visual_circuit_free(visual2); + } + + visual_circuit_free(visual); + circuit_free(circuit); + return 0; +} +``` + +For drawing straight from a circuit (without going through the IR explicitly), see [Text Drawing](1_draw_text.md) and [SVG Figure](2_draw_figure.md); for writing SVG markup out to files, see [File Output](5_render_to_file.md). diff --git a/docs/api/en/c/6_error_mitigation/0_overview.md b/docs/api/en/c/6_error_mitigation/0_overview.md new file mode 100644 index 00000000..7b53d4ba --- /dev/null +++ b/docs/api/en/c/6_error_mitigation/0_overview.md @@ -0,0 +1,112 @@ +# Error Mitigation (C) + +The error mitigation module suppresses the influence of noise on expectation-value estimates through three complementary pipelines: zero-noise extrapolation (ZNE) folds the circuit at several noise factors and extrapolates back to the zero-noise limit, virtual distillation (VD) uses multiple circuit copies and a copy-swap circuit to estimate the ratio `Tr(O ρ^M) / Tr(ρ^M)`, and the unified pipeline `CErrorMitigation` wraps both into a "sampling → post-processing" two-stage flow. Error codes, string and handle ownership conventions follow the [Overview](../0_overview.md). + +The three submodules share a single estimator callback type, `CEstimatorFn`: each mitigation flow hands the circuit to be estimated to the callback, which reports the expectation value and variance. + +--- + +## Page Navigation + +| Page | Content | +| --- | --- | +| [Zero-Noise Extrapolation](1_zne.md) | `CZneMitigation`: circuit folding, noise factors, extrapolation and the mitigation sequence; `CCircuitList` traversal. | +| [Virtual Distillation](2_virtual_distillation.md) | `CVirtualDistillation`: the copy-swap circuit, numerator/denominator circuits and the full protocol. | +| [Unified Pipeline](3_unified.md) | `CErrorMitigation`: the `new → run → get_mitigated` two-stage flow. | + +--- + +## Constants + +Mitigation method tags (the `method` parameter of `error_mitigation_new`): + +| Constant | Value | Method | Extra parameter | +| --- | --- | --- | --- | +| `MITIGATION_ZNE` | 0 | Zero-noise extrapolation. | `fold_levels` fold-level array. | +| `MITIGATION_VIRTUAL_DISTILLATION` | 1 | Virtual distillation. | `copies` copy count. | + +Post-processing tags (the `process_method` parameter of `error_mitigation_get_mitigated`): + +| Constant | Value | Flow | +| --- | --- | --- | +| `PROCESS_ZNE_POLYNOMIAL` | 0 | ZNE + polynomial extrapolation. | +| `PROCESS_ZNE_EXPONENTIAL` | 1 | ZNE + exponential extrapolation. | +| `PROCESS_VIRTUAL_DISTILLATION` | 2 | Virtual distillation. | + +ZNE extrapolation method tags (the `method` parameter of `zne_extrapolate`): + +| Constant | Value | Method | +| --- | --- | --- | +| `ZNE_EXTRAPOLATE_POLYNOMIAL` | 0 | Polynomial fit. | +| `ZNE_EXTRAPOLATE_EXPONENTIAL` | 1 | Exponential-decay fit. | + +--- + +## CEstimatorFn Callback Contract + +```c +typedef void (*CEstimatorFn)(const struct CCircuit *circuit, + const struct CHamiltonian *hamiltonian, + uintptr_t shots, + double *expectation, + double *variance); +``` + +Mitigation flows obtain expectation estimates through this callback. The contract: + +- **Pointer validity**: `circuit`, `hamiltonian` and all out pointers are valid only for the duration of the callback; do not keep using them or store them beyond the callback. +- **hamiltonian ownership**: the `hamiltonian` handle is owned by the library and must not be freed by the callback. The Hamiltonian the callback receives may already have been expanded to the width of the folded/copy circuit as required by the flow. +- **Out writes**: the callback must write the expectation value through `expectation`; write the variance through `variance` when known, NaN otherwise. In the virtual distillation denominator estimate, `hamiltonian` is NULL — treat it as "no observable". +- **shots semantics**: `shots == 0` means "not specified" and the callback decides the sampling scale; a non-zero value is the shot count suggested by the caller. + +Inside the callback you can freely call simulator interfaces (e.g. `statevector_from_circuit` + `statevector_expectation` from the [Statevector](../3_qis/1_statevector.md) page) to compute the estimate. + +--- + +## Example + +```c +#include +#include "cqlib_c.h" + +/* Estimator callback: exactly simulates the folded circuit and computes */ +static void estimate(const struct CCircuit *circuit, + const struct CHamiltonian *hamiltonian, + uintptr_t shots, + double *expectation, + double *variance) { + struct CStatevector *sv = statevector_from_circuit(circuit); + double value = 0.0; + statevector_expectation(sv, hamiltonian, &value); + statevector_free(sv); + *expectation = value; + *variance = 0.0; /* unknown variance: NaN is also valid */ + (void)shots; +} + +int main(void) { + struct CCircuit *qc = circuit_new(1); + circuit_x(qc, 0); + + /* Unified pipeline: ZNE with fold levels {0, 1, 2} */ + int32_t levels[3] = {0, 1, 2}; + struct CErrorMitigation *em = + error_mitigation_new(qc, MITIGATION_ZNE, levels, 3, 0); + + struct CPauliString *z = pauli_string_parse("Z"); + struct CHamiltonian *obs = hamiltonian_from_pauli(z); + + /* Sampling stage → post-processing stage */ + error_mitigation_run(em, MITIGATION_ZNE, obs, 0, 0, estimate); + double value = 0.0, variance = 0.0; + error_mitigation_get_mitigated(em, PROCESS_ZNE_POLYNOMIAL, 0, + &value, &variance); + + error_mitigation_free(em); + hamiltonian_free(obs); + circuit_free(qc); + return 0; +} +``` + +For the low-level entry points (fine-grained ZNE folding/extrapolation control, separate virtual distillation numerator/denominator execution), see [Zero-Noise Extrapolation](1_zne.md) and [Virtual Distillation](2_virtual_distillation.md). diff --git a/docs/api/en/c/6_error_mitigation/1_zne.md b/docs/api/en/c/6_error_mitigation/1_zne.md new file mode 100644 index 00000000..0b525958 --- /dev/null +++ b/docs/api/en/c/6_error_mitigation/1_zne.md @@ -0,0 +1,297 @@ +# Zero-Noise Extrapolation (C) + +Zero-noise extrapolation (ZNE) folds the circuit at several noise factors (`2 * fold level + 1`), estimates the expectation value for each folded circuit, and extrapolates back to the zero-noise limit. This page covers creating the `CZneMitigation` handle, generating folded circuits, reading the configuration, extrapolation and the mitigation sequence, plus traversal of the folded-circuit list `CCircuitList`. For the callback contract and module constants see the [module overview](0_overview.md); for error codes and handle ownership conventions see the [Overview](../0_overview.md). + +--- + +## Handle Lifecycle + +### zne_mitigation_new(circuit, fold_levels, num_levels) + +Creates a ZNE mitigation handle from a base circuit and a fold-level array. Noise factors are derived from the fold levels as `2 * level + 1`. + +- `circuit` (`const struct CCircuit *`): the base circuit; all folding operates on copies of it. +- `fold_levels` (`const int32_t *`): the fold-level array, e.g. `{0, 1, 2}`. +- `num_levels` (`uintptr_t`): the number of elements in `fold_levels`. + +Returns `struct CZneMitigation *`: the new handle, freed with `zne_mitigation_free`; NULL when `circuit` is NULL, `num_levels` is 0, or `fold_levels` is NULL. + +### zne_mitigation_free(ptr) + +Frees a ZNE mitigation handle. + +- `ptr` (`struct CZneMitigation *`): the handle to free; NULL is allowed. + +--- + +## Folding and the Circuit List + +### zne_mitigation_fold_circuit(ptr, gate_names) + +Folds the circuit at every configured level and returns the folded-circuit list (one circuit per fold level). + +- `ptr` (`const struct CZneMitigation *`): the ZNE mitigation handle. +- `gate_names` (`const char *`): optional comma-separated gate-name list (e.g. `"H,CX"`) that folds only the named gates; NULL folds the whole circuit globally. + +Returns `struct CCircuitList *`: the folded-circuit list, freed with `circuit_list_free`; NULL when `ptr` is NULL, `gate_names` contains an unknown gate name or invalid UTF-8, or folding fails. + +### circuit_list_len(ptr) + +Returns the number of circuits in the list. + +- `ptr` (`const struct CCircuitList *`): the folded-circuit list. + +Returns `uintptr_t`: the circuit count; 0 when `ptr` is NULL. + +### circuit_list_get(ptr, index) + +Returns the circuit at `index` as an independent clone. + +- `ptr` (`const struct CCircuitList *`): the folded-circuit list. +- `index` (`uintptr_t`): the circuit index. + +Returns `struct CCircuit *`: an owned clone of the circuit, freed with `circuit_free`; NULL when `ptr` is NULL or `index` is out of bounds. + +### circuit_list_free(ptr) + +Frees a folded-circuit list. Circuit clones obtained via `circuit_list_get` are not freed with the list — release each one with `circuit_free`. + +- `ptr` (`struct CCircuitList *`): the list to free; NULL is allowed. + +--- + +## Configuration Access + +### zne_circuit(ptr) + +Returns the original (unfolded) circuit as an independent clone. + +- `ptr` (`const struct CZneMitigation *`): the ZNE mitigation handle. + +Returns `struct CCircuit *`: the circuit clone, freed with `circuit_free`; NULL when `ptr` is NULL. + +### zne_mitigation_noise_factor(ptr, index) + +Returns the noise factor for fold level `index` (`2 * level + 1`). + +- `ptr` (`const struct CZneMitigation *`): the ZNE mitigation handle. +- `index` (`uintptr_t`): the fold-level index. + +Returns `int32_t`: the noise factor; 0 when `ptr` is NULL or `index` is out of bounds. + +### zne_fold_levels_len(ptr) / zne_fold_levels(ptr, buffer, len) + +Two-step read of the configured fold-level array: `zne_fold_levels_len` returns the element count, `zne_fold_levels` copies the array into the caller-provided buffer. + +- `ptr` (`const struct CZneMitigation *`): the ZNE mitigation handle. +- `buffer` (`int32_t *`): buffer receiving the fold levels, sized at least `zne_fold_levels_len(ptr)`. +- `len` (`uintptr_t`): the buffer length; must equal `zne_fold_levels_len(ptr)`. + +`zne_fold_levels_len` returns `uintptr_t`: the element count, 0 when `ptr` is NULL. `zne_fold_levels` returns `int32_t`: 0 on success; -1 (`ptr` or `buffer` NULL); -8 (`len` does not match the element count). + +### zne_noise_factors_len(ptr) / zne_noise_factors(ptr, buffer, len) + +Two-step read of the noise-factor array (`2 * level + 1` per fold level): `zne_noise_factors_len` returns the element count, `zne_noise_factors` copies the array into the caller-provided buffer. + +- `ptr` (`const struct CZneMitigation *`): the ZNE mitigation handle. +- `buffer` (`int32_t *`): buffer receiving the noise factors, sized at least `zne_noise_factors_len(ptr)`. +- `len` (`uintptr_t`): the buffer length; must equal `zne_noise_factors_len(ptr)`. + +`zne_noise_factors_len` returns `uintptr_t`: the element count, 0 when `ptr` is NULL. `zne_noise_factors` returns `int32_t`: 0 on success; -1 (`ptr` or `buffer` NULL); -8 (`len` does not match the element count). + +--- + +## Two-Step Folded-Circuit Output + +### zne_fold_circuits_len(ptr, gate_names) + +Returns the number of circuits produced by the two-step folded-circuit output (one per fold level). + +- `ptr` (`const struct CZneMitigation *`): the ZNE mitigation handle. +- `gate_names` (`const char *`): optional comma-separated gate-name list, with the same semantics as in `zne_mitigation_fold_circuit`. + +Returns `uintptr_t`: the circuit count; 0 when `ptr` is NULL or `gate_names` contains an unknown gate name / invalid UTF-8. + +### zne_fold_circuits(ptr, gate_names, buffer, len) + +Folds the circuit at every configured level and writes each folded circuit as an owned handle into the caller-provided buffer. + +- `ptr` (`const struct CZneMitigation *`): the ZNE mitigation handle. +- `gate_names` (`const char *`): optional comma-separated gate-name list; NULL folds globally. +- `buffer` (`struct CCircuit **`): buffer receiving the circuit handles, sized `zne_fold_circuits_len(ptr, gate_names)`; every element is an owned `CCircuit *` that must be freed with `circuit_free`. +- `len` (`uintptr_t`): the buffer length; must equal `zne_fold_circuits_len(ptr, gate_names)`. + +Returns `int32_t`: 0 on success; -1 (`ptr` or `buffer` NULL); -4 (`gate_names` contains an unknown gate name or invalid UTF-8); -8 (`len` does not match the fold-level count); -3 (folding failure). + +--- + +## Extrapolation + +The `method` parameter of `zne_extrapolate` takes the following tags: + +| Constant | Value | Method | +| --- | --- | --- | +| `ZNE_EXTRAPOLATE_POLYNOMIAL` | 0 | Polynomial fit. | +| `ZNE_EXTRAPOLATE_EXPONENTIAL` | 1 | Exponential-decay fit. | + +### zne_extrapolate(ptr, noisy_results, len, method, degree, out_value) + +Extrapolates the zero-noise expectation value from the noisy expectation values measured at each noise factor. + +- `ptr` (`const struct CZneMitigation *`): the ZNE mitigation handle. +- `noisy_results` (`const double *`): one expectation value per noise factor, in fold-level order. +- `len` (`uintptr_t`): the number of expectation values. +- `method` (`uint8_t`): the extrapolation method tag, see the table above. The polynomial method uses `degree`; the exponential method requires all values to be positive. +- `degree` (`uintptr_t`): the polynomial degree; must be smaller than the number of data points. +- `out_value` (`double *`): receives the extrapolated value. + +Returns `int32_t`: 0 on success; -1 (`ptr`, `noisy_results` or `out_value` NULL); -8 (invalid `method` tag, empty results, `len` not matching the noise-factor count, invalid polynomial degree, or non-positive values in an exponential fit); -7 (other fitting errors). + +### zne_poly_extrapolate(ptr, noisy_results, len, degree, out_value) + +Extrapolates the zero-noise expectation value with a polynomial fit of the given degree; equivalent to the polynomial branch of `zne_extrapolate`. + +- `ptr` (`const struct CZneMitigation *`): the ZNE mitigation handle. +- `noisy_results` (`const double *`): one expectation value per noise factor, in fold-level order. +- `len` (`uintptr_t`): the number of expectation values. +- `degree` (`uintptr_t`): the polynomial degree; must be smaller than the noise-factor count. +- `out_value` (`double *`): receives the extrapolated value. + +Returns `int32_t`: 0 on success; -1 (`ptr`, `noisy_results` or `out_value` NULL); -8 (empty results, `len` not matching the noise-factor count, or invalid degree); -7 (other fitting errors). + +### zne_exp_extrapolate(ptr, noisy_results, len, out_value) + +Extrapolates the zero-noise expectation value with an exponential-decay model `y(x) = A * exp(-x / τ)`, writing the fitted value `A` at `x = 0`. All input values must be positive. + +- `ptr` (`const struct CZneMitigation *`): the ZNE mitigation handle. +- `noisy_results` (`const double *`): one expectation value per noise factor, in fold-level order. +- `len` (`uintptr_t`): the number of expectation values. +- `out_value` (`double *`): receives the fitted value `A`. + +Returns `int32_t`: 0 on success; -1 (`ptr`, `noisy_results` or `out_value` NULL); -8 (empty results, `len` not matching the noise-factor count, or any non-positive value); -7 (other fitting errors). + +--- + +## Mitigation Sequence + +### zne_run_em_sequence_len(ptr) + +Returns the number of expectation values produced by the mitigation sequence (one per fold level). + +- `ptr` (`const struct CZneMitigation *`): the ZNE mitigation handle. + +Returns `uintptr_t`: the expectation-value count; 0 when `ptr` is NULL. + +### zne_run_em_sequence_with_shots_len(ptr) + +Returns the number of expectation values produced by the mitigation sequence with explicit shots (one per fold level); the same as `zne_run_em_sequence_len`. + +- `ptr` (`const struct CZneMitigation *`): the ZNE mitigation handle. + +Returns `uintptr_t`: the expectation-value count; 0 when `ptr` is NULL. + +### zne_run_em_sequence(ptr, gate_names, hamiltonian, estimator, buffer, len) + +Runs the mitigation sequence: folds the circuit at every configured level and estimates one expectation value per folded circuit through the `estimator` callback. The callback receives shot count 0 ("not specified"). + +- `ptr` (`const struct CZneMitigation *`): the ZNE mitigation handle. +- `gate_names` (`const char *`): optional comma-separated gate-name list; NULL folds globally. +- `hamiltonian` (`const struct CHamiltonian *`): the observable to estimate, see [Hamiltonian](../3_qis/7_hamiltonian.md) for construction. +- `estimator` (`CEstimatorFn`): the estimator callback; see the [module overview](0_overview.md) for the contract. +- `buffer` (`double *`): buffer receiving the expectation values, sized `zne_run_em_sequence_len(ptr)`. +- `len` (`uintptr_t`): the buffer length; must equal `zne_run_em_sequence_len(ptr)`. + +Returns `int32_t`: 0 on success; -1 (`ptr`, `hamiltonian` or `buffer` NULL); -4 (`gate_names` contains an unknown gate name or invalid UTF-8); -8 (`len` does not match the noise-factor count); -3 (folding failure); -7 (other execution errors). + +### zne_run_em_sequence_with_shots(ptr, gate_names, hamiltonian, shots, estimator, buffer, len) + +Same as `zne_run_em_sequence`, but forwards `shots` to the estimator callback as the suggested shot count. + +- `ptr` (`const struct CZneMitigation *`): the ZNE mitigation handle. +- `gate_names` (`const char *`): optional comma-separated gate-name list; NULL folds globally. +- `hamiltonian` (`const struct CHamiltonian *`): the observable to estimate. +- `shots` (`uintptr_t`): the shot count forwarded to the estimator. +- `estimator` (`CEstimatorFn`): the estimator callback. +- `buffer` (`double *`): buffer receiving the expectation values, sized `zne_run_em_sequence_with_shots_len(ptr)`. +- `len` (`uintptr_t`): the buffer length; must equal `zne_run_em_sequence_with_shots_len(ptr)`. + +Returns `int32_t`: the same error codes as `zne_run_em_sequence`. + +--- + +## Example + +A complete fold → per-circuit estimation → polynomial extrapolation flow (the estimator simulates exactly with the statevector, see [Statevector](../3_qis/1_statevector.md)): + +```c +#include +#include +#include "cqlib_c.h" + +/* Estimator callback: exactly simulates the folded circuit and computes */ +static void estimate(const struct CCircuit *circuit, + const struct CHamiltonian *hamiltonian, + uintptr_t shots, + double *expectation, + double *variance) { + struct CStatevector *sv = statevector_from_circuit(circuit); + double value = 0.0; + statevector_expectation(sv, hamiltonian, &value); + statevector_free(sv); + *expectation = value; + *variance = 0.0; + (void)shots; +} + +int main(void) { + /* Bell circuit: H(0) + CX(0, 1) */ + struct CCircuit *qc = circuit_new(2); + circuit_h(qc, 0); + circuit_cx(qc, 0, 1); + + /* Fold levels {0, 1, 2} → noise factors {1, 3, 5} */ + int32_t levels[3] = {0, 1, 2}; + struct CZneMitigation *zne = zne_mitigation_new(qc, levels, 3); + + struct CPauliString *zz = pauli_string_parse("ZZ"); + struct CHamiltonian *obs = hamiltonian_from_pauli(zz); /* takes ownership of zz */ + + /* Fold + estimate per circuit → one expectation value per noise factor */ + uintptr_t len = zne_run_em_sequence_len(zne); + double noisy_results[3]; + zne_run_em_sequence(zne, NULL, obs, estimate, noisy_results, len); + + /* Polynomial extrapolation back to the zero-noise limit */ + double value = 0.0; + zne_extrapolate(zne, noisy_results, len, ZNE_EXTRAPOLATE_POLYNOMIAL, 2, &value); + printf("ZNE estimate: %f\n", value); + + zne_mitigation_free(zne); + hamiltonian_free(obs); + circuit_free(qc); + return 0; +} +``` + +Fine-grained path: fold into a `CCircuitList` and traverse it, or fetch the owned circuit array directly with the two-step interface: + +```c +/* Fold into a list, then read one by one (each get returns an owned clone) */ +struct CCircuitList *folded = zne_mitigation_fold_circuit(zne, "H,CX"); +for (uintptr_t i = 0; i < circuit_list_len(folded); i++) { + struct CCircuit *c = circuit_list_get(folded, i); + int32_t factor = zne_mitigation_noise_factor(zne, i); /* noise factor of level i */ + /* estimate the expectation value on circuit c here */ + circuit_free(c); +} +circuit_list_free(folded); + +/* Two-step: the buffer receives owned circuit handles, free each one */ +uintptr_t m = zne_fold_circuits_len(zne, NULL); +struct CCircuit **buf = malloc(m * sizeof(struct CCircuit *)); +zne_fold_circuits(zne, NULL, buf, m); +for (uintptr_t i = 0; i < m; i++) { + circuit_free(buf[i]); +} +free(buf); +``` diff --git a/docs/api/en/c/6_error_mitigation/2_virtual_distillation.md b/docs/api/en/c/6_error_mitigation/2_virtual_distillation.md new file mode 100644 index 00000000..266ebd87 --- /dev/null +++ b/docs/api/en/c/6_error_mitigation/2_virtual_distillation.md @@ -0,0 +1,173 @@ +# Virtual Distillation (C) + +Virtual distillation estimates the ratio `Tr(O ρ^M) / Tr(ρ^M)` using multiple copies of the density matrix and a copy-swap circuit, where `M` is the copy count and `O` is the observable described by a Hamiltonian. This page covers creating and configuring the `CVirtualDistillation` handle, building the copy-swap circuit, running the numerator/denominator circuits separately, and the full protocol. For the callback contract see the [module overview](0_overview.md); for error codes and handle ownership conventions see the [Overview](../0_overview.md). + +--- + +## Handle and Configuration + +### virtual_distillation_new(circuit, copies) + +Creates a virtual distillation handle from a base circuit and a copy count. All derived circuits are built from the base circuit. + +- `circuit` (`const struct CCircuit *`): the base circuit. +- `copies` (`uintptr_t`): the copy count, at least 2. + +Returns `struct CVirtualDistillation *`: the new handle, freed with `virtual_distillation_free`; NULL when `circuit` is NULL or `copies` is less than 2. + +### virtual_distillation_free(ptr) + +Frees a virtual distillation handle. + +- `ptr` (`struct CVirtualDistillation *`): the handle to free; NULL is allowed. + +### virtual_distillation_copies(ptr) + +Returns the configured copy count. + +- `ptr` (`const struct CVirtualDistillation *`): the virtual distillation handle. + +Returns `uintptr_t`: the copy count; 0 when `ptr` is NULL. + +### virtual_distillation_set_copies(ptr, copies) + +Updates the copy count. On failure the original value is kept. + +- `ptr` (`struct CVirtualDistillation *`): the virtual distillation handle. +- `copies` (`uintptr_t`): the new copy count, at least 2. + +Returns `int32_t`: 0 on success; -1 (`ptr` NULL); -8 (`copies` less than 2). + +--- + +## Copy-Swap Circuit + +### virtual_distillation_build_circuit(ptr) + +Builds the copy-swap circuit used by the virtual distillation protocol: the base circuit is first decomposed into gates, then prepared `copies` times side by side — copy `i` is shifted onto the qubit range starting at `i × base width`, with no overlap between copies — and finally a `SWAP` is inserted bitwise between the first copy and each of the other copies. + +- `ptr` (`const struct CVirtualDistillation *`): the virtual distillation handle. + +The circuit width is `copies × base width` (the base width being the width of the decomposed circuit); the operation count is `copies` copies of the base operations plus `(copies - 1) × base width` `SWAP` gates. + +Returns `struct CCircuit *`: an owned handle of the copy-swap circuit, freed with `circuit_free`; NULL when `ptr` is NULL or construction fails. + +--- + +## Numerator and Denominator + +The estimator callback distinguishes the two circuits through the `hamiltonian` parameter: the numerator circuit carries the observable, the denominator circuit has none. + +### virtual_distillation_run_numerator_circuit(ptr, hamiltonian, shots, estimator, out_mean, out_variance) + +Runs the numerator circuit through the estimator callback to estimate `Tr(O ρ^M)`. The estimator receives the copy-swap circuit, a Hamiltonian expanded to the full copy-swap width, and `shots`. Expansion rule: the original Pauli terms keep their qubit indices, phases and coefficients; the new higher-index qubits are padded with `Z`. + +- `ptr` (`const struct CVirtualDistillation *`): the virtual distillation handle. +- `hamiltonian` (`const struct CHamiltonian *`): the observable; its qubit count must match the base circuit width, see [Hamiltonian](../3_qis/7_hamiltonian.md) for construction. +- `shots` (`uintptr_t`): the shot count forwarded to the estimator. +- `estimator` (`CEstimatorFn`): the estimator callback; see the [module overview](0_overview.md) for the contract. +- `out_mean` (`double *`): receives the estimated mean. +- `out_variance` (`double *`): receives the estimated variance. + +Returns `int32_t`: 0 on success; -1 (`ptr`, `hamiltonian`, `out_mean` or `out_variance` NULL); -3 (Hamiltonian qubit count not matching the base circuit width, or copy-swap circuit construction failure); -7 (other execution errors). + +### virtual_distillation_run_denominator_circuit(ptr, shots, estimator, out_mean, out_variance) + +Runs the denominator circuit through the estimator callback to estimate `Tr(ρ^M)`. The estimator receives the copy-swap circuit, a NULL Hamiltonian, and `shots`. + +- `ptr` (`const struct CVirtualDistillation *`): the virtual distillation handle. +- `shots` (`uintptr_t`): the shot count forwarded to the estimator. +- `estimator` (`CEstimatorFn`): the estimator callback. +- `out_mean` (`double *`): receives the estimated mean. +- `out_variance` (`double *`): receives the estimated variance. + +Returns `int32_t`: 0 on success; -1 (`ptr`, `out_mean` or `out_variance` NULL); -3 (execution failure). + +### virtual_distillation_build_copy_swap_circuit(ptr) + +Constructs the copy-swap circuit from the base circuit: the base circuit is first decomposed into gates, then prepared `copies` times side by side — copy `i` is shifted onto the qubit range starting at `i × base width`, with no overlap between copies — and finally a `SWAP` is inserted bitwise between the first copy and each of the other copies. The circuit width is `copies × base width` (the base width being the width of the decomposed circuit); the operation count is `copies` copies of the base operations plus `(copies - 1) × base width` `SWAP` gates. + +- `ptr` (`const struct CVirtualDistillation *`): the virtual distillation handle. + +Returns `struct CCircuit *`: an owned handle of the copy-swap circuit, freed with `circuit_free`; NULL when `ptr` is NULL or construction fails. + +--- + +## Full Protocol + +### virtual_distillation_run_vd(ptr, hamiltonian, shots_numerator, shots_denominator, estimator, out_mean, out_variance) + +Runs the full virtual distillation protocol: it first checks that the Hamiltonian qubit count equals the base circuit width, then runs the numerator circuit and the denominator circuit in turn, and finally combines them into the mitigated result. The numerator and denominator use their own shot counts. + +- `ptr` (`const struct CVirtualDistillation *`): the virtual distillation handle. +- `hamiltonian` (`const struct CHamiltonian *`): the observable; its qubit count must match the base circuit width. +- `shots_numerator` (`uintptr_t`): the shot count of the numerator circuit. +- `shots_denominator` (`uintptr_t`): the shot count of the denominator circuit. +- `estimator` (`CEstimatorFn`): the estimator callback. +- `out_mean` (`double *`): receives the mitigated expectation value `mu_num / mu_den`. +- `out_variance` (`double *`): receives the mitigated variance. + +Returns `int32_t`: 0 on success; -1 (any pointer NULL); -8 (Hamiltonian qubit count not matching the base circuit width); -7 (zero denominator mean or other execution errors). + +The variance is combined with a first-order Taylor approximation, assuming the numerator and denominator are independent: `var_num / mu_den^2 + mu_num^2 * var_den / mu_den^4`. Sampling randomness is owned by the estimator (the core API takes no seed). + +--- + +## Example + +```c +#include +#include "cqlib_c.h" + +/* The numerator circuit carries the observable, the denominator does not */ +static void estimate(const struct CCircuit *circuit, + const struct CHamiltonian *hamiltonian, + uintptr_t shots, + double *expectation, + double *variance) { + if (hamiltonian != NULL) { + *expectation = 1.5; + *variance = 0.25; + } else { + *expectation = 2.0; + *variance = 1.0; + } + (void)circuit; + (void)shots; +} + +int main(void) { + /* Two single-qubit copies */ + struct CCircuit *qc = circuit_new(1); + circuit_x(qc, 0); + struct CVirtualDistillation *vd = virtual_distillation_new(qc, 2); + + /* Copy-swap circuit: width = 2 × 1 = 2 */ + struct CCircuit *copy_swap = virtual_distillation_build_circuit(vd); + + struct CPauliString *z = pauli_string_parse("Z"); + struct CHamiltonian *obs = hamiltonian_from_pauli(z); /* takes ownership of z */ + + /* mu_vd = 1.5 / 2.0 = 0.75 */ + double mean = 0.0, variance = 0.0; + virtual_distillation_run_vd(vd, obs, 3, 2, estimate, &mean, &variance); + printf("VD estimate: %f\n", mean); + + circuit_free(copy_swap); + virtual_distillation_free(vd); + hamiltonian_free(obs); + circuit_free(qc); + return 0; +} +``` + +The numerator and denominator can also be run separately, each yielding its own mean and variance: + +```c +double num_mean = 0.0, num_var = 0.0; +virtual_distillation_run_numerator_circuit(vd, obs, 512, estimate, + &num_mean, &num_var); +double den_mean = 0.0, den_var = 0.0; +virtual_distillation_run_denominator_circuit(vd, 512, estimate, + &den_mean, &den_var); +``` diff --git a/docs/api/en/c/6_error_mitigation/3_unified.md b/docs/api/en/c/6_error_mitigation/3_unified.md new file mode 100644 index 00000000..d5efbb7e --- /dev/null +++ b/docs/api/en/c/6_error_mitigation/3_unified.md @@ -0,0 +1,130 @@ +# Unified Pipeline (C) + +The unified pipeline `CErrorMitigation` wraps zero-noise extrapolation and virtual distillation into a "sampling → post-processing" two-stage flow: `error_mitigation_run` folds/derives the circuits and samples through the estimator callback, and `error_mitigation_get_mitigated` produces the mitigated result once sampling completes. For the callback contract and constants see the [module overview](0_overview.md); for error codes and handle ownership conventions see the [Overview](../0_overview.md). + +Values of the `method` parameter: + +| Constant | Value | Method | Extra parameter | +| --- | --- | --- | --- | +| `MITIGATION_ZNE` | 0 | Zero-noise extrapolation. | `fold_levels` fold-level array. | +| `MITIGATION_VIRTUAL_DISTILLATION` | 1 | Virtual distillation. | `copies` copy count. | + +Values of the `process_method` parameter: + +| Constant | Value | Flow | +| --- | --- | --- | +| `PROCESS_ZNE_POLYNOMIAL` | 0 | ZNE + polynomial extrapolation. | +| `PROCESS_ZNE_EXPONENTIAL` | 1 | ZNE + exponential extrapolation. | +| `PROCESS_VIRTUAL_DISTILLATION` | 2 | Virtual distillation. | + +--- + +## error_mitigation_new(circuit, method, fold_levels, num_levels, copies) + +Creates a unified pipeline handle from a base circuit and a mitigation method. + +- `circuit` (`const struct CCircuit *`): the base circuit. +- `method` (`uint8_t`): the mitigation method tag, see the table above. +- `fold_levels` (`const int32_t *`): the fold-level array, used when `method == MITIGATION_ZNE` (e.g. `{0, 1, 2}`). +- `num_levels` (`uintptr_t`): the number of elements in `fold_levels`; must be greater than 0 when `method == MITIGATION_ZNE`. +- `copies` (`uintptr_t`): the copy count, used when `method == MITIGATION_VIRTUAL_DISTILLATION`, at least 2. + +Returns `struct CErrorMitigation *`: the new handle, freed with `error_mitigation_free`; NULL when `circuit` is NULL, `method` is not a valid tag, or the parameters do not match the method (ZNE without fold levels, VD with a copy count less than 2). + +--- + +## error_mitigation_free(ptr) + +Frees a unified pipeline handle. + +- `ptr` (`struct CErrorMitigation *`): the handle to free; NULL is allowed. + +--- + +## error_mitigation_run(ptr, method, hamiltonian, shots_a, shots_b, estimator) + +Executes the sampling stage: folds/derives the circuits according to the configured method and invokes the estimator callback once per circuit execution. `method` must match the method configured in `error_mitigation_new`. + +- `ptr` (`struct CErrorMitigation *`): the unified pipeline handle. +- `method` (`uint8_t`): the mitigation method tag; must match the one used at creation. +- `hamiltonian` (`const struct CHamiltonian *`): the observable to estimate, see [Hamiltonian](../3_qis/7_hamiltonian.md) for construction. +- `shots_a` (`uintptr_t`): the shot count for the ZNE method (0 = unspecified); the numerator-circuit shot count for the virtual distillation method. +- `shots_b` (`uintptr_t`): ignored by the ZNE method; the denominator-circuit shot count for the virtual distillation method. +- `estimator` (`CEstimatorFn`): the estimator callback; see the [module overview](0_overview.md) for the contract. + +Returns `int32_t`: 0 on success; -1 (`ptr` or `hamiltonian` NULL); -8 (`method` is not a valid tag); -7 (`method` not matching the configured method, or other execution errors). + +--- + +## error_mitigation_get_mitigated(ptr, process_method, degree, out_expectation, out_variance) + +Executes the post-processing stage: produces the mitigated result after `error_mitigation_run`. + +- `ptr` (`struct CErrorMitigation *`): the unified pipeline handle. +- `process_method` (`uint8_t`): the post-processing method tag, see the table above. +- `degree` (`uintptr_t`): the polynomial degree for ZNE extrapolation; 0 selects it automatically. +- `out_expectation` (`double *`): receives the mitigated expectation value. +- `out_variance` (`double *`): receives the mitigated variance; NaN when unavailable. + +Returns `int32_t`: 0 on success; -1 (`ptr`, `out_expectation` or `out_variance` NULL); -8 (`process_method` is not a valid tag); -7 (`process_method` not matching the configured method, or other post-processing errors). + +--- + +## Example + +A complete flow with the virtual distillation method (the callback branches on the numerator circuit carrying the observable vs. the denominator circuit having none, see the [module overview](0_overview.md)): + +```c +#include +#include "cqlib_c.h" + +/* Estimator callback: the numerator circuit carries the observable + (exact simulation); the denominator circuit has none */ +static void estimate(const struct CCircuit *circuit, + const struct CHamiltonian *hamiltonian, + uintptr_t shots, + double *expectation, + double *variance) { + if (hamiltonian != NULL) { + struct CStatevector *sv = statevector_from_circuit(circuit); + double value = 0.0; + statevector_expectation(sv, hamiltonian, &value); + statevector_free(sv); + *expectation = value; + *variance = 0.0; + } else { + /* Denominator Tr(rho^M): estimated from measurement outcomes by a + sampling estimator; a sample value is returned here */ + *expectation = 1.0; + *variance = 0.0; + } + (void)shots; +} + +int main(void) { + struct CCircuit *qc = circuit_new(1); + circuit_x(qc, 0); + + /* Unified pipeline: virtual distillation with 3 copies */ + struct CErrorMitigation *em = + error_mitigation_new(qc, MITIGATION_VIRTUAL_DISTILLATION, NULL, 0, 3); + + struct CPauliString *z = pauli_string_parse("Z"); + struct CHamiltonian *obs = hamiltonian_from_pauli(z); /* takes ownership of z */ + + /* Sampling stage: numerator shots=512, denominator shots=256 */ + error_mitigation_run(em, MITIGATION_VIRTUAL_DISTILLATION, obs, 512, 256, estimate); + + /* Post-processing stage: the mitigated expectation and variance directly */ + double value = 0.0, variance = 0.0; + error_mitigation_get_mitigated(em, PROCESS_VIRTUAL_DISTILLATION, 0, + &value, &variance); + + error_mitigation_free(em); + hamiltonian_free(obs); + circuit_free(qc); + return 0; +} +``` + +For the `new → run → get_mitigated` flow with the ZNE method, see the [module overview](0_overview.md); for fine-grained folding and extrapolation control see [Zero-Noise Extrapolation](1_zne.md), and for separate numerator/denominator execution see [Virtual Distillation](2_virtual_distillation.md). diff --git a/docs/api/en/python/0_circuit/0_overview.md b/docs/api/en/python/0_circuit/0_overview.md new file mode 100644 index 00000000..dec0bf09 --- /dev/null +++ b/docs/api/en/python/0_circuit/0_overview.md @@ -0,0 +1,312 @@ +# Quantum Circuit + +`cqlib.circuit` + +`cqlib.circuit` is the core module in Cqlib for constructing, representing and analyzing quantum circuits. It provides a unified interface ranging from basic quantum gates, parameterized circuits, composite gates, measurement and classical data to structured control flow and matrix conversion, and is the main entry point for quantum program development with Cqlib. + +## Overview + +`cqlib.circuit` mainly covers the following capabilities: + +- **Basic circuit construction**: create a set of logical qubits and append operations such as quantum gates, measurement, reset and barrier in order. +- **Parameterized circuit modeling**: use `Parameter` to represent gate angles, global phase or other tunable parameters, suitable for scenarios such as VQE, QAOA, quantum machine learning and parameter sweeps. +- **Gate and instruction system**: describe standard gates, custom gates, multi-controlled gates and composite gates through objects such as `StandardGate`, `UnitaryGate`, `MCGate` and `CircuitGate`. +- **Operation-level intermediate representation**: use `Instruction`, `ValueInstruction` and `ValueOperation` to represent "what to do", "which qubits it acts on" and "which parameters it carries". +- **Classical data and control flow representation**: provide `ClassicalType`, `ClassicalVar`, `ClassicalExpr` and structured control flow interfaces for describing program structures such as conditional branches, loops and multi-way selection. +- **Matrix and symbolic matrix conversion**: support converting small-scale circuits made only of quantum gates into numeric matrices or symbolic matrices, for teaching, unit testing, gate validation and compiler rule checking. +- **Parameterized template circuits**: provide common circuit templates such as TwoLocal, feature map, QAOA and Pauli evolution through `cqlib.circuit.ansatz`. + +--- + +## Common entry points + +```python +from cqlib import Circuit, Parameter + +theta = Parameter("theta") + +circuit = Circuit(2) +circuit.h(0) +circuit.cx(0, 1) +circuit.rz(1, theta) + +print(circuit.used_symbols) # ['theta'] +bound = circuit.assign_parameters({"theta": 0.5}) +print(bound.used_symbols) # [] +``` + +--- + +## Core concepts and terms + +This page gives a brief description of terms commonly used in the `cqlib.circuit` documentation. For more detailed interface signatures, parameter constraints and examples, see the following API pages. + +| Term | Description | +| --- | --- | +| **abstract circuit** | A quantum circuit oriented toward algorithm description, representing a quantum program with logical qubits and high-level operations, not directly bound to a specific hardware topology or native gate set. | +| **physical circuit** | A circuit that has completed layout, routing and gate set conversion according to target device constraints, usually containing only the physical qubit connections and native operations supported by the backend. | +| **qubit** | The basic logical unit of quantum information. Represented by `Qubit` in the Cqlib Python API, and may also be abbreviated as an integer index in most interfaces. | +| **quantum gate** | A reversible quantum operation that usually has a unitary matrix representation. Standard gates are represented by `StandardGate`; custom gates may be represented by `UnitaryGate`, `MCGate` or `CircuitGate`. | +| **instruction** | The type of an operation in a circuit; it may be a quantum gate, or `barrier`, `measure`, `reset`, `delay`, a classical data operation or a control flow structure. | +| **operation** | A concrete application of an instruction in a circuit, containing the instruction type, the qubits acted on, the parameter list and an optional label. The Python construction layer usually represents it with `ValueOperation`. | +| **parameter** | A construction-time symbolic expression, commonly used for gate angles, global phase and variational circuit parameters. Represented by `Parameter`, and can later be bound to a numeric value through `assign_parameters()`. | +| **measurement** | A computational basis measurement operation, which produces a classical result. Measurement is a non-unitary operation and cannot directly participate in ordinary unitary matrix conversion. | +| **classical data** | Classical-side objects used in a circuit to express conditions, variables and measurement results, such as `ClassicalType`, `ClassicalVar`, `ClassicalValue` and `ClassicalExpr`. | +| **control flow** | The mechanism for organizing circuit structure based on classical expressions, such as `if_`, `while_`, `for_uint` and `switch`. Whether a specific backend supports the related semantics needs to be confirmed together with the compilation and execution flow. | +| **global phase** | The overall phase factor of a circuit. It usually does not affect individual measurement probabilities, but is still meaningful in circuit composition, controlled operations, matrix equivalence checking and compiler rewriting. | +| **Ansatz** | A parameterized circuit template, commonly used in variational algorithms and quantum machine learning. `cqlib.circuit.ansatz` provides several built-in templates. | + +--- + +## `cqlib.circuit` API Overview + +### 1. Core circuit container + +| Object | Page | Description | +| --- | --- | --- | +| `Circuit` | [Circuit](1_circuit.md) | The main quantum circuit container, used to create a set of qubits, append quantum gates and non-unitary instructions, bind parameters, compose circuits, decompose composite gates, construct control flow and generate matrices. | + +### 2. Qubits and parameters + +| Object | Page | Description | +| --- | --- | --- | +| `Qubit` | [Qubit](2_qubit.md) | A lightweight logical qubit handle that holds a non-negative integer index and is comparable and hashable. | +| `Parameter` | [Parameter](3_parameter.md) | A symbolic or numeric parameter expression, supporting parsing, evaluation, simplification, substitution, differentiation and conservative equivalence checks. | + +### 3. Operations, instructions and gates + +| Object | Page | Description | +| --- | --- | --- | +| `Instruction` | [Operation / Instruction](4_operation_instruction.md) | A storage-layer instruction, used to represent operation types such as standard gates, multi-controlled gates, custom unitary gates, sub-circuit gates, directives and delay. | +| `ValueInstruction` | [Operation / Instruction](4_operation_instruction.md) | A construction-layer instruction, which can wrap an ordinary `Instruction` or a control flow operation. | +| `ValueOperation` | [Operation / Instruction](4_operation_instruction.md) | A self-contained operation, containing an instruction, qubits, parameters and an optional label; suitable for serialization, importers and compiler output. | +| `Directive` | [Operation / Instruction](4_operation_instruction.md) | A non-unitary directive, including `barrier`, `measure` and `reset`. | + +### 4. Gate definitions + +| Object | Page | Description | +| --- | --- | --- | +| `StandardGate` | [StandardGate](5_gates_standard.md) | The set of native standard gates in Cqlib, including Pauli, Clifford, rotation gates, controlled gates, two-body interaction gates and fSim gates. | +| `UnitaryGate` | [UnitaryGate](6_gates_unitary.md) | A user-defined unitary gate, which can be defined by a numeric matrix, a symbolic matrix or a frozen circuit. | +| `MCGate` | [MCGate](7_gates_mc_gate.md) | A multi-controlled standard gate, which can add any number of control qubits on top of a `StandardGate`. | +| `FrozenCircuit` | [CircuitGate / FrozenCircuit](8_gates_circuit_gate.md) | An immutable circuit snapshot, usually used for composite gate definitions. | +| `CircuitGate` | [CircuitGate / FrozenCircuit](8_gates_circuit_gate.md) | A composite gate defined by a `FrozenCircuit`, which can be reused as a single gate in other circuits. | + +### 5. Built-in special instructions + +| Instruction | Construction | Description | +| --- | --- | --- | +| `barrier` | `Circuit.barrier(qubits)` or `Directive.barrier()` | Insert a compilation boundary that prevents operations on the related qubits from being reordered across it. | +| `measure` | `Circuit.measure(qubit)`, `Circuit.measure_bits(qubits)` | Perform a computational basis measurement, producing a `Measurement` receipt and a classical result. | +| `reset` | `Circuit.reset(qubit)` or `Directive.reset()` | Reset a qubit to `\|0>`, which usually destroys the existing coherence. | +| `delay` | `Circuit.delay(qubit, duration)` | Insert idle time or a scheduling delay on the given qubit. | +| `store` | `Circuit.store(target, value)` | Write a classical expression into a classical variable. | + +### 6. Classical data and control flow + +| Object | Page | Description | +| --- | --- | --- | +| `CircuitId` | [Classical / Control Flow](9_classical_control_flow.md) | The identity of a circuit-local classical handle, used to prevent classical variables and values from being misused across circuits. | +| `ClassicalType` | [Classical / Control Flow](9_classical_control_flow.md) | Classical data types, including bit, bool, uint and bit vector. | +| `ClassicalVar` | [Classical / Control Flow](9_classical_control_flow.md) | A mutable classical storage handle, created by `Circuit.var()`. | +| `ClassicalValue` | [Classical / Control Flow](9_classical_control_flow.md) | An immutable classical value, usually produced by measurement. | +| `Measurement` | [Classical / Control Flow](9_classical_control_flow.md) | A measurement receipt, containing the measured values and the order of the measured qubits. | +| `ClassicalExpr` | [Classical / Control Flow](9_classical_control_flow.md) | A typed classical expression AST, used for conditions, comparisons, bit extraction and expression composition. | +| `ClassicalControlOp` | [Classical / Control Flow](9_classical_control_flow.md) | The structured control flow IR, including `if`, `while`, `for`, `switch`, `break` and `continue`. | +| `ValueControlBody` | [Classical / Control Flow](9_classical_control_flow.md) | A construction-layer control flow body, containing several `ValueOperation`s. | +| `ValueSwitchCase` | [Classical / Control Flow](9_classical_control_flow.md) | A construction-layer `switch` case, containing a match value and a branch body. | + +### 7. Matrix and symbolic matrix utilities + +| Object | Page | Description | +| --- | --- | --- | +| `circuit_to_matrix` | [Circuit To Matrix](12_circuit_to_matrix.md) | A functional interface for computing the dense numeric matrix of a circuit. | +| `Circuit.to_matrix` | [Circuit To Matrix](12_circuit_to_matrix.md) | A method-style interface, equivalent to `circuit_to_matrix()`. | +| `Circuit.to_symbolic_matrix` | [Circuit To Matrix](12_circuit_to_matrix.md) | Generate a symbolic matrix that preserves `Parameter` expressions. | +| `SymbolicComplex` | [SymbolicMatrix](10_symbolic_matrix.md) | A symbolic complex number whose real and imaginary parts are both `Parameter`. | +| `SymbolicMatrix` | [SymbolicMatrix](10_symbolic_matrix.md) | A dense symbolic matrix, suitable for small-scale circuit analysis and custom symbolic gates. | + +### 8. Circuit templates and ansatz + +| Object | Page | Description | +| --- | --- | --- | +| `EntanglementTopology` | [Ansatz](11_ansatz.md) | Entanglement topology, including linear, circular, full and custom. | +| `TwoLocal` | [Ansatz](11_ansatz.md) | A hardware-friendly ansatz alternating rotation layers and entanglement layers. | +| `AngleEncoding`, `BasisEncoding` | [Ansatz](11_ansatz.md) | Basic data encoding circuits. | +| `ZFeatureMap`, `IQPFeatureMap`, `ZZFeatureMap`, `PauliFeatureMap` | [Ansatz](11_ansatz.md) | Quantum machine learning feature map templates. | +| `BasicEntanglerLayers`, `StronglyEntanglingLayers` | [Ansatz](11_ansatz.md) | Common layered trainable circuit templates. | +| `QAOAAnsatz` | [Ansatz](11_ansatz.md) | The alternating QAOA cost/mixer structure. | +| `EvolutionStrategy`, `EvolutionInfo`, `PauliEvolutionAnsatz` | [Ansatz](11_ansatz.md) | Hamiltonian time evolution circuit templates. | +| `real_amplitudes`, `efficient_su2`, `zz_feature_map`, `pauli_feature_map` | [Ansatz](11_ansatz.md) | Convenience constructors for common templates. | + +--- + +## Circuit representation + +`Circuit` internally records the set of qubits, parameter information, classical data and the operation sequence at the same time. + +```text +Circuit +├── id: CircuitId +├── qubits: list[Qubit] +├── parameters: list[Parameter] +├── symbols: list[str] +├── global_phase: Parameter +├── classical_vars: list[ClassicalType] +├── classical_values: list[ClassicalType] +└── operations: list[ValueOperation] + ├── instruction: ValueInstruction + │ ├── Instruction(...) + │ └── ClassicalControlOp(...) + ├── qubits: list[Qubit] + ├── params: list[float | Parameter] + └── label: str | None +``` + +### 1. Quantum data + +Quantum data is identified by `Qubit`. `Circuit(3)` creates the logical qubits `0`, `1` and `2`; `Circuit([0, 2, 4])` creates sparse logical indices. + +```python +from cqlib import Circuit, Qubit + +circuit = Circuit([Qubit(10), Qubit(20)]) +assert circuit.num_qubits == 2 +``` + +### 2. Operations and instructions + +A circuit operation is determined jointly by "what to do" and "where it acts". `Instruction` describes the operation type, for example an `H` gate, an `RZ` gate, a measurement or control flow; `ValueOperation` further binds the qubits acted on, the parameters and an optional label. + +```python +from cqlib import Circuit, Qubit +from cqlib.circuit import ValueOperation +from cqlib.circuit.gates import StandardGate + +op = ValueOperation.from_standard_gate(StandardGate.H, [Qubit(0)]) +circuit = Circuit.from_operations([Qubit(0)], [op]) +``` + +This layered representation suits import and export, deserialization, compiler pass output and low-level testing. For ordinary users, directly using convenience methods such as `Circuit.h()` and `Circuit.cx()` is usually simpler. + +### 3. Parameters + +`Parameter` is used to construct parameterized circuits. A circuit automatically collects the free symbols that appear in it, for later numeric binding through `assign_parameters()`. + +```python +from cqlib import Circuit, Parameter + +theta = Parameter("theta") +circuit = Circuit(1) +circuit.rx(0, theta) + +assert circuit.symbols == ["theta"] +bound = circuit.assign_parameters({"theta": 0.5}) +``` + +### 4. Classical data and control flow + +Classical data is represented by objects such as `ClassicalType`, `ClassicalVar`, `ClassicalValue` and `ClassicalExpr`. + +```python +from cqlib import Circuit + +circuit = Circuit(2) +measurement = circuit.measure(0) + +circuit.if_( + measurement.expr().to_bool(), + lambda body: body.x(1), +) +``` + +--- + +## Quick examples + +### 1. Bell state preparation + +```python +from cqlib import Circuit + +circuit = Circuit(2) +circuit.h(0) +circuit.cx(0, 1) + +matrix = circuit.to_matrix() +``` + +This example creates a two-qubit Bell state circuit and computes its small-scale matrix representation. + +### 2. Parameterized rotation layer + +```python +from cqlib import Circuit, Parameter + +theta = Parameter("theta") +phi = Parameter("phi") + +circuit = Circuit(1) +circuit.rx(0, theta) +circuit.rz(0, theta + phi / 2) + +bound = circuit.assign_parameters({"theta": 0.1, "phi": 0.2}) +``` + +This example shows how to use `Parameter` to construct a parameterized circuit template that can be bound repeatedly. + +### 3. Reusable sub-circuit gate + +```python +from cqlib import Circuit, Parameter + +theta = Parameter("theta") + +block = Circuit(2) +block.rx(0, theta) +block.cx(0, 1) + +block_gate = block.to_gate("Block") + +main = Circuit(4) +main.append_circuit_gate(block_gate, [0, 1], params=[Parameter("a")]) +main.append_circuit_gate(block_gate, [2, 3], params=[Parameter("b")]) +``` + +This example wraps a parameterized sub-circuit as a `CircuitGate` and reuses it in a larger circuit. + +### 4. Conditional structure based on measurement results + +```python +from cqlib import Circuit + +circuit = Circuit(2) +circuit.h(0) +result = circuit.measure(0) + +circuit.if_( + result.expr().to_bool(), + lambda body: body.x(1), +) +``` + +This example shows how to construct a conditional branch based on a measurement result. Whether an actual backend supports such a structure needs to be confirmed together with the target device and the compilation flow. + +--- + +## Validation and error handling + +High-level `Circuit` methods check common errors as early as possible, for example a nonexistent qubit, duplicate qubits, a mismatch in the number of qubits a gate acts on, or a mismatch in the number of parameters. For circuits imported from an external IR, generated automatically by a program or assembled by hand, explicitly call: + +```python +circuit.validate() +``` + +Common exceptions include: + +| Exception | When it occurs | +| --- | --- | +| `CircuitError` | The circuit structure or an operation is invalid, for example a nonexistent qubit, a wrong gate target, computing a matrix for a non-unitary operation, or a control flow scope error. | +| `ParameterError` | Parsing, binding, simplification or evaluation of a parameter expression failed. | +| `QubitError` | A qubit index is invalid, for example negative, beyond the internal range, or of an incorrect input type. | +| `CqlibError` | The base class of Cqlib-specific exceptions, which can be used to catch Cqlib-related exceptions uniformly. | diff --git a/docs/api/en/python/0_circuit/10_symbolic_matrix.md b/docs/api/en/python/0_circuit/10_symbolic_matrix.md new file mode 100644 index 00000000..173dbe62 --- /dev/null +++ b/docs/api/en/python/0_circuit/10_symbolic_matrix.md @@ -0,0 +1,220 @@ +# SymbolicMatrix + +- `cqlib.circuit.SymbolicComplex` +- `cqlib.circuit.SymbolicMatrix` + +```python +from cqlib.circuit import SymbolicComplex, SymbolicMatrix +``` + +`SymbolicMatrix` is the basic type in Cqlib for representing symbolic matrices. It is composed of several `SymbolicComplex` elements, and the real and imaginary parts of each `SymbolicComplex` can hold `Parameter` expressions. Through this design, Cqlib can keep symbolic parameters in matrix elements instead of evaluating them immediately into ordinary complex numbers when the matrix is constructed. + +## Core concepts + +In Cqlib, a symbolic matrix consists of two levels of structure: + +| Type | Role | +| --- | --- | +| `Parameter` | Represents a number, a symbolic variable or a parameter expression. | +| `SymbolicComplex` | Represents one complex element, whose real and imaginary parts are both `Parameter`. | +| `SymbolicMatrix` | Represents a dense matrix composed of `SymbolicComplex` elements. | + +Elements in an ordinary numeric matrix are usually `complex`, whereas elements in a `SymbolicMatrix` can contain symbolic parameters. For example, a matrix element can be `exp(i * theta)`, `cos(theta)` or an expression composed of several parameters. This allows the same matrix structure to be evaluated repeatedly under different parameter values, and also makes it easy to preserve parameter relations at compilation and validation stages. + +## `SymbolicComplex` + +`SymbolicComplex` represents a symbolic complex number. Its real and imaginary parts are both `Parameter`, so it can represent either an ordinary complex number or a complex expression containing symbolic parameters. + +```python +SymbolicComplex(real: Parameter, imag: Parameter) +``` + +For example, a symbolic complex number can be written as: + +```text +real + i * imag +``` + +Here both `real` and `imag` can be constants, a single symbol or a more complex parameter expression. + +### 1. Construction methods + +`SymbolicComplex` provides a set of common static methods for quickly creating symbolic complex numbers such as zero, the imaginary unit, pure real numbers and phase factors. + +| Static method | Description | +| --- | --- | +| `zero()` | Create `0 + 0i`. | +| `one()` | Create `1 + 0i`. | +| `i()` | Create `0 + 1i`. | +| `from_real(value)` | Create a pure real complex number from a real number or a parameter expression. | +| `exp_i(theta)` | Create `exp(i * theta)`, that is, `cos(theta) + i sin(theta)`. | + +```python +from cqlib import Parameter +from cqlib.circuit import SymbolicComplex + +theta = Parameter("theta") + +zero = SymbolicComplex.zero() +one = SymbolicComplex.one() +imag = SymbolicComplex.i() +phase = SymbolicComplex.exp_i(theta) +``` + +### 2. Attributes and methods + +| Interface | Returns | Description | +| --- | --- | --- | +| `real` | `Parameter` | The real part of the symbolic complex number. | +| `imag` | `Parameter` | The imaginary part of the symbolic complex number. | +| `symbols` | `list[str]` | The free symbol names contained in the real and imaginary parts. | +| `evaluate(bindings=None)` | `complex` | Evaluate the symbolic complex number into a `complex` according to parameter bindings. | +| `simplify()` | `SymbolicComplex` | Simplify the real and imaginary part expressions. | +| `replace(symbol, value)` | `SymbolicComplex` | Replace the given symbol with a new parameter expression. | +| `is_zero_exact()` | `bool` | Determine whether it is syntactically exactly equal to `0`. | +| `is_one_exact()` | `bool` | Determine whether it is syntactically exactly equal to `1`. | +| `simplifies_to_zero()` | `bool` | Determine whether it equals `0` after simplification. | + +```python +from cqlib import Parameter +from cqlib.circuit import SymbolicComplex + +theta = Parameter("theta") +value = SymbolicComplex.exp_i(theta) + +numeric = value.evaluate({"theta": 0.0}) +print(numeric) # (1+0j) +``` + +## `SymbolicMatrix` + +`SymbolicMatrix` represents a dense matrix composed of `SymbolicComplex` elements. The matrix is passed in by rows, and every row should have the same length. + +```python +SymbolicMatrix(rows: list[list[SymbolicComplex]]) +``` + +The following example constructs a simple `2 × 2` symbolic matrix: + +```python +from cqlib.circuit import SymbolicComplex, SymbolicMatrix + +i = SymbolicComplex.i() +zero = SymbolicComplex.zero() + +matrix = SymbolicMatrix([ + [zero, i], + [i, zero], +]) +``` + +### 1. Attributes and methods + +| Interface | Returns | Description | +| --- | --- | --- | +| `shape` | `tuple[int, int]` | The matrix shape, in the form `(rows, cols)`. | +| `symbols` | `list[str]` | The free symbol names contained in all matrix elements. | +| `evaluate(bindings=None)` | `numpy.ndarray` | Evaluate the symbolic matrix into a numeric complex matrix. | +| `simplify()` | `SymbolicMatrix` | Simplify all elements in the matrix. | +| `substitute(replacements)` | `SymbolicMatrix` | Replace several symbols in the matrix at the same time. | +| `rows()` | `list[list[SymbolicComplex]]` | Return the nested row representation of the matrix. | +| `__getitem__((row, col))` | `SymbolicComplex` | Read the element at the given position; negative indices are supported. | +| `__len__()` | `int` | Return the number of matrix rows. | + +```python +import numpy as np +from cqlib import Parameter +from cqlib.circuit import SymbolicComplex, SymbolicMatrix + +theta = Parameter("theta") + +m = SymbolicMatrix([ + [SymbolicComplex.one(), SymbolicComplex.zero()], + [SymbolicComplex.zero(), SymbolicComplex.exp_i(theta)], +]) + +numeric = m.evaluate({"theta": np.pi}) +print(numeric.shape) # (2, 2) +``` + +### 2. Simplification, substitution and evaluation + +`SymbolicMatrix` supports simplification, symbolic substitution and numeric evaluation of the parameter expressions in matrix elements. These operations do not modify the original matrix object; they return a new symbolic matrix or a numeric matrix result. + +```python +from cqlib import Parameter +from cqlib.circuit import SymbolicComplex, SymbolicMatrix + +theta = Parameter("theta") +phi = Parameter("phi") + +m = SymbolicMatrix([ + [SymbolicComplex.one(), SymbolicComplex.zero()], + [SymbolicComplex.zero(), SymbolicComplex.exp_i(theta)], +]) + +renamed = m.substitute({"theta": phi}) +simplified = renamed.simplify() +numeric = simplified.evaluate({"phi": 0.5}) +``` + +### 3. Creating parameterized custom gates + +A typical use of `SymbolicMatrix` is defining parameterized custom unitary gates. Through `UnitaryGate.with_symbolic_matrix()`, a symbolic matrix can be registered as the matrix definition of a custom gate, and the parameter order for applying the gate can be specified. + +```python +from cqlib import Circuit, Parameter +from cqlib.circuit import SymbolicComplex, SymbolicMatrix +from cqlib.circuit.gates import UnitaryGate + +theta = Parameter("theta") + +symbolic = SymbolicMatrix([ + [SymbolicComplex.one(), SymbolicComplex.zero()], + [SymbolicComplex.zero(), SymbolicComplex.exp_i(theta)], +]) + +gate = UnitaryGate("DiagPhase", 1, num_params=1).with_symbolic_matrix( + symbolic, + ["theta"], +) + +circuit = Circuit(1) +circuit.append_unitary_gate(gate, [0], params=[Parameter("phi")]) +``` + +In the example above, `with_symbolic_matrix(symbolic, ["theta"])` means the custom gate has one positional parameter, corresponding to `theta` in the symbolic matrix. When the gate is appended to a circuit through `append_unitary_gate()`, the `params=[Parameter("phi")]` passed in replaces `theta` in the gate definition with the external parameter `phi` according to the parameter order. + +### 4. Obtaining a symbolic matrix from a circuit + +Besides constructing a symbolic matrix by hand, a symbolic matrix can also be generated from a parameterized circuit. `Circuit.to_symbolic_matrix(qubits_order=None)` converts a circuit into a matrix representation that preserves parameter expressions. + +```python +Circuit.to_symbolic_matrix(qubits_order=None) -> SymbolicMatrix +``` + +```python +from cqlib import Circuit, Parameter + +theta = Parameter("theta") + +circuit = Circuit(1) +circuit.rx(0, theta) + +matrix = circuit.to_symbolic_matrix() +print(matrix.symbols) +``` + +When a circuit contains symbolic parameters, `to_symbolic_matrix()` can preserve those parameter structures, which suits small-scale parameterized circuit validation, gate definition checks and compiler rule testing. + +### 5. Complexity and applicable boundary + +For a circuit with `n` qubits, the full matrix size is `2^n × 2^n` and the number of matrix elements is `4^n`. When matrix elements also contain symbolic expressions, the expression complexity can also grow with the number of gates and parameters. + +--- + +## Other behavior + +- Both `SymbolicComplex` and `SymbolicMatrix` support `copy` and `deepcopy`. +- `SymbolicComplex` supports `==` comparison; `SymbolicMatrix` does not provide equality comparison, so to compare two matrices, the results of `rows()` can be compared. +- Both carry a `repr` convenient for debugging. diff --git a/docs/api/en/python/0_circuit/11_ansatz.md b/docs/api/en/python/0_circuit/11_ansatz.md new file mode 100644 index 00000000..d473d254 --- /dev/null +++ b/docs/api/en/python/0_circuit/11_ansatz.md @@ -0,0 +1,458 @@ +# Ansatz + +`cqlib.circuit.ansatz` + +```python +from cqlib.circuit.ansatz import ( + EntanglementTopology, + TwoLocal, + BasicEntanglerLayers, + StronglyEntanglingLayers, + AngleEncoding, + BasisEncoding, + ZFeatureMap, + IQPFeatureMap, + ZZFeatureMap, + PauliFeatureMap, + QAOAAnsatz, + EvolutionStrategy, + EvolutionInfo, + PauliEvolutionAnsatz, + real_amplitudes, + efficient_su2, + zz_feature_map, + pauli_feature_map, +) +``` + +All names exported by the module are listed above, including the four convenience functions `real_amplitudes`, `efficient_su2`, `zz_feature_map` and `pauli_feature_map`, and `EvolutionInfo`. + +`cqlib.circuit.ansatz` provides a set of reusable parameterized circuit templates, mainly for scenarios such as variational quantum algorithms, quantum machine learning, feature maps, QAOA and Hamiltonian evolution. These templates allow structured quantum circuits to be generated quickly, without adding rotation gates and entanglement gates layer by layer by hand. + +--- + +## General template interfaces + +Ansatz templates mainly provide the following interfaces: + +| Method | Description | +| --- | --- | +| `validate()` | Check whether the template configuration is legal, for example whether the qubit count, gate types, topology and parameter settings match. | +| `build_circuit(prefix)` | Build a `Circuit` from the current configuration and generate parameter names with the given prefix. | +| `num_parameters()` | Return the number of parameters the template requires. | +| `num_qubits()` | Return the number of qubits the template acts on. | + +```python +from cqlib.circuit.ansatz import TwoLocal + +ansatz = TwoLocal(3) +ansatz.validate() + +circuit = ansatz.build_circuit("theta") + +assert len(circuit.symbols) == ansatz.num_parameters() +assert circuit.num_qubits == ansatz.num_qubits() +``` + +`validate()` raises `ValueError` when the configuration is illegal, for example when the qubit count, gate types, topology or parameter settings do not match each other; `build_circuit()` likewise raises `ValueError` in the same situations. Every template object supports `copy` and `deepcopy`, and carries a `repr` and a `str` convenient for debugging. + +--- + +## Immutable builder pattern + +Most configuration interfaces in the ansatz module use chained calls. For example: + +```python +from cqlib.circuit.ansatz import TwoLocal, EntanglementTopology +from cqlib.circuit.gates import StandardGate + +ansatz = ( + TwoLocal(4) + .reps(2) + .rotation_gates([StandardGate.RY, StandardGate.RZ]) + .entanglement_gate(StandardGate.CX) + .entanglement(EntanglementTopology.linear()) +) +``` + +Note that such configuration methods usually do not modify the original object but return an object with the new configuration. Therefore, several different configurations can safely be derived from the same base template: + +```python +base = TwoLocal(4) + +linear = base.entanglement(EntanglementTopology.linear()) +full = base.entanglement(EntanglementTopology.full()) +``` + +In the code above, `linear` and `full` are two template objects with different configurations, and `base` itself stays unchanged. + +--- + +## `EntanglementTopology` + +`EntanglementTopology` describes the topology on which two-qubit gates or multi-qubit terms act in a circuit. It determines between which qubits the template should establish connections when constructing an entanglement layer, feature map multi-body terms or Pauli evolution terms. + +```python +EntanglementTopology.linear() +EntanglementTopology.circular() +EntanglementTopology.full() +EntanglementTopology.custom(pairs) +``` + +| Topology | Description | +| --- | --- | +| `linear()` | Nearest-neighbor chain connection, for example `(0, 1), (1, 2), ...`. | +| `circular()` | Adds a connection between the ends on top of `linear`, for example `(n-1, 0)`. | +| `full()` | Fully connected topology, in which any two qubits can be connected. | +| `custom(pairs)` | User-defined connection pairs, suitable for scenarios where the hardware topology or the problem graph is known. | + +Common methods are as follows: + +| Method | Description | +| --- | --- | +| `generate_pairs(num_qubits)` | Generate two-qubit connection pairs according to the topology. | +| `generate_k_tuples(k, num_qubits)` | Generate groups of `k`-local qubits according to the topology. | + +```python +from cqlib.circuit.ansatz import EntanglementTopology + +topology = EntanglementTopology.linear() +pairs = topology.generate_pairs(4) + +assert pairs == [(0, 1), (1, 2), (2, 3)] +``` + +--- + +## `TwoLocal` + +`TwoLocal` is a common hardware-friendly ansatz template, composed of alternating single-qubit rotation layers and two-qubit entanglement layers. It suits VQE, classification models, regression models and general variational circuit experiments. + +```python +TwoLocal(num_qubits: int) +``` + +Common configuration methods are as follows: + +| Method | Description | +| --- | --- | +| `reps(n)` | Set the number of repeated layers. | +| `rotation_gates(gates)` | Set the single-qubit parameterized gates used in each layer. | +| `entanglement_gate(gate)` | Set the two-qubit gate used in the entanglement layer. | +| `entanglement(topology)` | Set the entanglement topology. | +| `skip_final_rotation_layer(skip)` | Set whether to skip the final rotation layer. | + +```python +from cqlib.circuit.ansatz import TwoLocal, EntanglementTopology +from cqlib.circuit.gates import StandardGate + +ansatz = ( + TwoLocal(3) + .reps(2) + .rotation_gates([StandardGate.RY, StandardGate.RZ]) + .entanglement_gate(StandardGate.CX) + .entanglement(EntanglementTopology.linear()) +) + +circuit = ansatz.build_circuit("theta") +``` + +The ansatz module also provides convenience functions for common `TwoLocal` configurations: + +```python +real_amplitudes(num_qubits, reps, entanglement) -> TwoLocal +efficient_su2(num_qubits, reps, entanglement) -> TwoLocal +``` + +| Function | Default structure | Typical use | +| --- | --- | --- | +| `real_amplitudes` | `RY` rotations + `CX` entanglement | Real-amplitude ansatz, VQE, simple classification tasks. | +| `efficient_su2` | `RY` / `RZ` rotations + `CX` entanglement | A more expressive general hardware-friendly ansatz. | + +--- + +## Feature Map + +A feature map encodes classical input data into a quantum circuit and is an important part of quantum machine learning and quantum kernel methods. Different feature maps use different encoding approaches, for example treating input features as rotation angles, as computational basis states, or as phase parameters in a Pauli evolution. + +Usually the parameters in a feature map represent input data features. In actual training, a feature map can be used in combination with a trainable ansatz. + +--- + +## `AngleEncoding` + +```python +AngleEncoding(num_qubits: int, rotation_gate: StandardGate) +``` + +`AngleEncoding` encodes each input feature as a single-qubit rotation angle. Its structure is simple and its parameters are intuitive, so it suits quickly constructing input encoding circuits in quantum machine learning. + +```python +from cqlib.circuit.ansatz import AngleEncoding +from cqlib.circuit.gates import StandardGate + +encoding = AngleEncoding(4, StandardGate.RX) +circuit = encoding.build_circuit("x") +``` + +The example above generates one input parameter prefixed with `x` for each of the 4 qubits and performs angle encoding through the `RX` gate. + +--- + +## `BasisEncoding` + +```python +BasisEncoding(bits: list[bool]) +``` + +`BasisEncoding` prepares a computational basis state from a given bitstring. Unlike angle encoding, it introduces no continuous parameters and instead decides from the Boolean values whether to apply an `X` gate to the corresponding qubit. + +```python +from cqlib.circuit.ansatz import BasisEncoding + +encoding = BasisEncoding([True, False, True]) +circuit = encoding.build_circuit("unused") + +assert encoding.num_parameters() == 0 +``` + +--- + +## `ZFeatureMap` + +```python +ZFeatureMap(num_qubits).reps(n) +``` + +`ZFeatureMap` is a first-order Pauli-Z feature map; it usually introduces one input parameter per qubit and encodes features through phase evolution in the Z direction. Its number of parameters usually equals the number of qubits. + +--- + +## `IQPFeatureMap` + +```python +IQPFeatureMap(num_qubits).reps(n).entanglement(topology) +``` + +`IQPFeatureMap` is an IQP-style diagonal feature map. It usually contains single-body feature terms and multi-body interaction terms, and controls the encoding structure through repeated layers and entanglement topology. By default, this template usually uses a multi-layer structure and fairly strong entanglement connections. + +```python +from cqlib.circuit.ansatz import IQPFeatureMap, EntanglementTopology + +fm = ( + IQPFeatureMap(3) + .reps(2) + .entanglement(EntanglementTopology.full()) +) + +circuit = fm.build_circuit("x") +``` + +--- + +## `ZZFeatureMap` + +```python +ZZFeatureMap(num_qubits).reps(n).entanglement(topology) +``` + +`ZZFeatureMap` is a second-order Pauli-Z feature map, usually containing single-qubit Z terms and two-qubit ZZ interaction terms. + +```python +from cqlib.circuit.ansatz import ZZFeatureMap, EntanglementTopology + +fm = ZZFeatureMap(3).reps(2).entanglement(EntanglementTopology.full()) +circuit = fm.build_circuit("x") +``` + +Convenience function: + +```python +zz_feature_map(num_qubits, reps, entanglement) -> ZZFeatureMap +``` + +--- + +## `PauliFeatureMap` + +```python +ansatz = ( + PauliFeatureMap(num_qubits) + .reps(n) + .paulis(paulis) + .entanglement(topology) + .parameter_prefix(prefix) +) +``` + +`PauliFeatureMap` supports constructing a feature map from arbitrary Pauli string templates. Single-body terms, multi-body terms and the corresponding entanglement topology can be specified, so feature interactions of different orders can be expressed flexibly. + +```python +from cqlib.qis import PauliString +from cqlib.circuit.ansatz import PauliFeatureMap, EntanglementTopology + +fm = ( + PauliFeatureMap(3) + .reps(2) + .paulis([PauliString.from_str("Z"), PauliString.from_str("ZZ")]) + .entanglement(EntanglementTopology.full()) +) + +circuit = fm.build_circuit("x") +``` + +Convenience function: + +```python +pauli_feature_map(num_qubits, reps, paulis, entanglement) -> PauliFeatureMap +``` + +When custom feature interaction forms are needed, `PauliFeatureMap` is more flexible than the fixed-structure `ZFeatureMap` or `ZZFeatureMap`. + +--- + +## Layer templates + +Layer templates construct common trainable parameterized layers. Unlike feature maps, the parameters in these templates are usually variables that an optimizer needs to train. + +### 1. `BasicEntanglerLayers` + +```python +ansatz = ( + BasicEntanglerLayers(num_qubits) + .reps(n) + .rotation_gate(gate) + .entanglement_gate(gate) +) +``` + +`BasicEntanglerLayers` represents the basic "single-qubit rotation + ring entanglement" layer structure. Each layer usually applies parameterized rotation gates on the qubits first and then applies entanglement gates in a fixed ring structure. + +--- + +### 2. `StronglyEntanglingLayers` + +```python +ansatz = ( + StronglyEntanglingLayers(num_qubits) + .reps(n) + .entanglement_gate(gate) + .ranges(ranges) +) +``` + +`StronglyEntanglingLayers` represents a more expressive entanglement layer template. It usually uses general single-qubit rotation gates and a ranged ring entanglement structure. `ranges` specifies the connection span of each layer, so that different layers can use different entanglement ranges. + +```python +from cqlib.circuit.ansatz import StronglyEntanglingLayers + +ansatz = StronglyEntanglingLayers(4).reps(3).ranges([1, 2]) +circuit = ansatz.build_circuit("w") +``` + +--- + +## `QAOAAnsatz` + +```python +QAOAAnsatz(cost_operator: Hamiltonian) +``` + +`QAOAAnsatz` constructs a QAOA circuit. In terms of circuit structure, QAOA alternately applies the time evolution of the cost Hamiltonian and of the mixer Hamiltonian, and is generally used for combinatorial optimization problems. + +Its basic form can be written as: + +```text +U(beta, gamma) = product_l exp(-i beta_l H_M) exp(-i gamma_l H_C) +``` + +Here `H_C` is the problem Hamiltonian, `H_M` is the mixer Hamiltonian, and `gamma_l` and `beta_l` are the variational parameters in the `l`-th layer. + +Common configuration methods are as follows: + +| Method | Description | +| --- | --- | +| `reps(n)` | Set the number of QAOA layers `p`; the total number of parameters is usually `2 * p`. | +| `mixer(mixer_operator)` | Set a custom mixer Hamiltonian. | +| `initial_state(circuit)` | Set the initial state preparation circuit. | +| `evolution_strategy(strategy)` | Set the Hamiltonian evolution strategy. | + +```python +from cqlib.qis import Hamiltonian, PauliString +from cqlib.circuit.ansatz import QAOAAnsatz + +h_c = Hamiltonian(2) +h_c.add_term(PauliString.from_str("ZZ"), 0.5) + +ansatz = QAOAAnsatz(h_c).reps(3) +circuit = ansatz.build_circuit("qaoa") + +assert ansatz.num_parameters() == 6 +``` + +--- + +## Hamiltonian Evolution + +Hamiltonian evolution templates construct quantum circuits of the form `exp(-i H t)`. For a Hamiltonian composed of Pauli terms, an exact evolution or a Trotter decomposition strategy can be chosen according to whether the terms commute. + +### 1. `EvolutionStrategy` + +`EvolutionStrategy` specifies how the circuit for a Hamiltonian evolution is decomposed. + +| Static method | Description | +| --- | --- | +| `exact()` | Use an exact product of Pauli rotations for a Hamiltonian whose terms commute pairwise; if they do not commute, an error is reported when the circuit is built. | +| `auto(steps=1)` | Automatically choose exact evolution or a first-order Trotter decomposition. | +| `trotter(mode, steps)` | Explicitly specify the Trotter-Suzuki decomposition mode and the number of steps. | + +```python +from cqlib.circuit.ansatz import EvolutionStrategy +from cqlib.qis import TrotterMode + +exact = EvolutionStrategy.exact() +auto = EvolutionStrategy.auto(steps=4) +second = EvolutionStrategy.trotter(TrotterMode.second_order(), steps=8) +``` + +--- + +### 2. `EvolutionInfo` + +`EvolutionInfo` is returned by `PauliEvolutionAnsatz.evolution_info()` and describes the current Hamiltonian evolution strategy and the actual decomposition information. + +| Attribute | Description | +| --- | --- | +| `is_exact` | Whether the current decomposition is a mathematically exact evolution. | +| `steps` | The number of decomposition steps actually used. | +| `trotter_mode` | The current Trotter mode; `None` when the exact strategy is used. | +| `all_terms_commute` | Whether the terms in the Hamiltonian commute pairwise. | +| `num_terms` | The number of Pauli terms after simplification. | + +--- + +### 3. `PauliEvolutionAnsatz` + +```python +ansatz = ( + PauliEvolutionAnsatz(hamiltonian) + .with_strategy(strategy) + .with_time_param_name(name) +) +``` + +`PauliEvolutionAnsatz` constructs a parameterized evolution circuit for `exp(-i H t)`. The generated circuit usually contains a single time parameter whose default name is `{prefix}_t`, and which can also be customized through `with_time_param_name()`. + +```python +from cqlib.qis import Hamiltonian, PauliString +from cqlib.circuit.ansatz import PauliEvolutionAnsatz, EvolutionStrategy + +h = Hamiltonian(2) +h.add_term(PauliString.from_str("ZZ"), 0.5) +h.add_term(PauliString.from_str("ZI"), 0.3) + +ansatz = PauliEvolutionAnsatz(h).with_strategy(EvolutionStrategy.auto()) +info = ansatz.evolution_info() +circuit = ansatz.build_circuit("evo") +``` + diff --git a/docs/api/en/python/0_circuit/12_circuit_to_matrix.md b/docs/api/en/python/0_circuit/12_circuit_to_matrix.md new file mode 100644 index 00000000..f856aae2 --- /dev/null +++ b/docs/api/en/python/0_circuit/12_circuit_to_matrix.md @@ -0,0 +1,200 @@ +# Circuit To Matrix + +- `cqlib.circuit.circuit_to_matrix` +- `cqlib.circuit.Circuit.to_matrix` +- `cqlib.circuit.Circuit.to_symbolic_matrix` + +```python +from cqlib.circuit import circuit_to_matrix +``` + +The `Circuit To Matrix` interfaces convert a quantum circuit into a matrix representation. Cqlib provides both numeric matrix and symbolic matrix capabilities: numeric matrices give a definite complex unitary matrix, while symbolic matrices preserve the `Parameter` expressions in a circuit. + +--- + +## Overview + +Cqlib provides the following three common entry points: + +| Interface | Returns | Description | +| --- | --- | --- | +| `Circuit.to_matrix(qubits_order=None)` | `numpy.ndarray[np.complex128]` | Convert the whole circuit into a numeric complex matrix. | +| `circuit_to_matrix(circuit, qubits_order=None)` | `numpy.ndarray[np.complex128]` | A functional entry equivalent to `Circuit.to_matrix()`. | +| `Circuit.to_symbolic_matrix(qubits_order=None)` | `SymbolicMatrix` | Convert the circuit into a symbolic matrix that preserves symbolic parameters. | + +## Numeric matrix conversion + +Numeric matrix conversion converts a circuit made only of quantum gates into a definite dense complex matrix. This matrix describes the overall linear transformation the circuit applies to a quantum state. + +```python +Circuit.to_matrix( + qubits_order: list[int] | None = None, +) -> np.ndarray[np.complex128] + +circuit_to_matrix( + circuit: Circuit, + qubits_order: list[int] | None = None, +) -> np.ndarray[np.complex128] +``` + +This conversion applies only to circuits made only of quantum gates. If unbound symbolic parameters remain in the circuit, or the circuit contains measurement, classical data operations or control flow, both `Circuit.to_matrix()` and `circuit_to_matrix()` raise `CircuitError`. + +The following example constructs a Bell circuit and computes the matrix through the object method and the functional entry respectively: + +```python +from cqlib import Circuit +from cqlib.circuit import circuit_to_matrix + +circuit = Circuit(2) +circuit.h(0) +circuit.cx(0, 1) + +a = circuit.to_matrix() +b = circuit_to_matrix(circuit) + +assert (a == b).all() +``` + +## Output shape + +If a circuit contains `n` qubits, the shape of the full matrix is: + +```text +(2**n, 2**n) +``` + +For example, a 3-qubit circuit corresponds to an `8 × 8` matrix: + +```python +from cqlib import Circuit + +circuit = Circuit(3) +matrix = circuit.to_matrix() + +assert matrix.shape == (8, 8) +``` + +Note that the matrix dimension depends on the number of qubits in the circuit. For example, `Circuit([0, 10])` is still a two-qubit circuit and its matrix shape is `(4, 4)`. + +## Qubit order + +`qubits_order` specifies the qubit arrangement order in the matrix representation. By default, Cqlib generates the matrix in the qubit order kept internally by the circuit. For contiguously numbered circuits this usually matches user intuition; but for circuits with sparse logical indices or a manually specified qubit order, it is advisable to pass `qubits_order` explicitly to avoid ambiguity in matrix interpretation. + +```python +from cqlib import Circuit + +circuit = Circuit([0, 2]) +circuit.cx(0, 2) + +default = circuit.to_matrix() +reordered = circuit.to_matrix([2, 0]) + +print(default.shape) +print(reordered.shape) +``` + +In the example above, the circuit contains only the logical qubits `0` and `2`, so the matrix is still a two-qubit matrix. `qubits_order=[2, 0]` means the reversed logical qubit order is used for the tensor axes of the matrix. + +Using `qubits_order` must satisfy the following requirements: + +- all qubits passed in must belong to the current circuit; +- the list length should agree with the number of qubits in the circuit; +- duplicate qubits are not allowed; + +## Parameterized circuits + +A numeric matrix requires all symbolic parameters in the circuit to be bound to concrete values. + +```python +from cqlib import Circuit, Parameter + +theta = Parameter("theta") + +circuit = Circuit(1) +circuit.rx(0, theta) + +bound = circuit.assign_parameters({"theta": 0.5}) +matrix = bound.to_matrix() +``` + +## Symbolic matrix conversion + +Symbolic matrix conversion preserves the `Parameter` expressions in a circuit. This interface returns a `SymbolicMatrix`, whose elements can contain symbolic complex numbers and parameter expressions. + +```python +Circuit.to_symbolic_matrix( + qubits_order: list[int] | None = None, +) -> SymbolicMatrix +``` + +Example: + +```python +from cqlib import Circuit, Parameter + +theta = Parameter("theta") + +circuit = Circuit(1) +circuit.rz(0, theta) + +symbolic = circuit.to_symbolic_matrix() +assert "theta" in symbolic.symbols + +numeric = symbolic.evaluate({"theta": 0.25}) +``` + +In the example above, `symbolic` preserves the parameter `theta`. Concrete parameter values can later be passed through `evaluate()` to evaluate the symbolic matrix into a numeric matrix. + +## Typical application scenarios + +### 1. Checking the matrix of a Bell circuit + +```python +import numpy as np +from cqlib import Circuit + +circuit = Circuit(2) +circuit.h(0) +circuit.cx(0, 1) + +matrix = circuit.to_matrix() + +assert matrix.shape == (4, 4) +assert np.allclose(matrix.conj().T @ matrix, np.eye(4)) +``` + +The code above checks whether the matrix is close to the identity matrix through `matrix.conj().T @ matrix`, verifying that the circuit as a whole is unitary. + +### 2. Validating a custom gate matrix + +```python +import numpy as np +from cqlib.circuit.gates import UnitaryGate + +mat = np.array([[0, 1], [1, 0]], dtype=np.complex128) +gate = UnitaryGate("CustomX", 1).with_matrix(mat) + +assert np.allclose(gate.matrix(), mat) +``` + +When a custom gate is defined through `UnitaryGate`, the gate matrix itself can be validated first and the gate then appended to a circuit for use. + +### 3. Comparing the matrices of a composite gate before and after expansion + +```python +from cqlib import Circuit + +sub = Circuit(2) +sub.h(0) +sub.cx(0, 1) + +gate = sub.to_gate("Bell") + +outer = Circuit(2) +outer.append_circuit_gate(gate, [0, 1]) + +assert outer.to_matrix().shape == sub.to_matrix().shape +``` + +For a `CircuitGate` obtained by wrapping a sub-circuit, matrix comparison can check whether the same quantum behavior is preserved before and after wrapping. For stricter validation, `np.allclose()` can be used further to compare the elements of the two matrices. + diff --git a/docs/api/en/python/0_circuit/13_circuit_dag.md b/docs/api/en/python/0_circuit/13_circuit_dag.md new file mode 100644 index 00000000..14fb0518 --- /dev/null +++ b/docs/api/en/python/0_circuit/13_circuit_dag.md @@ -0,0 +1,479 @@ +# Circuit DAG + +`cqlib.circuit` + +A `CircuitDag` is an algorithmic view of a circuit at the level of operation dependencies: a node represents one operation, and an edge represents the ordering between two operations because they share the same resource. The resources that can be shared include qubits, mutable classical variables, immutable classical values, and a global order used to carry operations that have no concrete data resource. This view does not change the storage structure of the circuit itself and is mainly used for dependency analysis, layered scheduling and subgraph replacement. + +This page covers `CircuitDag`, `DagWire`, `DagControlFlow` and `DagSwitchCase`. + +--- + +## Import + +```python +from cqlib.circuit import CircuitDag, DagControlFlow, DagSwitchCase, DagWire +``` + +--- + +## CircuitDag + +`CircuitDag` is the operation-level dependency graph of a circuit. Every operation in the graph corresponds to one operation node, and each wire additionally carries an input sentinel node and an output sentinel node. + +### Constructing a circuit DAG + +```python +CircuitDag.from_circuit(circuit: Circuit) -> CircuitDag +circuit.dag() -> CircuitDag +``` + +`CircuitDag.from_circuit()` is a static method that constructs a dependency graph from an existing circuit. `Circuit.dag()` is an equivalent entry that constructs the dependency graph directly on the current circuit. + +After construction finishes, the DAG invariants are validated automatically. If the circuit itself does not satisfy the constraints, or an operation references a resource outside the circuit, construction raises `CircuitError`. + +Example: + +```python +from cqlib import Circuit, CircuitDag + +circuit = Circuit(2) +circuit.h(0) +circuit.x(1) +circuit.cx(0, 1) + +dag = circuit.dag() +same = CircuitDag.from_circuit(circuit) +``` + +### Exporting back to a circuit + +```python +dag.to_circuit() -> Circuit +``` + +`to_circuit()` rebuilds a circuit in the deterministic topological order of the DAG. The returned circuit carries the qubits, parameter table, classical variables, classical values and global phase recorded in the DAG. If the DAG contains a cycle, references illegal parameters, or the rebuilt operation sequence fails the circuit construction checks, `CircuitError` is raised. + +```python +recovered = dag.to_circuit() +print([operation.name for operation in recovered.operations]) +``` + +### Validation + +```python +dag.validate() -> None +``` + +`validate()` checks the consistency of the graph and its metadata, including: whether the graph contains a cycle, whether the input and output sentinels of each wire are legal, whether operation node indices are unique, whether the qubits and parameter indices referenced by operations are valid, whether classical variables and classical values belong to the current circuit, and whether control flow payloads match their owning operation. + +After importing a DAG from external IR or constructing one by hand, it is advisable to call it explicitly once: + +```python +dag.validate() +``` + +### Attributes + +| Attribute | Type | Description | +| --- | --- | --- | +| `num_qubits` | `int` | The number of qubits. | +| `num_ops` | `int` | The number of operation nodes. | +| `is_empty` | `bool` | Whether there are no operation nodes. | +| `qubits` | `list[Qubit]` | Return the qubits in insertion order. | +| `parameters` | `list[Parameter]` | The parameter expressions in the parameter table. | +| `symbols` | `list[str]` | The free symbol names appearing in the circuit. | +| `classical_vars` | `list[ClassicalType]` | The allocated mutable classical variable types. | +| `classical_values` | `list[ClassicalType]` | The immutable classical value types, usually produced by measurement. | + +```python +assert dag.num_qubits == 2 +assert dag.num_ops == 3 +assert len(dag) == 3 +assert dag.is_empty is False +assert dag.qubits == circuit.qubits +``` + +### Other behavior + +- `len(dag)` returns the number of operation nodes, the same as `num_ops`. +- `repr` has the form `CircuitDag(num_qubits=2, num_ops=3)`. +- Supports `copy` and `deepcopy`; the copied DAG has the same topological order as the original DAG. + +--- + +## Node identifiers + +On the Python side a node is represented by an `int`. Node identifiers are obtained from the following entries: + +| Way to obtain | Description | +| --- | --- | +| `op_nodes()` | Return all operation nodes in circuit source order. | +| `topological_op_nodes()` | Return all operation nodes in deterministic topological order. | +| `front_layer()`, `layers()`, `node_layers()` | The nodes in the layering results. | +| `wire_in(wire)` / `wire_out(wire)` | The input and output sentinel nodes of a wire. | +| `apply_operation_front()` / `apply_operation_back()` | Return the new node identifier after appending an operation. | + +A node is not necessarily an operation node. `is_operation()` and `node_kind()` can be used to determine the node kind: + +```python +node = dag.topological_op_nodes()[0] + +assert dag.is_operation(node) +assert dag.node_kind(node) == "operation" +assert dag.node_kind(dag.wire_in(DagWire.qubit(0))) == "wire_in" +``` + +The values of `node_kind()` are `"wire_in"`, `"wire_out"` and `"operation"`; it returns `None` when the node does not exist. + +`operation(node)` returns the `ValueOperation` corresponding to that node, containing the instruction, the qubits acted on, the parameters and the label; it returns `None` when the node is not an operation node. When a node identifier does not belong to the current DAG, traversal-type methods raise `CircuitError`. + +```python +from cqlib import Circuit, Parameter + +theta = Parameter("theta") + +circuit = Circuit(1) +circuit.rx(0, theta) + +dag = circuit.dag() +operation = dag.operation(dag.topological_op_nodes()[0]) + +assert operation.name == "RX" +assert operation.params[0] == theta +assert dag.parameters == [theta] +assert dag.symbols == ["theta"] +``` + +--- + +## DagWire + +`DagWire` represents the resource carried by a DAG edge, and has four forms: + +| Constructor | Description | +| --- | --- | +| `DagWire.qubit(qubit)` | A qubit wire; the argument is a logical qubit index `int` or a `Qubit` object. | +| `DagWire.classical_var(var)` | A mutable classical variable wire. | +| `DagWire.classical_value(value)` | An immutable classical value wire, usually a measurement result. | +| `DagWire.global_order()` | The global order, used to carry a stable order between operations that have no concrete data resource. | + +All of the above are static methods. + +### Attributes + +| Attribute | Type | Description | +| --- | --- | --- | +| `kind` | `str` | The wire kind, whose value is `"qubit"`, `"classical_var"`, `"classical_value"` or `"global_order"`. | +| `qubit_value` | `Qubit \| None` | The qubit carried by a qubit wire; `None` for the other forms. | +| `classical_var_value` | `ClassicalVar \| None` | The variable carried by a classical variable wire; `None` for the other forms. | +| `classical_value_value` | `ClassicalValue \| None` | The value carried by a classical value wire; `None` for the other forms. | + +### Other behavior + +- Supports `==` and `hash`, and can be used as a dictionary key or a set element. +- Supports `copy` and `deepcopy`. +- `repr` has the form `DagWire.qubit(0)`. + +```python +from cqlib import Circuit +from cqlib.circuit import ClassicalType, DagWire + +circuit = Circuit(1) +measured = circuit.measure(0) +flag = circuit.var(ClassicalType.bool()) +circuit.store(flag, measured.expr().bit_to_bool()) + +dag = circuit.dag() + +qubit_wire = DagWire.qubit(0) +value_wire = DagWire.classical_value(measured.value) +var_wire = DagWire.classical_var(flag) + +assert qubit_wire.kind == "qubit" +assert qubit_wire.qubit_value == circuit.qubits[0] +assert value_wire.classical_value_value == measured.value +assert var_wire.classical_var_value == flag +assert DagWire.global_order().kind == "global_order" +``` + +--- + +## Wire queries + +| Method | Description | +| --- | --- | +| `wires()` | Return all wires already materialized in the current DAG. | +| `has_wire(wire)` | Determine whether a wire is already materialized in the current DAG. | +| `is_wire_idle(wire)` | Determine whether a wire carries no operation at all. | +| `wire_in(wire)` | Return the identifier of the input sentinel node of the wire, or `None` if it does not exist. | +| `wire_out(wire)` | Return the identifier of the output sentinel node of the wire, or `None` if it does not exist. | +| `nodes_on_wire(wire)` | Return the operation nodes on the wire in wire order. | + +When a wire is not a legal resource, or the timeline on the wire is broken or branches, `is_wire_idle()` and `nodes_on_wire()` raise `CircuitError`. + +```python +nodes = dag.topological_op_nodes() +measure_node, store_node = nodes + +assert dag.has_wire(value_wire) +assert dag.nodes_on_wire(value_wire) == [measure_node, store_node] +assert dag.wire_in(qubit_wire) in dag.predecessors(measure_node) +``` + +--- + +## Graph traversal + +Predecessor and successor queries come in three levels: all neighbors regardless of resource, neighbors restricted to one wire, and neighbors classified by quantum or classical resource. + +| Method | Description | +| --- | --- | +| `op_nodes()` | Return all operation nodes in source order. | +| `topological_op_nodes()` | Return all operation nodes in topological order. | +| `predecessors(node)` | Return all predecessor nodes, including sentinel nodes. | +| `successors(node)` | Return all successor nodes, including sentinel nodes. | +| `predecessors_on_wire(node, wire)` | Return the operation predecessors connected only through the given wire. | +| `successors_on_wire(node, wire)` | Return the operation successors connected only through the given wire. | +| `quantum_predecessors(node)` | Return the operation predecessors connected through qubit wires. | +| `quantum_successors(node)` | Return the operation successors connected through qubit wires. | +| `classical_predecessors(node)` | Return the operation predecessors connected through classical variable wires or classical value wires. | +| `classical_successors(node)` | Return the operation successors connected through classical variable wires or classical value wires. | + +The neighbors returned by `predecessors()` and `successors()` may include wire sentinel nodes; the other methods return only operation nodes. Methods with a `wire` argument raise `CircuitError` when that wire does not belong to the current DAG; they likewise raise `CircuitError` when the node identifier does not exist. + +```python +assert dag.quantum_successors(measure_node) == [] +assert dag.classical_predecessors(store_node) == [measure_node] +assert dag.predecessors_on_wire(store_node, value_wire) == [measure_node] +``` + +--- + +## Layers and depth + +| Method | Description | +| --- | --- | +| `front_layer()` | Return the operation nodes with no operation predecessor, that is, the earliest executable layer. | +| `layers()` | Return the layering result in as-soon-as-possible scheduling, where each layer is a list of operation nodes. | +| `node_layers()` | Return the mapping from each operation node to the layer index it belongs to. | +| `depth()` | Return the dependency depth, that is, the number of layers. | + +`layers()` and `node_layers()` use the same set of layer indices, and `depth()` equals the maximum layer index plus one. When the graph contains a cycle, or the layer index of an operation predecessor is inconsistent with the topological order, `CircuitError` is raised. + +```python +circuit = Circuit(2) +circuit.h(0) +circuit.x(1) +circuit.cx(0, 1) +dag = circuit.dag() + +assert len(dag.front_layer()) == 2 +assert [len(layer) for layer in dag.layers()] == [2, 1] +assert dag.depth() == 2 +``` + +The dictionary returned by `node_layers()` is arranged in topological order, with node identifiers as keys and layer indices as values. + +```python +assert list(dag.node_layers().keys()) == dag.topological_op_nodes() +``` + +--- + +## Control flow + +When a circuit contains structured control flow, a control flow operation node additionally carries a recursive DAG payload. The construction interfaces and low-level IR of control flow itself are described in [Classical Data / Control Flow](9_classical_control_flow.md). + +| Method | Description | +| --- | --- | +| `control_flow(node)` | Return the `DagControlFlow` carried by that node, or `None` if there is none. | +| `has_control_flow()` | Determine whether structured control flow exists among the top-level operations. | +| `has_nested_control_flow()` | Determine whether structured control flow is further contained inside a control flow body. | +| `has_measurement()` | Determine whether measurement is contained directly or recursively among the top-level operations. | + +### DagControlFlow + +The `kind` attribute gives the control flow form, whose value is `"if"`, `"while"`, `"for"`, `"switch"`, `"break"` or `"continue"`. The attributes exposed by each form are as follows; an attribute that does not apply to that form returns `None` or an empty list. + +| Attribute | Applicable form | Type | Description | +| --- | --- | --- | --- | +| `kind` | All | `str` | The control flow form. | +| `then_body` | `if` | `CircuitDag \| None` | The branch body executed when the condition holds. | +| `else_body` | `if` | `CircuitDag \| None` | The `else` branch body; `None` if there is no such branch. | +| `body` | `while`, `for` | `CircuitDag \| None` | The loop body. | +| `cases` | `switch` | `list[DagSwitchCase]` | The exact integer match branches. | +| `default_body` | `switch` | `CircuitDag \| None` | The `default` branch body; `None` if there is no such branch. | + +```python +from cqlib import Circuit +from cqlib.circuit import ClassicalType + +circuit = Circuit(1) +flag = circuit.var(ClassicalType.bool()) +circuit.if_else( + flag.expr(), + lambda body: body.x(0), + lambda body: body.z(0), +) + +dag = circuit.dag() +control = dag.control_flow(dag.topological_op_nodes()[0]) + +assert control.kind == "if" +assert [op.name for op in control.then_body.to_circuit().operations] == ["X"] +assert [op.name for op in control.else_body.to_circuit().operations] == ["Z"] +``` + +### DagSwitchCase + +`DagSwitchCase` represents an exact integer match branch in a `switch`. + +| Attribute | Type | Description | +| --- | --- | --- | +| `value` | `int` | The integer value this branch matches. | +| `body` | `CircuitDag` | The control flow body executed when that branch is taken. | + +```python +circuit = Circuit(1) +target = circuit.var(ClassicalType.uint(8)) + +circuit.switch( + target.expr(), + lambda case: ( + case.value(1, lambda body: body.x(0)), + case.value(2, lambda body: body.y(0)), + case.default(lambda body: body.z(0)), + ), +) + +dag = circuit.dag() +control = dag.control_flow(dag.topological_op_nodes()[0]) + +assert control.kind == "switch" +assert [case.value for case in control.cases] == [1, 2] +assert [op.name for op in control.cases[0].body.to_circuit().operations] == ["X"] +assert [op.name for op in control.default_body.to_circuit().operations] == ["Z"] +``` + +A control flow body is itself a `CircuitDag`, so interfaces such as `to_circuit()`, `layers()` and `control_flow()` can be called on it further for recursive analysis of nested structures. + +--- + +## Operation counts and runs + +| Method | Description | +| --- | --- | +| `operation_count_by_name()` | Count the top-level operations by instruction name. | +| `operation_count_by_name_recursive()` | Count the operations among the top level and inside each control flow body by instruction name. | +| `collect_1q_runs()` | Collect contiguous single-qubit gate runs on each qubit wire. | +| `collect_2q_runs()` | Collect contiguous two-qubit gate runs on each qubit wire. | + +The counting result is returned as a dictionary whose keys are instruction names and whose values are counts. Runs are returned as a list of lists, in which each element is a sequence of contiguous gate node identifiers, in the same order as on the wire. Non-gate operations, measurement, reset, barrier and multi-qubit operations interrupt a single-qubit gate run; the same two-qubit operation may appear in the runs of both of its qubit wires at the same time. + +```python +from cqlib import Circuit +from cqlib.circuit import ClassicalExpr, ClassicalType + +circuit = Circuit(2) +circuit.h(0) +circuit.x(0) +circuit.cx(0, 1) +circuit.cz(0, 1) +counter = circuit.var(ClassicalType.uint(8)) +circuit.for_uint( + counter, + ClassicalExpr.uint_literal(8, 0), + ClassicalExpr.uint_literal(8, 2), + ClassicalExpr.uint_literal(8, 1), + lambda body, _: body.z(1), +) + +dag = circuit.dag() +top_counts = dag.operation_count_by_name() +recursive_counts = dag.operation_count_by_name_recursive() +oneq_runs = dag.collect_1q_runs() +twoq_runs = dag.collect_2q_runs() + +assert dag.has_control_flow() +assert dag.has_nested_control_flow() is False +assert top_counts["H"] == 1 +assert top_counts["for"] == 1 +assert "Z" not in top_counts +assert recursive_counts["Z"] == 1 +assert any(len(run) == 2 for run in oneq_runs) +assert any(len(run) == 2 for run in twoq_runs) +``` + +--- + +## Modifying the graph + +| Method | Description | +| --- | --- | +| `add_parameter(parameter)` | Insert a parameter into the DAG parameter table and return `(index, is_new)`. | +| `apply_operation_front(operation)` | Append an operation at the front of the DAG and return the new node identifier. | +| `apply_operation_back(operation)` | Append an operation at the back of the DAG and return the new node identifier. | +| `remove_op_node(node)` | Delete an operation node and return the deleted `ValueOperation`. | +| `substitute_node(node, operation)` | Replace the given operation node with one `ValueOperation`. | +| `substitute_node_with_dag(node, replacement)` | Replace the given operation node with another `CircuitDag`. | + +What is passed in when appending an operation is a self-contained `ValueOperation`. When replacing a control flow node, the replacement content must contain only one control flow operation of the same type; the resources read or written by a replacement DAG must fall within the resource range of the replaced node, otherwise `CircuitError` is raised. + +```python +from cqlib import Circuit +from cqlib.circuit import StandardGate, ValueOperation + +circuit = Circuit(2) +circuit.h(0) +circuit.cx(0, 1) +dag = circuit.dag() +q0, q1 = dag.qubits + +front = ValueOperation.from_standard_gate(StandardGate.X, [q1]) +back = ValueOperation.from_standard_gate(StandardGate.Z, [q0]) +front_node = dag.apply_operation_front(front) +back_node = dag.apply_operation_back(back) + +assert dag.operation(front_node).name == "X" +assert dag.operation(back_node).name == "Z" +assert [op.name for op in dag.to_circuit().operations] == ["X", "H", "CX", "Z"] + +nodes = dag.topological_op_nodes() +removed = dag.remove_op_node(nodes[1]) +assert removed.name == "H" +assert [op.name for op in dag.to_circuit().operations] == ["X", "CX", "Z"] + +nodes = dag.topological_op_nodes() +replacement = ValueOperation.from_standard_gate(StandardGate.Y, [q1]) +dag.substitute_node(nodes[0], replacement) +assert [op.name for op in dag.to_circuit().operations] == ["Y", "CX", "Z"] +``` + +Replacing one node with a whole subgraph: + +```python +circuit = Circuit(1) +circuit.h(0) +dag = circuit.dag() + +replacement_circuit = Circuit(1) +replacement_circuit.x(0) +replacement_circuit.z(0) +replacement = replacement_circuit.dag() + +dag.substitute_node_with_dag(dag.topological_op_nodes()[0], replacement) + +assert [op.name for op in dag.to_circuit().operations] == ["X", "Z"] +``` + +--- + +## Typical use + +Compiler passes and routing algorithms usually perform dependency analysis on the DAG: `front_layer()` and `node_layers()` determine which operations are already ready, `predecessors()` and `successors()` determine whether an ordering constraint exists between two operations, run collection such as `collect_1q_runs()` locates gate sequences that can be processed as a whole, and the `substitute_node()` series of interfaces writes the transformation result back into the graph. + +--- + +## Exceptions + +- `CircuitError`: a node identifier does not belong to the current DAG, a wire does not belong to the current DAG, the graph contains a cycle, the timeline of a wire is broken or branches, a control flow payload does not match its owning operation, the resource range or control flow form of a replacement DAG is illegal, a parameter index is out of range, a classical variable or classical value belongs to another circuit, and so on. diff --git a/docs/api/en/python/0_circuit/1_circuit.md b/docs/api/en/python/0_circuit/1_circuit.md new file mode 100644 index 00000000..43593572 --- /dev/null +++ b/docs/api/en/python/0_circuit/1_circuit.md @@ -0,0 +1,365 @@ +# Circuit + +`cqlib.circuit.Circuit` + +```python +from cqlib import Circuit +``` + +`Circuit` is the most central circuit container in the Cqlib Python API, used to represent a complete quantum program. It is responsible for holding the set of qubits, the operation sequence in order, symbolic parameters, the global phase, classical variables and classical values, and structured control flow. + +--- + +## Constructing a circuit + +```python +Circuit(qubits: int | list[int] | list[Qubit]) +``` + +`Circuit` supports several construction forms: the number of qubits can be given directly, or logical qubit indices can be specified explicitly, or existing `Qubit` objects can be passed in. + +| Form | Meaning | +| --- | --- | +| `Circuit(3)` | Create a three-qubit circuit containing `Qubit(0)`, `Qubit(1)` and `Qubit(2)`. | +| `Circuit([0, 2, 4])` | Create a circuit with sparse logical qubit indices. | +| `Circuit([Qubit(5), Qubit(7)])` | Create a circuit with existing `Qubit` objects. | + +Example: + +```python +from cqlib import Circuit, Qubit + +a = Circuit(3) +b = Circuit([0, 2, 4]) +c = Circuit([Qubit(10), Qubit(11)]) +``` + +--- + +## Low-level construction + +```python +Circuit.from_operations( + qubits: list[Qubit], + operations: list[ValueOperation], + classical_vars: list[ClassicalType] | None = None, + classical_values: list[ClassicalType] | None = None, +) -> Circuit +``` + +`Circuit.from_operations()` rebuilds a circuit from an existing qubit list and operation sequence. This interface is a relatively low-level construction entry point, usually used for deserialization, IR import, restoring compiler pass output, or constructing an exact low-level circuit object in tests. + +```python +from cqlib import Circuit, Qubit +from cqlib.circuit import StandardGate, ValueOperation + +op = ValueOperation.from_standard_gate(StandardGate.H(), [Qubit(0)]) +circuit = Circuit.from_operations([Qubit(0)], [op]) +``` + +--- + +## Common circuit attributes + +| Attribute | Type | Description | +| --- | --- | --- | +| `id` | `CircuitId` | The unique identity of the current circuit, used for classical handles, measurement values and control flow scope management. | +| `num_qubits` | `int` | The number of qubits contained in the circuit. | +| `width` | `int` | An alias of `num_qubits`. | +| `qubits` | `list[Qubit]` | The qubit list kept in the circuit's internal order. | +| `parameters` | `list[Parameter]` | The list of parameter expressions recorded in the circuit. | +| `symbols` | `list[str]` | All free symbol names in the circuit. This list is a stable symbol registry and may contain symbols that are no longer referenced by the executable IR. | +| `used_symbols` | `list[str]` | The free symbol names actually referenced by the current executable IR. | +| `global_phase` | `Parameter` | The overall global phase of the circuit. | +| `classical_vars` | `list[ClassicalType]` | The list of allocated mutable classical variable types. | +| `classical_values` | `list[ClassicalType]` | The list of immutable classical value types produced by operations such as measurement. | +| `operations` | `list[ValueOperation]` | The self-contained operation list returned in append order. | + +`operations` returns a list of `ValueOperation`s that can be inspected and rebuilt at the Python level. Each operation contains information such as the instruction type, the qubits acted on, the parameters and the label. This attribute is commonly used for circuit debugging, structural analysis, test assertions and comparison before and after a compiler pass. + +```python +from cqlib import Circuit + +circuit = Circuit(2) +circuit.h(0) +circuit.cx(0, 1) + +print(circuit.num_qubits) +print(circuit.operations) +``` + +--- + +## Basic circuit operations + +| Method | Description | +| --- | --- | +| `add_qubits(qubits)` | Append new qubits to the current circuit. | +| `append(operation)` | Append a self-contained `ValueOperation` to the circuit. | +| `operation(index)` | Return the `ValueOperation` at the given position; raises `IndexError` when the index is out of range. | +| `remove_operation(index)` | Remove the operation at the given position and return the removed `ValueOperation`; raises `IndexError` when the index is out of range. | +| `remove_operations(indices)` | Remove the operations at several positions in a batch and return the list of removed `ValueOperation`s; raises `IndexError` when an index is out of range. If a measurement result to be removed is still referenced by `store`, raises `CircuitError`. | +| `__len__()` | Return the number of operations in the circuit. | +| `__getitem__(index)` | Support reading operations with a forward or negative index. | +| `uses_symbol(symbol)` | Determine whether the current executable IR references the given symbol; returns `bool`. | +| `var(ty)` | Allocate a mutable classical variable owned by the current circuit, where `ty` is a `ClassicalType`. | +| `store(target, value)` | Write the classical expression `value` into the classical variable `target`. | +| `dag()` | Return the directed acyclic graph representation `CircuitDag` corresponding to the current circuit. | +| `validate()` | Validate the circuit structure, classical handles, scopes and control flow invariants. | +| `depth(recurse=False)` | Compute the circuit depth. | +| `set_global_phase(phase)` | Set the circuit global phase. | + + +`depth()` estimates circuit depth. In a linear quantum circuit, depth usually means the number of operation layers on the longest qubit path after scheduling as soon as possible (ASAP). Ordinary gates and ordinary instructions normally contribute one layer; `barrier` constrains reordering on the qubits it covers; a `CircuitGate` can be treated as an opaque operation by default. When a circuit contains control flow structures such as `if`, `while`, `for`, `switch`, `break` or `continue`, depth computation needs extra handling of branch and loop semantics: the default `recurse=False` does not expand these structures, and in that case `depth()` raises `CircuitError` if control flow exists in the circuit; passing `recurse=True` recursively expands branches and loop bodies and returns an estimated depth. + + +```python +from cqlib import Circuit + +circuit = Circuit(2) +circuit.h(0) +circuit.cx(0, 1) + +assert len(circuit) == 2 +first = circuit[0] +last = circuit[-1] +depth = circuit.depth() +``` + +--- + +## Standard gate convenience methods + +`Circuit` provides convenience methods for a set of commonly used quantum gates: + +### 1. Single-qubit fixed gates + +| Method | Gate | +| --- | --- | +| `i(qubit)` | Identity | +| `h(qubit)` | Hadamard | +| `x(qubit)` | Pauli-X | +| `y(qubit)` | Pauli-Y | +| `z(qubit)` | Pauli-Z | +| `s(qubit)` | S | +| `sdg(qubit)` | S dagger | +| `t(qubit)` | T | +| `tdg(qubit)` | T dagger | +| `x2p(qubit)` | X half-pi positive | +| `x2m(qubit)` | X half-pi negative | +| `y2p(qubit)` | Y half-pi positive | +| `y2m(qubit)` | Y half-pi negative | + +### 2. Parameterized single-qubit gates + +| Method | Parameter | Description | +| --- | --- | --- | +| `rx(qubit, theta)` | `float \| Parameter` | Rotation around the X axis. | +| `ry(qubit, theta)` | `float \| Parameter` | Rotation around the Y axis. | +| `rz(qubit, theta)` | `float \| Parameter` | Rotation around the Z axis. | +| `phase(qubit, lambda)` | `float \| Parameter` | The phase gate `P(lambda)`. | +| `u(qubit, theta, phi, lambda)` | `float \| Parameter` | General single-qubit gate. | +| `xy(qubit, theta)` | `float \| Parameter` | The XY interaction family. | +| `xy2p(qubit, theta)` | `float \| Parameter` | Positive half-angle XY gate. | +| `xy2m(qubit, theta)` | `float \| Parameter` | Negative half-angle XY gate. | +| `rxy(qubit, theta, phi)` | `float \| Parameter` | Rotation around an arbitrary axis in the XY plane. | + +### 3. Multi-qubit gates + +| Method | Description | +| --- | --- | +| `cx(control, target)` | CNOT gate. | +| `cy(control, target)` | controlled-Y gate. | +| `cz(control, target)` | controlled-Z gate. | +| `swap(a, b)` | Exchange the states of two qubits. | +| `ccx(control1, control2, target)` | Toffoli gate. | +| `rxx(a, b, theta)` | `exp(-i theta XX / 2)`. | +| `ryy(a, b, theta)` | `exp(-i theta YY / 2)`. | +| `rzz(a, b, theta)` | `exp(-i theta ZZ / 2)`. | +| `rzx(a, b, theta)` | `exp(-i theta ZX / 2)`. | +| `crx(control, target, theta)` | controlled-RX gate. | +| `cry(control, target, theta)` | controlled-RY gate. | +| `crz(control, target, theta)` | controlled-RZ gate. | +| `fsim(a, b, theta, phi)` | fSim(theta, phi) gate. | + +Example: + +```python +from cqlib import Circuit, Parameter + +theta = Parameter("theta") + +circuit = Circuit(3) +circuit.h(0) +circuit.cx(0, 1) +circuit.rzz(1, 2, theta) +circuit.crx(0, 2, 0.25) +``` + +--- + +## Adding operation interfaces + +Besides the convenience gate methods, `Circuit` also provides more general interfaces for adding operations, used to add explicitly constructed gate objects, composite gates or low-level operation objects. + +| Method | Description | +| --- | --- | +| `append_gate(gate, qubits, label=None)` | Append a `StandardGate`. | +| `append_mc_gate(gate, qubits, label=None)` | Append an `MCGate`. | +| `append_unitary_gate(gate, qubits, params=None)` | Append a `UnitaryGate`. | +| `append_circuit_gate(gate, qubits, params=None)` | Append a `CircuitGate`. | +| `append(operation)` | Append a low-level `ValueOperation`. | + +```python +from cqlib import Circuit, Parameter +from cqlib.circuit import StandardGate + +theta = Parameter("theta") + +circuit = Circuit(1) +circuit.append_gate(StandardGate.RX(theta), [0], label="first-rx") +``` + +--- + +## Non-unitary instructions + +A quantum circuit can contain not only ordinary quantum gates, but also instructions that are not ordinary unitary gates, such as measurement, reset, barrier and delay. + +| Method | Description | +| --- | --- | +| `barrier(qubits)` | Insert a barrier, used to prevent the compiler from reordering operations on the related qubits across this boundary. | +| `reset(qubit)` | Reset a qubit. | +| `delay(qubit, duration)` | Insert idle waiting time on the given qubit. | +| `measure(qubit)` | Measure a single qubit and return a `Measurement`. | +| `measure_bits(qubits)` | Measure several qubits and return a multi-bit result. | +| `measure_into(qubit, target)` | Measure a single qubit and write the result into an existing classical variable. | +| `measure_bits_into(qubits, target)` | Measure several qubits and write the result into an existing classical variable. | + +--- + +## Parameter binding + +```python +assign_parameters(bindings: dict[str, float] | None = None) -> Circuit +``` + +`assign_parameters()` binds the symbolic parameters in a circuit to concrete numeric values and returns a new bound circuit. The method does not modify the original circuit, so it suits using one parameterized circuit as a template and repeatedly generating concrete circuits under different parameter values. + +```python +from cqlib import Circuit, Parameter + +theta = Parameter("theta") + +circuit = Circuit(1) +circuit.rx(0, theta) + +bound = circuit.assign_parameters({"theta": 3.141592653589793}) +assert bound.symbols == [] +``` + +Note that bound values must be finite numbers. If a bound value is `nan`, `inf` or another invalid number, a `ParameterError` is usually raised. If only some symbols are bound, the unbound symbols remain in the returned new circuit. + +--- + +## Inverse, decomposition and composition + +| Method | Description | +| --- | --- | +| `inverse()` | Return a new circuit: the operation order is reversed and every invertible operation is inverted. | +| `decompose()` | Return a new circuit, recursively expanding composite gates represented by `CircuitGate`. | +| `to_gate(name)` | Wrap the current circuit as a reusable `CircuitGate`. | +| `compose(other, qubits=None)` | Append another circuit to the current circuit, with an optional qubit mapping. | + +```python +from cqlib import Circuit + +bell = Circuit(2) +bell.h(0) +bell.cx(0, 1) + +bell_gate = bell.to_gate("Bell") + +larger = Circuit(4) +larger.append_circuit_gate(bell_gate, [0, 1]) +larger.append_circuit_gate(bell_gate, [2, 3]) +``` + +The behavior of `compose(other, qubits=None)` is as follows: + +- `qubits=None`: merge `other` into the current circuit by its logical qubit indices, adding new qubits to the current circuit when necessary; +- `qubits=[...]`: map `other.qubits` in order to the target qubits in the current circuit. + +Unlike methods returning a new circuit such as `inverse()` and `decompose()`, `compose()` modifies the current circuit in place and returns `None`. + +It is worth distinguishing that `compose()` appends the operations of another circuit directly to the current circuit, whereas `to_gate()` preserves the module boundary of the sub-circuit. + +--- + +## Matrix conversion + +| Method | Description | +| --- | --- | +| `to_matrix(qubits_order=None)` | Return a numeric matrix, usually of type `numpy.ndarray[np.complex128]`. | +| `to_symbolic_matrix(qubits_order=None)` | Return a `SymbolicMatrix`, preserving symbolic parameters. | + +```python +from cqlib import Circuit + +circuit = Circuit(2) +circuit.h(0) +circuit.cx(0, 1) + +matrix = circuit.to_matrix() +matrix_reversed = circuit.to_matrix([1, 0]) +``` +--- + +## Structured control flow entry points + +`Circuit` provides two kinds of control flow construction: + +- `if_()`, `if_else()`, `while_()`, `for_uint()`, `switch()`; +- building a `ClassicalControlOp` by hand and then calling `append_control()`. + +In the callback bodies of `while_()` and `for_uint()`, `break_loop()` can be called to leave the current loop or `switch` branch early, or `continue_loop()` to skip the rest of the current iteration. These two may only appear inside the corresponding control flow body, otherwise `CircuitError` is raised. + +```python +from cqlib import Circuit +from cqlib.circuit import ClassicalExpr + +circuit = Circuit(2) +circuit.h(0) + +circuit.if_( + ClassicalExpr.bool_literal(True), + lambda body: body.x(1), +) +``` + +--- + +## Complete example: parameterized sub-circuit reuse + +The following example shows how to first construct a parameterized sub-circuit, wrap it as a `CircuitGate`, then reuse it several times in a larger circuit and bind different parameter values each time. + +```python +from cqlib import Circuit, Parameter + +theta = Parameter("theta") + +layer = Circuit(2) +layer.rx(0, theta) +layer.cx(0, 1) +layer.rz(1, theta / 2) + +layer_gate = layer.to_gate("ParamLayer") + +model = Circuit(4) +model.append_circuit_gate(layer_gate, [0, 1], params=[Parameter("a")]) +model.append_circuit_gate(layer_gate, [2, 3], params=[Parameter("b")]) + +bound = model.assign_parameters({"a": 0.1, "b": 0.2}) +``` + +This example reflects a typical reuse pattern of `Circuit`: first construct a parameterizable circuit module, then wrap it as a composite gate through `to_gate()`, and finally call it several times in the main circuit and bind parameters separately. diff --git a/docs/api/en/python/0_circuit/2_qubit.md b/docs/api/en/python/0_circuit/2_qubit.md new file mode 100644 index 00000000..d1e14fb1 --- /dev/null +++ b/docs/api/en/python/0_circuit/2_qubit.md @@ -0,0 +1,185 @@ +# Qubit + +`cqlib.circuit.Qubit` + +```python +from cqlib import Qubit +``` + + +`Qubit` is the basic type in Cqlib for representing logical qubits. It is usually used as a handle for qubits in a quantum circuit, identifying the logical qubit index with a non-negative integer. Note that `Qubit` only represents a logical index; it does not hold the quantum state itself, nor is it bound to a specific physical device location. It also does not record which `Circuit` it belongs to. Whether a certain `Qubit` can be used in a circuit depends on whether that qubit has already been registered in the current `Circuit`. + +--- + +## Constructor + +```python +Qubit(index: int) +``` + +`Qubit(index)` creates a logical qubit handle. `index` must be a non-negative integer and is stored internally as an unsigned integer. The acceptable range is `0` to `4294967295` (that is, `2**32 - 1`): passing a negative integer or an integer greater than that upper bound raises `QubitError`, while passing a non-integer (for example a float or a string) raises `TypeError`. + +| Parameter | Type | Description | +| --- | --- | --- | +| `index` | `int` | The logical qubit index; the value must be between `0` and `4294967295`. | + +Example: + +```python +from cqlib import Qubit + +q0 = Qubit(0) +q5 = Qubit(5) + +print(q0.index) +print(q5.index) +``` + +--- + +## Attributes + +`Qubit` provides the following common attributes: + +| Attribute | Type | Description | +| --- | --- | --- | +| `index` | `int` | The logical qubit index. | +| `id` | `int` | The internal raw index, numerically equal to `index` by default. | + +Usually, using `index` directly is sufficient. `id` is more for low-level debugging, internal representation, or alignment with other low-level data structures. + +```python +from cqlib import Qubit + +q = Qubit(3) + +print(q.index) # 3 +print(q.id) # 3 +``` + +--- + +## Comparison, ordering and hashing + +`Qubit` supports common comparison, ordering and hashing operations, including: + +- `==` / `!=` +- `<` / `<=` / `>` / `>=` +- `hash()` +- `copy.copy()` / `copy.deepcopy()` +- `str()` / `repr()` + +These operations are all judged based on the qubit index. That is, two `Qubit` objects are regarded as the same logical qubit as long as their indices are equal. + +```python +from cqlib import Qubit + +assert Qubit(0) == Qubit(0) +assert Qubit(0) < Qubit(1) + +mapping = {Qubit(0): "ancilla"} +assert mapping[Qubit(0)] == "ancilla" +``` + +--- + +## `Circuit` and `Qubit` + +Most `Circuit` gate methods accept both integer indices and `Qubit` objects. For simple circuits, using integer indices directly is usually more concise; for scenarios that need explicit management of logical qubit indices, using `Qubit` objects is clearer. + +```python +from cqlib import Circuit, Qubit + +circuit = Circuit(2) + +circuit.h(0) +circuit.cx(Qubit(0), Qubit(1)) +``` + +In the example above, both `h(0)` and `cx(Qubit(0), Qubit(1))` act on logical qubits already registered in the current circuit. + +When constructing a circuit, a list of `Qubit` objects can also be passed in directly: + +```python +from cqlib import Circuit, Qubit + +circuit = Circuit([Qubit(10), Qubit(20)]) +circuit.cx(Qubit(10), Qubit(20)) +``` + +--- + +## Sparse logical indices + +The index of a `Qubit` is a logical identifier and is not required to be contiguous starting from `0`. For example, `Qubit(10)` and `Qubit(20)` can together form a two-qubit circuit. + +```python +from cqlib import Circuit, Qubit + +circuit = Circuit([Qubit(10), Qubit(20)]) + +assert circuit.num_qubits == 2 +assert [q.index for q in circuit.qubits] == [10, 20] +``` + +Note that a logical index is not necessarily the same as the axis order in a matrix or statevector. For `Circuit([Qubit(10), Qubit(20)])`, the circuit contains only two qubits internally and the matrix dimension is `4 × 4`. By default, matrix conversion interprets qubits according to the order of `qubits` saved in the circuit. + +If the matrix qubit order needs to be controlled explicitly, pass `qubits_order` during matrix conversion: + +```python +matrix = circuit.to_matrix([20, 10]) +``` + +This means the qubit axes are interpreted in the order of logical qubits `20` and `10` when the matrix is constructed. + +--- + +## Semantic boundary + +`Qubit` is the "address" or "handle" of a logical qubit, not a quantum state object. `Qubit`s with the same index can appear in different circuits, but those circuits are independent of each other. + +```python +from cqlib import Circuit, Qubit + +q0 = Qubit(0) + +left = Circuit([q0]) +right = Circuit([q0]) + +left.x(q0) +right.h(q0) +``` + +In the example above, both `left` and `right` use `Qubit(0)`, but they are two different circuits and do not affect each other. + +--- + +## Difference from physical qubits + +`Qubit.index` represents a logical qubit index and is not the same as a physical qubit index on a real device. When executing against hardware, a logical qubit usually needs layout and mapping before it can correspond to a physical qubit on a specific device. For example, the logical qubit `Qubit(0)` may be mapped to the physical qubit `Q5` on one device, and may be mapped to another physical location on another device or in another compilation result. + +--- + +## Complete example + +The following example shows typical usage of `Qubit`, including constructing logical qubits, creating a circuit with sparse indices, adding gate operations and specifying the matrix qubit order. + +```python +from cqlib import Circuit, Qubit + +q10 = Qubit(10) +q20 = Qubit(20) + +circuit = Circuit([q10, q20]) +circuit.h(q10) +circuit.cx(q10, q20) + +print(circuit.num_qubits) +print([q.index for q in circuit.qubits]) + +matrix_default = circuit.to_matrix() +matrix_reversed = circuit.to_matrix([20, 10]) + +print(matrix_default.shape) +print(matrix_reversed.shape) +``` diff --git a/docs/api/en/python/0_circuit/3_parameter.md b/docs/api/en/python/0_circuit/3_parameter.md new file mode 100644 index 00000000..6b04e2f7 --- /dev/null +++ b/docs/api/en/python/0_circuit/3_parameter.md @@ -0,0 +1,278 @@ +# Parameter + +`cqlib.circuit.Parameter` + +```python +from cqlib import Parameter +``` + +`Parameter` is the core type in Cqlib for representing circuit parameters and mathematical expressions. It can represent either a definite numeric constant or a symbolic variable that has not been assigned a value yet, and can also be composed into more complex parameter expressions through arithmetic operations and mathematical functions. + +In quantum circuits, `Parameter` is commonly used to describe rotation angles, delay durations, global phase, elements of custom symbolic gate matrices, and the parameters to be optimized in variational algorithms. Through parameterized expressions, a circuit template with symbolic parameters can first be constructed, and parameter binding, matrix computation, simulation execution or compilation can then be performed under different values in a later flow. + +--- + +## Creating parameters + +`Parameter` supports creating parameter objects from numbers, symbol names and expression strings, and also provides constructors for common mathematical constants. + +```python +Parameter(value: int | float | str) +Parameter.from_expression(expr: str) -> Parameter +Parameter.pi() -> Parameter +Parameter.e() -> Parameter +``` + +| Input | Description | +| --- | --- | +| `Parameter(3.14)` | Create a numeric constant parameter. | +| `Parameter("theta")` | Create a symbolic parameter named `theta`. | +| `Parameter("2 * theta + pi / 2")` | Parse a parameter expression from an expression string. | +| `Parameter.pi()` | Create the mathematical constant `pi`. | +| `Parameter.e()` | Create the mathematical constant `e`. | + +Example: + +```python +from cqlib import Parameter + +theta = Parameter("theta") +expr = 2 * theta + Parameter.pi() / 2 +``` + +In the example above, `theta` is a free symbol and `expr` is an expression composed of a symbolic parameter, a numeric constant and a mathematical constant. + +--- + +## Supported expression capabilities + +The expression elements supported by `Parameter` include: + +- numeric constants and symbolic variables; +- mathematical constants: `pi`, `e`; +- arithmetic operators: `+`, `-`, `*`, `/`, `**`; +- parentheses, used to control expression precedence; +- common functions: `sin`, `cos`, `tan`, `asin`, `acos`, `atan`, `exp`, `ln`, `log`, `sqrt`, `abs`, `sinh`, `cosh`, `tanh`, `floor`, `ceil`, `round`. + +Besides expression strings, `Parameter` also provides mathematical functions in method form, so that expressions can be constructed directly. + +```python +from cqlib import Parameter + +theta = Parameter("theta") + +expr = theta.sin() + theta.cos() +root = (theta * theta + 1).sqrt() +``` + +--- + +## Arithmetic operators + +`Parameter` supports common arithmetic operations: + +| Operation | Python form | +| --- | --- | +| Addition | `a + b`, `1.0 + a` | +| Subtraction | `a - b`, `1.0 - a` | +| Multiplication | `a * b`, `2.0 * a` | +| Division | `a / b`, `1.0 / a` | +| Exponentiation | `a ** b`, `a.pow(b)` | +| Negation | `-a` | + +Example: + +```python +from cqlib import Parameter + +theta = Parameter("theta") +phi = Parameter("phi") + +expr = 2 * theta - phi / 3 + Parameter.pi() +``` + +Every arithmetic operation returns a new `Parameter` object and does not modify the original parameter. Therefore, the same symbolic parameter can be safely reused in several expressions. + +--- + +## Mathematical function methods + +`Parameter` supports common mathematical function methods: + +| Method | Description | +| --- | --- | +| `sin()` / `cos()` / `tan()` | Trigonometric functions. | +| `asin()` / `acos()` / `atan()` | Inverse trigonometric functions. | +| `exp()` / `ln()` / `log(base=None)` | Exponential and logarithmic functions. | +| `sqrt()` / `abs()` | Square root and absolute value. | +| `sinh()` / `cosh()` / `tanh()` | Hyperbolic functions. | +| `floor()` / `ceil()` / `round()` | Rounding-related functions. | + +Example: + +```python +from cqlib import Parameter + +x = Parameter("x") +y = Parameter("y") + +expr = (2 * x + y).sin().exp() +``` + +--- + +## Evaluation + +`evaluate(bindings)` computes a parameter expression into a concrete floating-point number. + +```python +evaluate(bindings: dict[str, float] | None = None) -> float +``` + +Example: + +```python +from cqlib import Parameter + +theta = Parameter("theta") +expr = 2 * theta + 1 + +assert expr.evaluate({"theta": 0.5}) == 2.0 +``` + +A constant expression without free symbols can be evaluated directly: + +```python +from cqlib import Parameter + +value = (Parameter.pi() / 2).evaluate() +print(value) +``` + +--- + +## Symbol information and state checks + +`Parameter` provides a set of query interfaces for checking which free symbols an expression contains, and whether the expression is a constant or a specific value. + +| Interface | Returns | Description | +| --- | --- | --- | +| `symbols` | `list[str]` | All unique free symbols contained in the expression. | +| `as_symbol()` | `str / None` | Return the symbol name when the expression is exactly a single symbol; otherwise return `None`. | +| `is_constant()` | `bool` | Return `True` when the expression contains no free symbols. | +| `is_exact_zero()` | `bool` | Return `True` when the expression is syntactically exactly `0`. | +| `is_zero()` | `bool` | Return `True` when the expression can be evaluated and the result equals `0`. | +| `is_one()` | `bool` | Return `True` when the expression can be evaluated and the result equals `1`. | + +Example: + +```python +from cqlib import Parameter + +theta = Parameter("theta") +phi = Parameter("phi") +expr = theta + phi + +assert set(expr.symbols) == {"theta", "phi"} +assert theta.as_symbol() == "theta" +assert Parameter(1).is_one() +``` + +--- + +## Simplification and canonicalization + +`Parameter` supports expression simplification and canonicalization. + +```python +simplify() -> Parameter +canonicalized() -> Parameter +``` + +- `simplify()` returns a new algebraically simplified expression. + +```python +from cqlib import Parameter + +x = Parameter("x") +expr = (x + 0).simplify() +``` + +- `canonicalized()` returns the canonical storage form used internally by Cqlib for parameter interning, comparison, deduplication or compiler processing. + +--- + +## Replacement and substitution + +`replace()` and `substitute()` are used to replace symbols in an expression. + +```python +replace(symbol: str, param: Parameter) -> Parameter +substitute(bindings: dict[str, Parameter]) -> Parameter +``` + +Example: + +```python +from cqlib import Parameter + +x = Parameter("x") +y = Parameter("y") + +expr = x + 2 +replaced = expr.replace("x", 3 * y) +substituted = replaced.substitute({"y": Parameter(0.5)}) +``` + +`replace(symbol, param)` replaces a single symbol; `substitute(bindings)` replaces several symbols at once. Both return a new `Parameter` expression and do not modify the original object. + +--- + +## Differentiation + +`derivative(var)` differentiates with respect to the given symbolic variable and returns a new `Parameter` expression. + +```python +derivative(var: str) -> Parameter +``` + +Example: + +```python +from cqlib import Parameter + +theta = Parameter("theta") +expr = theta.sin() + theta * theta + +dtheta = expr.derivative("theta") +``` + +This interface performs symbolic differentiation at the level of classical parameter expressions. For example, it can compute the derivative of a gate angle expression, a global phase expression or a symbolic matrix element with respect to a variable. + +--- + +## Equivalence checks + +`Parameter` provides equivalence check interfaces, used to determine whether two expressions can be proven equal. + +```python +provably_equal(other: Parameter, tolerance: float = 1e-12) -> bool +provably_equal_modulo( + other: Parameter, + modulus: Parameter, + tolerance: float = 1e-12, +) -> bool +``` + +Example: + +```python +from cqlib import Parameter + +theta = Parameter("theta") + +expr1 = theta + 0 +expr2 = theta + +assert expr1.provably_equal(expr2) +``` diff --git a/docs/api/en/python/0_circuit/4_operation_instruction.md b/docs/api/en/python/0_circuit/4_operation_instruction.md new file mode 100644 index 00000000..59ba67c5 --- /dev/null +++ b/docs/api/en/python/0_circuit/4_operation_instruction.md @@ -0,0 +1,226 @@ +# Operation / Instruction + +- `cqlib.circuit.Instruction` +- `cqlib.circuit.ValueInstruction` +- `cqlib.circuit.ValueOperation` +- `cqlib.circuit.Directive` + +```python +from cqlib.circuit import Instruction, ValueInstruction, ValueOperation, Directive +``` + +`Instruction`, `ValueInstruction` and `ValueOperation` are the basic representations in `cqlib.circuit` for describing circuit operations. Semantically, `Instruction` describes "which instruction is executed", for example a standard gate, a composite gate, a custom unitary gate or a non-unitary instruction; `ValueOperation` describes "one concrete application of this instruction in a circuit", including which qubits it acts on, which parameters it carries, and whether it carries label information. + +--- + +## Operation IR levels + +Cqlib splits circuit operations into several levels, so that ordinary quantum gates, non-unitary instructions and structured control flow can all be supported. + +| Type | Role | +| --- | --- | +| `Instruction` | A storage-layer instruction, representing concrete instruction types such as standard gates, multi-controlled gates, custom unitary gates, sub-circuit gates, `Directive` and `delay`. | +| `ValueInstruction` | A construction-layer instruction, which can wrap an ordinary `Instruction` or a structured control flow object `ClassicalControlOp`. | +| `ValueOperation` | A self-contained operation, combining a `ValueInstruction`, the qubits acted on, the parameter list and an optional label. | +| `Directive` | A non-unitary instruction type, used to represent special circuit semantics such as `barrier`, `measure` and `reset`. | + +This layered design separates "the definition of an instruction" from "the application of that instruction in a circuit". + +--- + +## `Instruction` + +`Instruction` describes the instruction type corresponding to an operation. It only cares about "what to execute"; it does not contain the qubits that the instruction concretely acts on, nor does it represent its position in a circuit. + +### 1. Creation interfaces + +The commonly used static construction methods are as follows: + +| Static method | Description | +| --- | --- | +| `Instruction.from_name(name)` | Create a standard gate instruction by standard gate name; name matching is case-insensitive; raises `ValueError` when the name matches no standard gate. | +| `Instruction.from_standard_gate(gate)` | Create a standard gate instruction from a `StandardGate`; `gate` must be an unbound definition without parameters, otherwise raises `CircuitError`. | +| `Instruction.from_mc_gate(gate)` | Create a multi-controlled gate instruction from an `MCGate`; its base gate must likewise be an unbound definition without parameters, otherwise raises `CircuitError`. | +| `Instruction.from_unitary_gate(gate)` | Create a custom unitary gate instruction from a `UnitaryGate`. | +| `Instruction.from_circuit_gate(gate)` | Create a sub-circuit composite gate instruction from a `CircuitGate`. | +| `Instruction.from_directive(directive)` | Create a non-unitary instruction from a `Directive`. | +| `Instruction.delay()` | Create a `delay` instruction. | + +Example: + +```python +from cqlib.circuit import Instruction, Directive, StandardGate + +h_inst = Instruction.from_standard_gate(StandardGate.H()) +barrier_inst = Instruction.from_directive(Directive.barrier()) +delay_inst = Instruction.delay() +``` + +### 2. Attributes + +| Attribute | Type | Description | +| --- | --- | --- | +| `name` | `str` | The readable name of the instruction. Standard gate instructions use canonical uppercase names, for example `"H"` and `"CX"`; `Directive` and `delay` instructions use lowercase names, for example `"barrier"`, `"measure"` and `"delay"`. | +| `instruction_type` | `str` | The instruction category, for example `"standard"`, `"mcgate"`, `"unitary"`, `"circuit"`, `"directive"`, `"classical_data"`, `"classical_control"` and `"delay"`. | +| `is_standard` | `bool` | Whether it is a standard gate instruction. | +| `is_mcgate` | `bool` | Whether it is a multi-controlled gate instruction. | +| `is_unitary` | `bool` | Whether it is a user-defined unitary gate instruction. | +| `is_circuit_gate` | `bool` | Whether it is a sub-circuit composite gate instruction. | +| `is_directive` | `bool` | Whether it is a non-unitary `Directive` instruction. | +| `is_classical_control` | `bool` | Whether it is a structured control flow instruction. | +| `is_classical_data` | `bool` | Whether it is an instruction related to classical data. | +| `is_delay` | `bool` | Whether it is a delay instruction. | +| `standard_gate` | `StandardGate / None` | When the instruction is a standard gate, return the internal standard gate object. | +| `directive` | `Directive / None` | When the instruction is a `Directive`, return the internal directive object. | + +--- + +## `ValueInstruction` + +`ValueInstruction` is a construction-layer instruction, used to represent ordinary instructions and structured control flow uniformly. It provides a unified entry for `ValueOperation`, so that a circuit operation can represent ordinary quantum gates and non-unitary instructions as well as control flow structures such as `if`, `while`, `for` and `switch`. + +### 1. Creation interfaces + +| Static method | Description | +| --- | --- | +| `ValueInstruction.from_instruction(instruction)` | Wrap an ordinary `Instruction` as a construction-layer instruction. | +| `ValueInstruction.from_classical_control(control)` | Wrap a `ClassicalControlOp` as a construction-layer control flow instruction. | + +### 2. Attributes + +| Attribute | Type | Description | +| --- | --- | --- | +| `is_instruction` | `bool` | Whether it wraps an ordinary `Instruction`. | +| `is_classical_control` | `bool` | Whether it wraps structured control flow. | +| `instruction` | `Instruction / None` | The ordinary instruction content. | +| `classical_control` | `ClassicalControlOp / None` | The control flow content. | +| `name` | `str` | The instruction name; returns the structure name when control flow is wrapped, for example `"if"` and `"while"`. | +| `instruction_type` | `str` | The instruction category, with the same values as `Instruction.instruction_type`; it is `"classical_control"` when control flow is wrapped. | +| `is_standard` / `is_mcgate` / `is_unitary` / `is_circuit_gate` / `is_directive` / `is_classical_data` / `is_delay` | `bool` | Category check attributes with the same names as on `Instruction`; all are `False` when control flow is wrapped. | +| `standard_gate` | `StandardGate / None` | When a standard gate instruction is wrapped, return the internal standard gate object. | +| `directive` | `Directive / None` | When a `Directive` instruction is wrapped, return the internal directive object. | + +--- + +## `ValueOperation` + +`ValueOperation` represents one complete operation in a circuit. It contains the instruction to execute, the qubits that instruction acts on, the parameters applied and an optional label. + +The construction signature is as follows: + +```python +ValueOperation( + instruction: ValueInstruction, + qubits: list[Qubit], + params: list[float | Parameter] | None = None, + label: str | None = None, +) +``` + +Here, `qubits` are the logical qubits this operation acts on; `params` are the numeric or symbolic parameters passed in when it is applied; `label` is optional user metadata, used for debugging, annotating the source or recording compiler information. + +### 1. Factory methods + +| Static method | Description | +| --- | --- | +| `from_instruction(instruction, qubits, params=None, label=None)` | Create an operation from an ordinary `Instruction`, the qubits acted on and explicit parameters. | +| `from_standard_gate(gate, qubits, label=None)` | Create an operation from a `StandardGate` that already contains parameter information. | +| `from_mc_gate(gate, qubits, label=None)` | Create an operation from an `MCGate` that already contains the base gate parameters. | +| `from_classical_control(control)` | Create a control flow operation from a `ClassicalControlOp`. | + +Example: + +```python +from cqlib import Qubit +from cqlib.circuit import ValueOperation, StandardGate + +op = ValueOperation.from_standard_gate( + StandardGate.RX(0.25), + [Qubit(0)], + label="rx-layer-0", +) +``` + +### 2. Attributes and methods + +| Interface | Type | Description | +| --- | --- | --- | +| `instruction` | `ValueInstruction` | The construction-layer instruction corresponding to the operation. | +| `qubits` | `list[Qubit]` | The list of qubits the operation acts on. | +| `params` | `list[float \| Parameter]` | The parameter list carried by this operation. | +| `label` | `str \| None` | The optional label. | +| `name` | `str` | The operation name; a control flow operation returns the structure name, for example `"if"`. | +| `num_qubits` | `int` | The number of qubits this operation acts on. | +| `num_params` | `int` | The number of parameters carried by this operation. | +| `instruction_type` | `str` | The instruction category, with the same values as `Instruction.instruction_type`. | +| `is_standard` / `is_mcgate` / `is_unitary` / `is_circuit_gate` / `is_directive` / `is_classical_data` / `is_classical_control` / `is_delay` | `bool` | Category check attributes with the same names as on `Instruction`. | +| `matrix()` | `np.ndarray` | Return the unitary matrix corresponding to the operation; raises `ValueError` when it carries symbolic parameters or is a control flow operation. | + + +--- + +## `Directive` + +`Directive` represents non-unitary circuit instructions. + +| Static method | Description | +| --- | --- | +| `Directive.barrier()` | Create a barrier instruction, used to prevent the compiler from reordering operations on the related qubits across the boundary. | +| `Directive.measure()` | Create a computational basis measurement instruction. | +| `Directive.reset()` | Create a reset instruction, which resets a qubit. | + +| Method | Description | +| --- | --- | +| `name()` | Return the stable lowercase instruction name: `"barrier"`, `"measure"` or `"reset"`. | +| `is_barrier()` | Determine whether it is a barrier instruction. | +| `is_measure()` | Determine whether it is a measurement instruction. | +| `is_reset()` | Determine whether it is a reset instruction. | +| `inverse()` | Return the inverse of the instruction; `barrier` returns itself, while `measure` and `reset` return `None`. | + +Example: + +```python +from cqlib.circuit import Directive + +barrier = Directive.barrier() +assert barrier.is_barrier() +assert Directive.measure().inverse() is None +assert Directive.reset().inverse() is None +``` + +--- + +## `ValueOperation` and `Circuit` + +When a circuit is constructed directly through the convenience methods provided by `Circuit`, for example `h()`, `cx()` and `rz()`, Cqlib automatically creates the corresponding `Instruction` and `ValueOperation` internally. + +When operation-level IR needs to be constructed by hand, `ValueOperation` can be created explicitly and then restored to a circuit object through `Circuit.from_operations()`. + +```python +from cqlib import Circuit, Qubit +from cqlib.circuit import ValueOperation, StandardGate + +ops = [ + ValueOperation.from_standard_gate(StandardGate.H(), [Qubit(0)]), + ValueOperation.from_standard_gate(StandardGate.CX(), [Qubit(0), Qubit(1)]), +] + +circuit = Circuit.from_operations([Qubit(0), Qubit(1)], ops) +``` + +--- + +## The `label` tag + +`label` is readable metadata attached to an operation instance. It does not change the matrix, the quantum semantics or the execution result of the gate, and is mainly used for debugging, visualization, recording the source or identifying operations generated at a compilation stage. + +```python +from cqlib import Qubit +from cqlib.circuit import ValueOperation, StandardGate + +op = ValueOperation.from_standard_gate( + StandardGate.RZ(0.1), + [Qubit(0)], + label="calibrated-rz", +) +``` diff --git a/docs/api/en/python/0_circuit/5_gates_standard.md b/docs/api/en/python/0_circuit/5_gates_standard.md new file mode 100644 index 00000000..9e9c3c82 --- /dev/null +++ b/docs/api/en/python/0_circuit/5_gates_standard.md @@ -0,0 +1,261 @@ +# StandardGate + +`cqlib.circuit.gates.StandardGate` + +```python +from cqlib import Parameter +from cqlib.circuit.gates import StandardGate +``` + +`StandardGate` represents the set of standard quantum gates built into Cqlib, used to describe common single-qubit gates, multi-qubit gates, parameterized rotation gates, controlled gates, two-body Pauli interaction gates and some hardware-related gates. It is one of the basic gate types in `Circuit` construction, gate-level analysis, matrix computation, compiler transformation and IR export. + +--- + +## Basic operations + +A standard gate can be appended explicitly to a circuit through `Circuit.append_gate()`. + +```python +from cqlib import Circuit, Parameter +from cqlib.circuit.gates import StandardGate + +theta = Parameter("theta") + +circuit = Circuit(2) +circuit.append_gate(StandardGate.H, [0]) +circuit.append_gate(StandardGate.RX(theta), [0]) +circuit.append_gate(StandardGate.CX, [0, 1]) +``` + +In the example above, `StandardGate.H` and `StandardGate.CX` are fixed gates without parameters; `StandardGate.RX(theta)` is a parameterized gate instance carrying the parameter `theta`. When appending a gate, the `qubits` argument specifies the qubits the gate acts on, and the number of qubits must agree with the gate's `num_qubits`. + +--- + +## Standard gate categories + +The standard gates built into Cqlib can be categorized by the number of qubits they act on and by their parameter form: + +### 1. Single-qubit fixed gates + +Single-qubit fixed gates act on one qubit and require no angle parameter when used. + +| Gate | Description | +| --- | --- | +| `StandardGate.I` | Identity gate, which leaves the quantum state unchanged. | +| `StandardGate.H` | Hadamard gate, used to construct superposition states. | +| `StandardGate.X` | Pauli-X gate, equivalent to a bit flip. | +| `StandardGate.Y` | Pauli-Y gate. | +| `StandardGate.Z` | Pauli-Z gate, used for a phase flip. | +| `StandardGate.S` | S phase gate, equivalent to `sqrt(Z)`. | +| `StandardGate.SDG` | The inverse gate of `S`. | +| `StandardGate.T` | T phase gate, corresponding to a `pi/4` phase. | +| `StandardGate.TDG` | The inverse gate of `T`. | +| `StandardGate.X2P` | Positive half-angle gate in the X direction. | +| `StandardGate.X2M` | Negative half-angle gate in the X direction. | +| `StandardGate.Y2P` | Positive half-angle gate in the Y direction. | +| `StandardGate.Y2M` | Negative half-angle gate in the Y direction. | + +### 2. Parameterized single-qubit gates + +Parameterized single-qubit gates control their effect through one or more angle parameters. + +| Gate | Number of parameters | Description | +| --- | --- | --- | +| `StandardGate.RX(theta)` | 1 | X-axis rotation gate, usually expressed as `exp(-i theta X / 2)`. | +| `StandardGate.RY(theta)` | 1 | Y-axis rotation gate, usually expressed as `exp(-i theta Y / 2)`. | +| `StandardGate.RZ(theta)` | 1 | Z-axis rotation gate, usually expressed as `exp(-i theta Z / 2)`. | +| `StandardGate.RXY(theta, phi)` | 2 | Rotation gate around an arbitrary axis in the XY plane. | +| `StandardGate.U(theta, phi, lambda_)` | 3 | General single-qubit gate. | +| `StandardGate.Phase(lambda_)` | 1 | The phase gate `P(lambda)`. | +| `StandardGate.XY(theta)` | 1 | The gate family related to XY interaction. | +| `StandardGate.XY2P(theta)` | 1 | Positive half-angle XY gate. | +| `StandardGate.XY2M(theta)` | 1 | Negative half-angle XY gate. | + +A parameter can be an ordinary number or a `Parameter` expression. When a symbolic parameter is used, the gate can be part of a parameterized circuit and bound later through `Circuit.assign_parameters()`. + +### 3. Multi-qubit gates + +Multi-qubit gates describe interactions between qubits and are an important foundation for constructing entangled states, controlled logic and the core circuits of quantum algorithms. + +| Gate | Number of parameters | Description | +| --- | --- | --- | +| `StandardGate.CX` | 0 | controlled-X, that is, the CNOT gate. | +| `StandardGate.CY` | 0 | controlled-Y gate. | +| `StandardGate.CZ` | 0 | controlled-Z gate. | +| `StandardGate.SWAP` | 0 | Exchange the states of two qubits. | +| `StandardGate.CCX` | 0 | Toffoli gate, that is, the doubly controlled X gate. | +| `StandardGate.RXX(theta)` | 1 | XX rotation gate. | +| `StandardGate.RYY(theta)` | 1 | YY rotation gate. | +| `StandardGate.RZZ(theta)` | 1 | ZZ rotation gate. | +| `StandardGate.RZX(theta)` | 1 | ZX rotation gate. | +| `StandardGate.CRX(theta)` | 1 | controlled-RX gate. | +| `StandardGate.CRY(theta)` | 1 | controlled-RY gate. | +| `StandardGate.CRZ(theta)` | 1 | controlled-RZ gate. | +| `StandardGate.FSIM(theta, phi)` | 2 | fSim gate, common in some superconducting quantum computing models. | + +For controlled gates, calling `append_gate()` usually passes qubits in the order of control qubit first and target qubit last. For example, `StandardGate.CX` should act on `[control, target]`. + +### 4. Global phase gate + +| Gate | Number of parameters | Description | +| --- | --- | --- | +| `StandardGate.GPhase(lambda_)` | 1 | Zero-qubit global phase marker. | + +--- + +## Attributes + +`StandardGate` provides a set of attributes for querying basic structural information about a gate. These attributes are commonly used for circuit validation, compiler rule matching, gate set coverage checks and document generation. + +| Attribute | Type | Description | +| --- | --- | --- | +| `num_qubits` | `int` | The total number of qubits the gate acts on. | +| `num_ctrl_qubits` | `int` | The number of control qubits built into the gate. | +| `num_params` | `int` | The number of parameters the gate requires. | +| `params` | `list[Parameter]` | The parameter list already bound on the current gate instance. | + +```python +from cqlib.circuit.gates import StandardGate + +assert StandardGate.H.num_qubits == 1 +assert StandardGate.CX.num_ctrl_qubits == 1 +assert StandardGate.RX.num_params == 1 + +gate = StandardGate.RZ(0.5) +assert len(gate.params) == 1 +``` + +--- + +## Lookup and enumeration + +```python +StandardGate.all() -> list[StandardGate] +StandardGate.from_name(name: str) -> StandardGate +``` + +`all()` returns all built-in standard gate definitions in the standard gate enumeration order; every item in the list is a gate definition with no bound parameters. + +`from_name(name)` looks up a standard gate definition by name; matching is case-insensitive; raises `ValueError` when the name matches no standard gate. + +```python +from cqlib.circuit.gates import StandardGate + +assert len(StandardGate.all()) == 36 + +assert StandardGate.from_name("H") == StandardGate.H +assert StandardGate.from_name("cx") == StandardGate.CX +``` + +--- + +## Parameter binding + +A parameterized standard gate can produce a gate instance with parameters by calling the gate factory and passing in the parameters. + +```python +gate(*args: float | Parameter) -> StandardGate +``` + +```python +from cqlib import Parameter +from cqlib.circuit.gates import StandardGate + +theta = Parameter("theta") + +rx = StandardGate.RX(theta) +u = StandardGate.U(theta, 0.1, 0.2) +``` + +--- + +## Matrix computation + +```python +matrix(params: list[float] | None = None) -> np.ndarray[np.complex128] +``` + +`matrix()` returns the local numeric unitary matrix of a standard gate, that is, the matrix of the gate itself on the space of the qubits it acts on. + +For a fixed gate, `matrix()` can be called directly. For a parameterized gate, there are two common ways: + +- if the gate instance has already bound evaluable constant parameters, `matrix()` can be called directly; +- if the gate instance or gate definition contains symbolic parameters, concrete numeric parameters need to be passed in `matrix(params)`, or parameter binding needs to be completed in a circuit before matrix conversion. + +The length of the `params` passed in must agree with the gate's `num_params`, otherwise `CircuitError` is raised; if bound parameters cannot be evaluated to constants, `ParameterError` is raised. Calling `matrix()` directly on a parameterized gate with no parameters bound likewise raises `CircuitError`. + +```python +import numpy as np +from cqlib.circuit.gates import StandardGate + +h = StandardGate.H.matrix() +rx = StandardGate.RX.matrix([np.pi / 2]) + +assert h.shape == (2, 2) +assert rx.shape == (2, 2) +``` + +--- + +## Inverse gate + +```python +inverse() -> StandardGate +``` + +`inverse()` returns the inverse gate of the current standard gate. For self-inverse gates, for example `H`, `X`, `Y`, `Z`, `CX` and `CZ`, the inverse gate is usually the gate itself; for phase gates, `S` and `SDG`, and `T` and `TDG`, are mutually inverse; for parameterized rotation gates, the inverse gate usually corresponds to negating the angle, or an equivalent inverse gate is returned according to the gate definition. + +```python +from cqlib.circuit.gates import StandardGate + +assert StandardGate.H.inverse() == StandardGate.H +assert StandardGate.S.inverse() == StandardGate.SDG +assert StandardGate.T.inverse() == StandardGate.TDG +``` + +When a gate instance carries parameters, `inverse()` handles the parameters according to the inverse transformation rule of that gate: for example, `StandardGate.RZ(0.5).inverse()` returns `StandardGate.RZ(-0.5)`. This capability is commonly used to construct inverse circuits, uncompute structures, gate-level tests and compiler optimization validation. + +Calling `inverse()` on a parameterized gate that has not bound all parameters raises `CircuitError`; if the gate itself is not invertible, `RuntimeError` is raised. + +--- + +## Generating multi-controlled gates + +```python +control(num_controls: int) -> MCGate +``` + +`control(num_controls)` adds the given number of control qubits on top of an existing standard gate and returns an `MCGate`. This interface suits constructing multi-controlled `X` gates, multi-controlled phase gates, multi-controlled rotation gates and the controlled operations commonly found in `oracle`. + +```python +from cqlib.circuit.gates import StandardGate + +mcx = StandardGate.X.control(3) + +assert mcx.num_ctrl_qubits == 3 +assert mcx.base_gate == StandardGate.X +``` + +When a multi-controlled gate is applied, qubits are usually passed in the order of control qubits first and target qubit last. For example, a triply controlled `X` gate needs four qubits in total, with the first three as control qubits and the last as the target qubit. + +```python +from cqlib import Circuit + +circuit = Circuit(4) +circuit.append_mc_gate(mcx, [0, 1, 2, 3]) # controls: 0, 1, 2; target: 3 +``` + +--- + +## `StandardGate` and `Circuit` + +`Circuit` provides a large number of convenience gate methods, which in essence append the corresponding `StandardGate` to the circuit: + +| `Circuit` method | Corresponding standard gate | +| --- | --- | +| `h(q)` | `StandardGate.H` | +| `x(q)` | `StandardGate.X` | +| `rx(q, theta)` | `StandardGate.RX(theta)` | +| `cx(control, target)` | `StandardGate.CX` | +| `rzz(a, b, theta)` | `StandardGate.RZZ(theta)` | +| `fsim(a, b, theta, phi)` | `StandardGate.FSIM(theta, phi)` | diff --git a/docs/api/en/python/0_circuit/6_gates_unitary.md b/docs/api/en/python/0_circuit/6_gates_unitary.md new file mode 100644 index 00000000..d9f22d1c --- /dev/null +++ b/docs/api/en/python/0_circuit/6_gates_unitary.md @@ -0,0 +1,190 @@ +# UnitaryGate + +`cqlib.circuit.gates.UnitaryGate` + +```python +from cqlib.circuit.gates import UnitaryGate +``` + +`UnitaryGate` is used to define user-defined unitary gates outside the Cqlib standard gate set. Unlike `StandardGate`, the behavior of a `UnitaryGate` is usually described by a matrix, a symbolic matrix or a frozen circuit provided by the user, so it is better suited to representing special oracles in an algorithm, hardware-related calibration gates, problem-specific black-box transformations, or other custom quantum operations that cannot be expressed directly with built-in standard gates. + +--- + +## Constructor + +```python +UnitaryGate(label: str, num_qubits: int, num_params: int = 0) +``` + +| Parameter | Description | +| --- | --- | +| `label` | The readable name of the gate, commonly used for printing, debugging, export and visualization. | +| `num_qubits` | The number of qubits the gate acts on. | +| `num_params` | The number of positional parameters to pass in each time the gate is applied; the default is `0`. | + +```python +from cqlib.circuit.gates import UnitaryGate + +oracle = UnitaryGate("Oracle", num_qubits=2) +``` + +The constructor only creates a custom gate object with a name, a qubit count and a parameter count. After creation, a concrete definition can be attached to the gate through `with_matrix()`, `with_symbolic_matrix()` or `with_circuit()`. + +--- + +## Numeric matrix definition + +```python +with_matrix(matrix: ArrayLike) -> UnitaryGate +``` + +`with_matrix()` defines a custom gate through a numeric matrix. The matrix passed in must be a two-dimensional square matrix with shape `2**num_qubits × 2**num_qubits`. For example, a single-qubit gate corresponds to a `(2, 2)` matrix, a two-qubit gate to a `(4, 4)` matrix, and a three-qubit gate to an `(8, 8)` matrix. + +```python +import numpy as np +from cqlib import Circuit +from cqlib.circuit.gates import UnitaryGate + +z_like = np.array([[1, 0], [0, -1]], dtype=np.complex128) +gate = UnitaryGate("ZLike", 1).with_matrix(z_like) + +circuit = Circuit(1) +circuit.append_unitary_gate(gate, [0]) +``` + +--- + +## Symbolic matrix definition + +```python +with_symbolic_matrix(matrix: SymbolicMatrix, params: list[str]) -> UnitaryGate +``` + +`with_symbolic_matrix()` defines a parameterized custom unitary gate; the symbolic matrix can keep `Parameter` expressions in its matrix elements, so that the same gate definition can bind different parameter values at different circuit positions. + +Here, `matrix` is the symbolic matrix definition of the gate and `params` specifies the order of positional parameters when the gate is applied. Parameters passed in through `Circuit.append_unitary_gate(..., params=[...])` when appending the gate are bound into the symbolic matrix in the order of the names given in `params`. + +```python +from cqlib import Parameter +from cqlib.circuit import SymbolicComplex, SymbolicMatrix +from cqlib.circuit.gates import UnitaryGate + +theta = Parameter("theta") +matrix = SymbolicMatrix([ + [SymbolicComplex.one(), SymbolicComplex.zero()], + [SymbolicComplex.zero(), SymbolicComplex.exp_i(theta)], +]) + +phase_like = UnitaryGate("PhaseLike", 1, num_params=1).with_symbolic_matrix( + matrix, + ["theta"], +) +``` + +In the example above, `PhaseLike` is a single-qubit phase gate with one positional parameter. The symbolic matrix uses `theta` to represent the phase parameter, and `with_symbolic_matrix(..., ["theta"])` means that the first parameter is bound to the symbol `theta` when the gate is appended. + +When the gate is applied, either a numeric parameter or a new `Parameter` expression can be passed in: + +```python +from cqlib import Circuit, Parameter + +circuit = Circuit(1) +circuit.append_unitary_gate(phase_like, [0], params=[Parameter("phi")]) +``` + +Here `phi` is the positional parameter passed in when the gate is applied, and it replaces `theta` in the symbolic matrix definition. If a number is passed in, for example `params=[0.25]`, it means this application uses a concrete numeric phase. + +--- + +## Frozen circuit definition + +```python +with_circuit(circuit: FrozenCircuit) -> UnitaryGate +``` + +When a custom gate can be described by an existing circuit structure, a `FrozenCircuit` can be used as the gate definition. This form suits keeping circuit-level structural information in `UnitaryGate` form, so that the inner operations can still be referenced or expanded later when needed. + +```python +from cqlib import Circuit +from cqlib.circuit.gates import FrozenCircuit, UnitaryGate + +sub = Circuit(2) +sub.h(0) +sub.cx(0, 1) + +frozen = FrozenCircuit(sub.qubits, sub.operations) +gate = UnitaryGate("BellPrep", 2).with_circuit(frozen) +``` + +To add a circuit to other circuits as a reusable composite gate, use: + +```python +bell_gate = sub.to_gate("BellPrep") +``` + + +| Attribute | Type | Description | +| --- | --- | --- | +| `label` | `str` | The readable name of the gate. | +| `num_qubits` | `int` | The number of qubits the gate acts on. | +| `num_params` | `int` | The number of positional parameters to pass in each time the gate is applied. | +| `symbolic_matrix` | `SymbolicMatrix / None` | The symbolic matrix definition; usually `None` if no symbolic matrix definition was used. | +| `matrix_params` | `list[str] / None` | The order of parameter names in the symbolic matrix. | +| `circuit` | `FrozenCircuit / None` | The frozen circuit definition; usually `None` if no circuit definition was used. | + +--- + +## Matrix evaluation + +```python +matrix() -> np.ndarray +matrix_for_params(params: list[float]) -> np.ndarray +``` + +`matrix()` returns the numeric matrix attached by `with_matrix()`. If the gate has not been defined yet, or was defined with `with_symbolic_matrix()` or `with_circuit()` instead, there is no numeric matrix that can be returned directly, and `CircuitError` is raised. + +`matrix_for_params(params)` computes the numeric matrix of the gate under the given parameters, and supports all three definition forms: numeric matrix, symbolic matrix and frozen circuit; `params` are provided in the order given by `matrix_params`. + +```python +import numpy as np +from cqlib.circuit.gates import UnitaryGate + +z_like = np.array([[1, 0], [0, -1]], dtype=np.complex128) +gate = UnitaryGate("ZLike", 1).with_matrix(z_like) + +assert gate.matrix().shape == (2, 2) +assert np.array(gate).shape == (2, 2) +``` + +`UnitaryGate` also implements the array protocol, so the matrix representation can be obtained directly through `np.array(gate)` or `np.asarray(gate)`. + +--- + +## Appending to `Circuit` + +```python +Circuit.append_unitary_gate( + gate: UnitaryGate, + qubits: list[int | Qubit], + params: list[float | Parameter] | None = None, +) -> None +``` + +Once defined, a `UnitaryGate` can be appended to a circuit through `Circuit.append_unitary_gate()`. Appending requires the list of qubits the gate acts on; if the gate is parameterized, the positional parameters of this application are also required. + +Appending must satisfy the following conditions: + +- the number of `qubits` must equal `gate.num_qubits`; +- if `gate.num_params > 0`, the length of `params` should agree with `gate.num_params`; +- the qubits acted on must already exist in the target circuit. + +--- + +## Comparison of definition forms + +| Definition form | Preserves structure | Supports symbolic parameters | Typical use | +| --- | --- | --- | --- | +| `with_matrix()` | No | No | Fixed black-box matrices, oracles, calibration gates, small-scale test gates. | +| `with_symbolic_matrix()` | No | Yes | Parameterized black-box matrices, tunable phase gates, symbolic verification. | +| `with_circuit()` | Yes | Depends on the inner circuit | Scenarios that need to preserve decomposition information while applying the gate in `UnitaryGate` form. | + diff --git a/docs/api/en/python/0_circuit/7_gates_mc_gate.md b/docs/api/en/python/0_circuit/7_gates_mc_gate.md new file mode 100644 index 00000000..4f1525b8 --- /dev/null +++ b/docs/api/en/python/0_circuit/7_gates_mc_gate.md @@ -0,0 +1,197 @@ +# MCGate + +`cqlib.circuit.gates.MCGate` + +```python +from cqlib.circuit.gates import MCGate +``` + +`MCGate` represents a multi-controlled quantum gate, that is, one or more control qubits added on top of an existing `StandardGate`. The base gate acts on the corresponding target qubits only when all control qubits satisfy the control condition. Multi-controlled gates are commonly used to construct Toffoli gates, multi-controlled phase gates, oracles, conditional flip operations and controlled subroutines in quantum algorithms. + +--- + +## Constructor + +```python +MCGate(num_controls: int, gate: StandardGate) +``` + +| Parameter | Description | +| --- | --- | +| `num_controls` | The number of additional control qubits to add. | +| `gate` | The base standard gate being controlled, that is, the gate actually executed when the control condition is satisfied. | + +The following example constructs a doubly controlled `X` gate and a triply controlled `H` gate: + +```python +from cqlib.circuit.gates import MCGate, StandardGate + +ccx = MCGate(2, StandardGate.X) +mch = MCGate(3, StandardGate.H) +``` + +Alternatively, the `control()` method can be called directly on a standard gate object to generate a multi-controlled gate. + +```python +from cqlib.circuit.gates import StandardGate + +mcx = StandardGate.X.control(4) +``` + +In the example above, `mcx` represents a quadruply controlled `X` gate. Applying this gate requires 4 control qubits and 1 target qubit. + +--- + +## Attributes + +| Attribute | Type | Description | +| --- | --- | --- | +| `num_ctrl_qubits` | `int` | The total number of control qubits of the multi-controlled gate. | +| `num_qubits` | `int` | The total number of qubits the gate acts on, equal to the number of control qubits plus the number of qubits the base gate acts on. | +| `num_params` | `int` | The number of parameters the base gate requires. | +| `base_gate` | `StandardGate` | The base standard gate before controls were added. | +| `params` | `list[Parameter]` | The parameters currently bound on the base gate. | + +```python +from cqlib import Parameter +from cqlib.circuit.gates import MCGate, StandardGate + +theta = Parameter("theta") +gate = MCGate(2, StandardGate.RZ(theta)) + +assert gate.num_ctrl_qubits == 2 +assert gate.num_qubits == 3 +assert gate.num_params == 1 +``` + +In the example above, the base gate is a single-qubit `RZ(theta)` gate and 2 control qubits are added outside it, so this `MCGate` acts on 3 qubits in total: the first two are control qubits and the last is the target qubit of `RZ`. + +--- + +## Order of control and target qubits + +When an `MCGate` is appended to a circuit, qubits must be passed in the order of control qubits first and target qubits last: + +```text +[control_0, control_1, ..., control_k, target_0, target_1, ...] +``` + +For example, `MCGate(2, StandardGate.X)` is equivalent to a two-control `X` gate in Toffoli form. + +```python +from cqlib import Circuit +from cqlib.circuit.gates import MCGate, StandardGate + +circuit = Circuit(3) + +ccx = MCGate(2, StandardGate.X) +circuit.append_mc_gate(ccx, [0, 1, 2]) +``` + +In the example above, qubits `0` and `1` are control qubits and qubit `2` is the target qubit. The `X` gate acts on the target qubit only when the control qubits satisfy the control condition. + +--- + +## Matrix + +```python +matrix(params: list[float] | None = None) -> np.ndarray[np.complex128] +``` + +`matrix()` returns the numeric unitary matrix of the multi-controlled gate. The matrix dimension is determined by `num_qubits` and equals `2**num_qubits × 2**num_qubits`. For a parameterized base gate, concrete values can be passed through the `params` argument to compute the matrix temporarily. + +```python +import numpy as np +from cqlib.circuit.gates import MCGate, StandardGate + +gate = MCGate(1, StandardGate.RZ) +matrix = gate.matrix([np.pi / 2]) + +assert matrix.shape == (4, 4) +``` + +In the example above, `gate` is a singly controlled `RZ` gate acting on 2 qubits in total, so the matrix size is `4 × 4`. + +Note that `matrix()` returns the local matrix of the gate itself; it does not include the global phase of the enclosing `Circuit` and does not take other operations in the circuit into account. + +The number of parameters must agree with `num_params` when calling: `CircuitError` is raised when the length of the explicitly passed `params` does not equal `num_params`, or when `params` is not passed and the parameters on the gate have not all been bound to concrete values. Numeric parameters passed in must be finite, otherwise `ParameterError` is raised. + +--- + +## Inverse gate + +```python +inverse() -> MCGate +``` + +`inverse()` returns the inverse gate of the current multi-controlled gate. The inverse of a multi-controlled gate can be understood as: keep the control structure unchanged and replace only the base gate with its inverse. In other words, the inverse of a multi-controlled gate is equivalent to "controlling the inverse of the base gate". + +```python +from cqlib.circuit.gates import MCGate, StandardGate + +gate = MCGate(1, StandardGate.S) +inverse = gate.inverse() + +assert inverse.base_gate == StandardGate.SDG +``` + +In the example above, the inverse of the `S` gate is `SDG`, so the inverse of a singly controlled `S` gate is a singly controlled `SDG` gate. For self-inverse gates, for example `X`, `Z` and `H`, the inverse of their multi-controlled form is usually the same as the gate itself. + +Calling `inverse()` requires the parameters on the gate to be fully bound: `CircuitError` is raised when the number of bound parameters does not equal `num_params`; `RuntimeError` is raised when the base gate itself is not invertible. + +--- + +## Relation to standard controlled gates + +Some standard gates in Cqlib already represent common controlled gates. For example, `CX` is a singly controlled `X` gate, `CZ` is a singly controlled `Z` gate, and `CCX` is a doubly controlled `X` gate. Semantically, these standard gates can all be regarded as special cases of `MCGate`. + +| Standard gate | Equivalent form | +| --- | --- | +| `StandardGate.CX` | `MCGate(1, StandardGate.X)` | +| `StandardGate.CY` | `MCGate(1, StandardGate.Y)` | +| `StandardGate.CZ` | `MCGate(1, StandardGate.Z)` | +| `StandardGate.CCX` | `MCGate(2, StandardGate.X)` | +| `StandardGate.CRX(theta)` | `MCGate(1, StandardGate.RX(theta))` | +| `StandardGate.CRY(theta)` | `MCGate(1, StandardGate.RY(theta))` | +| `StandardGate.CRZ(theta)` | `MCGate(1, StandardGate.RZ(theta))` | + +--- + +## Base gates that already carry control qubits + +If the base gate itself already contains control qubits, such as `StandardGate.CX`, adding control qubits with `MCGate` stacks the new control qubits on top of the existing control structure. + +```python +from cqlib.circuit.gates import MCGate, StandardGate + +gate = MCGate(1, StandardGate.CX) + +assert gate.num_ctrl_qubits == 2 +assert gate.num_qubits == 3 +``` + +In the example above, `StandardGate.CX` already contains 1 control qubit and 1 target qubit. `MCGate(1, StandardGate.CX)` adds 1 more control qubit outside it, so the whole thing is a three-qubit gate with 2 control qubits and 1 target qubit. + +--- + +## `MCGate` and `Circuit` + +To add an `MCGate` to a circuit, use `Circuit.append_mc_gate()`. This interface checks whether the number of qubits passed in agrees with `gate.num_qubits`, and checks legality such as parameters and duplicate qubit references when necessary. + +```python +from cqlib import Circuit +from cqlib.circuit.gates import MCGate, StandardGate + +circuit = Circuit(4) + +mcx = MCGate(3, StandardGate.X) +circuit.append_mc_gate(mcx, [0, 1, 2, 3]) +``` + +--- + +## Other behavior + +- Supports `==` and `hash`; comparison and hashing take the control structure, the base gate and the bound parameters into account at the same time, so an `MCGate` can be used as a dictionary key or a set element. +- Supports `copy` and `deepcopy`. +- `repr` can be evaluated directly to reconstruct the original gate: without parameters it has the form `MCGate(2, StandardGate.X)`, and with parameters it has the form `MCGate(1, StandardGate.RX(1.5))`. diff --git a/docs/api/en/python/0_circuit/8_gates_circuit_gate.md b/docs/api/en/python/0_circuit/8_gates_circuit_gate.md new file mode 100644 index 00000000..c37d9d57 --- /dev/null +++ b/docs/api/en/python/0_circuit/8_gates_circuit_gate.md @@ -0,0 +1,277 @@ +# CircuitGate / FrozenCircuit + +- `cqlib.circuit.gates.FrozenCircuit` +- `cqlib.circuit.gates.CircuitGate` + +```python +from cqlib.circuit.gates import FrozenCircuit, CircuitGate +``` + +`FrozenCircuit` and `CircuitGate` are used to wrap an already constructed circuit as a reusable composite gate. The two are usually used together: `FrozenCircuit` holds an immutable circuit definition, and `CircuitGate` wraps that definition into a gate object that can be appended to other circuits. + +--- + +## `Circuit`, `FrozenCircuit` and `CircuitGate` + +`Circuit`, `FrozenCircuit` and `CircuitGate` can be understood as three different levels: + +| Type | Role | Typical use | +|---|---|---| +| `Circuit` | A mutable circuit container, to which gates, measurement, control flow and other operations can still be appended | Constructing and editing quantum circuits | +| `FrozenCircuit` | An immutable circuit snapshot, holding the structural definition of a sub-circuit | Serving as a stable definition of a composite gate or of low-level IR | +| `CircuitGate` | A composite gate defined by a `FrozenCircuit` | Reusing a sub-circuit as a single gate | + +A typical flow is as follows: + +```text +Circuit ──to_gate(name)──> CircuitGate ──append_circuit_gate()──> Circuit + │ + └──内部持有 FrozenCircuit 定义 +``` + +--- + +## Construction + +A `CircuitGate` can be constructed directly by calling `Circuit.to_gate(name)`: + +```python +Circuit.to_gate(name: str) -> CircuitGate +``` + +Example: wrap a Bell state preparation circuit as a composite gate and reuse it in a larger circuit. + +```python +from cqlib import Circuit + +bell = Circuit(2) +bell.h(0) +bell.cx(0, 1) + +bell_gate = bell.to_gate("Bell") + +circuit = Circuit(4) +circuit.append_circuit_gate(bell_gate, [0, 1]) +circuit.append_circuit_gate(bell_gate, [2, 3]) +``` + +In the example above, `bell.to_gate("Bell")` wraps the two-qubit Bell sub-circuit as a `CircuitGate` named `Bell`. The composite gate can then be appended to other circuits like an ordinary gate and act on the given qubits. + +--- + +## FrozenCircuit + +`FrozenCircuit` represents an immutable circuit snapshot. It holds the qubit order, the operation sequence and optional classical type information of a sub-circuit. + +```python +FrozenCircuit( + qubits: list[Qubit], + operations: list[ValueOperation], + classical_vars: list[ClassicalType] | None = None, + classical_values: list[ClassicalType] | None = None, +) +``` + +| Parameter | Description | +|---|---| +| `qubits` | The storage order of qubits in the sub-circuit. This order affects the qubit mapping when the composite gate is applied. | +| `operations` | The self-contained operation sequence in the sub-circuit. | +| `classical_vars` | An optional table of classical variable types, usually used in lower-level IR scenarios. | +| `classical_values` | An optional table of classical value types, usually used in lower-level IR scenarios. | + +Example: + +```python +from cqlib import Circuit +from cqlib.circuit.gates import FrozenCircuit + +sub = Circuit(2) +sub.h(0) +sub.cx(0, 1) + +frozen = FrozenCircuit(sub.qubits, sub.operations) +``` + +| Attribute | Type | Description | +|---|---|---| +| `qubits` | `list[Qubit]` | The qubit order held in the sub-circuit definition. | +| `num_operations` | `int` | The number of operations the sub-circuit contains. | +| `operations` | `list[ValueOperation]` | The self-contained operation list in the sub-circuit. | +| `symbols` | `list[str]` | The symbol names registered inside the sub-circuit in insertion order, which may include symbols that are no longer referenced. | +| `used_symbols` | `list[str]` | The symbol names actually referenced after freezing. | + +`FrozenCircuit.used_symbols` can be used to find out whether the sub-circuit is a parameterized circuit. Note the difference between `symbols` and `used_symbols`: the former is a stable symbol registry, in which symbols that are no longer referenced may remain after operations are removed or replaced; the latter contains only the symbols actually referenced at present. A `CircuitGate` constructed with the parameter signature omitted uses `used_symbols` as its positional parameter signature, so a composite gate generated from it also requires positional parameters to be passed in the order of `used_symbols` when it is applied. + +--- + +## CircuitGate + +`CircuitGate` is a composite gate defined by a `FrozenCircuit`. It wraps a sub-circuit as a gate object, so that it can be added to other circuits through `Circuit.append_circuit_gate()`. + +```python +CircuitGate( + name: str, + circuit: FrozenCircuit, + signature_params: list[str] | None = None, +) -> CircuitGate +``` + +| Parameter | Description | +|---|---| +| `name` | The composite gate name, mainly used for display, debugging and IR expression. | +| `circuit` | The immutable circuit snapshot used to define the composite gate. | +| `signature_params` | The positional parameter signature. When omitted, `circuit.used_symbols` is taken; when given explicitly, parameters are bound in the order of that list. An explicit signature may contain parameter names that the sub-circuit does not reference, and those parameters still count toward the number of parameters of the gate. | + +Example: + +```python +from cqlib.circuit.gates import CircuitGate + +gate = CircuitGate("Bell", frozen) +``` + +| Attribute | Type | Description | +|---|---|---| +| `name` | `str` | The composite gate name. | +| `num_qubits` | `int` | The number of qubits to provide when applying the gate. | +| `num_params` | `int` | The number of positional parameters to provide when applying the gate, equal to the length of `signature_params`. | +| `signature_params` | `list[str]` | The positional parameter signature, which determines the binding order and the number of positional parameters when the gate is applied. | +| `used_symbols` | `list[str]` | The symbol names actually referenced in the sub-circuit on which the gate is based. | +| `symbols` | `list[str]` | A backward-compatible alias of `used_symbols`; it does not represent the binding order of positional parameters. | +| `circuit` | `FrozenCircuit` | The immutable circuit definition held inside the composite gate. | + +--- + +## Explicit parameter signature + +When the positional parameters of a gate need to be decoupled from the symbols actually appearing in the sub-circuit, the static method `CircuitGate.with_signature()` can be used to declare the call signature explicitly: + +```python +CircuitGate.with_signature( + name: str, + circuit: FrozenCircuit, + signature_params: list[str], +) -> CircuitGate +``` + +```python +from cqlib import Circuit, Parameter +from cqlib.circuit.gates import CircuitGate, FrozenCircuit + +body = Circuit(1) +body.rx(0, Parameter("used")) + +frozen_body = FrozenCircuit(body.qubits, body.operations) + +gate = CircuitGate.with_signature("Declared", frozen_body, ["used", "declared_unused"]) +``` + +When a signature is declared explicitly, duplicate parameter names are not allowed in `signature_params`; at the same time, every symbol actually referenced in the sub-circuit must already be declared in `signature_params`. Both cases raise `CircuitError`. + +A parameter that is declared but not referenced by the sub-circuit still counts toward the number of parameters of the gate, so `gate.num_params` equals the length of `signature_params` rather than the length of `gate.used_symbols`. + +--- + +## Parameterized sub-circuit gates + +If a sub-circuit contains symbolic parameters, `CircuitGate` records those symbolic parameters and binds them positionally in the order of `gate.signature_params` when the gate is applied. + +```python +from cqlib import Circuit, Parameter + +theta = Parameter("theta") + +block = Circuit(1) +block.rx(0, theta) +block.rz(0, theta / 2) + +gate = block.to_gate("ParamBlock") + +circuit = Circuit(1) +circuit.append_circuit_gate(gate, [0], params=[Parameter("alpha")]) +``` + +In the example above, the sub-circuit `block` contains the symbolic parameter `theta`. After it is wrapped as `ParamBlock`, the `alpha` passed in when the composite gate is applied externally replaces the inner `theta` according to the positional parameter rule. + +Note that the parameter binding of a `CircuitGate` is positional. That is, the `i`-th parameter passed in when applying the gate replaces the `i`-th symbol in `gate.signature_params`. + +```python +print(gate.signature_params) +print(gate.num_params) +``` + +--- + +## Appending to a Circuit + +A `CircuitGate` needs to be appended to a circuit through `Circuit.append_circuit_gate()`: + +```python +Circuit.append_circuit_gate( + gate: CircuitGate, + qubits: list[int | Qubit], + params: list[float | Parameter] | None = None, +) -> None +``` + +Example: + +```python +from cqlib import Circuit, Parameter + +alpha = Parameter("alpha") + +sub = Circuit(1) +sub.rx(0, Parameter("theta")) + +rx_block = sub.to_gate("RxBlock") + +main = Circuit(1) +main.append_circuit_gate(rx_block, [0], params=[alpha]) +``` + +When appending, note that: + +- the number of `qubits` must equal `gate.num_qubits`; +- the number of `params` must equal `gate.num_params`; +- the order of `qubits` determines the mapping from the qubits inside the sub-circuit to the qubits of the outer circuit; + +--- + +## Inverse gate + +```python +CircuitGate.inverse() -> CircuitGate +``` + +`inverse()` returns a new `CircuitGate`. The underlying frozen circuit of the new gate is obtained by inverting the original circuit operation by operation and reversing the order, the name of the new gate has the suffix `_dg` appended to the original name, and the positional parameter signature stays unchanged. + +```python +inverse_gate = gate.inverse() +``` + +This interface applies to invertible sub-circuits. If the sub-circuit definition contains measurement, `reset` or other non-invertible operations, for example classical data operations, control flow, and instructions that cannot be inverted, the inversion process raises `CircuitError`. Moreover, `inverse()` does not modify the original `CircuitGate` but returns a new composite gate definition. + +--- + +## Decomposition + +`Circuit.decompose()` can expand a `CircuitGate` in a circuit back into the base operation sequence of its inner definition. + +```python +outer = Circuit(1) +outer.append_circuit_gate(gate, [0], params=[0.2]) + +expanded = outer.decompose() +``` + +After decomposition, the composite gate is replaced by its inner original operations. For a parameterized `CircuitGate`, the positional parameters passed in when it is applied are substituted into the inner operations during decomposition. + +--- + +## Other behavior + +- Both `FrozenCircuit` and `CircuitGate` support structural equality comparison with `==`. Comparison of `FrozenCircuit` ignores the circuit identity and the symbolic matrix cache and compares only the definition itself; comparison of `CircuitGate` includes the name, the ordered signature and the underlying definition circuit. +- `repr` has the form `FrozenCircuit(qubits=2, operations=2)` and `CircuitGate("Bell", qubits=2, params=0)`. +- Both support `copy` and `deepcopy`. Since both are immutable objects, the copy equals the original object. + diff --git a/docs/api/en/python/0_circuit/9_classical_control_flow.md b/docs/api/en/python/0_circuit/9_classical_control_flow.md new file mode 100644 index 00000000..2795589d --- /dev/null +++ b/docs/api/en/python/0_circuit/9_classical_control_flow.md @@ -0,0 +1,621 @@ +# Classical Data / Control Flow + +- `cqlib.circuit.CircuitId` +- `cqlib.circuit.ClassicalType` +- `cqlib.circuit.ClassicalVar` +- `cqlib.circuit.ClassicalValue` +- `cqlib.circuit.Measurement` +- `cqlib.circuit.ClassicalExpr` +- `cqlib.circuit.ClassicalControlOp` +- `cqlib.circuit.ValueControlBody` +- `cqlib.circuit.ValueSwitchCase` + +```python +from cqlib.circuit import ( + CircuitId, + ClassicalType, + ClassicalExpr, + ClassicalControlOp, + ValueControlBody, + ValueSwitchCase, +) +``` + +The classical data and control flow API describes the classical-side information and structured control logic in a quantum circuit. Typical objects include classical types, classical variables, measurement results, classical expressions, and control flow structures such as `if`, `while`, `for` and `switch`. + +--- + +## Core objects + +Classical data and control flow objects fall into the following categories: + +| Type | Role | +|---|---| +| `ClassicalType` | Describes the static type of a classical value, such as `Bit`, `Bool`, `UInt` and `BitVec`. | +| `ClassicalVar` | A mutable classical variable handle, created by `Circuit.var()`. | +| `ClassicalValue` | An immutable classical value handle, usually produced by measurement. | +| `Measurement` | A measurement receipt, recording the measurement result handle and the order of the measured qubits. | +| `ClassicalExpr` | A side-effect-free classical expression, which can represent literals, variable reads, comparisons and logical composition. | +| `ClassicalControlOp` | A low-level control flow object, used to represent `if`, `while`, `for`, `switch`, `break` and `continue`. | +| `ValueControlBody` | The operation sequence in a control flow branch body or loop body. | +| `ValueSwitchCase` | A single integer match branch in a `switch`. | + +--- + +## `ClassicalType` + +`ClassicalType` describes the static type of a classical value. Control flow conditions, loop variables, measurement results and classical expressions all rely on type information for legality checks. + +```python +ClassicalType.bit() -> ClassicalType +ClassicalType.bool() -> ClassicalType +ClassicalType.uint(width: int) -> ClassicalType +ClassicalType.bit_vec(width: int) -> ClassicalType +``` + +Common classical types are as follows: + +| Type constructor | Description | Typical use | +|---|---|---| +| `ClassicalType.bit()` | A single bit, whose value is `0` or `1`. | Single-bit measurement results, bit expressions. | +| `ClassicalType.bool()` | A logical Boolean value, representing true or false. | `if` and `while` conditions. | +| `ClassicalType.uint(width)` | An unsigned integer with the given bit width. | Counters, loop variables, `switch` targets. | +| `ClassicalType.bit_vec(width)` | A bit vector with the given bit width. | Multi-bit measurement results, bit concatenation, bit packing results. | + +The bit width of `uint(width)` and `bit_vec(width)` must be greater than zero, otherwise `CircuitError` is raised. + +```python +from cqlib.circuit import ClassicalType + +bit_ty = ClassicalType.bit() +flag_ty = ClassicalType.bool() +u8_ty = ClassicalType.uint(8) +bv4_ty = ClassicalType.bit_vec(4) + +print(bit_ty.width) # 1 +print(u8_ty.width) # 8 +``` + +`ClassicalType` can also create common literal expressions of that type: + +```python +u4 = ClassicalType.uint(4) + +zero = u4.zero_literal() +one = u4.one_literal() +``` + +--- + +## `CircuitId` + +`CircuitId` is a circuit-local identity, used to distinguish the classical variables and classical values created by different `Circuit`s. + +```python +CircuitId() -> CircuitId +Circuit.id -> CircuitId +``` + +`Circuit.id` gives the identity of the owning circuit. When low-level IR construction needs to create handles directly without a circuit, `CircuitId()` can be used to construct an identity explicitly; the same group of related handles must use the same `CircuitId`. + +```python +from cqlib import Circuit +from cqlib.circuit import CircuitId + +circuit = Circuit(1) +print(circuit.id) + +explicit = CircuitId() +print(explicit) +``` + +--- + +## `ClassicalVar` + +`ClassicalVar` represents a mutable classical variable handle, created by `Circuit.var(type)`. + +```python +from cqlib import Circuit +from cqlib.circuit import ClassicalType + +circuit = Circuit(1) + +flag = circuit.var(ClassicalType.bool()) +counter = circuit.var(ClassicalType.uint(4)) +``` + +When low-level IR construction needs to create variable handles directly without a circuit, use: + +```python +ClassicalVar(circuit_id: CircuitId, index: int, ty: ClassicalType) -> ClassicalVar +``` + +Common attributes and methods are as follows: + +| Interface | Description | +|---|---| +| `id` | The local identity of the variable in its owning circuit; the value is the same as `index`. | +| `index` | The position of the variable in its owning circuit. | +| `circuit_id` | The circuit identity that created the variable. | +| `ty` | The static classical type of the variable. | +| `expr()` | Construct a `ClassicalExpr` that reads the current value of the variable. | + +```python +from cqlib.circuit import ClassicalExpr + +circuit.store(flag, ClassicalExpr.bool_literal(True)) +circuit.store(counter, ClassicalExpr.uint_literal(4, 3)) + +print(flag.index) +print(flag.ty) +print(flag.circuit_id == circuit.id) +``` + +`store(target, value)` writes an expression into a classical variable. When writing, `value.ty` must be exactly the same as `target.ty`, otherwise `store()` raises `CircuitError` directly; if `target` belongs to another circuit, `CircuitError` is likewise raised. + +Note that `ClassicalVar` has a circuit ownership relation and should not be mixed directly between different `Circuit`s. If the same logic needs to be expressed in another circuit, the corresponding variable should be created again in that circuit. + +--- + +## `ClassicalValue` + +`ClassicalValue` represents an immutable classical value handle, usually produced by a measurement operation. + +```python +from cqlib import Circuit + +circuit = Circuit(1) + +measurement = circuit.measure(0) +value = measurement.value +``` + +When low-level IR construction needs to create classical value handles directly without a circuit, use: + +```python +ClassicalValue(circuit_id: CircuitId, index: int, ty: ClassicalType) -> ClassicalValue +``` + +Common attributes and methods are as follows: + +| Interface | Description | +|---|---| +| `index` | The position of the classical value in its owning circuit. | +| `circuit_id` | The circuit identity that created the classical value. | +| `ty` | The static type of the classical value. | +| `expr()` | Construct an expression that reads the classical value. | + +```python +expr = value.expr() +``` + +--- + +## `Measurement` + +`Measurement` is the receipt object returned by a measurement operation, recording the measurement result handle and the order of the measured qubits. + +```python +Measurement(value: ClassicalValue, qubits: list[Qubit]) -> Measurement +``` + +`Measurement` is usually returned by `Circuit.measure()` or `Circuit.measure_bits()`; low-level IR construction can also create it directly with the form above. + +Common attributes and methods are as follows: + +| Attribute/method | Description | +|---|---| +| `value` | The `ClassicalValue` result handle produced by the measurement. | +| `qubits` | The order of the measured qubits. | +| `ty` | The measurement result type; `Bit` for a single qubit and `BitVec` for several qubits. | +| `width` | The number of measured bits. | +| `expr()` | Construct an expression that reads the measurement result. | +| `check_qubits(num_qubits)` | Check whether the measured qubits are within the given qubit range. | +| `project(full)` | Project the result corresponding to this measurement out of a complete `Outcome`. | +| `project_basis(basis)` | Project the result corresponding to this measurement from a computational basis index. | + +```python +from cqlib import Circuit + +circuit = Circuit(3) + +m = circuit.measure_bits([0, 2]) + +print(m.width) # 2 +print(m.ty) +print(m.qubits) + +expr = m.expr() +``` + +If a measurement result is needed as a condition, a single-bit measurement result can be converted to `Bool` directly: + +```python +single = circuit.measure(1) +condition = single.expr().to_bool() +``` + +--- + +## `ClassicalExpr` + +`ClassicalExpr` describes classical computation relations, such as reading variables, reading measurement values, constructing literals, performing logical operations, comparing expressions, conditional selection, bit extraction and bit concatenation. + +### 1. Creating expressions + +| Static method | Description | +|---|---| +| `ClassicalExpr.var(var)` | Read a mutable classical variable. | +| `ClassicalExpr.value(value)` | Read an immutable classical value. | +| `ClassicalExpr.bool_literal(value)` | Construct a `Bool` literal. | +| `ClassicalExpr.bit_literal(value)` | Construct a `Bit` literal. | +| `ClassicalExpr.uint_literal(width, value)` | Construct a `UInt` literal with the given bit width. | +| `ClassicalExpr.bit_vec_literal(width, value)` | Construct a `BitVec` literal with the given bit width. | + +```python +from cqlib.circuit import ClassicalExpr + +true_expr = ClassicalExpr.bool_literal(True) +bit_one = ClassicalExpr.bit_literal(True) +u4_three = ClassicalExpr.uint_literal(4, 3) +bits = ClassicalExpr.bit_vec_literal(4, 0b1010) + +print(true_expr.ty) # ClassicalType.bool() +print(bits.ty) # ClassicalType.bit_vec(4) +``` + +An expression object carries a `ty` attribute that returns its static classical type, which can be used to check the types of conditions and operands before control flow is constructed. + +### 2. Logical and bit operations + +`ClassicalExpr` supports common logical operations. The operators `~`, `&`, `|` and `^` correspond to negation, AND, OR and XOR respectively. + +| Method / operator | Description | +|---|---| +| `not_()` / `~expr` | Logical or bit negation. | +| `and_(rhs)` / `expr & rhs` | AND operation. | +| `or_(rhs)` / `expr \| rhs` | OR operation. | +| `xor(rhs)` / `expr ^ rhs` | XOR operation. | + +```python +from cqlib.circuit import ClassicalExpr + +a = ClassicalExpr.bool_literal(True) +b = ClassicalExpr.bool_literal(False) + +expr = (a & ~b) | b +print(expr.simplified()) +``` + +### 3. Type conversion + +| Method | Description | +|---|---| +| `bit_to_bool()` | Convert `Bit` to `Bool`. | +| `to_bool()` | Convert `Bit` to `Bool`, equivalent to `bit_to_bool()`. | +| `bit_vec_to_uint()` | Convert `BitVec` to `UInt`. | +| `to_uint()` | Convert `BitVec` to `UInt`, equivalent to `bit_vec_to_uint()`. | + +```python +from cqlib.circuit import ClassicalExpr + +bit_as_bool = ClassicalExpr.bit_literal(True).bit_to_bool() +bits_as_uint = ClassicalExpr.bit_vec_literal(4, 0b1010).to_uint() +``` + +When an expression is to be used as an `if_()` or `while_()` condition, its type should be `Bool`. If the original expression is a `Bit`, it can be converted explicitly through `to_bool()`; a `UInt` or `BitVec` expression needs to extract a single `Bit` with `extract_bit()` first and then convert it to `Bool`. + +### 4. Comparison and selection + +Runtime comparisons can use the static methods provided by `ClassicalExpr`. + +| Static method | Description | +|---|---| +| `equal(lhs, rhs)` | Equality comparison. | +| `not_equal(lhs, rhs)` | Inequality comparison. | +| `lt(lhs, rhs)` | Less-than comparison. | +| `le(lhs, rhs)` | Less-than-or-equal comparison. | +| `gt(lhs, rhs)` | Greater-than comparison. | +| `ge(lhs, rhs)` | Greater-than-or-equal comparison. | +| `select(condition, then_expr, else_expr)` | Select one of two expressions according to a `Bool` condition. | + +```python +from cqlib.circuit import ClassicalExpr + +x = ClassicalExpr.uint_literal(3, 2) +y = ClassicalExpr.uint_literal(3, 5) + +cond1 = ClassicalExpr.lt(x, y) +cond2 = ClassicalExpr.equal(x, y) +selected = ClassicalExpr.select(cond1, x, y) +``` + +A comparison result is usually a `Bool` expression and can be used directly as the condition of `if_()`, `if_else()` or `while_()`. The two branch expressions of `select()` should have compatible types. + +### 5. Bit extraction and concatenation + +| Method | Description | +|---|---| +| `extract_bit(index)` | Extract one `Bit` from a `UInt` or `BitVec`. | +| `extract_bits(offset, width)` | Extract a contiguous bit range from a `UInt` or `BitVec`; the result is a `BitVec` of the given bit width. | +| `concat(parts)` | Concatenate several `Bit` or `BitVec` expressions; the result is a `BitVec`. | +| `pack_bits(bits)` | Pack several `Bit` expressions into a `BitVec`. | + +```python +from cqlib.circuit import ClassicalExpr + +bits = ClassicalExpr.bit_vec_literal(4, 0b1010) + +low_bit = bits.extract_bit(0) +middle = bits.extract_bits(offset=1, width=2) + +packed = ClassicalExpr.pack_bits( + [ + ClassicalExpr.bit_literal(True), + ClassicalExpr.bit_literal(False), + ] +) +``` + +### 6. Simplification and literal checks + +| Method | Description | +|---|---| +| `simplified()` | Return the simplified expression. | +| `is_bool_true()` / `is_bool_false()` | Determine whether it is a `Bool` constant. | +| `is_bit_true()` / `is_bit_false()` | Determine whether it is a `Bit` constant. | + +### 7. Object behavior + +- Supports `==` and `hash`, and can be used as a dictionary key or a set element. +- `expr1 == expr2` is a structural equality comparison of expressions, determining whether two expression objects represent the same structure; to construct an expression for a runtime comparison, use static methods such as `ClassicalExpr.equal()`. +- Supports `copy` and `deepcopy`. +- `repr` gives the structural representation of the expression, which is convenient for debugging. + +--- + +## High-level control flow API + +`Circuit` provides a set of high-level closure-style control flow interfaces, used to construct structured conditional branches, loops and multi-way selection: + +```python +circuit.if_(condition, body) +circuit.if_else(condition, then_body, else_body) +circuit.while_(condition, body) +circuit.for_uint(var, start, stop, step, body) +circuit.switch(target, build) +circuit.break_loop() +circuit.continue_loop() +``` + +These interfaces construct a branch body or loop body through a callback function. The callback receives a temporary circuit builder, in which the operations to be placed inside the control flow body can be appended. After construction is complete, the whole control flow structure is added to the circuit as one operation. + +### 1. `if_` + +`if_(condition, body)` constructs a conditional structure without an `else` branch. `condition` must be a `Bool` expression. + +```python +from cqlib import Circuit +from cqlib.circuit import ClassicalExpr + +circuit = Circuit(2) + +circuit.if_( + ClassicalExpr.bool_literal(True), + lambda body: body.x(1), +) +``` + +If the condition comes from a single-bit measurement result, it usually needs to be converted to `Bool` first: + +```python +m = circuit.measure(0) +circuit.if_(m.expr().to_bool(), lambda body: body.x(1)) +``` + +### 2. `if_else` + +`if_else(condition, then_body, else_body)` constructs a conditional structure with an `else` branch. `then_body` is executed when the condition is true, otherwise `else_body` is executed. + +```python +circuit.if_else( + m.expr().to_bool(), + lambda then_body: then_body.x(1), + lambda else_body: else_body.z(1), +) +``` + +### 3. `while_` + +`while_(condition, body)` constructs a loop structure based on a `Bool` condition. + +```python +from cqlib.circuit import ClassicalType, ClassicalExpr + +flag = circuit.var(ClassicalType.bool()) +circuit.store(flag, ClassicalExpr.bool_literal(True)) + +circuit.while_( + flag.expr(), + lambda body: body.store(flag, ClassicalExpr.bool_literal(False)), +) +``` + +`break_loop()` and `continue_loop()` can be used inside a loop body to control the loop flow: + +```python +circuit.while_( + ClassicalExpr.bool_literal(True), + lambda body: body.break_loop(), +) +``` + +### 4. `for_uint` + +`for_uint(var, start, stop, step, body)` constructs a half-open interval loop based on an unsigned integer variable. The loop variable `var` must be of type `UInt`, and `start`, `stop` and `step` must agree with the bit width of the variable. + +```python +from cqlib.circuit import ClassicalType, ClassicalExpr + +i = circuit.var(ClassicalType.uint(4)) +u4 = ClassicalType.uint(4) + +circuit.for_uint( + i, + u4.zero_literal(), + ClassicalExpr.uint_literal(4, 3), + u4.one_literal(), + lambda body, i_expr: body.x(0), +) +``` + +The loop range uses `[start, stop)` semantics. The `body` callback receives two arguments: a temporary circuit builder and the `UInt` expression corresponding to the current loop variable. + +### 5. `switch` + +`switch(target, build)` performs exact integer matching on a `UInt` expression and selects the corresponding branch to execute. + +```python +target = ClassicalExpr.uint_literal(2, 1) + +def build_cases(case): + case.value(0, lambda body: body.x(0)) + case.value(1, lambda body: body.z(0)) + case.default(lambda body: body.h(0)) + +circuit.switch(target, build_cases) +``` + +### 6. Transactional semantics + +The closure-style control flow API has transactional semantics. That is, the corresponding branch body or control flow structure is committed to the circuit only after the callback function completes successfully. If an exception is raised during the callback, or validation fails while appending an operation, this construction is rolled back and leaves no half-constructed state in the circuit. + +```python +circuit = Circuit(1) + +try: + circuit.if_( + ClassicalExpr.bool_literal(True), + lambda body: (_ for _ in ()).throw(RuntimeError("failed")), + ) +except RuntimeError: + pass +``` + +--- + +## Low-level control flow IR + +For IR converters, deserializers, compiler tests or low-level structure construction, the low-level control flow objects can also be used directly. + +### `ValueControlBody` + +```python +ValueControlBody(operations: list[ValueOperation]) +``` + +`ValueControlBody` represents the operation sequence in a control flow body. + +| Interface | Description | +|---|---| +| `operations` | The operation sequence inside the control flow body. | +| `__len__()` | Return the number of operations. | +| `has_measurement()` | Determine whether the control flow body contains measurement directly or recursively. | +| `reads_value(value)` | Determine whether the control flow body reads the given `ClassicalValue`. | + +### `ValueSwitchCase` + +```python +ValueSwitchCase(value: int, body: ValueControlBody) +``` + +`ValueSwitchCase` represents an exact integer match branch in a `switch`. `value` is the match value and `body` is the control flow body executed when that branch is taken. + +### `ClassicalControlOp` + +`ClassicalControlOp` is a low-level control flow object that can directly represent `if`, `while`, `for`, `switch` and jump instructions. + +| Static method | Description | +|---|---| +| `ClassicalControlOp.if_(condition, then_body, else_body=None)` | Construct `if` or `if-else` control flow. | +| `ClassicalControlOp.while_(condition, body)` | Construct a `while` loop. | +| `ClassicalControlOp.for_uint(var, start, stop, step, body)` | Construct a `for` loop based on a `UInt` variable. | +| `ClassicalControlOp.switch(target, cases, default=None)` | Construct a `switch` multi-way selection. | +| `ClassicalControlOp.break_()` | Construct a `break` jump. | +| `ClassicalControlOp.continue_()` | Construct a `continue` jump. | + +Common attributes are as follows: + +| Attribute | Description | +|---|---| +| `kind` | The control flow type, such as `"if"`, `"while"`, `"for"`, `"switch"`, `"break"` and `"continue"`. | +| `condition` | The `if` or `while` condition. | +| `then_body` / `else_body` | The branch bodies of `if` control flow. | +| `body` | The body of `while` or `for` control flow. | +| `var` / `start` / `stop` / `step` | The components related to a `for` loop. | +| `target` / `cases` / `default` | The components related to a `switch` multi-way selection. | +| `has_measurement()` | Determine whether measurement is contained inside the control flow. | +| `reads_value(value)` | Determine whether the given classical value is read inside the control flow. | + +`ValueControlBody`, `ValueSwitchCase` and `ClassicalControlOp` all support `copy` and `deepcopy`, and carry a `repr` convenient for debugging. + +--- + +## Validation rules + +`Circuit.validate()` checks whether classical data and control flow structures satisfy the internal consistency requirements of a circuit. + +Validation usually covers the following rules: + +- classical variables and classical values must belong to the current circuit; +- a measurement value must be defined before it is used; +- values defined inside a control flow scope must not escape the scope; +- the conditions of `if` and `while` must be `Bool` expressions; +- the loop variable of `for_uint` must be of type `UInt`; +- `start`, `stop` and `step` of `for_uint` must agree with the bit width of the loop variable; +- the target of `switch` must be a `UInt` expression; +- `break` and `continue` must be located in a valid scope; +- low-level control flow objects constructed by hand must be compatible with the current circuit. + +```python +from cqlib import Circuit +from cqlib.circuit import ClassicalExpr + +circuit = Circuit(1) +circuit.if_(ClassicalExpr.bool_literal(True), lambda body: body.x(0)) + +circuit.validate() +``` + +--- + +## Relation to matrix conversion + +A circuit containing measurement, classical data writes or control flow usually cannot be represented directly as one fixed unitary matrix. The reason is that such a circuit describes a program with runtime classical information, conditional branches or loop structures, rather than a fixed sequence of purely quantum gates. + +```python +from cqlib import Circuit +from cqlib.circuit import CircuitError, ClassicalExpr + +circuit = Circuit(1) +circuit.if_(ClassicalExpr.bool_literal(True), lambda body: body.x(0)) + +try: + circuit.to_matrix() +except CircuitError: + print("control-flow circuits do not have one fixed unitary matrix") +``` + +If only the pure quantum operations in one branch need to be validated, that branch body can be constructed separately as a static `Circuit`, matrix-validated first, and then composed into the control flow. + +```python +branch = Circuit(1) +branch.x(0) +branch_matrix = branch.to_matrix() + +circuit = Circuit(1) +circuit.if_(ClassicalExpr.bool_literal(True), lambda body: body.compose(branch)) +``` + +This approach handles "gate-level validation" and "control flow structure construction" separately, making testing and debugging clearer. diff --git a/docs/api/en/python/0_overview.md b/docs/api/en/python/0_overview.md new file mode 100644 index 00000000..6c6a65bb --- /dev/null +++ b/docs/api/en/python/0_overview.md @@ -0,0 +1,81 @@ +# Python API Overview + +## Welcome to the Cqlib Python API reference! + +## Documentation navigation + +### Quantum Circuit (`cqlib.circuit`) + +- [Overview](0_circuit/0_overview.md) +- [Circuit](0_circuit/1_circuit.md) +- [Qubit](0_circuit/2_qubit.md) +- [Parameter](0_circuit/3_parameter.md) +- [Operation / Instruction](0_circuit/4_operation_instruction.md) +- [StandardGate](0_circuit/5_gates_standard.md) +- [UnitaryGate](0_circuit/6_gates_unitary.md) +- [MCGate](0_circuit/7_gates_mc_gate.md) +- [CircuitGate / FrozenCircuit](0_circuit/8_gates_circuit_gate.md) +- [Classical / Control Flow](0_circuit/9_classical_control_flow.md) +- [SymbolicMatrix](0_circuit/10_symbolic_matrix.md) +- [Ansatz](0_circuit/11_ansatz.md) +- [Circuit To Matrix](0_circuit/12_circuit_to_matrix.md) +- [CircuitDag](0_circuit/13_circuit_dag.md) + +### Intermediate Representation (`cqlib.ir`) + +- [Overview](1_ir/0_overview.md) +- [QCIS](1_ir/1_qcis.md) +- [OpenQASM 2.0](1_ir/2_qasm2.md) +- [OpenQASM 3.0](1_ir/3_qasm3.md) + +### Device (`cqlib.device`) + +- [Overview](2_device/0_overview.md) +- [Topology](2_device/1_topology.md) +- [Device and noise properties](2_device/2_properties_device.md) +- [Layout](2_device/3_layout.md) +- [Noise](2_device/4_noise.md) +- [Result](2_device/5_result.md) +- [Qubit Identifiers](2_device/6_qubits.md) + +### Quantum Information (`cqlib.qis`) + +- [Overview](3_qis/0_overview.md) +- [Statevector](3_qis/1_statevector.md) +- [DensityMatrix](3_qis/2_density_matrix.md) +- [DensityMatrixNoise](3_qis/3_density_matrix_noise.md) +- [Stabilizer](3_qis/4_stabilizer.md) +- [ClassicalState](3_qis/5_classical_state.md) +- [Pauli](3_qis/6_pauli.md) +- [Hamiltonian](3_qis/7_hamiltonian.md) +- [Evolution](3_qis/8_evolution.md) +- [Metrics / Entropy](3_qis/9_metrics_entropy.md) + +### Compilation (`cqlib.compile`) + +- [Overview](4_compile/0_overview.md) +- [Compiler](4_compile/1_compiler.md) +- [Transform](4_compile/2_transform.md) +- [Layout](4_compile/3_layout.md) +- [Routing](4_compile/4_routing.md) +- [SABRE](4_compile/5_sabre.md) +- [Decompose / Resynthesis](4_compile/6_decompose_resynthesis.md) +- [Knowledge](4_compile/7_knowledge.md) +- [Resource](4_compile/8_resource.md) +- [Commutation](4_compile/9_commutation.md) + +### Visualization (`cqlib.visualization`) + +- [Overview](5_visualization/0_overview.md) +- [Text circuit diagrams](5_visualization/1_draw_text.md) +- [SVG circuit diagrams](5_visualization/2_draw_figure.md) +- [Quantum state visualization](5_visualization/3_state_plots.md) +- [Measurement result visualization](5_visualization/4_result_plots.md) +- [Write to file and inline display](5_visualization/5_render_to_file.md) + +### Error Mitigation (`cqlib.error_mitigation`) + +- [Overview](6_error_mitigation/0_overview.md) +- [Zero-noise extrapolation (ZNE)](6_error_mitigation/1_zne.md) +- [Virtual distillation](6_error_mitigation/2_virtual_distillation.md) +- [Unified pipeline](6_error_mitigation/3_unified.md) diff --git a/docs/api/en/python/1_ir/0_overview.md b/docs/api/en/python/1_ir/0_overview.md new file mode 100644 index 00000000..a2b0183c --- /dev/null +++ b/docs/api/en/python/1_ir/0_overview.md @@ -0,0 +1,193 @@ +# Intermediate Representation + +`cqlib.ir` + +`cqlib.ir` is the intermediate representation module of Cqlib, providing bidirectional conversion between `Circuit` and three circuit text formats: QCIS, OpenQASM 2.0 and OpenQASM 3.0. Each format is a submodule, and the interface shape is exactly the same across all of them. + +## Overview + +The intermediate representation solves the problem of textualizing circuits: a circuit is text when it travels between files, tools and services, whereas Cqlib represents it uniformly with `Circuit` internally. Each submodule of `cqlib.ir` provides two directions, "text in, `Circuit` out" and "`Circuit` in, text out", and the conversion does not modify the `Circuit` passed in. + +### Unified interface: parsing and export + +The three submodules use the same set of four functions: + +- Parsing direction: `loads(text)` parses from a string, `load(path)` reads a file and then parses it; both return a `Circuit`. +- Export direction: `dumps(circuit)` returns a text string, `dump(circuit, path)` writes the text to a file and returns `None`. + +`loads` / `dumps` do not touch the file system, whereas `load` / `dump` do read and write files, so "file read/write failure" and "content parse failure" are two different kinds of exceptions. + +### The three formats + +| Format | Submodule | Text characteristics | +| --- | --- | --- | +| QCIS | `cqlib.ir.qcis` | Composed of operation lines; export only accepts the QCIS native gate set. | +| OpenQASM 2.0 | `cqlib.ir.qasm2` | Carries the `OPENQASM 2.0;` and `include "qelib1.inc";` header and `qreg` declarations; supports custom gates. | +| OpenQASM 3.0 | `cqlib.ir.qasm3` | See [qasm3](3_qasm3.md). | + +### Round-trip conversion and boundaries + +The circuit obtained from `loads(dumps(circuit))` is equivalent to the input circuit in its operation sequence; this is round-trip conversion. The expressive power of the text formats is limited, however: + +- QCIS only accepts gates inside the native gate set; if a circuit contains other gates, they must first be decomposed into that gate set before exporting. +- QCIS text does not record the number of qubits, so a circuit that has qubits but no operations exports as an empty string. + +### Relationship to compilation + +Export is an optional form of writing compilation output to file: once a circuit is lowered to the target gate set, it can be written out in the corresponding format. Format differences show up only in the gate set and the text header, while the interface on the `Circuit` side stays unchanged, so a circuit can also be read in one format and written out in another, which completes format conversion. + +--- + +## Common entry points + +```python +from cqlib import Circuit +from cqlib.ir import qcis + +c1 = Circuit(2) +c1.h(0) +c1.cz(0, 1) + +qcis_text = qcis.dumps(c1) +c2 = qcis.loads(qcis_text) + +assert c2.num_qubits == 2 +assert len(c2) == 2 +``` + +--- + +## Core concepts and terms + +| Term | Description | +| --- | --- | +| **Intermediate Representation** | The representation layer between `Circuit` and the circuit text formats, provided by the format submodules under `cqlib.ir`. | +| **Parsing** | The text-to-`Circuit` direction, corresponding to `loads` (string) and `load` (file). | +| **Export** | The `Circuit`-to-text direction, corresponding to `dumps` (string) and `dump` (file). | +| **Round-trip conversion** | Exporting and then parsing back to an equivalent circuit, that is `loads(dumps(circuit))`. | +| **Format conversion** | Reading in one format and writing out another, for example writing OpenQASM 2.0 text as QCIS text. | +| **Native gate set** | The gate set defined by the text format itself; when exporting, every gate in the circuit must fall inside that set. | + +--- + +## `cqlib.ir` API overview + +### Parsing entry points + +| Name | Description | +| --- | --- | +| [`qcis.loads`](1_qcis.md) / [`qcis.load`](1_qcis.md) | Parse a `Circuit` from a QCIS string or file. | +| [`qasm2.loads`](2_qasm2.md) / [`qasm2.load`](2_qasm2.md) | Parse a `Circuit` from an OpenQASM 2.0 string or file. | +| [`qasm3.loads`](3_qasm3.md) / [`qasm3.load`](3_qasm3.md) | Parse a `Circuit` from an OpenQASM 3.0 string or file. | + +### Export entry points + +| Name | Description | +| --- | --- | +| [`qcis.dumps`](1_qcis.md) / [`qcis.dump`](1_qcis.md) | Export a `Circuit` as a QCIS string or file. | +| [`qasm2.dumps`](2_qasm2.md) / [`qasm2.dump`](2_qasm2.md) | Export a `Circuit` as an OpenQASM 2.0 string or file. | +| [`qasm3.dumps`](3_qasm3.md) / [`qasm3.dump`](3_qasm3.md) | Export a `Circuit` as an OpenQASM 3.0 string or file. | + +### Format submodules + +| Name | Description | +| --- | --- | +| [`cqlib.ir.qcis`](1_qcis.md) | The QCIS text format; export is constrained by the QCIS native gate set. | +| [`cqlib.ir.qasm2`](2_qasm2.md) | The OpenQASM 2.0 text format, supporting standard gates, parameter expressions, measurement and custom gates. | +| [`cqlib.ir.qasm3`](3_qasm3.md) | The OpenQASM 3.0 text format. | + +--- + +## Quick examples + +### 1. Round-trip conversion of QCIS text + +```python +from cqlib.circuit import Circuit +from cqlib.ir.qcis import dumps, loads + +c1 = Circuit(2) +c1.h(0) +c1.cz(0, 1) + +qcis = dumps(c1) +c2 = loads(qcis) + +assert c2.num_qubits == 2 +assert len(c2) == 2 +``` + +The exported text is one operation per line, so the content can be asserted precisely: + +```python +from cqlib.circuit import Circuit +from cqlib.ir.qcis import dumps + +c = Circuit(2) +c.cx(0, 1) + +assert dumps(c) == "CX Q0 Q1\n" +``` + +### 2. Parsing and exporting OpenQASM 2.0 text + +```python +from cqlib.ir import qasm2 + +code = """OPENQASM 2.0; +include "qelib1.inc"; +qreg q[1]; +h q[0]; +""" + +circuit = qasm2.loads(code) + +text = qasm2.dumps(circuit) +lines = text.strip().split("\n") +assert lines[1] == "OPENQASM 2.0;" +assert lines[2] == 'include "qelib1.inc";' +assert "h q[0];" in text +``` + +### 3. Reading and writing QCIS files + +```python +import os +import tempfile + +from cqlib.circuit import Circuit +from cqlib.ir import qcis + +c = Circuit(2) +c.h(0) +c.cz(0, 1) + +with tempfile.NamedTemporaryFile(mode="w", suffix=".qcis", delete=False) as f: + path = f.name + +try: + qcis.dump(c, path) + + with open(path) as f: + assert "H Q0" in f.read() + + c2 = qcis.load(path) + assert c2.num_qubits == 2 +finally: + os.unlink(path) +``` + +--- + +## Validation and error handling + +Content problems raise `ValueError`, file problems raise `OSError` (`IOError` is its alias), and the message prefix of each distinguishes the format and the action. + +| Exception | When it occurs | +| --- | --- | +| `ValueError` | The text syntax is invalid or parsing failed; the message prefix is `QCIS parse error:`, `QASM parse error:`, `QASM3 parse error:`. | +| `ValueError` | Export failed, for example the circuit contains a gate the target format cannot express; the message prefix is `QCIS dump error:`, `QASM dump error:`, `QASM3 dump error:`. | +| `OSError` | `load` failed to read the file; the message prefix is ` load error:`. | +| `OSError` | `dump` failed to write the file; the message prefix is ` dump error:`. | + +With `load` / `dump`, the same prefix may come from a file read/write or from content processing; to distinguish the two, judge by the exception type. Before exporting, the circuit must already fall inside the target format's gate set, otherwise decompose the gates first and then export. diff --git a/docs/api/en/python/1_ir/1_qcis.md b/docs/api/en/python/1_ir/1_qcis.md new file mode 100644 index 00000000..66ec0cb5 --- /dev/null +++ b/docs/api/en/python/1_ir/1_qcis.md @@ -0,0 +1,135 @@ +# qcis + +`cqlib.ir.qcis` provides a bidirectional conversion interface between QCIS text and `Circuit`. + +## Import + +```python +from cqlib.ir import qcis +``` + +--- + +## Functions + +### qcis.loads(qcis) + +Parse a circuit from a QCIS string. + +Parameters: + +- `qcis` (`str`): the QCIS text. + +Returns: + +- `Circuit` + +Raises: + +- `ValueError`: invalid QCIS syntax or parse failure (`QCIS parse error: ...`). + +Example: + +```python +from cqlib.ir import qcis + +qcis_code = """H Q0 +CZ Q0 Q1 +""" +circuit = qcis.loads(qcis_code) +``` + +### qcis.load(path) + +Parse a circuit from a QCIS file. + +Parameters: + +- `path` (`str`): the QCIS file path. + +Returns: + +- `Circuit` + +Raises: + +- `OSError`: file read failure (nonexistent file, insufficient permissions, and so on). +- `ValueError`: QCIS parsing of the file content failed (`QCIS parse error: ...`). + +Description: + +- `load` reads a "file path"; +- `loads` reads "string content". + +### qcis.dumps(circuit) + +Serialize a circuit into a QCIS string. + +Parameters: + +- `circuit` (`Circuit`) + +Returns: + +- `str` + +Raises: + +- `ValueError`: export failure (commonly an unsupported gate; error prefix `QCIS dump error: ...`). + +### qcis.dump(circuit, path) + +Serialize a circuit and write it to a QCIS file. + +Parameters: + +- `circuit` (`Circuit`) +- `path` (`str`) + +Returns: + +- `None` + +Raises: + +- `OSError`: file write failure. +- `ValueError`: export failure (for example the circuit contains a gate QCIS cannot express; error prefix `QCIS dump error: ...`). + +## Notes + +`qcis.dumps` / `qcis.dump` only support the QCIS native gate set. A non-native gate raises an error, and the circuit must first be decomposed/compiled into the QCIS gate set before exporting. + +The current QCIS native gate set is: + +- Single-qubit gates: `H`, `S`, `SD`, `T`, `TD`, `X`, `X2P`, `X2M`, `Y`, `Y2P`, `Y2M`, `Z` +- Parameterized single-qubit gates: `RX`, `RXY`, `RY`, `RZ`, `U`, `XY`, `XY2P`, `XY2M`, `PHASE` +- Multi-qubit gates: `RXX`, `RYY`, `RZX`, `RZZ`, `SWAP`, `CX`, `CCX`, `CY`, `CZ`, `CRX`, `CRY`, `CRZ`, `FSIM` + +Besides gates, QCIS text also represents measurement (`M`), barrier (`B`) and delay (`I Qn t`, where `t` is the number of delay ticks in units of 0.5 ns and must be a non-negative integer). + +`SDG` and `TDG` are accepted during parsing as equivalent spellings of `SD` and `TD`, and are uniformly written as `SD` and `TD` when exporting. Standard identity gates and global phase are not inside the QCIS native gate set and raise an error when exporting; `I` in QCIS always means a delay, not an identity gate. + +## Example + +```python +from cqlib.ir import qcis + +qcis_code = """H Q0 +CZ Q0 Q1 +""" +circuit = qcis.loads(qcis_code) + +print(qcis.dumps(circuit)) + +qcis.dump(circuit, "input.qcis") + +# 1) 读文件 +c = qcis.load("input.qcis") + +# 2) 处理电路(示例:分解) +c2 = c.decompose() + +# 3) 导出文本或文件 +text = qcis.dumps(c2) +qcis.dump(c2, "output.qcis") +``` diff --git a/docs/api/en/python/1_ir/2_qasm2.md b/docs/api/en/python/1_ir/2_qasm2.md new file mode 100644 index 00000000..4fc79b42 --- /dev/null +++ b/docs/api/en/python/1_ir/2_qasm2.md @@ -0,0 +1,152 @@ +# qasm2 + +`cqlib.ir.qasm2` provides a bidirectional conversion interface between OpenQASM 2.0 and `Circuit`. + +## Import + +```python +from cqlib.ir import qasm2 +``` + +--- + +## Functions + +### qasm2.loads(qasm) + +Parse a circuit from an OpenQASM 2.0 string. + +Parameters: + +- `qasm` (`str`): the QASM text. + +Returns: + +- `Circuit` + +Raises: + +- `ValueError`: invalid QASM syntax or parse failure (`QASM parse error: ...`). + +Example: + +```python +from cqlib.ir import qasm2 + +code = """OPENQASM 2.0; +include "qelib1.inc"; +qreg q[2]; +h q[0]; +cz q[0],q[1]; +""" + +circuit = qasm2.loads(code) +``` + +### qasm2.load(path) + +Read and parse a circuit from a QASM file. + +Parameters: + +- `path` (`str`): the file path. + +Returns: + +- `Circuit` + +Raises: + +- `OSError`: file read failure (`QASM load error: ...`). +- `ValueError`: parsing of the file content failed (`QASM load error: ...`). + +### qasm2.dumps(circuit) + +Export a circuit as an OpenQASM 2.0 string. + +Parameters: + +- `circuit` (`Circuit`) + +Returns: + +- `str` + +Raises: + +- `ValueError`: export failure (`QASM dump error: ...`). + +Description: + +- The output contains the standard header, for example: +`OPENQASM 2.0;`, `include "qelib1.inc";`, `qreg`/`creg` declarations. + +### qasm2.dump(circuit, path) + +Export a circuit as a QASM file. + +Parameters: + +- `circuit` (`Circuit`) +- `path` (`str`): the output path. + +Returns: + +- `None` + +Raises: + +- `OSError`: file write failure (`QASM dump error: ...`). +- `ValueError`: export failure (`QASM dump error: ...`). + +## Supported features + +The `qasm2` interface currently supports: + +- Common standard gates: `h/x/y/z/s/sdg/t/tdg/id`, `cx/cy/cz/swap/ccx`, and so on +- Parameterized gates: `rx/ry/rz/u1/u2/u3/p`, plus `sx/sxdg/crx/cry/crz/rxx/ryy/rzz/fsim` and `ch/cu1/cu3` +- Parameter expressions: such as `pi`, `pi/2`, `3*pi/4`; loading also accepts `1e-5` and `asin`, `acos`, `atan` +- Instructions: `measure`, `barrier`, `reset` +- Declarations and definitions: `qreg`, `creg`, `opaque`, plus `gate` custom gate definitions and `if (creg == integer) qop;` conditional statements +- Export and loading of custom gates (`CircuitGate`); a unitary gate carrying only a matrix is exported as an `opaque` declaration + +`include "qelib1.inc";` is treated as a built-in inclusion and does not require the file to exist locally; the qelib1 gate names are likewise available when `include` is not written, and cannot be redefined. + +## Limitations + +Export makes trade-offs according to the expressive power of OpenQASM 2.0; the following constructs raise an error: + +- `else` branches in classical control, loops, `switch` and nested control flow +- Classical types that cannot be represented as `creg`, such as `bool` and unsigned integers +- Measurement and reset inside a `gate` definition body +- Bitwise conditions (such as `if (c[0] == 1)`) and conditional barriers + +`rzx` appears only in the extension gate definitions on the export side and is not accepted when loading; global phase is not written as a statement but output only as a comment, and is not restored when the text is loaded again. + +## Common workflow + +```python +from cqlib.ir import qasm2 + +code = """OPENQASM 2.0; +include "qelib1.inc"; +qreg q[2]; +h q[0]; +cz q[0],q[1]; +""" + +circuit = qasm2.loads(code) +print(qasm2.dumps(circuit)) + +qasm2.dump(circuit, "input.qasm") + +# 1) 读取 QASM 文件 +c = qasm2.load("input.qasm") + +# 2) 处理电路(示例) +c2 = c.decompose() + +# 3) 导出为字符串或文件 +text = qasm2.dumps(c2) +qasm2.dump(c2, "output.qasm") +``` diff --git a/docs/api/en/python/1_ir/3_qasm3.md b/docs/api/en/python/1_ir/3_qasm3.md new file mode 100644 index 00000000..ae0e5c87 --- /dev/null +++ b/docs/api/en/python/1_ir/3_qasm3.md @@ -0,0 +1,226 @@ +# qasm3 + +The `qasm3` submodule under `cqlib.ir` provides a bidirectional conversion interface between OpenQASM 3.0 and `Circuit`. + +## Import + +```python +from cqlib.ir import qasm3 +``` + +--- + +## Functions + +### qasm3.loads(qasm_text) + +Parse a circuit from an OpenQASM 3.0 string. + +Parameters: + +- `qasm_text` (`str`): the QASM text. + +Returns: + +- `Circuit` + +Raises: + +- `ValueError`: a syntax error, a semantic error, or use of a feature the current implementation does not support (`QASM3 parse error: ...`). + +Example: + +```python +from cqlib.ir import qasm3 + +code = """OPENQASM 3; +include "stdgates.inc"; +qubit[2] q; +h q[0]; +cx q[0], q[1]; +""" + +circuit = qasm3.loads(code) +``` + +### qasm3.load(path) + +Read and parse a circuit from a QASM file. + +Parameters: + +- `path` (`str`): the file path. + +Returns: + +- `Circuit` + +Raises: + +- `OSError`: file read failure (nonexistent file, insufficient permissions, and so on). +- `ValueError`: parsing of the file content failed (`QASM3 load error: ...`). + +Description: + +- `load` reads a "file path", and `loads` reads "string content". +- An `include` with a relative path is resolved from the directory of the input file. + +### qasm3.dumps(circuit) + +Export a circuit as an OpenQASM 3.0 string. + +Parameters: + +- `circuit` (`Circuit`) + +Returns: + +- `str` + +Raises: + +- `ValueError`: export failure (`QASM3 dump error: ...`). + +Description: + +- The output is canonicalized text; whitespace, comments and variable names of the original input are not preserved. +- The content is, in order: the header, the standard library inclusion, extension gate definitions, custom gate definitions, quantum and classical declarations, and the main circuit operations. + +### qasm3.dump(circuit, path) + +Export a circuit as a QASM file. + +Parameters: + +- `circuit` (`Circuit`) +- `path` (`str`): the output path. + +Returns: + +- `None` + +Raises: + +- `OSError`: file write failure. +- `ValueError`: export failure (`QASM3 dump error: ...`). + +## Supported features + +The `qasm3` interface currently supports: + +- The headers `OPENQASM 3;` and `OPENQASM 3.0;` +- The standard library inclusion `include "stdgates.inc";`; parsing uses a built-in standard library and does not require the file to exist in the directory +- Quantum declarations: the scalar `qubit q;` and the one-dimensional register `qubit[2] q;` +- Classical declarations: `bit`, `bit[n]`, `bool`, `uint[n]` +- Angle input declarations such as `input angle[64] theta;`, taken as symbolic parameters of the circuit +- Standard gates mapped to `StandardGate`: `id`, `i`, `x`, `y`, `z`, `h`, `s`, `sdg`, `t`, `tdg`, `rx`, `ry`, `rz`, `p`, `phase`, `u1`, `u`, `U`, `u3`, `u2`, `cx`, `cy`, `cz`, `swap`, `ccx`, `crx`, `cry`, `crz`, plus `sx`, `sxdg` (corresponding to X2P and X2M respectively) +- Cqlib extension gates: `x2p`, `x2m`, `y2p`, `y2m`, `xy2p`, `xy2m`, `rxx`, `ryy`, `rzz`, `rzx`, `fsim`, `gphase` +- Definition and invocation of custom `gate`s, including custom gates with parameters +- Measurement: `c = measure q;`, `v[0] = measure q[0];`, and the standalone `measure q;` +- The instructions `reset` and `barrier` +- Global phase `gphase(theta);` +- Control flow: `if` / `else`, statically expandable `for`, and `switch` with constant branches + +## Differences from the OpenQASM 2.0 interface + +On top of the same `Circuit` representation, the main differences between this interface and the OpenQASM 2.0 interface: + +| Aspect | OpenQASM 2.0 interface | OpenQASM 3.0 interface | +|---|---|---| +| Header and standard library | `OPENQASM 2.0;`, `include "qelib1.inc";` | `OPENQASM 3;` or `OPENQASM 3.0;`, `include "stdgates.inc";` | +| Quantum declarations | `qreg q[2];` | `qubit[2] q;`, and also supports the scalar `qubit q;` | +| Classical declarations | `creg c[2];` | `bit`, `bit[n]`, `bool`, `uint[n]` | +| Measurement | `measure q[0] -> c[0];` | `c[0] = measure q[0];`, `c = measure q;` | +| Control flow | `if (c == 1) ...` followed by a single statement | `if` / `else` blocks, statically expandable `for`, and `switch` with constant branches | +| Global phase | Given only as a comment when exporting; cannot be loaded | `gphase(theta);`, loadable and exportable | +| Input declarations | None | Angle and floating-point `input` declarations | + +## Limitations + +The following OpenQASM 3.0 features are not supported in the current implementation; a clear error is given when they are encountered, with no partial fallback: + +- Timing and pulses: `delay`, `box`, `cal`, `defcal` +- Subroutines and external calls: `def` subroutines, `extern` +- Hardware-related: hardware qubits, `output` declarations, and `input` declarations other than angle and floating-point +- Other statements: `alias`, `pragma`, annotations, legacy declarations +- Gate modifiers: `negctrl`, `pow`; `ctrl` and `inv` are available only when a corresponding implementation exists for the target gate +- Some standard gates: `ch`, `cp`, `cu`, `cswap` +- Control flow: a lowering path for `while` exists in the implementation, but loops with a runtime condition are currently rejected by the semantic check; `for` supports only the statically expanded form whose range is constant +- Complex classical arithmetic, complex lvalue slicing, multi-dimensional indexing +- Qubit declarations inside a scope + +The export side has further constraints: + +- `delay`, matrix-only `UnitaryGate`, plain assignments writing to a scalar `bit` variable, and measurement inside a gate definition body are not exported +- Control flow such as `while`, `for`, `break`, `continue` is not exported +- The two standard gates `XY` and `RXY` are not exported + +## Round-trip behavior + +The export result can be loaded again by this interface. Canonicalization rules: + +- The header is fixed to `OPENQASM 3.0;` and `include "stdgates.inc";`, regardless of how the header was written in the input. +- Top-level qubits are uniformly exported as `qubit[n] q`; the single-qubit case is likewise written as `qubit[1] q`. +- User classical variables are named `c0`, `c1`, ..., and immutable measurement values are named `v0`, `v1`, .... +- A measurement immediately followed by a compatible store is folded into assignment form, such as `c0 = measure q;`. +- A standalone measurement is written into the automatically generated `bit[k] meas` register, such as `meas[0] = measure q[0];`. +- A non-contiguous or permuted register measurement is split into per-bit assignments, such as `c0[0] = measure q[2];`. +- Gates already provided by `stdgates.inc` are called directly; an extension gate generates a `gate` definition once before the main circuit, and its name is still used at the call site. +- Global phase is exported as a `gphase(...);` statement and enters the global phase of the circuit when loaded again. +- Automatically generated names avoid the gate names and register names already present in the circuit. + +Example: + +```python +from cqlib.ir import qasm3 + +code = """OPENQASM 3; +include "stdgates.inc"; +qubit[2] q; +h q[0]; +cx q[0], q[1]; +""" + +circuit = qasm3.loads(code) +print(qasm3.dumps(circuit)) +``` + +Output: + +```text +OPENQASM 3.0; +include "stdgates.inc"; + +qubit[2] q; + +h q[0]; +cx q[0],q[1]; +``` + +## Common workflow + +```python +from cqlib.ir import qasm3 + +code = """OPENQASM 3; +include "stdgates.inc"; +qubit[2] q; +h q[0]; +cx q[0], q[1]; +""" + +circuit = qasm3.loads(code) +print(qasm3.dumps(circuit)) + +qasm3.dump(circuit, "input.qasm") + +# 1) 读取 QASM 文件 +c = qasm3.load("input.qasm") + +# 2) 处理电路(示例) +c2 = c.decompose() + +# 3) 导出为字符串或文件 +text = qasm3.dumps(c2) +qasm3.dump(c2, "output.qasm") +``` diff --git a/docs/api/en/python/2_device/0_overview.md b/docs/api/en/python/2_device/0_overview.md new file mode 100644 index 00000000..897c611e --- /dev/null +++ b/docs/api/en/python/2_device/0_overview.md @@ -0,0 +1,257 @@ +# Device + +`cqlib.device` + +`cqlib.device` describes the static information of a quantum device and the state of task execution: the topology formed by qubits and couplings, the calibration properties of qubits and edges, the layout from logical qubits to physical qubits, noise models, and execution results and status. + +## Overview + +The device module splits hardware-related data into several mutually independent groups of objects: the topology describes only connectivity, properties describe only calibration data, the layout describes only the mapping, and the noise model describes only channels. Compilation and execution each take what they need, with no coupling between them. + +### Topology + +`Topology` consists of a set of physical qubits and a list of couplings. Couplings are directed: a coupling on `(control, target)` does not imply one in the reverse direction, so connectivity queries fall into two categories, `successors` / `predecessors` (directed) and `neighbors_undirected` (undirected), and degree queries likewise fall into `out_degree` / `in_degree`. A coupling may carry a name label, used for identification only. + +### Device and properties + +`Device` layers device-level defaults and local overrides on top of a topology, and records the device name, registered qubits, invalid qubits, native gate set and calibration time. Qubit-level properties are described by `QubitProp` (`t1`, `t2`, `frequency`, readout error and readout confusion probabilities), and edge-level properties by `EdgeProp`; both hold a group of `InstructionProp` (an instruction, an error rate and an optional duration). + +When querying the properties of a qubit, local values take precedence, and an unset value falls back to the device-level default: `device.get_t1(0)` takes the local value, and `device.get_t1(2)` takes `default_t1`. The device also provides static constructors such as `line`, `ring`, `star` and `grid`, which directly generate a topology and qubit set in common shapes. + +### Native instruction capability + +Device-level `native_gates` is the global default capability; if a qubit or a directed edge configures its own instruction list through `add_native_instruction`, that list is a complete override of the capability at that place and does not write back to the default. Calibration data only attaches an error rate and a duration to supported instructions; it never turns an unsupported instruction into a supported one. `validate_operation` / `validate_circuit` use the same set of rules to validate whether a circuit can run on the device. + +### Layout + +`Layout` maintains the mapping from logical qubits to physical qubits; a physical qubit that carries no logical qubit is a vacant physical qubit. The mapping can be read as a whole through `l2p_map` / `p2l_map`, or queried per qubit; `swap_physical` swaps the logical qubits carried at two physical positions, which is the basic operation for moving logical qubits during routing. + +### Noise model + +`NoiseModel` registers noise channels by qubit and operation: readout error is represented by `ReadoutError`, single-qubit channels are generated by the static methods of `SingleQubitNoise` (bit flip, phase flip, Pauli, depolarizing, amplitude damping, phase damping), and two-qubit channels are generated by `TwoQubitNoise` (depolarizing, independent channel composition, correlated Pauli). A channel can be converted into Kraus operators with `to_kraus`, for use in density matrix simulation. Queries use `OperationKey` to specify the gate and qubit combination. + +```python +from cqlib.circuit import StandardGate +from cqlib.device import NoiseModel, ReadoutError, TwoQubitNoise + +model = NoiseModel() +model.add_readout_error(0, ReadoutError(0.1, 0.2)) +model.add_two_qubit_error(StandardGate.CX, 0, 1, TwoQubitNoise.depolarizing(0.02)) +``` + +### Execution result + +`ExecutionResult` records the lifecycle of one task: it is in `Status.queued()` after creation, enters the running state after `start`, and ends the task with `finish` / `fail` / `cancel`. Measurement results are represented by `Outcome`, which compresses the bitstring into 64-bit chunks and supports both reading a bit by position and restoring the bitstring; `counts` and `probabilities` are the measurement counts and the normalized probabilities of each result respectively. + +--- + +## Common entry points + +```python +from cqlib import Qubit +from cqlib.circuit import Instruction, StandardGate +from cqlib.device import Device, QubitProp, Topology + +topo = Topology([0, 1, 2], [(0, 1, "G1"), (1, 2, "G2")]) +device = Device("mock_backend", [Qubit(0), Qubit(1), Qubit(2)], topo) +device.default_t1 = 50.0 +device.native_gates = [Instruction.from_standard_gate(StandardGate.X)] + +qp0 = QubitProp(0.02) +qp0.t1 = 80.0 +device.add_qubit_properties(0, qp0) + +assert device.get_t1(0) == 80.0 # 局部属性 +assert device.get_t1(2) == 50.0 # 回退到设备级默认值 +``` + +--- + +## Core concepts and terms + +| Term | Description | +| --- | --- | +| **Physical qubit** | A physical position on the device, represented by `PhysicalQubit`. | +| **Logical qubit** | A wire in a circuit, represented by `LogicalQubit`; even with the same index, it is distinct from a physical qubit. | +| **Topology** | The graph formed by physical qubits and the couplings between them, represented by `Topology`. | +| **Coupling** | A connection between two physical qubits over which a two-qubit gate can be executed. | +| **Directed coupling** | A coupling with a direction: the existence of a coupling on `(control, target)` does not imply one in the reverse direction. | +| **Native instruction** | An instruction that can be executed directly on the device; the device-level set is the default capability, and qubits and edges can each override it. | +| **Local override** | A qubit's or an edge's native instruction list; once non-empty, it completely replaces the device-level default capability and does not write back to the default. | +| **Default fallback** | When querying the properties of a qubit, a qubit without a local value returns the device-level default. | +| **Initial layout** | The initial mapping from logical qubits to physical qubits, represented by `Layout`. | +| **Vacant physical qubit** | A physical qubit that currently carries no logical qubit. | +| **Noise channel** | A set of operators describing one kind of noise effect, convertible into Kraus operators with `to_kraus`. | +| **Readout error** | The probability of reading `\|0>` as 1 or reading `\|1>` as 0 during measurement. | +| **Measurement result** | The bitstring obtained from one sample, represented by `Outcome`. | + +--- + +## `cqlib.device` API overview + +### Topology + +| Name | Description | +| --- | --- | +| [`Topology`](1_topology.md) | The directed graph formed by physical qubits and a coupling list, with add/remove and connectivity queries. | +| [`Topology.line`](1_topology.md) | Static method that constructs a topology in a straight-line shape. | + +### Device and properties + +| Name | Description | +| --- | --- | +| [`Device`](2_properties_device.md) | The device object: topology, qubit set, native gate set, calibration time and default properties. | +| [`Device.line`](2_properties_device.md) / [`Device.ring`](2_properties_device.md) / [`Device.star`](2_properties_device.md) / [`Device.grid`](2_properties_device.md) | Static methods that build a device in common shapes. | +| [`QubitProp`](2_properties_device.md) | Qubit properties: readout error, confusion probabilities, `t1` / `t2`, frequency and native instruction properties. | +| [`EdgeProp`](2_properties_device.md) | Edge properties: the native instruction properties on that directed edge. | +| [`InstructionProp`](2_properties_device.md) | Instruction properties: the instruction object, the error rate and an optional duration. | +| [`Device.validate_circuit`](2_properties_device.md) / [`Device.validate_operation`](2_properties_device.md) | Validate a circuit or a single operation against the native capability and the topology. | + +### Layout + +| Name | Description | +| --- | --- | +| [`Layout`](3_layout.md) | The mapping from logical qubits to physical qubits, with bind, unbind and physical qubit swap. | +| [`Layout.from_pairs`](3_layout.md) | Construct a layout from `(logical id, physical id)` pairs and the total number of physical qubits. | +| [`LogicalQubit`](3_layout.md) / [`PhysicalQubit`](3_layout.md) | The types for logical and physical qubits, whose string forms are `L0` and `P11` respectively. | + +### Noise model + +| Name | Description | +| --- | --- | +| [`NoiseModel`](4_noise.md) | A container registering noise channels by qubit and operation. | +| [`SingleQubitNoise`](4_noise.md) | Single-qubit noise channels, including bit flip, phase flip, Pauli, depolarizing and two kinds of damping. | +| [`TwoQubitNoise`](4_noise.md) | Two-qubit noise channels, including depolarizing, independent composition and correlated Pauli. | +| [`ReadoutError`](4_noise.md) | Readout error, recording `P(0\|1)` and `P(1\|0)` separately. | +| [`OperationKey`](4_noise.md) | The key for noise queries, formed by combining a gate and qubit indices. | + +### Execution result + +| Name | Description | +| --- | --- | +| [`ExecutionResult`](5_result.md) | The result of task execution, with counts, probabilities and timestamps. | +| [`Status`](5_result.md) | Task status: queued, running, completed, failed and cancelled. | +| [`Outcome`](5_result.md) | A compact measurement result bitstring, supporting bitwise access and restoration. | + +--- + +## Quick examples + +### 1. Build a device and configure properties + +```python +from cqlib import Qubit +from cqlib.circuit import Instruction, StandardGate +from cqlib.device import Device, EdgeProp, InstructionProp, QubitProp, Topology + +topo = Topology([0, 1, 2], [(0, 1, "G1"), (1, 2, "G2")]) +device = Device("mock_backend", [Qubit(0), Qubit(1), Qubit(2)], topo) +device.default_t1 = 50.0 +device.default_t2 = 35.0 +device.default_readout_error = 0.05 +device.native_gates = [ + Instruction.from_standard_gate(StandardGate.X), + Instruction.from_standard_gate(StandardGate.CX), +] + +qp0 = QubitProp(0.02) +qp0.t1 = 80.0 +qp0.t2 = 70.0 +device.add_qubit_properties(0, qp0) + +ep01 = EdgeProp() +cx_inst = Instruction.from_standard_gate(StandardGate.CX) +ep01.add_native_instruction(InstructionProp(cx_inst, 0.06)) +device.add_edge_properties(0, 1, ep01) +``` + +Writing a nonexistent qubit or edge is rejected: + +```python +import pytest + +with pytest.raises(ValueError): + device.add_qubit_properties(9, QubitProp(0.01)) + +with pytest.raises(ValueError): + device.add_edge_properties(0, 9, EdgeProp()) +``` + +### 2. Build a layout and swap physical qubits + +```python +from cqlib import Qubit +from cqlib.device import Layout, PhysicalQubit + +layout = Layout( + logical=[0, 1], physical=[10, 11, 12], init_map={Qubit(0): Qubit(11)} +) +assert layout.num_logical == 2 +assert layout.num_physical == 3 +assert layout.num_vacant_physical == 1 +assert layout.get_physical(0) == PhysicalQubit(11) + +on_11 = layout.get_logical(11) +on_12 = layout.get_logical(12) + +layout.swap_physical(11, 12) +assert layout.get_logical(11) == on_12 +assert layout.get_logical(12) == on_11 +``` + +The mapping tables can be read as a whole: + +```python +from cqlib import Qubit +from cqlib.device import Layout, LogicalQubit, PhysicalQubit + +layout = Layout(logical=[0], physical=[10, 11], init_map={Qubit(0): Qubit(11)}) +assert layout.l2p_map == {LogicalQubit(0): PhysicalQubit(11)} +assert layout.p2l_map == {PhysicalQubit(11): LogicalQubit(0)} +``` + +### 3. Record an execution result + +```python +from cqlib import Qubit +from cqlib.device import ExecutionResult + +result = ExecutionResult( + task_id="task-1", + qubits=[Qubit(0), Qubit(1)], + shots=100, + num_qubits=2, + backend="sim", +) +assert result.status.kind == "queued" + +result.start() +assert result.status.kind == "running" + +result.finish({"00": 60, "11": 40}) +assert result.status.kind == "completed" +assert result.counts["00"] == 60 + +result.calc_probabilities() +assert result.probabilities["00"] == 0.6 +``` + +--- + +## Validation and error handling + +Failures in the device binding layer uniformly raise `ValueError`, with no custom exception types; exception messages come directly from the underlying error description. + +| Exception | When it occurs | +| --- | --- | +| `ValueError` | The qubit or edge is not in the device/topology, for example adding properties to a nonexistent qubit or adding edge properties in a nonexistent direction. | +| `ValueError` | Construction or modification conflicts of the topology and the layout: duplicate qubits, duplicate couplings, self-coupling, duplicate or already bound logical qubits, an already occupied physical qubit, and so on. | +| `ValueError` | Invalid probability-like parameters: an error rate, readout error or noise channel parameter outside `[0, 1]` or not a finite value; the sum of the three probabilities of a Pauli channel exceeds 1. | +| `ValueError` | Invalid time-like parameters: `t1` / `t2` / `frequency` must be finite positive numbers. | +| `ValueError` | Invalid native instructions: device-level native gates accept only standard gates of at most two qubits, while the qubit level and the edge level accept only one-qubit and two-qubit standard gates respectively; when rejected, the device keeps its previous capability. | +| `ValueError` | `validate_operation` / `validate_circuit` validation failure: the qubit is unusable, a directed coupling is missing, the instruction is not in the native gate set, or the instruction has not been decomposed yet. | +| `ValueError` | Invalid measurement data: the bitstring of an `Outcome` contains a character other than `0` / `1`; a key of `finish` or `from_counts` has a length not equal to `num_qubits`, or different keys fold to the same result. | +| `ValueError` | The time passed to `set_calibration_time` is outside the representable range. | +| `TypeError` / `OverflowError` | The parameter type or value domain does not match, for example a qubit index passed as a non-integer or a negative number. | + +Probability-like parameters are validated at write time, so an invalid value never lands on a device or noise model object. diff --git a/docs/api/en/python/2_device/1_topology.md b/docs/api/en/python/2_device/1_topology.md new file mode 100644 index 00000000..0ac47928 --- /dev/null +++ b/docs/api/en/python/2_device/1_topology.md @@ -0,0 +1,125 @@ +# Topology + +`cqlib.device.Topology` describes the relationship between hardware physical qubits and coupling edges. +The `cqlib` top level also re-exports the same type, so it can be imported directly from `cqlib`. + +## Import + +```python +from cqlib.device import Topology +``` + +--- + +## Construction + +### `Topology(qubits, couplings)` + +Parameters: + +- `qubits` (`list[int | Qubit | PhysicalQubit]`): the list of physical qubits. +- `couplings` (`list[tuple[control, target, name]]`): the list of directed couplings, each entry a `(control, target, name)` triple. + - `control -> target`: the coupling direction. + - `name`: the name label of the coupling, used for identification only and not involved in connectivity or duplicate determination. + +Description: + +- The topology is stored with directed edge semantics; `(control, target)` and `(target, control)` are two different couplings. +- Only one coupling is allowed for the same ordered pair of qubits, and a different name still counts as a duplicate; duplicate qubits and self-coupling are both rejected. + +### `Topology.line(qubits)` + +Static method that constructs a directed straight-line topology `qubits[0] -> qubits[1] -> ...` in the given order, with `len(qubits) - 1` couplings whose names are empty strings. + +Raises: + +- `ValueError`: the qubit list contains duplicate qubits. + +## Attributes + +- `num_qubits -> int` +- `num_couplings -> int` +- `qubits -> list[PhysicalQubit]` + +## Modification methods + +### `add_qubits(qubits)` + +Add physical qubits. + +Raises: + +- `ValueError`: a qubit in the list to add is duplicated, or duplicates a qubit already in the topology. + +### `add_couplings(couplings)` + +Add directed couplings; the parameter is a list of `(control, target, name)` triples. + +Raises: + +- `ValueError`: an endpoint qubit is not in the topology, the coupling already exists, or a self-coupling occurs. + +### `remove_qubits(qubits)` + +Remove physical qubits (the associated coupling edges are removed as well). + +Raises: + +- `ValueError`: a qubit to remove does not exist or appears more than once. + +### `remove_couplings(couplings)` + +Remove directed couplings. The parameter format is `list[tuple[control, target]]`; only the given direction is removed, and the reverse coupling is unaffected. + +Raises: + +- `ValueError`: a coupling to remove does not exist or appears more than once. + +## Query methods + +- `supports_directed_coupling(control, target) -> bool`: whether a coupling exists in the `control -> target` direction. +- `supports_coupling_either_direction(a, b) -> bool`: whether a coupling exists between the two qubits in either direction. +- `successors(qubit) -> list[PhysicalQubit]`: outgoing neighbors. +- `predecessors(qubit) -> list[PhysicalQubit]`: incoming neighbors. +- `neighbors_undirected(qubit) -> list[PhysicalQubit]`: undirected neighbors, each appearing only once across both directions. +- `undirected_edges() -> list[tuple[PhysicalQubit, PhysicalQubit]]`: the list of undirected edges, each pair sorted by value, with two couplings in opposite directions folded into one. +- `get_coupling_name(control, target) -> str | None`: the coupling name, or `None` when the coupling does not exist. +- `contains_qubit(qubit) -> bool`: whether the qubit is in the topology. +- `out_degree(qubit) -> int`: the number of outgoing edges. +- `in_degree(qubit) -> int`: the number of incoming edges. + +Description: + +- `successors` / `out_degree` are counted over outgoing edges, and `predecessors` / `in_degree` over incoming edges. +- When a qubit is not in the topology, neighbor queries return an empty list and degree queries return `0`, without raising an exception. + +## Other behavior + +- Supports `copy` and `deepcopy`. +- `repr(topology) == "Topology(num_qubits=3, num_couplings=2)"`. + +## Example + +```python +from cqlib.device import PhysicalQubit, Topology + +topo = Topology([0, 1, 2], [(0, 1, "G1"), (1, 2, "G2")]) + +assert topo.num_qubits == 3 +assert topo.num_couplings == 2 +assert topo.supports_directed_coupling(1, 2) is True +assert topo.supports_directed_coupling(2, 1) is False +assert topo.supports_coupling_either_direction(2, 1) is True +assert topo.get_coupling_name(1, 2) == "G2" +assert set(topo.successors(1)) == {PhysicalQubit(2)} +assert set(topo.neighbors_undirected(1)) == {PhysicalQubit(0), PhysicalQubit(2)} +assert topo.out_degree(1) == 1 +assert topo.in_degree(1) == 1 + +topo.add_qubits([3]) +topo.add_couplings([(2, 3, "G2")]) +assert topo.supports_coupling_either_direction(2, 3) is True +topo.remove_couplings([(2, 3)]) +topo.remove_qubits([3]) +assert topo.num_qubits == 3 +``` diff --git a/docs/api/en/python/2_device/2_properties_device.md b/docs/api/en/python/2_device/2_properties_device.md new file mode 100644 index 00000000..a81f14fd --- /dev/null +++ b/docs/api/en/python/2_device/2_properties_device.md @@ -0,0 +1,219 @@ +# Device / Properties + +This page covers the core types in `cqlib.device` used for device calibration modeling: + +- `InstructionProp` +- `QubitProp` +- `EdgeProp` +- `Device` + +## Import + +```python +from cqlib.circuit import Instruction, StandardGate +from cqlib.device import Device, EdgeProp, InstructionProp, QubitProp, Topology +``` + +--- + +## InstructionProp + +Instruction properties: an instruction, its error rate and an optional duration. + +### `InstructionProp(instruction, error_rate)` + +Parameters: + +- `instruction` (`Instruction`): the instruction object. +- `error_rate` (`float`): the error rate, which must be within `[0, 1]`. + +Raises: + +- `ValueError`: `error_rate` is outside `[0, 1]` or is not a finite value. + +### Attributes + +- `instruction -> Instruction`: the instruction object, readable and writable. +- `error_rate -> float`: the error rate, readable and writable, validated against `[0, 1]` when written. +- `length -> float | None`: the instruction duration, readable and writable, required to be a non-negative finite value when written. + +### Other behavior + +- Supports `copy` and `deepcopy`. +- `repr(prop) == "InstructionProp(instruction=X, error_rate=0.01)"`, with `length=...` appended after `length` is set. + +## QubitProp + +Qubit properties: readout error, readout confusion probabilities, `t1` / `t2`, frequency and native instruction properties. + +### `QubitProp(readout_error)` + +Parameters: + +- `readout_error` (`float`): the readout error, which must be within `[0, 1]`. + +Raises: + +- `ValueError`: `readout_error` is outside `[0, 1]` or is not a finite value. + +### Attributes + +- `readout_error -> float`: read-only. +- `prob_meas0_prep1 -> float | None`: readable and writable, the value must be within `[0, 1]`. +- `prob_meas1_prep0 -> float | None`: readable and writable, the value must be within `[0, 1]`. +- `t1 -> float | None`: readable and writable, a finite positive number is required. +- `t2 -> float | None`: readable and writable, a finite positive number is required. +- `frequency -> float | None`: readable and writable, a finite positive number is required. +- `native_instructions -> list[InstructionProp]`: read-only, the native instruction properties of this qubit. + +### Methods + +- `add_native_instruction(prop) -> None`: append a native instruction property. + +Raises: + +- `ValueError`: an invalid probability-like parameter, `t1` / `t2` / `frequency` is not a finite positive number, or the appended instruction is not a one-qubit standard gate. +- When a write fails, the previous value stays unchanged. + +## EdgeProp + +Edge properties: the native instruction properties on a directed edge. + +### `EdgeProp()` + +Constructor with no arguments. + +### Methods + +- `add_native_instruction(prop) -> None`: append a native instruction property. + +Raises: + +- `ValueError`: the appended instruction is not a two-qubit standard gate, or its error rate or duration is invalid. + +### Attributes + +- `native_instructions -> list[InstructionProp]`: read-only. + +## Device + +The device object: topology, registered qubit set, native gate set, calibration time and device-level defaults. + +### `Device(name, qubits, topology)` + +Parameters: + +- `name` (`str`): the device name. +- `qubits` (`list[int | Qubit | PhysicalQubit]`): the registered physical qubits. +- `topology` (`Topology`): the device topology. + +Raises: + +- `ValueError`: the topology contains a qubit that is not registered in `qubits`. + +### Static constructors + +- `Device.line(name, num_qubits) -> Device`: a directed straight line formed by qubits `0..num_qubits`. +- `Device.line_from_qubits(name, physical_qubits) -> Device`: a directed straight line connected in the given physical qubit order. +- `Device.bidirectional_line(name, num_qubits) -> Device`: a bidirectional straight line. +- `Device.ring(name, num_qubits) -> Device`: a bidirectional ring. +- `Device.star(name, num_qubits, center) -> Device`: a bidirectional star centered on `center`. +- `Device.grid(name, rows, cols) -> Device`: a `rows × cols` bidirectional grid, with qubit indices arranged in row-major order. +- `Device.from_edges(name, num_qubits, edges) -> Device`: qubits `0..num_qubits` plus explicit directed edges `list[tuple[int, int]]`. + +### Attributes + +- `name -> str`: read-only. +- `qubits -> list[PhysicalQubit]`: read-only, the registered physical qubits. +- `invalid_qubits -> list[PhysicalQubit]`: readable and writable; the written value must already be registered on the device, otherwise it is rejected and the previous value is kept. +- `topology -> Topology`: read-only. +- `native_gates -> list[Instruction]`: readable and writable; writing accepts only standard gates of at most two qubits. +- `usable_qubits -> list[PhysicalQubit]`: read-only, the registered qubits that are not marked invalid. +- `num_usable_qubits -> int`: read-only. +- `default_t1 -> float | None`: readable and writable, the device-level default `t1`. +- `default_t2 -> float | None`: readable and writable, the device-level default `t2`. +- `default_readout_error -> float | None`: readable and writable, the device-level default readout error. +- `default_single_qubit_error -> float | None`: readable and writable, the device-level default single-qubit error rate. +- `default_two_qubit_error -> float | None`: readable and writable, the device-level default two-qubit error rate. +- `calibration_time -> datetime | None`: read-only, the calibration timestamp with nanosecond precision. + +### Write methods + +- `set_calibration_time(datetime) -> None`: write the calibration timestamp. +- `add_qubit_properties(qubit, props) -> None`: write the properties of a qubit. +- `add_edge_properties(control, target, props) -> None`: write the properties of a directed edge. + +Raises: + +- `ValueError`: the qubit is not in the device or not in the topology, the directed edge is not in the topology, a default value is invalid, or the time is outside the representable range. +- When any of the three kinds of writes fails, the device keeps its previous values. + +### Query methods + +- `qubit_properties(qubit) -> QubitProp | None` +- `edge_properties(control, target) -> EdgeProp | None` +- `get_t1(qubit) -> float | None` +- `get_t2(qubit) -> float | None` +- `get_readout_error(qubit) -> float | None` +- `single_qubit_error(qubit, instruction) -> float | None` +- `two_qubit_error(control, target, instruction) -> float | None` +- `edge_error(control, target) -> float | None` +- `is_usable_qubit(qubit) -> bool` +- `supports_native_instruction(instruction, qargs) -> bool` +- `validate_operation(operation) -> None` +- `validate_circuit(circuit) -> None` + +Description: + +- `get_t1` / `get_t2` / `get_readout_error` return local properties first and fall back to the device-level default when they are missing. +- `single_qubit_error` / `two_qubit_error` take the local native instruction property first and fall back to the default single-qubit / two-qubit error rate when it is missing; they return `None` when the qubit is unusable or the direction does not exist. +- `edge_error` reads calibration data only and takes the minimum error rate of the native instructions on that directed edge. +- `validate_operation` / `validate_circuit` raise `ValueError` when validation fails. + +### Other behavior + +- Supports `copy` and `deepcopy`. +- `repr(device) == "Device(name='mock_backend')"`. + +## Example + +```python +from cqlib import Qubit +from cqlib.circuit import Instruction, StandardGate +from cqlib.device import Device, EdgeProp, InstructionProp, QubitProp, Topology + +topo = Topology([0, 1, 2], [(0, 1, "G1"), (1, 2, "G2")]) +device = Device("mock_backend", [Qubit(0), Qubit(1), Qubit(2)], topo) + +device.default_t1 = 50.0 +device.default_t2 = 35.0 +device.default_readout_error = 0.05 +device.native_gates = [ + Instruction.from_standard_gate(StandardGate.X), + Instruction.from_standard_gate(StandardGate.CX), +] + +x_inst = Instruction.from_standard_gate(StandardGate.X) +cx_inst = Instruction.from_standard_gate(StandardGate.CX) + +qp0 = QubitProp(0.02) +qp0.t1 = 80.0 +qp0.t2 = 70.0 +qp0.add_native_instruction(InstructionProp(x_inst, 0.001)) +device.add_qubit_properties(0, qp0) + +ep01 = EdgeProp() +ep01.add_native_instruction(InstructionProp(cx_inst, 0.02)) +device.add_edge_properties(0, 1, ep01) + +assert device.get_t1(0) == 80.0 +assert device.get_t1(2) == 50.0 # 回退到设备级默认值 +assert device.single_qubit_error(0, x_inst) == 0.001 +assert device.two_qubit_error(0, 1, cx_inst) == 0.02 +assert device.edge_error(0, 1) == 0.02 +assert device.num_usable_qubits == 3 + +device.invalid_qubits = [2] +assert device.is_usable_qubit(2) is False +assert device.num_usable_qubits == 2 +``` diff --git a/docs/api/en/python/2_device/3_layout.md b/docs/api/en/python/2_device/3_layout.md new file mode 100644 index 00000000..e78bb97b --- /dev/null +++ b/docs/api/en/python/2_device/3_layout.md @@ -0,0 +1,115 @@ +# Layout + +`Layout` maintains the mapping from logical qubits to physical qubits. + +## Import + +```python +from cqlib.device import Layout +``` + +--- + +## Constructor + +### `Layout(logical, physical, init_map=None)` + +Parameters: + +- `logical` (`list[int | Qubit | LogicalQubit]`): the list of logical qubits. +- `physical` (`list[int | Qubit | PhysicalQubit]`): the list of physical qubits, which may be more than the logical qubits. +- `init_map` (`dict[LogicalQubit, PhysicalQubit] | None`): an optional initial mapping (logical -> physical); keys and values may also be `int` or `Qubit`. + +Raises: + +- `ValueError`: there are more logical qubits than physical qubits, a logical or physical qubit is duplicated, a qubit in the mapping does not belong to the layout, or the same logical or physical qubit is mapped more than once. + +### `Layout.from_pairs(pairs, physical_count)` + +Static method that constructs a layout from `(logical id, physical id)` pairs and the total number of physical qubits. The physical qubits are `0..physical_count`, and a physical qubit not referenced by `pairs` is a vacant physical qubit. + +Raises: + +- `ValueError`: a logical id is duplicated, a physical id is duplicated, or a physical id is outside `0..physical_count`. + +## Attributes + +- `num_logical -> int` +- `num_physical -> int` +- `num_vacant_physical -> int` +- `logical_qubits -> list[LogicalQubit]` +- `physical_qubits -> list[PhysicalQubit]` +- `vacant_physical_qubits -> list[PhysicalQubit]` +- `l2p_map -> dict[LogicalQubit, PhysicalQubit]` +- `p2l_map -> dict[PhysicalQubit, LogicalQubit]` + +## Methods + +### `get_physical(logical_id) -> PhysicalQubit | None` + +Look up the physical qubit by logical qubit; returns `None` when it is not bound. + +### `get_logical(physical_id) -> LogicalQubit | None` + +Look up the logical qubit by physical qubit; returns `None` when the physical qubit is vacant. + +### `is_physical_vacant(physical_id) -> bool` + +Whether the physical qubit is vacant. + +### `bind(logical_id, physical_id) -> None` + +Bind a logical qubit to a vacant physical qubit. + +Raises: + +- `ValueError`: the physical qubit does not belong to the layout, the logical qubit is already bound, or the physical qubit is already occupied. + +### `unbind(logical_id) -> PhysicalQubit` + +Unbind a logical qubit and return the physical qubit that is released. + +Raises: + +- `ValueError`: the logical qubit is not bound. + +### `swap_physical(phys_a, phys_b) -> None` + +Swap the logical qubits carried on two physical qubits; this is the basic operation for moving logical qubits during routing. Either physical qubit may be vacant: swapping with a vacant physical qubit moves the logical qubit to the vacant position. + +Raises: + +- `ValueError`: `phys_a` or `phys_b` is not in the layout's set of physical qubits. + +## Other behavior + +- Supports `copy` and `deepcopy`. +- `repr(layout) == "Layout(num_logical=2, num_vacant_physical=1, num_physical=3)"`. + +## Example + +```python +from cqlib import Qubit +from cqlib.device import Layout, LogicalQubit, PhysicalQubit + +layout = Layout(logical=[0, 1], physical=[10, 11, 12], init_map={Qubit(0): Qubit(11)}) + +print(layout.num_logical) # 2 +print(layout.num_physical) # 3 +print(layout.num_vacant_physical) # 1 + +assert layout.get_physical(0) == PhysicalQubit(11) +assert layout.get_logical(11) == LogicalQubit(0) +assert layout.get_logical(12) is None +assert layout.is_physical_vacant(12) is True + +before_11 = layout.get_logical(11) +before_12 = layout.get_logical(12) +layout.swap_physical(11, 12) +assert layout.get_logical(12) == before_11 +assert layout.get_logical(11) == before_12 + +released = layout.unbind(0) +assert released == PhysicalQubit(12) +assert layout.is_physical_vacant(12) is True +``` diff --git a/docs/api/en/python/2_device/4_noise.md b/docs/api/en/python/2_device/4_noise.md new file mode 100644 index 00000000..8845505b --- /dev/null +++ b/docs/api/en/python/2_device/4_noise.md @@ -0,0 +1,194 @@ +# NoiseModel / Noise Channels + +This page covers the APIs in `cqlib.device` related to noise modeling: + +- `SingleQubitNoise` +- `TwoQubitNoise` +- `ReadoutError` +- `OperationKey` +- `NoiseModel` + +## Import + +```python +from cqlib.circuit import StandardGate +from cqlib.device import ( + NoiseModel, + OperationKey, + ReadoutError, + SingleQubitNoise, + TwoQubitNoise, +) +``` + +--- + +## SingleQubitNoise + +### Static constructors + +- `bit_flip(p) -> SingleQubitNoise`: bit flip channel, applying X with probability `p`. +- `phase_flip(p) -> SingleQubitNoise`: phase flip channel, applying Z with probability `p`. +- `pauli(px, py, pz) -> SingleQubitNoise`: general Pauli channel; the three probabilities must satisfy `px + py + pz <= 1`. +- `depolarizing(p) -> SingleQubitNoise`: depolarizing channel, applying a random Pauli error with probability `p`, with each of the three Paulis taking `p/3`. +- `amplitude_damping(gamma) -> SingleQubitNoise`: amplitude damping channel. +- `phase_damping(lambda_) -> SingleQubitNoise`: phase damping channel. + +Raises: + +- `ValueError`: a probability is outside `[0, 1]` or is not a finite value; the sum of the three probabilities of `pauli` exceeds 1. + +### Methods and attributes + +- `is_valid() -> bool`: whether the noise parameters are physically valid. +- `to_kraus() -> list[numpy.ndarray]`: the list of Kraus operators, each a 2×2 complex matrix. + +Description: + +- The channel kind is determined by the static constructor; there is no `kind` attribute. + +### Other behavior + +- Supports `==`: two channels are equal when the static constructor and the parameters are the same. +- `repr(SingleQubitNoise.bit_flip(0.01)) == "SingleQubitNoise.bit_flip(0.01)"`. + +## TwoQubitNoise + +### Static constructors + +- `depolarizing(p) -> TwoQubitNoise`: two-qubit depolarizing channel. +- `independent(q0_noise, q1_noise) -> TwoQubitNoise`: the independent composition of two single-qubit channels. +- `correlated_pauli(op_q0, op_q1, p) -> TwoQubitNoise`: correlated Pauli channel; `op_q0` / `op_q1` are `Pauli` objects, for example `Pauli.x()`, `Pauli.z()`. + +Raises: + +- `ValueError`: `p` is outside `[0, 1]` or is not a finite value. +- `TypeError`: `op_q0` / `op_q1` is not a `Pauli` object. + +### Methods and attributes + +- `is_valid() -> bool` +- `to_kraus() -> list[numpy.ndarray]`: the list of Kraus operators, each a 4×4 complex matrix. +- `kind -> str`: `depolarizing` / `independent` / `correlated_pauli`. + +### Other behavior + +- Supports `==`. + +## ReadoutError + +### `ReadoutError(p_0_given_1, p_1_given_0)` + +Parameters: + +- `p_0_given_1` (`float`): the probability of reading 0 when the true state is 1. +- `p_1_given_0` (`float`): the probability of reading 1 when the true state is 0. + +Raises: + +- `ValueError`: a probability is outside `[0, 1]` or is not a finite value. + +### Attributes + +- `p_0_given_1 -> float` +- `p_1_given_0 -> float` + +### Methods + +- `is_valid() -> bool` + +### Other behavior + +- Supports `==`. +- `repr(ReadoutError(0.1, 0.2)) == "ReadoutError(p_0_given_1=0.1, p_1_given_0=0.2)"`. + +## OperationKey + +### Static constructors + +- `new_single(gate, q0) -> OperationKey` +- `new_double(gate, q0, q1) -> OperationKey` +- `new_triple(gate, q0, q1, q2) -> OperationKey` + +Raises: + +- `ValueError`: a multi-qubit gate has duplicate qubits. + +### Attributes + +- `gate -> StandardGate`: the gate type. The key records only the gate type; the object returned for a parameterized gate has parameters all equal to 0. +- `qubits -> list[int]`: the qubit indices involved in the operation. + +Description: + +- `__eq__` and `__hash__` are implemented, so it can be used as a dictionary key; keys are unequal when the gate or the qubit combination differs. +- `repr(OperationKey.new_single(StandardGate.X, 0)) == "OperationKey(gate=X, qubits=[0])"`. + +## NoiseModel + +### `NoiseModel()` + +### Write methods + +- `add_readout_error(qubit, error) -> None`: register a readout error for a qubit. +- `add_single_qubit_error(gate, qubit, noise) -> None`: register a noise channel for a single-qubit gate on the given qubit. +- `add_two_qubit_error(gate, q0, q1, noise) -> None`: register a noise channel for a two-qubit gate on the given qubit pair. + +Raises: + +- `ValueError`: a noise parameter is invalid, or the two qubits of a two-qubit gate are the same. + +### Query methods + +- `get_readout_error(qubit) -> ReadoutError | None` +- `get_single_qubit_errors(key) -> list[SingleQubitNoise] | None` +- `get_two_qubit_errors(key) -> list[TwoQubitNoise] | None` + +Description: + +- The same gate and qubit combination can have several registered channels, and a query returns a list; an unregistered combination returns `None`. + +## Example + +```python +import pytest +from cqlib.circuit import StandardGate +from cqlib.device import ( + NoiseModel, + OperationKey, + ReadoutError, + SingleQubitNoise, + TwoQubitNoise, +) +from cqlib.qis import Pauli + +nm = NoiseModel() +nm.add_readout_error(0, ReadoutError(0.1, 0.2)) +nm.add_single_qubit_error(StandardGate.X, 0, SingleQubitNoise.bit_flip(0.01)) +nm.add_single_qubit_error(StandardGate.X, 0, SingleQubitNoise.phase_flip(0.02)) +nm.add_two_qubit_error(StandardGate.CX, 0, 1, TwoQubitNoise.depolarizing(0.02)) +nm.add_two_qubit_error( + StandardGate.CX, 0, 1, TwoQubitNoise.correlated_pauli(Pauli.x(), Pauli.z(), 0.05) +) + +skey = OperationKey.new_single(StandardGate.X, 0) +tkey = OperationKey.new_double(StandardGate.CX, 0, 1) + +assert len(nm.get_single_qubit_errors(skey)) == 2 +assert len(nm.get_two_qubit_errors(tkey)) == 2 +assert nm.get_readout_error(0) == ReadoutError(0.1, 0.2) +assert nm.get_readout_error(9) is None + +assert TwoQubitNoise.depolarizing(0.02).kind == "depolarizing" +assert TwoQubitNoise.correlated_pauli(Pauli.x(), Pauli.z(), 0.05).kind == "correlated_pauli" +assert SingleQubitNoise.depolarizing(0.1).is_valid() is True + +with pytest.raises(ValueError): + SingleQubitNoise.pauli(0.5, 0.5, 0.5) + +with pytest.raises(ValueError): + TwoQubitNoise.correlated_pauli(Pauli.x(), Pauli.z(), 1.5) + +with pytest.raises(TypeError): + TwoQubitNoise.correlated_pauli("X", Pauli.z(), 0.05) +``` diff --git a/docs/api/en/python/2_device/5_result.md b/docs/api/en/python/2_device/5_result.md new file mode 100644 index 00000000..01c88946 --- /dev/null +++ b/docs/api/en/python/2_device/5_result.md @@ -0,0 +1,169 @@ +# Outcome / Status / ExecutionResult + +This page covers the APIs related to quantum task results: + +- `Outcome` +- `Status` +- `ExecutionResult` + +## Import + +```python +from cqlib.device import ExecutionResult, Outcome, Status +``` + +--- + +## Outcome + +### `Outcome(bitstring)` + +Construct a measurement result object from a binary string. + +Raises: + +- `ValueError`: the string contains an illegal character (not `0/1`). + +### Static methods + +- `Outcome.from_bitstring(bitstring) -> Outcome`: construct from a binary string. +- `Outcome.from_indices(width, indices) -> Outcome`: construct a result of width `width` whose value is `1` only at the positions listed in `indices`. + +### Methods and attributes + +- `is_one(index) -> bool`: whether the given position is `1`. +- `to_bitstring(num_qubits) -> str`: output the bitstring with the given width. +- `chunks -> list[int]`: the chunks of the internal storage. + +Description: + +- The bitstring is always written in standard binary form (most significant bit on the left), while the internal storage uses little-endian chunks. +- The bitstring width is not stored by `Outcome`: parsing `"001"` and parsing `"1"` yield the same `Outcome`. The output width must therefore be specified by the caller through `to_bitstring`. +- `Outcome` supports `==` and `hash(...)`. + +### Other behavior + +- Supports `==`, `hash(...)`, `copy` and `deepcopy`. +- `repr(Outcome("101")) == "Outcome(chunks=[5])"`. + +## Status + +### Static constructors + +- `Status.queued()` +- `Status.running()` +- `Status.completed()` +- `Status.failed(error_msg, error_code)` +- `Status.cancelled()` + +### Attributes and methods + +- `kind -> str` (`queued/running/completed/failed/cancelled`) +- `error_msg -> str | None` +- `error_code -> int | None` +- `is_terminal() -> bool` +- `is_success() -> bool` + +### Other behavior + +- Supports `==` and `hash(...)`: two statuses are equal only when the status is the same and the error message and error code are the same; comparison with an object that is not a `Status` returns `False`. +- `repr(Status.completed()) == "Status(completed)"`, `str(Status.completed()) == "Completed"`. + +## ExecutionResult + +### `ExecutionResult(task_id, qubits, shots, num_qubits, backend=None)` + +Parameters: + +- `task_id` (`str`) +- `qubits` (`list[int | Qubit]`): the list of measured qubits. +- `shots` (`int`) +- `num_qubits` (`int`) +- `backend` (`str | None`) + +The constructed result is in the `queued` state. + +### `ExecutionResult.from_counts(task_id, qubits, shots, num_qubits, counts, backend=None)` + +Static method that constructs a completed result directly from measurement counts; it is equivalent to calling the constructor, `start()`, `finish(counts)` and `calc_probabilities()` in order. + +`counts` is a `dict[str, int]`, and each key must be a valid binary bitstring whose length equals `num_qubits`. + +Raises: + +- `ValueError`: the bitstring of `counts` is invalid, or its length does not equal `num_qubits`. + +### Lifecycle methods + +- `start() -> None` +- `finish(counts) -> None` +- `fail(msg, code) -> None` +- `cancel() -> None` +- `calc_probabilities() -> None` + +In `finish(counts)`, `counts` has type `dict[str, int]`, and each key must be a valid binary bitstring whose length equals `num_qubits`; writing counts clears the computed probabilities, so `calc_probabilities()` must be called again. `calc_probabilities()` produces no probabilities when the total count is 0. + +Raises: + +- `ValueError`: the bitstring of `counts` is invalid, or its length does not equal `num_qubits`. + +### Attributes + +- `task_id -> str` +- `shots -> int` +- `num_qubits -> int` +- `qubits -> list[Qubit]`: the list of measured qubits. +- `status -> Status` +- `created_at -> str`: the creation time, an ISO 8601 string. +- `started_at -> str | None`: the start time, an ISO 8601 string. +- `finished_at -> str | None`: the finish time, an ISO 8601 string. +- `backend -> str | None` +- `counts -> dict[str, int]` +- `probabilities -> dict[str, float] | None`: available only after `calc_probabilities()` is called. + +Description: + +- `qubits[i]` corresponds to bit `i` of the bitstring (weight `2**i`); the bitstring is written with the most significant bit on the left, so the string order is the reverse of the `qubits` order. + +### Other behavior + +- Supports `copy` and `deepcopy`. +- `repr(result) == "ExecutionResult(task_id='task-1', status='queued', shots=100, num_qubits=2)"`. + +## Example + +```python +import pytest +from cqlib import Qubit +from cqlib.device import ExecutionResult, Outcome, Status + +outcome = Outcome("101") +assert outcome.is_one(0) is True +assert outcome.is_one(1) is False +assert outcome.to_bitstring(3) == "101" +assert Outcome.from_bitstring("101") == outcome +assert Outcome.from_indices(3, [0, 2]) == outcome + +failed = Status.failed("backend down", 42) +assert failed.kind == "failed" +assert failed.error_code == 42 +assert failed.is_terminal() is True + +result = ExecutionResult("task-1", [Qubit(0), Qubit(1)], 100, 2, "sim") +print(result.status.kind) # queued + +result.start() +result.finish({"00": 60, "11": 40}) +result.calc_probabilities() + +print(result.status.kind) # completed +print(result.counts) # {'00': 60, '11': 40} +print(result.probabilities) # {'00': 0.6, '11': 0.4} + +done = ExecutionResult.from_counts("task-2", [Qubit(0), Qubit(1)], 100, 2, {"01": 60, "10": 40}) +print(done.status.kind) # completed +assert done.counts == {"01": 60, "10": 40} + +with pytest.raises(ValueError): + done.finish({"100": 1}) +``` diff --git a/docs/api/en/python/2_device/6_qubits.md b/docs/api/en/python/2_device/6_qubits.md new file mode 100644 index 00000000..a3c338fc --- /dev/null +++ b/docs/api/en/python/2_device/6_qubits.md @@ -0,0 +1,156 @@ +# Qubit Identifiers + +`cqlib.device` + +`LogicalQubit` and `PhysicalQubit` are the strongly typed qubit identifiers used by the device-side interfaces: the former identifies a logical qubit in a circuit, and the latter identifies a physical qubit on a device. Both types use a numeric id as their internal representation, but they are different types, so two identifiers with the same numeric value are not equal. + +## Import + +```python +from cqlib import Qubit +from cqlib.device import LogicalQubit, PhysicalQubit +``` + +--- + +## LogicalQubit + +The logical qubit identifier, used for logical qubits in a circuit. + +### LogicalQubit(id) + +Parameters: + +- `id` (`int`): the numeric id, a non-negative integer. + +Raises: + +- `TypeError`: `id` is not an integer. +- `OverflowError`: `id` is negative or outside the `u32` range. + +### Attributes + +- `id -> int`: the numeric id. +- `index -> int`: the index form of the numeric id. +- `qubit -> Qubit`: the corresponding circuit qubit. + +### Other behavior + +- Supports `==`, `<`, `<=`, `>`, `>=`, `hash`, `copy` and `deepcopy`. +- Comparison holds only within this type: comparison with a `Qubit` or a `PhysicalQubit` is judged unequal and does not raise an exception. +- `str(LogicalQubit(0)) == "L0"`; `repr(LogicalQubit(0)) == "LogicalQubit(0)"`. + +Example: + +```python +from cqlib import Qubit +from cqlib.device import LogicalQubit, PhysicalQubit + +logical = LogicalQubit(0) +assert logical.id == 0 +assert logical.index == 0 +assert logical.qubit == Qubit(0) +assert logical != Qubit(0) +assert logical != PhysicalQubit(0) +assert str(logical) == "L0" +assert repr(logical) == "LogicalQubit(0)" + +assert sorted([LogicalQubit(2), LogicalQubit(0)]) == [LogicalQubit(0), LogicalQubit(2)] +assert LogicalQubit(2) >= LogicalQubit(2) +``` + +--- + +## PhysicalQubit + +The physical qubit identifier, used for qubit positions on a device. + +### PhysicalQubit(id) + +Parameters: + +- `id` (`int`): the numeric id, a non-negative integer. + +Raises: + +- `TypeError`: `id` is not an integer. +- `OverflowError`: `id` is negative or outside the `u32` range. + +### Attributes + +- `id -> int`: the numeric id. +- `index -> int`: the index form of the numeric id. +- `qubit -> Qubit`: the corresponding circuit qubit. + +### Other behavior + +- Supports `==`, `<`, `<=`, `>`, `>=`, `hash`, `copy` and `deepcopy`. +- Comparison holds only within this type: comparison with a `Qubit` or a `LogicalQubit` is judged unequal and does not raise an exception. +- `str(PhysicalQubit(11)) == "P11"`; `repr(PhysicalQubit(11)) == "PhysicalQubit(11)"`. + +Example: + +```python +from cqlib import Qubit +from cqlib.device import LogicalQubit, PhysicalQubit + +physical = PhysicalQubit(11) +assert physical.id == 11 +assert physical.index == 11 +assert physical.qubit == Qubit(11) +assert physical == PhysicalQubit(11) +assert physical != Qubit(11) +assert physical != LogicalQubit(11) +assert str(physical) == "P11" +assert repr(physical) == "PhysicalQubit(11)" + +assert sorted([PhysicalQubit(2), PhysicalQubit(0)]) == [PhysicalQubit(0), PhysicalQubit(2)] +assert PhysicalQubit(1) < PhysicalQubit(2) +``` + +--- + +## Role in the layout + +The layout maintains the mapping from logical qubits to physical qubits, which is the main scenario where the two types are used together: logical qubits come from the circuit, physical qubits come from the device, and the mapping result is recorded with strongly typed identifiers, avoiding a mix of the two kinds of numeric values. + +Example: + +```python +from cqlib import Qubit +from cqlib.device import Layout, LogicalQubit, PhysicalQubit + +layout = Layout(logical=[0, 1], physical=[10, 11, 12], init_map={Qubit(0): Qubit(11)}) + +assert layout.num_logical == 2 +assert layout.num_physical == 3 +assert layout.num_vacant_physical == 1 + +assert set(layout.logical_qubits) == {LogicalQubit(0), LogicalQubit(1)} +assert set(layout.physical_qubits) == {PhysicalQubit(10), PhysicalQubit(11), PhysicalQubit(12)} + +assert layout.get_physical(0) == PhysicalQubit(11) +assert layout.get_logical(11) == LogicalQubit(0) +assert layout.l2p_map == {LogicalQubit(0): PhysicalQubit(11), LogicalQubit(1): PhysicalQubit(10)} +assert layout.p2l_map == {PhysicalQubit(10): LogicalQubit(1), PhysicalQubit(11): LogicalQubit(0)} + +before_11 = layout.get_logical(11) +before_12 = layout.get_logical(12) +layout.swap_physical(11, 12) +assert layout.get_logical(11) == before_12 +assert layout.get_logical(12) == before_11 + +layout.unbind(0) +assert layout.get_physical(0) is None +``` + +--- + +## Interfaces that accept qubit parameters + +The device-side interfaces accept an `int` or an identifier object for a qubit parameter, and accept `Qubit` as well, all parsed by numeric id; qubits are always returned as strongly typed identifier objects. A logical qubit parameter position accepts `int | LogicalQubit | Qubit`, and a physical qubit parameter position accepts `int | PhysicalQubit | Qubit`. + +- The layout `Layout`: constructor parameters `logical`, `physical`, `init_map`, and `get_physical`, `get_logical`, `bind`, `unbind`, `is_physical_vacant`, `swap_physical`. +- The topology `Topology`: constructor parameters `qubits`, `couplings`, and `add_qubits`, `remove_qubits`, `add_couplings`, `remove_couplings`, `contains_qubit`, `successors`, `predecessors`, `neighbors_undirected`, `out_degree`, `in_degree`, `supports_directed_coupling`, `supports_coupling_either_direction`, `get_coupling_name`. +- The device `Device`: the constructor parameter `qubits` and the parameters of `line_from_qubits`, and `add_qubit_properties`, `add_edge_properties`, `qubit_properties`, `edge_properties`, `get_t1`, `get_t2`, `get_readout_error`, `is_usable_qubit`, `single_qubit_error`, `two_qubit_error`, `edge_error`, `supports_native_instruction`, `set_invalid_qubits`. +- Noise `NoiseModel`: `add_readout_error`, `add_single_qubit_error`, `add_two_qubit_error`, `get_readout_error`; and `new_single`, `new_double`, `new_triple` of `OperationKey`. diff --git a/docs/api/en/python/3_qis/0_overview.md b/docs/api/en/python/3_qis/0_overview.md new file mode 100644 index 00000000..e92b10c1 --- /dev/null +++ b/docs/api/en/python/3_qis/0_overview.md @@ -0,0 +1,218 @@ +# Quantum Information + +`cqlib.qis` + +`cqlib.qis` is the quantum information module of Cqlib. It provides several representations and simulators of quantum states, the Pauli operator algebra, an observable framework centered on Hamiltonian, and analysis tools such as state fidelity, trace distance, entropy and entanglement measures. These facilities can be used independently for computations at the quantum information level, and can also consume circuits from `cqlib.circuit` directly to obtain evolution results. + +## Overview + +The quantum information module addresses two kinds of problems: **representation and evolution**, that is, storing a quantum state and letting gates act on it; and **measures**, that is, extracting comparable numerical values from states or observables. The two share the same underlying operator algebra. + +### Four representations of quantum states + +The four simulators differ in storage cost and applicable scope, and which one to choose depends on the gate set of the circuit and the quantities to be observed: + +- The statevector represents a pure state with 2^N complex amplitudes; it is the most compact representation, but cannot describe classical mixtures and noise. +- The density matrix represents a mixed state with a 2^N × 2^N matrix; the cost is an order of magnitude higher, but it supports quantum channels, partial traces and physicality validation. +- The noisy density matrix binds a noise model on top of the density matrix, applies Kraus noise automatically according to gates, and provides probability distributions under readout noise. +- The stabilizer table represents a Clifford state with N commuting Pauli generators; the storage cost is linear in the number of qubits, but only Clifford gates are accepted. + +The first three share the same set of gate application entry points (`apply_circuit`, `apply_standard_gate`, single-qubit and two-qubit gate methods), and the differences between them are concentrated in state storage and noise handling. + +### Qubit index convention + +All four representations use the same qubit numbering convention: qubit i corresponds to bit i of the basis state index, with qubit 0 as the least significant bit; in bitstring representations, the higher-numbered qubit is on the left. This convention is consistent across all pages and is not repeated. + +### From circuit to state + +All four simulators provide a `from_circuit()` static method, which simulates a circuit starting from |0…0⟩ to obtain the final state; the input circuit is left unchanged. When both the final state and the runtime classical data are needed, use `StabilizerState.run_circuit()`. + +### Observables and time evolution + +`PauliString` and `Hamiltonian` describe observables, and both can give expectation values; `Hamiltonian` can also be converted into an evolution circuit through Trotter decomposition, returning to the circuit space of `cqlib.circuit` for execution. + +--- + +## Common entry points + +```python +import math + +from cqlib import Circuit +from cqlib.qis import PauliString +from cqlib.qis.state import Statevector + +circuit = Circuit(2) +circuit.h(0) +circuit.cx(0, 1) + +sv = Statevector.from_circuit(circuit) +assert math.isclose(sv.probabilities()[0], 0.5, abs_tol=1e-10) +assert math.isclose(sv.probabilities()[3], 0.5, abs_tol=1e-10) +assert math.isclose(sv.expectation(PauliString.from_str("ZZ")), 1.0, abs_tol=1e-10) +``` + +--- + +## Core concepts and terms + +| Term | Description | +| --- | --- | +| **Statevector** | An N-qubit pure state described by 2^N complex amplitudes, where the amplitude of the basis state \|i⟩ is at index `i`. | +| **Density matrix** | An N-qubit quantum state described by a 2^N × 2^N Hermitian positive semidefinite matrix, which can represent classical mixtures of pure states. | +| **Noisy simulation** | A simulation approach that applies Kraus noise according to a noise model on top of the density matrix, where gate noise takes effect immediately after each gate application. | +| **Stabilizer table** | A compact representation of a Clifford state by N commuting Pauli generators, whose storage cost grows linearly with the number of qubits. | +| **Clifford gate** | A gate that maps the Pauli group onto itself, such as H, S, X, Y, Z and controlled Pauli gates. Non-Clifford gates cannot be simulated with a stabilizer table. | +| **Destabilizer** | A set of Pauli operators paired with the stabilizer generators, used to determine measurement outcomes and maintain the canonical form of the stabilizer table. | +| **Symplectic representation** | A compact storage form of a Pauli string, describing an operator by two bit masks for X and Z together with a phase bit. | +| **Observable** | An object for which an expectation value and a variance can be computed, such as `PauliString` and `Hamiltonian`. | +| **Expectation value** | The average of an observable in a given state: ⟨ψ\|O\|ψ⟩ for a pure state and Tr(ρ·O) for a mixed state. | +| **Partial trace** | The operation of tracing out part of the qubits and keeping the rest, used to obtain the reduced density matrix of a subsystem. | +| **Trotter decomposition** | A decomposition that approximates the time evolution operator e^{-iHt} by a sequence of Pauli rotations. | +| **Readout noise** | Classical flips introduced at the measurement stage, which make readout results inconsistent with the true measurement results. | +| **Runtime classical data** | Classical values produced during circuit execution and queried by handle after execution ends. | + +--- + +## `cqlib.qis` API overview + +### Module entry + +| Name | Description | +| --- | --- | +| [`cqlib.qis`](0_overview.md) | Module overview, that is, this page. | + +### State simulation + +| Name | Description | +| --- | --- | +| [`Statevector`](1_statevector.md) | Pure-state simulator, representing an N-qubit quantum state with 2^N complex amplitudes. | +| [`DensityMatrix`](2_density_matrix.md) | Mixed-state simulator, supporting quantum channels, partial traces and physicality validation. | +| [`DensityMatrixNoise`](3_density_matrix_noise.md) | Noisy mixed-state simulator, applying gate noise and readout noise according to a noise model. | +| [`StabilizerState`](4_stabilizer.md) | Clifford state simulator, representing states with a stabilizer table and supporting generator queries. | +| [`StabilizerCircuitResult`](4_stabilizer.md) | The result of `StabilizerState.run_circuit()`, carrying the final state and the runtime classical data. | + +### Runtime classical data + +| Name | Description | +| --- | --- | +| [`RuntimeValue`](5_classical_state.md) | The typed value of a single classical value in one execution. | +| [`ClassicalState`](5_classical_state.md) | A snapshot of all runtime classical data after one execution ends, with values queried by handle. | + +### Pauli algebra and observables + +| Name | Description | +| --- | --- | +| [`Phase`](6_pauli.md) | The phase factor produced when elements of the Pauli group are multiplied. | +| [`Pauli`](6_pauli.md) | A single-qubit Pauli operator. | +| [`PauliString`](6_pauli.md) | A tensor product of multi-qubit Pauli operators, stored in the symplectic representation. | +| [`Observable`](7_hamiltonian.md) | The protocol for observables, defining the entry points for computing expectation values and variances. | +| [`Hamiltonian`](7_hamiltonian.md) | An observable represented as a linear combination of Pauli strings, whose expectation value can be computed and from which evolution circuits can be generated. | + +### Time evolution + +| Name | Description | +| --- | --- | +| [`TrotterMode`](8_evolution.md) | The Trotter decomposition mode of the time evolution operator, including first order, second order and randomized. | +| [`Hamiltonian.to_trotter_circuit`](8_evolution.md) | Convert `Hamiltonian` into a Trotter-approximated evolution circuit. | +| [`Hamiltonian.to_evolution_circuit`](8_evolution.md) | Convert `Hamiltonian` into a time evolution circuit. | + +### Metrics and entropy + +| Name | Description | +| --- | --- | +| [`purity_pure`](9_metrics_entropy.md) / [`purity_mixed`](9_metrics_entropy.md) | Purity of pure and mixed states. | +| [`state_fidelity_pure`](9_metrics_entropy.md) / [`state_fidelity_pure_mixed`](9_metrics_entropy.md) / [`state_fidelity_mixed`](9_metrics_entropy.md) | State fidelity between pure states, between a pure state and a mixed state, and between mixed states. | +| [`trace_distance_pure`](9_metrics_entropy.md) / [`trace_distance_mixed`](9_metrics_entropy.md) | Trace distance of pure and mixed states. | +| [`entropy`](9_metrics_entropy.md) | Von Neumann entropy of a density matrix. | +| [`partial_transpose`](9_metrics_entropy.md) / [`logarithmic_negativity`](9_metrics_entropy.md) | Partial transpose and logarithmic negativity. | +| [`linear_entropy`](9_metrics_entropy.md) / [`renyi_entropy`](9_metrics_entropy.md) | Linear entropy and Rényi entropy. | +| [`entanglement_entropy_pure`](9_metrics_entropy.md) | Entanglement entropy of a pure state on a given subsystem. | +| [`negativity`](9_metrics_entropy.md) / [`concurrence`](9_metrics_entropy.md) / [`entanglement_of_formation`](9_metrics_entropy.md) | Negativity, concurrence and entanglement of formation. | + +--- + +## Quick examples + +### 1. Pure-state and mixed-state simulation of the same circuit + +```python +import math + +from cqlib import Circuit +from cqlib.qis import Hamiltonian, PauliString +from cqlib.qis.state import DensityMatrix, Statevector + +circuit = Circuit(2) +circuit.h(0) +circuit.cx(0, 1) + +# 纯态:用态矢量计算期望值 +sv = Statevector.from_circuit(circuit) +assert math.isclose(sv.expectation(PauliString.from_str("ZZ")), 1.0, abs_tol=1e-10) + +# 混合态:用密度矩阵计算期望值,并取子系统的约化态 +dm = DensityMatrix.from_circuit(circuit) +hamiltonian = Hamiltonian(2) +hamiltonian.add_term(PauliString.from_str("ZZ"), 0.5) +hamiltonian.add_term(PauliString.from_str("XX"), 0.5) +hamiltonian.simplify() +assert math.isclose(dm.expectation(hamiltonian), 1.0, abs_tol=1e-10) + +reduced = dm.partial_trace([0]) +assert math.isclose(reduced.probabilities()[0], 0.5, abs_tol=1e-10) +``` + +### 2. Simulation with noise + +```python +import math + +from cqlib.circuit import StandardGate +from cqlib.device import NoiseModel, SingleQubitNoise +from cqlib.qis.state import DensityMatrixNoise + +noise_model = NoiseModel() +noise_model.add_single_qubit_error(StandardGate.X, 0, SingleQubitNoise.bit_flip(0.1)) + +sim = DensityMatrixNoise(1, noise_model) +sim.apply_x(0) + +probs = sim.probabilities() +assert math.isclose(probs[1], 0.9, abs_tol=0.01) +assert math.isclose(probs[0], 0.1, abs_tol=0.01) +``` + +### 3. Clifford circuits and the stabilizer table + +```python +from cqlib import Circuit +from cqlib.qis.state import StabilizerState + +circuit = Circuit(2) +circuit.h(0) +circuit.cx(0, 1) + +state = StabilizerState.from_circuit(circuit) +assert state.probabilities()[0] == 0.5 +assert state.probabilities()[3] == 0.5 + +# 稳定子表的逐行文本表示 +assert state.to_stim_format().count("\n") == 2 +assert len(state.get_stabilizers()) == 2 +``` + +--- + +## Validation and error handling + +The simulators validate at the stages of construction, gate application and state read/write. The exception tables on each page list all trigger conditions, and the shared categories are as follows: + +| Exception | When it occurs | +| --- | --- | +| `IndexError` | Qubit index out of range. | +| `ValueError` | Dimension or length inconsistent with the number of qubits, amplitudes not normalized, density matrix violating physical constraints, invalid Kraus operator, duplicate target qubits of a two-qubit gate, circuit containing non-Clifford operations or control flow, or observable type or qubit count mismatch. | +| `TypeError` | Unsupported input type, for example an initial state that is neither an array nor a list received by the constructor, or a tolerance parameter of a validation method that is not a floating-point number. | +| `OverflowError` | `num_qubits` is negative. | + +The Raises section of each page gives the complete trigger conditions of that page's API. diff --git a/docs/api/en/python/3_qis/1_statevector.md b/docs/api/en/python/3_qis/1_statevector.md new file mode 100644 index 00000000..3a31f359 --- /dev/null +++ b/docs/api/en/python/3_qis/1_statevector.md @@ -0,0 +1,263 @@ +# Statevector + +`cqlib.qis.state` + +The pure-state simulator in `cqlib.qis.state`, covering the `Statevector` class: state construction, gate application, measurement and sampling, and expectation value computation. + +## Import + +```python +from cqlib.qis.state import Statevector +``` + +--- + +## Statevector + +A simulator that describes an N-qubit pure quantum state |ψ⟩ = Σᵢ αᵢ|i⟩ with 2^N complex amplitudes. + +Amplitudes are stored contiguously by index in memory, and the amplitude of the basis state |i⟩ is at index `i`. Qubit i corresponds to bit i of the index, with qubit 0 as the least significant bit; in bitstring representations, the higher-numbered qubit is on the left. After construction all amplitudes are 0 except |0…0⟩, which is 1. + +### Statevector(num_qubits) + +Create a state in |0…0⟩. + +Parameters: + +- `num_qubits` (`int`): the number of qubits. + +Example: + +```python +import math + +from cqlib.qis.state import Statevector + +sv = Statevector(2) +assert sv.num_qubits == 2 + +probs = sv.probabilities() +assert len(probs) == 4 +assert math.isclose(probs[0], 1.0) +assert math.isclose(probs[1], 0.0) +``` + +--- + +### Static methods + +- `Statevector.from_state(num_qubits, initial_state) -> Statevector`: construct from the given amplitudes. `initial_state` is a NumPy complex array of length 2^num_qubits, or a list of complex numbers and floats. + - A `ValueError` is raised when the length does not match 2^num_qubits or the amplitudes are not normalized. +- `Statevector.from_circuit(circuit) -> Statevector`: simulate the evolution of the circuit starting from |0…0⟩ and return the resulting state. The input circuit is not modified. + - A `ValueError` is raised when the circuit contains non-unitary operations or the number of circuit qubits does not match the state. + +Example: + +```python +import math + +import numpy as np + +from cqlib import Circuit +from cqlib.qis.state import Statevector + +amps = np.array([1 / np.sqrt(2), 1 / np.sqrt(2)], dtype=complex) +sv = Statevector.from_state(1, amps) +assert math.isclose(sv.probabilities()[0], 0.5) +assert math.isclose(sv.probabilities()[1], 0.5) + +circuit = Circuit(2) +circuit.h(0) +circuit.cx(0, 1) + +bell = Statevector.from_circuit(circuit) +assert math.isclose(bell.probabilities()[0], 0.5, abs_tol=1e-10) +assert math.isclose(bell.probabilities()[3], 0.5, abs_tol=1e-10) +``` + +--- + +### Attributes + +- `num_qubits -> int`: the number of qubits. +- `data -> numpy.ndarray`: a one-dimensional complex amplitude array of length 2^num_qubits. Each read returns a copy; modifying the returned value does not affect the state. + +Example: + +```python +import numpy as np + +from cqlib.qis.state import Statevector + +sv = Statevector(2) +data = sv.data +assert len(data) == 4 +assert data[0] == 1.0 +assert np.allclose(data[1:], 0) + +# data 是副本,就地修改不会写回状态 +data[0] = 999.0 +assert sv.data[0] == 1.0 +``` + +--- + +### Methods + +All gate application and state rewriting methods modify the current state in place and return `None`. + +**Circuit application** + +- `apply_circuit(circuit) -> None`: apply the circuit to the current state. A `ValueError` is raised when the number of circuit qubits does not match. + +**Standard gate objects** + +- `apply_standard_gate(gate, qubits, params=None) -> None`: apply a gate as described by `StandardGate`. + - `gate` (`StandardGate`): the standard gate. + - `qubits` (`list[int]`): the list of target qubit indices. + - `params` (`list[float] | None`): the parameters of a parameterized gate, defaulting to an empty list. + +**Single-qubit gates (no parameters)** + +`apply_x`, `apply_y`, `apply_z`, `apply_h`, `apply_s`, `apply_sdg`, `apply_t`, `apply_tdg`, `apply_x2p`, `apply_x2m`, `apply_y2p`, `apply_y2m`, all with the signature `(qubit) -> None`. + +**Single-qubit gates (with angle)** + +- `apply_rx(qubit, theta)`, `apply_ry(qubit, theta)`, `apply_rz(qubit, theta)`: rotate by `theta` radians around the X, Y and Z axes. +- `apply_phase(qubit, theta)`: phase gate, where `theta` is the phase angle. +- `apply_xy(qubit, theta)`: XY gate. +- `apply_xy2p(qubit, theta)`: XY2P gate, equivalent to `Rz(θ - π/2) Ry(π/2) Rz(π/2 - θ)`. +- `apply_xy2m(qubit, theta)`: XY2M gate, equivalent to `Rz(-θ + π/2) Ry(-π/2) Rz(-π/2 + θ)`. +- `apply_rxy(qubit, theta, phi)`: RXY gate. +- `apply_u(qubit, theta, phi, lambda_)`: general single-qubit unitary gate, `U(θ, φ, λ) = Rz(φ) Ry(θ) Rz(λ)`. + +**Global phase** + +- `apply_gphase(phi)`: multiply the whole state by the global phase `e^{iφ}`. Does not change `probabilities()`. + +**Pauli string rotations** + +- `apply_pauli_rotation(pauli, theta)`: apply `exp(-i θ/2 · P)`, where `P` must be a Hermitian Pauli string covering all qubits of the state. + +**Controlled gates and two-qubit gates** + +- `apply_cx(control, target)`: controlled X (CNOT) gate. +- `apply_cy(control, target)`: controlled Y gate. +- `apply_cz(q0, q1)`: controlled Z gate. +- `apply_crx(control, target, theta)`: controlled RX gate. +- `apply_cry(control, target, theta)`: controlled RY gate. +- `apply_crz(control, target, theta)`: controlled RZ gate. +- `apply_swap(q0, q1)`: SWAP gate. +- `apply_rxx(q0, q1, theta)`: RXX (Ising XX) gate. +- `apply_ryy(q0, q1, theta)`: RYY (Ising YY) gate. +- `apply_rzz(q0, q1, theta)`: RZZ (Ising ZZ) gate. +- `apply_rzx(q0, q1, theta)`: RZX gate. +- `apply_fsim(q0, q1, theta, phi)`: fermionic simulation gate, where `theta` is the iSWAP angle and `phi` is the controlled phase angle. +- `apply_ccx(c0, c1, target)`: CCX (Toffoli) gate. + +A `ValueError` is raised when the two qubit indices of a two-qubit gate are the same. + +**Custom matrix gates** + +- `apply_single_qubit_gate(qubit, matrix) -> None`: apply the single-qubit gate given by `matrix`. + - `matrix` (`numpy.ndarray`): a 2×2 complex matrix; a `ValueError` is raised when the shape does not match. +- `apply_double_qubits_gate(q0, q1, matrix) -> None`: apply the two-qubit gate given by `matrix`. + - `matrix` (`numpy.ndarray`): a 4×4 complex matrix; a `ValueError` is raised when the shape does not match. +- `apply_unitary_gate(qubits, matrix) -> None`: apply a unitary gate on any number of qubits, acting on `qubits`. + - `qubits` (`list[int]`): the list of qubit indices the gate acts on. + - `matrix` (`numpy.ndarray`): a 2^n × 2^n complex matrix, where n is the length of `qubits`; a `ValueError` is raised when the shape does not match. + +**Expectation values** + +- `expectation(observable) -> float`: compute ⟨ψ|O|ψ⟩. + - `observable` (`Hamiltonian | PauliString`): the observable. A `ValueError` is raised when the qubit count does not match or the type is not supported. + +**Measurement and state rewriting** + +- `probabilities() -> list[float]`: return the probability p(i) = |αᵢ|² of each basis state, with length 2^num_qubits. Does not modify the state. +- `reset(qubit) -> None`: measure the given qubit and flip it to |0⟩ if the result is 1. +- `measure(qubit) -> bool`: measure a single qubit, collapse the state, and return the result of that qubit. +- `measure_all() -> Outcome`: measure all qubits, collapse the state, and return an `Outcome`. The bitstring is ordered from the highest qubit number to the lowest. +- `sample_shots(shots) -> list[Outcome]`: sample `shots` times according to the current probability distribution, without modifying the state. +- `sample(measurement, shots) -> ExecutionResult`: sample according to a measurement receipt, without modifying the state. + - `measurement` (`Measurement`): the measurement receipt obtained from a measurement operation of a circuit. + - `shots` (`int`): the number of shots. +- `probs(measurement) -> dict[Outcome, float]`: compute marginal probabilities according to a measurement receipt, without modifying the state. + +**Copying** + +- `copy() -> Statevector`: return an independent copy; modifying the original state does not affect the copy. + +--- + +### Other behavior + +- `repr(sv)` returns a string of the form `Statevector(num_qubits=N, amplitudes=2^N)`. +- `__eq__` is not implemented: `==` is the default object identity comparison and does not compare state contents. +- `__copy__` and `__deepcopy__` are not implemented: the standard library `copy.copy()` and `copy.deepcopy()` raise `TypeError`. Use the `copy()` method when a copy is needed. + +--- + +### Raises + +- `IndexError`: qubit index out of range. +- `ValueError`: other validation failures, including amplitude length mismatch or amplitudes not normalized, duplicate target qubits of a two-qubit gate, custom gate matrix shape mismatch, observable type or qubit count mismatch, and circuit qubit count mismatch in `apply_circuit`. + +--- + +Example: + +```python +import math + +import numpy as np + +from cqlib import Circuit +from cqlib.qis import PauliString +from cqlib.qis.state import Statevector + +# 用线路构造 Bell 态 +circuit = Circuit(2) +circuit.h(0) +circuit.cx(0, 1) + +sv = Statevector.from_circuit(circuit) +probs = sv.probabilities() +assert math.isclose(probs[0], 0.5, abs_tol=1e-10) +assert math.isclose(probs[3], 0.5, abs_tol=1e-10) + +# 期望值 +assert math.isclose(sv.expectation(PauliString.from_str("ZZ")), 1.0, abs_tol=1e-10) + +# 采样不改变状态,结果只落在关联基态上 +shots = sv.sample_shots(64) +assert len(shots) == 64 +assert {outcome.to_bitstring(2) for outcome in shots} <= {"00", "11"} +assert math.isclose(sv.probabilities()[0], 0.5, abs_tol=1e-10) + +# 测量并坍缩:先在基态上验证确定性结果 +basis = Statevector(1) +basis.apply_x(0) +assert basis.measure(0) is True +assert math.isclose(basis.probabilities()[1], 1.0, abs_tol=1e-10) + +# 全量测量后概率集中在单个基态上 +sv.measure_all() +assert math.isclose(sum(sv.probabilities()), 1.0, abs_tol=1e-10) + +# 副本独立:全局相位不改变概率分布 +clone = sv.copy() +clone.apply_gphase(np.pi / 2) +assert np.allclose(sv.probabilities(), clone.probabilities(), atol=1e-10) +``` + +--- + +## Exceptions + +The validation failures of `Statevector` fall into two categories by source: + +| Exception | When it occurs | +| --- | --- | +| `IndexError` | Qubit index out of range. | +| `ValueError` | Amplitude length inconsistent with 2^num_qubits, amplitudes not normalized, duplicate target qubits of a two-qubit gate, custom gate matrix shape mismatch, observable type or qubit count mismatch, circuit qubit count mismatch, or circuit containing non-unitary operations. | diff --git a/docs/api/en/python/3_qis/2_density_matrix.md b/docs/api/en/python/3_qis/2_density_matrix.md new file mode 100644 index 00000000..c1d50ebe --- /dev/null +++ b/docs/api/en/python/3_qis/2_density_matrix.md @@ -0,0 +1,292 @@ +# DensityMatrix + +`cqlib.qis.state` + +The mixed-state simulator in `cqlib.qis.state`, covering the `DensityMatrix` class: state construction, gate application and quantum channels, partial traces, physicality validation, measurement and sampling, and expectation value computation. + +## Import + +```python +from cqlib.qis.state import DensityMatrix +``` + +--- + +## DensityMatrix + +A simulator that describes an N-qubit quantum state with a 2^N × 2^N density matrix ρ. Unlike the statevector, which can only represent pure states, the density matrix can represent classical mixtures of pure states. + +Data is stored contiguously in row-major order. Qubit i corresponds to bit i of the basis state index, with qubit 0 as the least significant bit; in bitstring representations, the higher-numbered qubit is on the left. After construction the state is |0…0⟩⟨0…0|. + +### DensityMatrix(num_qubits) + +Create a pure state in |0…0⟩⟨0…0|. + +Parameters: + +- `num_qubits` (`int`): the number of qubits. + +Example: + +```python +import math + +from cqlib.qis.state import DensityMatrix + +dm = DensityMatrix(2) +assert dm.num_qubits == 2 + +probs = dm.probabilities() +assert len(probs) == 4 +assert math.isclose(probs[0], 1.0) +assert math.isclose(probs[1], 0.0) +``` + +--- + +### Static methods + +- `DensityMatrix.from_state(num_qubits, initial_state) -> DensityMatrix`: construct from pure-state amplitudes, taking the outer product ρ = |ψ⟩⟨ψ| internally. `initial_state` is a NumPy complex array or a list of complex numbers of length 2^num_qubits. + - A `ValueError` is raised when the length does not match or the amplitudes are not normalized. +- `DensityMatrix.from_density_matrix(num_qubits, dm_state) -> DensityMatrix`: construct directly from flattened density matrix data. + - `dm_state` (`numpy.ndarray | list`): a complex sequence of length 4^num_qubits. + - A `ValueError` is raised when the length does not match, the trace is not 1, or it is not Hermitian or not positive semidefinite. +- `DensityMatrix.from_circuit(circuit) -> DensityMatrix`: simulate the evolution of the circuit starting from |0…0⟩⟨0…0| and return the resulting state. The input circuit is not modified. + - A `ValueError` is raised when the circuit contains unsupported operations. +- `DensityMatrix.maximally_mixed(num_qubits) -> DensityMatrix`: construct the maximally mixed state I / 2^N, that is, a diagonal matrix whose diagonal entries are all 1/2^N. A `num_qubits` of 0 gives the 1×1 matrix `[1]`. + +Example: + +```python +import math + +import numpy as np + +from cqlib import Circuit +from cqlib.qis.state import DensityMatrix + +amps = np.array([1 / np.sqrt(2), 1 / np.sqrt(2)], dtype=complex) +dm = DensityMatrix.from_state(1, amps) +assert math.isclose(dm.probabilities()[0], 0.5) +assert math.isclose(dm.probabilities()[1], 0.5) + +# |1><1| +data = np.array([0, 0, 0, 1], dtype=complex) +dm = DensityMatrix.from_density_matrix(1, data) +assert math.isclose(dm.probabilities()[1], 1.0) + +circuit = Circuit(2) +circuit.h(0) +circuit.cx(0, 1) + +bell = DensityMatrix.from_circuit(circuit) +assert math.isclose(bell.probabilities()[0], 0.5) +assert math.isclose(bell.probabilities()[3], 0.5) + +mixed = DensityMatrix.maximally_mixed(2) +assert np.allclose(mixed.data, np.eye(4, dtype=complex) / 4, atol=1e-12) +assert math.isclose(mixed.trace(), 1.0) +``` + +--- + +### Attributes + +- `num_qubits -> int`: the number of qubits. +- `data -> numpy.ndarray`: a two-dimensional complex matrix of shape (2^num_qubits, 2^num_qubits). Each read returns a copy; modifying the returned value does not affect the state. + +Example: + +```python +import numpy as np + +from cqlib.qis.state import DensityMatrix + +dm = DensityMatrix(2) +data = dm.data +assert data.shape == (4, 4) +assert data[0, 0] == 1.0 +assert np.allclose(data[0, 1:], 0) + +# data 是副本,就地修改不会写回状态 +data[0, 0] = 999.0 +assert dm.data[0, 0] == 1.0 +``` + +--- + +### Methods + +All gate application and state rewriting methods modify the current state in place and return `None`. + +**Circuit application** + +- `apply_circuit(circuit) -> None`: apply the circuit to the current state. A `ValueError` is raised when the number of circuit qubits does not match the state. + +**Standard gate objects** + +- `apply_standard_gate(gate, qubits, params=None) -> None`: apply a gate as described by `StandardGate`. + - `gate` (`StandardGate`): the standard gate. + - `qubits` (`list[int]`): the list of target qubit indices. + - `params` (`list[float] | None`): the parameters of a parameterized gate, defaulting to an empty list. + +**Single-qubit gates (no parameters)** + +`apply_x`, `apply_y`, `apply_z`, `apply_h`, `apply_s`, `apply_sdg`, `apply_t`, `apply_tdg`, `apply_x2p`, `apply_x2m`, `apply_y2p`, `apply_y2m`, all with the signature `(qubit) -> None`. + +**Single-qubit gates (with angle)** + +- `apply_rx(qubit, theta)`, `apply_ry(qubit, theta)`, `apply_rz(qubit, theta)`: rotate by `theta` radians around the X, Y and Z axes. +- `apply_phase(qubit, theta)`: phase gate. +- `apply_xy(qubit, theta)`: XY gate. +- `apply_xy2p(qubit, theta)`: XY2P gate. +- `apply_xy2m(qubit, theta)`: XY2M gate. +- `apply_rxy(qubit, theta, phi)`: RXY gate. +- `apply_u(qubit, theta, phi, lambda_)`: general single-qubit unitary gate, `U(θ, φ, λ) = Rz(φ) Ry(θ) Rz(λ)`. + +**Global phase** + +- `apply_gphase(phi) -> None`: has no observable effect on the density matrix; the state is unchanged after the call. + +**Controlled gates and two-qubit gates** + +- `apply_cx(control, target)`: controlled X (CNOT) gate. +- `apply_cy(control, target)`: controlled Y gate. +- `apply_cz(q0, q1)`: controlled Z gate. +- `apply_crx(control, target, theta)`: controlled RX gate. +- `apply_cry(control, target, theta)`: controlled RY gate. +- `apply_crz(control, target, theta)`: controlled RZ gate. +- `apply_swap(q0, q1)`: SWAP gate. +- `apply_rxx(q0, q1, theta)`: RXX (Ising XX) gate. +- `apply_ryy(q0, q1, theta)`: RYY (Ising YY) gate. +- `apply_rzz(q0, q1, theta)`: RZZ (Ising ZZ) gate. +- `apply_rzx(q0, q1, theta)`: RZX gate. +- `apply_fsim(q0, q1, theta, phi)`: fermionic simulation gate, where `theta` is the iSWAP angle and `phi` is the controlled phase angle. +- `apply_ccx(c0, c1, target)`: CCX (Toffoli) gate. + +**Custom matrix gates** + +- `apply_single_qubit_gate(qubit, matrix) -> None`: apply the single-qubit gate given by a 2×2 complex matrix; a `ValueError` is raised when the shape does not match. +- `apply_double_qubits_gate(q0, q1, matrix) -> None`: apply the two-qubit gate given by a 4×4 complex matrix; a `ValueError` is raised when the shape does not match. +- `apply_unitary_gate(qubits, matrix) -> None`: apply a unitary gate on any number of qubits, acting on `qubits`, with the evolution ρ → U ρ U†. The matrix shape must be 2^n × 2^n, where n is the length of `qubits`. + - `qubits` (`list[int]`): the list of qubit indices the gate acts on. + - `matrix` (`numpy.ndarray`): the unitary matrix; a `ValueError` is raised when the shape does not match the number of qubits. + +**Quantum channels** + +- `apply_kraus(qubits, ops) -> None`: apply the quantum channel given by Kraus operators, with the evolution ρ → Σ_k K_k ρ K_k†. + - `qubits` (`list[int]`): the list of qubit indices the channel acts on. + - `ops` (`list[numpy.ndarray]`): the list of Kraus operators, each a flattened complex array. A `ValueError` is raised when a Kraus operator is invalid. + +**Reduction and validation** + +- `partial_trace(keep) -> DensityMatrix`: take the partial trace over the remaining qubits and return the reduced density matrix containing only the qubits given by `keep`. + - `keep` (`list[int]`): the list of qubit indices to keep after reduction. An `IndexError` is raised when an index is out of range. +- `trace() -> float`: return the trace (the sum of the diagonal entries). The trace of a physical state is always 1. The real part of the result is taken. +- `is_hermitian(tol=1e-10) -> bool`: determine whether ρ = ρ† holds within the tolerance. +- `is_positive_semidefinite(tol=1e-10) -> bool`: determine whether all eigenvalues satisfy λᵢ ≥ -tol. For a Hermitian matrix this test is deterministic; for a non-Hermitian matrix or a matrix containing NaN or Inf it returns `False`. +- `validate_physical(tol=1e-10) -> None`: validate Hermiticity, positive semidefiniteness and unit trace at once. A `ValueError` is raised when any constraint is not satisfied. + +**Expectation values** + +- `expectation(observable) -> float`: compute Tr(ρ · O). + - `observable` (`Hamiltonian | PauliString`): the observable. A `ValueError` is raised when the qubit count does not match or the type is not supported. + +**Measurement and sampling** + +- `probabilities() -> list[float]`: return the probability of each basis state, that is, the diagonal entries ρ_ii of the density matrix, with length 2^num_qubits. Does not modify the state. +- `reset(qubit) -> None`: measure the given qubit and flip it to |0⟩ if the result is 1. +- `measure(qubit) -> bool`: measure a single qubit, collapse the state, and return the result of that qubit. +- `measure_all() -> Outcome`: measure all qubits, collapse the state, and return an `Outcome`. The bitstring is ordered from the highest qubit number to the lowest. +- `sample_shots(shots) -> list[Outcome]`: sample `shots` times according to the current probability distribution, without modifying the state. +- `sample(measurement, shots) -> ExecutionResult`: sample according to a measurement receipt, without modifying the state. + - `measurement` (`Measurement`): the measurement receipt obtained from a measurement operation of a circuit. + - `shots` (`int`): the number of shots. +- `probs(measurement) -> dict[Outcome, float]`: compute marginal probabilities according to a measurement receipt, without modifying the state. + +**Copying** + +- `copy() -> DensityMatrix`: return an independent copy; modifying the original state does not affect the copy. + +--- + +### Other behavior + +- `repr(dm)` returns a string of the form `DensityMatrix(num_qubits=N, shape=(2^N, 2^N))`. +- `__eq__` is not implemented: `==` is the default object identity comparison and does not compare matrix contents. +- `__copy__` and `__deepcopy__` are not implemented: the standard library `copy.copy()` and `copy.deepcopy()` raise `TypeError`. Use the `copy()` method when a copy is needed. + +--- + +### Raises + +- `IndexError`: qubit index of `partial_trace` out of range. +- `ValueError`: other validation failures, including amplitude length mismatch or amplitudes not normalized, density matrix data length mismatch or violation of physical constraints, duplicate target qubits of a two-qubit gate, custom gate matrix shape mismatch, invalid Kraus operator, observable type or qubit count mismatch, and failure of `validate_physical`. +- `TypeError`: `from_state` and `from_density_matrix` received an input that is neither an array nor a list; the `tol` of `is_hermitian`, `is_positive_semidefinite` and `validate_physical` is not a floating-point number. + +--- + +Example: + +```python +import math + +import numpy as np + +from cqlib import Circuit +from cqlib.qis import Hamiltonian, PauliString +from cqlib.qis.state import DensityMatrix + +# 用线路构造 Bell 态 +circuit = Circuit(2) +circuit.h(0) +circuit.cx(0, 1) + +dm = DensityMatrix.from_circuit(circuit) +assert dm.is_hermitian() is True +assert dm.is_positive_semidefinite() is True +assert dm.validate_physical() is None +assert math.isclose(dm.trace(), 1.0, abs_tol=1e-10) + +# 观测量 +assert math.isclose(dm.expectation(PauliString.from_str("ZI")), 0.0, abs_tol=1e-10) + +hamiltonian = Hamiltonian(2) +hamiltonian.add_term(PauliString.from_str("ZZ"), 0.5) +hamiltonian.add_term(PauliString.from_str("XX"), 0.5) +hamiltonian.simplify() +assert math.isclose(dm.expectation(hamiltonian), 1.0, abs_tol=1e-10) + +# 部分迹:Bell 态的单比特约化是最大混合态 +reduced = dm.partial_trace([0]) +assert reduced.num_qubits == 1 +assert math.isclose(reduced.probabilities()[0], 0.5, abs_tol=1e-10) +assert math.isclose(reduced.probabilities()[1], 0.5, abs_tol=1e-10) + +# 量子信道:退极化信道保持迹为 1 +noisy = DensityMatrix(1) +p = 0.3 +k0 = np.sqrt(1 - p) * np.eye(2, dtype=complex) +k1 = np.sqrt(p / 3) * np.array([[0, 1], [1, 0]], dtype=complex) +k2 = np.sqrt(p / 3) * np.array([[0, -1j], [1j, 0]], dtype=complex) +k3 = np.sqrt(p / 3) * np.array([[1, 0], [0, -1]], dtype=complex) +noisy.apply_kraus([0], [k0.flatten(), k1.flatten(), k2.flatten(), k3.flatten()]) +assert np.isclose(noisy.trace(), 1.0, atol=1e-10) + +# 副本独立 +snapshot = dm.copy() +dm.apply_h(0) +assert math.isclose(snapshot.probabilities()[0], 0.5, abs_tol=1e-10) +``` + +--- + +## Exceptions + +The validation failures of `DensityMatrix` fall into three categories by source: + +| Exception | When it occurs | +| --- | --- | +| `IndexError` | Qubit index of `partial_trace` out of range. | +| `ValueError` | Amplitude length inconsistent with 2^num_qubits, amplitudes not normalized, density matrix data length mismatch or violation of the trace, Hermiticity or positive semidefinite constraints, duplicate target qubits of a two-qubit gate, custom gate matrix shape mismatch, invalid Kraus operator, observable type or qubit count mismatch, or failure of `validate_physical`. | +| `TypeError` | `from_state` or `from_density_matrix` received an unsupported type; wrong type of the `tol` parameter. | diff --git a/docs/api/en/python/3_qis/3_density_matrix_noise.md b/docs/api/en/python/3_qis/3_density_matrix_noise.md new file mode 100644 index 00000000..a2983041 --- /dev/null +++ b/docs/api/en/python/3_qis/3_density_matrix_noise.md @@ -0,0 +1,278 @@ +# DensityMatrixNoise + +`cqlib.qis.state` + +The noisy mixed-state simulator in `cqlib.qis.state`, covering the `DensityMatrixNoise` class: noise model binding, gate application with noise, and probabilities and sampling under readout noise. + +## Import + +```python +from cqlib.device import NoiseModel, ReadoutError, SingleQubitNoise +from cqlib.qis.state import DensityMatrixNoise +``` + +--- + +## DensityMatrixNoise + +A simulator that applies Kraus noise automatically according to a noise model on top of the density matrix kernel. After each gate application, the noise corresponding to that gate in the model takes effect on the state immediately; readout noise takes effect at the probability computation and sampling stages. + +Data is stored contiguously in row-major order. Qubit i corresponds to bit i of the basis state index, with qubit 0 as the least significant bit; in bitstring representations, the higher-numbered qubit is on the left. After construction the state is |0…0⟩⟨0…0|. + +### DensityMatrixNoise(num_qubits, noise_model=None) + +Create a simulator. Omitting the noise model is equivalent to an ideal simulation. + +Parameters: + +- `num_qubits` (`int`): the number of qubits. +- `noise_model` (`NoiseModel | None`): the definition of gate noise and readout noise, defaulting to `None`. + +Example: + +```python +import math + +from cqlib.device import NoiseModel +from cqlib.qis.state import DensityMatrixNoise + +sim = DensityMatrixNoise(2) +assert sim.num_qubits == 2 +assert sim.state.shape == (4, 4) + +probs = sim.probabilities() +assert len(probs) == 4 +assert math.isclose(probs[0], 1.0) + +with_model = DensityMatrixNoise(2, NoiseModel()) +assert with_model.num_qubits == 2 +``` + +--- + +### Static methods + +- `DensityMatrixNoise.from_circuit(circuit, noise_model=None) -> DensityMatrixNoise`: simulate a circuit and return the simulator after evolution. The circuit is decomposed to basis gates before execution, and noise is applied after each gate according to the model. The input circuit is not modified. + - `circuit` (`Circuit`): the circuit to simulate. + - `noise_model` (`NoiseModel | None`): the noise model, defaulting to `None`. + - A `ValueError` is raised when the circuit contains unsupported operations. + +Example: + +```python +import math + +from cqlib import Circuit +from cqlib.device import NoiseModel +from cqlib.qis.state import DensityMatrixNoise + +circuit = Circuit(2) +circuit.h(0) +circuit.cx(0, 1) + +ideal = DensityMatrixNoise.from_circuit(circuit) +assert math.isclose(ideal.probabilities()[0], 0.5, abs_tol=1e-10) +assert math.isclose(ideal.probabilities()[3], 0.5, abs_tol=1e-10) + +noisy = DensityMatrixNoise.from_circuit(circuit, NoiseModel()) +assert noisy.num_qubits == 2 +``` + +--- + +### Attributes + +- `num_qubits -> int`: the number of qubits. +- `state -> numpy.ndarray`: a two-dimensional complex matrix of shape (2^num_qubits, 2^num_qubits) representing the current state. Each read returns a copy; modifying the returned value does not affect the simulator. + +Example: + +```python +import math + +from cqlib.qis.state import DensityMatrixNoise + +sim = DensityMatrixNoise(2) +sim.apply_h(0) +sim.apply_cx(0, 1) + +rho = sim.state +assert rho.shape == (4, 4) +assert math.isclose(abs(rho[0, 3]), 0.5, abs_tol=1e-10) + +rho[0, 0] = 999.0 +assert sim.state[0, 0] != 999.0 +``` + +--- + +### Methods + +All gate application and state rewriting methods modify the current state in place and return `None`. After application, each gate method applies the noise corresponding to that gate according to the noise model. + +**Circuit application** + +- `apply_circuit(circuit) -> None`: apply the circuit to the current state; the circuit is decomposed to basis gates before execution. A `ValueError` is raised when the number of circuit qubits does not match the state. + +**Standard gate objects** + +- `apply_standard_gate_noise(gate, qubits, params=None) -> None`: apply a gate as described by `StandardGate` and apply the corresponding noise. + - `gate` (`StandardGate`): the standard gate. + - `qubits` (`list[int]`): the list of target qubit indices. + - `params` (`list[float] | None`): the parameters of a parameterized gate, defaulting to an empty list. + +**Single-qubit gates (no parameters)** + +`apply_x`, `apply_y`, `apply_z`, `apply_h`, `apply_s`, `apply_sdg`, `apply_t`, `apply_tdg`, `apply_x2p`, `apply_x2m`, `apply_y2p`, `apply_y2m`, all with the signature `(q) -> None`. + +**Single-qubit gates (with angle)** + +- `apply_rx(q, theta)`, `apply_ry(q, theta)`, `apply_rz(q, theta)`: rotate by `theta` radians around the X, Y and Z axes. +- `apply_phase(q, theta)`: phase gate. +- `apply_xy(q, theta)`: XY gate. +- `apply_xy2p(q, theta)`: XY2P gate. +- `apply_xy2m(q, theta)`: XY2M gate. +- `apply_rxy(q, theta, phi)`: arbitrary rotation on the Bloch sphere. +- `apply_u(q, theta, phi, lambda_)`: general single-qubit unitary gate, `U(θ, φ, λ) = Rz(φ) Ry(θ) Rz(λ)`. + +**Global phase** + +- `apply_gphase(phi) -> None`: apply a global phase gate. + +**Controlled gates and two-qubit gates** + +- `apply_cx(control, target)`: controlled X (CNOT) gate. +- `apply_cy(control, target)`: controlled Y gate. +- `apply_cz(q0, q1)`: controlled Z gate. +- `apply_crx(control, target, theta)`: controlled RX gate. +- `apply_cry(control, target, theta)`: controlled RY gate. +- `apply_crz(control, target, theta)`: controlled RZ gate. +- `apply_swap(q0, q1)`: SWAP gate. +- `apply_rxx(q0, q1, theta)`: RXX (Ising XX) gate. +- `apply_ryy(q0, q1, theta)`: RYY (Ising YY) gate. +- `apply_rzz(q0, q1, theta)`: RZZ (Ising ZZ) gate. +- `apply_rzx(q0, q1, theta)`: RZX gate. +- `apply_fsim(q0, q1, theta, phi)`: fermionic simulation gate, where `theta` is the iSWAP angle and `phi` is the controlled phase angle. +- `apply_ccx(c1, c2, t)`: CCX (Toffoli) gate, where `c1` and `c2` are the control qubits and `t` is the target qubit. + +**Custom matrix gates** + +- `apply_unitary_gate(qubits, matrix) -> None`: apply a unitary gate on any number of qubits, acting on `qubits`. This entry point does not apply noise. + - `qubits` (`list[int]`): the list of qubit indices the gate acts on. + - `matrix` (`numpy.ndarray`): a 2^n × 2^n complex matrix, where n is the length of `qubits`; a `ValueError` is raised when the shape does not match the number of qubits. + +**Expectation values** + +- `expectation(observable) -> float`: compute the expectation value on the noisy state. + - `observable` (`Hamiltonian | PauliString`): the observable. A `ValueError` is raised when the qubit count does not match or the type is not supported. + +**Probabilities** + +- `probabilities() -> list[float]`: return the probability of each basis state without readout noise, with length 2^num_qubits. Does not modify the state. +- `probabilities_with_readout(qubits) -> list[float]`: return the probability distribution after readout noise is applied on the given qubits, with length 2^n, where n is the length of `qubits`. + - `qubits` (`list[int]`): the list of qubit indices taking part in readout noise modeling. An `IndexError` is raised when an index is out of range. + +**Measurement and sampling** + +- `measure(qubit) -> bool`: measure a single qubit, collapse the state, and return the result of that qubit. +- `measure_all() -> Outcome`: measure all qubits, collapse the state, and return an `Outcome`. The bitstring is ordered from the highest qubit number to the lowest. +- `sample_shots(shots) -> list[Outcome]`: sample `shots` times according to the distribution including readout noise, without modifying the state. +- `sample(measurement, shots) -> ExecutionResult`: sample according to a measurement receipt, without modifying the state. + - `measurement` (`Measurement`): the measurement receipt obtained from a measurement operation of a circuit. + - `shots` (`int`): the number of shots. +- `probs(measurement) -> dict[Outcome, float]`: compute marginal probabilities without readout noise according to a measurement receipt, without modifying the state. +- `probs_with_readout(measurement) -> dict[Outcome, float]`: compute marginal probabilities with readout noise applied according to a measurement receipt, without modifying the state. + +**Copying** + +- `copy() -> DensityMatrixNoise`: return an independent copy, with the noise model copied as well. Modifying the original simulator does not affect the copy. + +--- + +### Other behavior + +- `repr(sim)` returns a string of the form `DensityMatrixNoise(num_qubits=N, state_shape=(2^N, 2^N))`. +- `__eq__` is not implemented: `==` is the default object identity comparison and does not compare state contents. +- `__copy__` and `__deepcopy__` are not implemented: the standard library `copy.copy()` and `copy.deepcopy()` raise `TypeError`. Use the `copy()` method when a copy is needed. + +--- + +### Raises + +- `IndexError`: qubit index out of range. +- `ValueError`: other validation failures, including custom gate matrix shape inconsistent with the number of qubits, observable type or qubit count mismatch, circuit containing unsupported operations, and circuit qubit count inconsistent with the state. +- `OverflowError`: `num_qubits` is negative. + +--- + +Example: + +```python +import math + +import numpy as np + +from cqlib.circuit import StandardGate +from cqlib.device import NoiseModel, ReadoutError, SingleQubitNoise +from cqlib.qis.state import DensityMatrixNoise + +# 理想模拟:不带噪声模型时与纯密度矩阵结果一致 +ideal = DensityMatrixNoise(2) +ideal.apply_h(0) +ideal.apply_cx(0, 1) +assert math.isclose(ideal.probabilities()[0], 0.5, abs_tol=1e-10) +assert math.isclose(ideal.probabilities()[3], 0.5, abs_tol=1e-10) + +# 比特翻转噪声:X 门后以 p=0.1 翻转 +noise_model = NoiseModel() +noise_model.add_single_qubit_error(StandardGate.X, 0, SingleQubitNoise.bit_flip(0.1)) + +sim = DensityMatrixNoise(1, noise_model) +sim.apply_x(0) +probs = sim.probabilities() +assert math.isclose(probs[1], 0.9, abs_tol=0.01) +assert math.isclose(probs[0], 0.1, abs_tol=0.01) + +# 理想概率与读出噪声下的概率相互独立 +circuit_readout = NoiseModel() +circuit_readout.add_readout_error(0, ReadoutError(p_0_given_1=0.1, p_1_given_0=0.2)) + +readout_sim = DensityMatrixNoise(1, circuit_readout) +readout_sim.apply_x(0) +assert readout_sim.probabilities() == [0.0, 1.0] +readout_probs = readout_sim.probabilities_with_readout([0]) +assert math.isclose(readout_probs[0], 0.1, abs_tol=1e-10) +assert math.isclose(readout_probs[1], 0.9, abs_tol=1e-10) + +# 副本独立,且保留噪声行为 +snapshot = sim.copy() +sim.apply_h(0) +assert math.isclose(snapshot.probabilities()[1], 0.9, abs_tol=0.01) + +# 自定义酉门入口不施加噪声 +matrix = np.array( + [ + [1 / np.sqrt(2), 0, 1 / np.sqrt(2), 0], + [0, 1 / np.sqrt(2), 0, 1 / np.sqrt(2)], + [0, 1 / np.sqrt(2), 0, -1 / np.sqrt(2)], + [1 / np.sqrt(2), 0, -1 / np.sqrt(2), 0], + ], + dtype=complex, +) +plain = DensityMatrixNoise(2) +plain.apply_unitary_gate([0, 1], matrix) +assert math.isclose(plain.probabilities()[0], 0.5, abs_tol=1e-10) +assert math.isclose(np.trace(plain.state @ plain.state).real, 1.0, abs_tol=1e-10) +``` + +--- + +## Exceptions + +The validation failures of `DensityMatrixNoise` fall into three categories by source: + +| Exception | When it occurs | +| --- | --- | +| `IndexError` | Qubit index out of range, including `qubits` of `probabilities_with_readout` out of range. | +| `ValueError` | Custom gate matrix shape inconsistent with the number of qubits, observable type or qubit count mismatch, circuit qubit count inconsistent with the state, or circuit containing unsupported operations. | +| `OverflowError` | `num_qubits` is negative. | diff --git a/docs/api/en/python/3_qis/4_stabilizer.md b/docs/api/en/python/3_qis/4_stabilizer.md new file mode 100644 index 00000000..c25ac6e3 --- /dev/null +++ b/docs/api/en/python/3_qis/4_stabilizer.md @@ -0,0 +1,267 @@ +# Stabilizer + +`cqlib.qis.state` + +The stabilizer simulator in `cqlib.qis.state`, covering the two classes `StabilizerState` and `StabilizerCircuitResult`: simulation of Clifford circuits, stabilizer generator queries, measurement and sampling, and reading of runtime classical data after execution. + +## Import + +```python +from cqlib.qis.state import StabilizerCircuitResult, StabilizerState +``` + +--- + +## StabilizerState + +A simulator that describes an N-qubit Clifford state with a stabilizer table. Clifford gates map the Pauli group onto itself, so the state can be represented compactly by N commuting Pauli generators instead of storing all amplitudes. + +This class accepts only Clifford gates. A circuit containing non-Clifford operations such as T gates or rotations by general angles cannot be simulated, and a `ValueError` is raised in that case. + +Qubit i corresponds to bit i of the basis state index, with qubit 0 as the least significant bit; in bitstring representations, the higher-numbered qubit is on the left. After construction the state is |0…0⟩. + +### StabilizerState(num_qubits) + +Create a state in |0…0⟩. + +Parameters: + +- `num_qubits` (`int`): the number of qubits. + +Example: + +```python +from cqlib.qis.state import StabilizerState + +state = StabilizerState(3) +assert state.num_qubits == 3 +assert state.probabilities() == [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] +assert "StabilizerState" in repr(state) +``` + +--- + +### Static methods + +- `StabilizerState.from_circuit(circuit) -> StabilizerState`: simulate the evolution of a Clifford circuit and return the final state. The input circuit is not modified. A `ValueError` is raised when the circuit contains non-Clifford operations. +- `StabilizerState.run_circuit(circuit) -> StabilizerCircuitResult`: execute a Clifford circuit and return a result object carrying both the final state and the runtime classical data. The input circuit is not modified. + - A `ValueError` is raised when the circuit contains control flow or non-Clifford operations. + +Example: + +```python +from cqlib import Circuit +from cqlib.qis.state import StabilizerState + +circuit = Circuit(2) +circuit.h(0) +circuit.cx(0, 1) + +bell = StabilizerState.from_circuit(circuit) +assert bell.num_qubits == 2 +assert bell.probabilities()[0] == 0.5 +assert bell.probabilities()[3] == 0.5 +``` + +--- + +### Attributes + +- `num_qubits -> int`: the number of qubits. + +--- + +### Methods + +All gate application and state rewriting methods modify the current state in place and return `None`. + +**Circuit application** + +- `apply_circuit(circuit) -> None`: apply a Clifford circuit to the current state. A `ValueError` is raised when the circuit contains non-Clifford operations or the number of qubits does not match. + +**Single-qubit Clifford gates** + +`apply_h`, `apply_s`, `apply_sdg`, `apply_x`, `apply_y`, `apply_z`, `apply_x2p`, `apply_x2m`, `apply_y2p`, `apply_y2m`, all with the signature `(qubit) -> None`. + +**Two-qubit Clifford gates** + +- `apply_cx(control, target)`: controlled X gate. +- `apply_cy(control, target)`: controlled Y gate. +- `apply_cz(q0, q1)`: controlled Z gate. +- `apply_swap(q0, q1)`: SWAP gate. + +A `ValueError` is raised when the two qubit indices are the same. + +**Stabilizer table queries** + +- `get_stabilizers() -> list[PauliString]`: return the N stabilizer generators. +- `get_destabilizers() -> list[PauliString]`: return the N destabilizer generators. +- `to_stim_format() -> str`: return a line-by-line text representation of the stabilizer generators, one generator per line with a sign prefix, for example `+XX`. + +**Expectation values** + +- `pauli_expectation(pauli) -> int`: return the expectation value of a Pauli string, which is `-1`, `0` or `1`. A `ValueError` is raised when the qubit count does not match. + +**Probabilities** + +- `probabilities() -> list[float]`: return the probability distribution over basis states, with length 2^num_qubits. Does not modify the state. +- `probability_of(bits) -> float`: return the probability of the given computational basis bitstring. + - `bits` (`list[bool]`): the value of each qubit given by qubit number. A `ValueError` is raised when the length does not match `num_qubits`. + +**Measurement and sampling** + +- `reset(qubit) -> None`: measure the given qubit and flip it to |0⟩ if the result is 1. +- `measure(qubit) -> bool`: measure a single qubit, collapse the state, and return the result of that qubit. +- `measure_all() -> Outcome`: measure all qubits, collapse the state, and return an `Outcome`. The bitstring is ordered from the highest qubit number to the lowest. +- `sample_shots(shots) -> list[Outcome]`: sample `shots` times according to the current probability distribution, without modifying the state. +- `sample(measurement, shots) -> ExecutionResult`: sample according to a measurement receipt, without modifying the state. + - `measurement` (`Measurement`): the measurement receipt obtained from a measurement operation of a circuit. + - `shots` (`int`): the number of shots. +- `probs(measurement) -> dict[Outcome, float]`: compute marginal probabilities according to a measurement receipt, without modifying the state. + +**Copying** + +- `copy() -> StabilizerState`: return an independent copy; modifying the original state does not affect the copy. + +--- + +### Other behavior + +- `repr(state)` returns a string of the form `StabilizerState(num_qubits=N)`. +- `__eq__` is not implemented: `==` is the default object identity comparison and does not compare stabilizer table contents. +- `__copy__` and `__deepcopy__` are not implemented: the standard library `copy.copy()` and `copy.deepcopy()` raise `TypeError`. Use the `copy()` method when a copy is needed. + +--- + +### Raises + +- `IndexError`: qubit index out of range. +- `ValueError`: non-Clifford operations, circuit containing control flow, duplicate target qubits of a two-qubit gate, bitstring length of `probability_of` inconsistent with `num_qubits`, qubit count mismatch of `pauli_expectation`, or circuit qubit count inconsistent with the state. +- `OverflowError`: `num_qubits` is negative. + +--- + +Example: + +```python +import math + +from cqlib.qis import PauliString +from cqlib.qis.state import StabilizerState + +state = StabilizerState(2) +state.apply_h(0) +state.apply_cx(0, 1) + +# 概率与单位置概率 +probs = state.probabilities() +assert probs[0] == 0.5 +assert probs[3] == 0.5 +assert math.isclose(state.probability_of([False, False]), 0.5, abs_tol=1e-10) +assert math.isclose(state.probability_of([True, False]), 0.0, abs_tol=1e-10) + +# 稳定子表 +stabilizers = state.get_stabilizers() +destabilizers = state.get_destabilizers() +assert len(stabilizers) == 2 +assert len(destabilizers) == 2 +assert all(isinstance(pauli, PauliString) for pauli in stabilizers) +assert state.to_stim_format().count("\n") == 2 + +# 观测值 +assert state.pauli_expectation(PauliString.from_str("ZZ")) == 1 +assert state.pauli_expectation(PauliString.from_str("ZI")) == 0 + +# 采样不改变状态,结果只落在关联基态上 +shots = state.sample_shots(64) +assert len(shots) == 64 +assert {outcome.to_bitstring(2) for outcome in shots} <= {"00", "11"} +assert state.probabilities()[0] == 0.5 + +# 副本独立:快照保留副本创建时的分布 +snapshot = state.copy() +state.apply_x(0) +assert snapshot.probabilities()[3] == 0.5 +assert state.probabilities()[1] == 0.5 +assert state.probabilities()[2] == 0.5 + +# 全量测量后状态坍缩到单个基态 +outcome = state.measure_all() +assert outcome.to_bitstring(2) in {"01", "10"} + +# 测量与复位:在确定性基态上结果可预期 +basis = StabilizerState(1) +basis.apply_x(0) +assert basis.measure(0) is True +assert basis.probabilities()[1] == 1.0 + +basis.reset(0) +assert basis.probabilities()[0] == 1.0 +``` + +--- + +## StabilizerCircuitResult + +The object returned by `StabilizerState.run_circuit()`, carrying both the final stabilizer state and the runtime classical data produced during execution. + +This object has no public constructor and can only be returned by `StabilizerState.run_circuit()`. + +### Attributes + +- `state -> StabilizerState`: the final stabilizer state after circuit execution ends. +- `classical -> ClassicalState`: the set of runtime classical values produced during execution, see [Classical State](5_classical_state.md). + +### Other behavior + +- `repr(result)` returns a string of the form `StabilizerCircuitResult(num_qubits=N)`. +- `__eq__` is not implemented: `==` is the default object identity comparison. +- `__copy__` and `__deepcopy__` are not implemented: the standard library `copy.copy()` and `copy.deepcopy()` raise `TypeError`. When the result needs to be preserved, call `StabilizerState.run_circuit()` again. + +--- + +Example: + +```python +from cqlib import Circuit +from cqlib.qis.state import ( + ClassicalState, + RuntimeValue, + StabilizerCircuitResult, + StabilizerState, +) + +circuit = Circuit(2) +circuit.x(0) +measurement = circuit.measure(0) +circuit.reset(0) +circuit.h(1) + +result = StabilizerState.run_circuit(circuit) +assert isinstance(result, StabilizerCircuitResult) +assert "StabilizerCircuitResult" in repr(result) + +# 最终状态 +assert result.state.num_qubits == 2 +assert result.state.probabilities()[0] == 0.5 +assert result.state.probabilities()[2] == 0.5 + +# 运行期经典数据 +assert isinstance(result.classical, ClassicalState) +measured = result.classical.value(measurement.value) +assert isinstance(measured, RuntimeValue) +assert measured.kind == "bit" +assert measured.as_bit() is True +``` + +--- + +## Exceptions + +The validation failures of the two classes fall into three categories by source: + +| Exception | When it occurs | +| --- | --- | +| `IndexError` | Qubit index out of range. | +| `ValueError` | Circuit containing non-Clifford operations or control flow, duplicate target qubits of a two-qubit gate, bitstring length mismatch of `probability_of`, qubit count mismatch of `pauli_expectation`, or circuit qubit count inconsistent with the state. | +| `OverflowError` | `num_qubits` is negative. | diff --git a/docs/api/en/python/3_qis/5_classical_state.md b/docs/api/en/python/3_qis/5_classical_state.md new file mode 100644 index 00000000..05471a7b --- /dev/null +++ b/docs/api/en/python/3_qis/5_classical_state.md @@ -0,0 +1,131 @@ +# Runtime classical state + +`cqlib.qis.state` + +`ClassicalState` and `RuntimeValue` describe the runtime classical data produced during circuit execution. `RuntimeValue` is the typed value of a single classical value in one execution; `ClassicalState` is a snapshot of all runtime classical data after one execution ends, with values queried by handle. Both are given by the simulator after execution completes and have no public constructor. + +## Import + +```python +from cqlib.qis.state import ClassicalState, RuntimeValue +``` + +--- + +## RuntimeValue + +The runtime value of a single classical value. The kind of the value is distinguished by `kind`, and the static type is given by `ty`. + +### Attributes + +- `kind -> str`: the kind of the value, one of `"bit"`, `"bool"`, `"uint"` or `"bit_vec"`. +- `ty -> ClassicalType`: the classical type corresponding to this value. + +### Methods + +- `to_bitstring() -> str | None`: return a bitstring when the value is of kind `bit` or `bit_vec`, and `None` for the other kinds. +- `as_bit() -> bool`: read the value as a `bit`. +- `as_bool() -> bool`: read the value as a `bool`. +- `as_uint() -> int`: read the value as a `uint`. +- `as_bitvec_outcome() -> Outcome`: read the value as a `bit_vec` and return a measurement receipt. + +### Other behavior + +- Equality is determined by value: two values are equal only when the kind, bit width and payload are all the same; comparison with other types is always false. +- Equal values have the same hash, so they can be put into a set directly for deduplication. +- Supports `copy` and `deepcopy`. +- `repr` has the form `RuntimeValue.bit(true)`, `RuntimeValue.uint(width=8, value=3)`, `RuntimeValue.bit_vec(width=2, bits='10')`. + +### Raises + +- `TypeError`: raised when `as_bit`, `as_bool`, `as_uint` or `as_bitvec_outcome` is used with a mismatched value kind. + +--- + +## ClassicalState + +A snapshot of the runtime classical state after one circuit execution ends, with values queried by classical value handle or classical variable handle. + +### Methods + +- `value(value) -> RuntimeValue | None`: query the runtime value produced by this execution for an immutable classical value; returns `None` when there is no value. `value` is a `ClassicalValue`, usually taken from a measurement receipt. +- `var(var) -> RuntimeValue | None`: query the current runtime value of a mutable classical variable; returns `None` when there is no value. `var` is a `ClassicalVar`. + +### Other behavior + +- No public constructor; can only be obtained from an execution result. +- Supports `copy` and `deepcopy`. +- `repr` is always `ClassicalState()`. + +--- + +## Example + +### Query the value produced by a measurement + +```python +from cqlib.circuit import Circuit +from cqlib.qis.state import StabilizerState + +circuit = Circuit(2) +circuit.x(0) +measurement = circuit.measure(0) +circuit.reset(0) +circuit.h(1) + +result = StabilizerState.run_circuit(circuit) +measured = result.classical.value(measurement.value) +assert measured.kind == "bit" +assert measured.as_bit() is True +assert measured.to_bitstring() == "1" +``` + +### Query a mutable classical variable + +```python +from cqlib.circuit import Circuit, ClassicalExpr, ClassicalType +from cqlib.qis.state import StabilizerState + +circuit = Circuit(1) +flag = circuit.var(ClassicalType.bool()) +circuit.store(flag, ClassicalExpr.bool_literal(True)) + +result = StabilizerState.run_circuit(circuit) +stored = result.classical.var(flag) +assert stored.kind == "bool" +assert stored.as_bool() is True +assert stored.to_bitstring() is None +``` + +### Value comparison and deduplication + +Values of the same kind and the same payload are equal and have the same hash, so they can be used directly for set deduplication; values of different kinds are not equal even when they are semantically close. + +```python +from cqlib.circuit import Circuit +from cqlib.qis.state import StabilizerState + + +def measured_bit(): + circuit = Circuit(1) + circuit.x(0) + measurement = circuit.measure(0) + return StabilizerState.run_circuit(circuit).classical.value(measurement.value) + + +first = measured_bit() +second = measured_bit() +assert first == second +assert hash(first) == hash(second) +assert len({first, second}) == 1 +``` + +--- + +## Validation and error handling + +| Exception | When it occurs | +| --- | --- | +| `TypeError` | Reading a `RuntimeValue` with a mismatched accessor, for example calling `as_uint()` on a value of kind `bit`. | + +The definitions of classical types, classical variables and classical value handles are given in [Classical / Control Flow](../0_circuit/9_classical_control_flow.md); the definition of `Outcome` is given in [Result](../2_device/5_result.md). diff --git a/docs/api/en/python/3_qis/6_pauli.md b/docs/api/en/python/3_qis/6_pauli.md new file mode 100644 index 00000000..5c223a66 --- /dev/null +++ b/docs/api/en/python/3_qis/6_pauli.md @@ -0,0 +1,247 @@ +# Pauli operators + +`cqlib.qis` + +`Phase`, `Pauli` and `PauliString` are the three levels of the Pauli group: `Phase` represents the phase factor produced when group elements are multiplied, `Pauli` is a single-qubit Pauli operator, and `PauliString` is a tensor product of multi-qubit Pauli operators. `PauliString` is stored in the symplectic representation, and provides conversions between the character form, the bit masks and the matrix form. + +## Import + +```python +from cqlib.qis import Phase, Pauli, PauliString +``` + +--- + +## Phase + +A phase factor in the Pauli group, isomorphic to the cyclic group of order 4, representing $i^n$ with $n \in \{0, 1, 2, 3\}$. + +### Phase(val) + +Parameters: + +- `val` (`int`): the phase exponent, taken modulo 4, with an acceptable range of `0`–`255`. + +### Static methods + +- `Phase.plus()`: return $+1$, that is, $i^0$. +- `Phase.i()`: return $+i$, that is, $i^1$. +- `Phase.minus()`: return $-1$, that is, $i^2$. +- `Phase.minus_i()`: return $-i$, that is, $i^3$. + +### Attributes + +- `exponent -> int`: the phase exponent, with values `0`–`3`. + +### Methods + +- `to_complex() -> complex`: convert to a Python complex number. + +### Other behavior + +- Both `+` and `*` denote group multiplication, and the result is taken modulo 4. +- Supports `==` and `hash`. +- `str` is `1`, `i`, `-1` or `-i`; `repr` has the form `Phase(1)`. + +### Raises + +- `OverflowError`: raised when `val` is outside `0`–`255`. + +--- + +## Pauli + +A single-qubit Pauli operator. + +### Static methods + +- `Pauli.x()`, `Pauli.y()`, `Pauli.z()`, `Pauli.i()`: the only construction entry points of the four Pauli operators. + +### Methods + +- `to_symplectic() -> tuple[int, int]`: return the symplectic representation $(x, z)$. `I` is `(0, 0)`, `X` is `(1, 0)`, `Y` is `(1, 1)` and `Z` is `(0, 1)`. +- `to_matrix() -> numpy.ndarray`: return the $2 \times 2$ complex matrix. +- `mul_with_phase(other) -> tuple[Pauli, Phase]`: multiply with another single-qubit Pauli operator and return the resulting operator together with the phase factor. + +### Other behavior + +- No public constructor. +- `*` denotes multiplication but does not return the phase factor; use `mul_with_phase` when the phase is needed. +- Supports `==` and `hash`. +- `str` is the operator name; `repr` has the form `Pauli.X`. + +--- + +## PauliString + +A multi-qubit Pauli operator, that is, $P = \bigotimes_{i=0}^{N-1} P_i$, where $P_i \in \{I, X, Y, Z\}$. + +### PauliString(num_qubits) + +Parameters: + +- `num_qubits` (`int`): the number of qubits. The result is an all-`I` string with phase $+1$. + +### PauliString.from_str(s) + +Construct from the character form. The format is `[+|-][i|j]`, where the operator sequence consists of `I`, `X`, `Y` and `Z`; qubits are ordered by decreasing index, so the first character corresponds to the highest qubit index. `j` is equivalent to `i`, and is normalized to `i` internally. + +Parameters: + +- `s` (`str`): the character form, for example `"XZI"`, `"-iZII"`, `"+XYZ"`. + +Returns: + +- `PauliString`: the new Pauli string. + +Raises: + +- `ValueError`: raised when the string is empty, contains an illegal character, or gives only a phase without operators. + +### Attributes + +- `num_qubits -> int`: the number of qubits. +- `phase -> Phase`: the global phase, readable and writable. +- `x_bits -> list[bool]`: the bit vector of the $X$ component, indexed by qubit index. +- `z_bits -> list[bool]`: the bit vector of the $Z$ component. +- `x_mask -> int`: the integer mask of the $X$ component. +- `z_mask -> int`: the integer mask of the $Z$ component. + +### Methods + +- `get_pauli(idx) -> Pauli`: read the operator on the given qubit. +- `set_pauli(idx, pauli) -> None`: write the operator on the given qubit. +- `y_phase() -> complex`: return the phase factor contributed by $Y$ operators. Since $Y = iXZ$, $n$ occurrences of $Y$ contribute $i^n$. +- `commutes_with(other) -> bool`: determine whether two Pauli strings commute, based on whether the symplectic inner product modulo 2 is zero. +- `support() -> list[int]`: return the qubit indices carrying an operator other than `I`, in ascending order. +- `to_matrix() -> numpy.ndarray`: return the dense matrix, expanded in little-endian tensor order (qubit `0` is the least significant tensor factor, that is, $P_{N-1} \otimes \cdots \otimes P_0$), including the global phase. The shape is $(2^N, 2^N)$ and the type is `complex128`. The space complexity is $O(4^N)$, which suits analysis and validation of small-scale systems. +- `copy() -> PauliString`: return a copy. +- `expectation(probs) -> float`: compute the expectation value $\langle P \rangle = \sum_s p(s)\langle s | P | s \rangle$ from a probability distribution. If the string contains non-diagonal operators such as `X` or `Y`, the expectation value is `0` for any computational basis probability distribution. +- `expectation_statevector(sv) -> float`: compute the expectation value from a statevector. +- `expectation_density_matrix(dm) -> float`: compute the expectation value from a density matrix. +- `expectation_probs(measurements) -> float`: compute the expectation value from measurement probabilities. +- `variance_statevector(sv) -> float`: compute the variance from a statevector. + +### Other behavior + +- `len(ps)` equals `num_qubits`; a zero-qubit string is falsy. +- A string can be iterated directly, yielding `Pauli` one by one in ascending qubit index, including `I`. The iterator has snapshot semantics: modifications to the original string after the iterator is created do not affect this iteration. The iterator provides `__length_hint__`, giving the number of remaining elements. +- `ps[i]` reads the operator on the given qubit and supports negative indices, where `-1` means the highest qubit index. +- `*` and `*=` denote multiplication, and the phase of the result accumulates by group multiplication. +- Supports `==`; as a mutable type it does not define `hash`, so it is unhashable and cannot be used as a dictionary key. +- `str` has the form `+XYZ` or `-iZII`; `repr` has the form `PauliString(num_qubits=3, phase=1, x_bits=[...], z_bits=[...])`. + +### Raises + +- `ValueError`: `from_str` parsing failure; the two operands of `commutes_with` or `*` have inconsistent qubit counts. +- `IndexError`: index out of range in `get_pauli` or `set_pauli`; index out of range in `ps[i]`. + +### Expectation value parameters + +The `probs` of `expectation(probs)` is a dictionary from state strings to probabilities. State strings are written in the little-endian convention: the rightmost character corresponds to qubit `0`, so for example `"01"` means qubit `0` is `1` and qubit `1` is `0`. The length of a state string must equal `num_qubits`. + +The `measurements` of `expectation_probs(measurements)` is a list of `(PauliString, dict)` tuples, each giving a measurement basis together with the corresponding probability distribution. If the target operator cannot be derived from the given measurement bases, an error is raised. + +### Example + +```python +from cqlib.qis import Pauli, PauliString, Phase + +# 乘法与相位 +x, y, z, i = Pauli.x(), Pauli.y(), Pauli.z(), Pauli.i() +res, phase = x.mul_with_phase(y) # XY = iZ +assert res == z +assert phase == Phase.i() +assert x * y == z +assert x.to_symplectic() == (1, 0) + +# 字符形式与属性 +ps = PauliString.from_str("-iZII") +assert ps.num_qubits == 3 +assert str(ps) == "-iZII" +assert ps.phase == Phase.minus_i() +assert PauliString.from_str("+jZZ").phase == Phase.i() + +# 掩码、支撑集与迭代 +ps = PauliString.from_str("XIZ") +assert ps.x_mask == 0b100 +assert ps.z_mask == 0b001 +assert ps.support() == [0, 2] +assert len(ps) == 3 +assert list(ps) == [Pauli.z(), Pauli.i(), Pauli.x()] # 按比特下标升序,含 I +``` + +```python +from cqlib.qis import Pauli, PauliString + +# 构造与逐比特读写 +ps = PauliString(3) +assert str(ps) == "+III" +ps.set_pauli(0, Pauli.x()) +ps.set_pauli(1, Pauli.z()) +ps.set_pauli(2, Pauli.y()) +assert str(ps) == "+YZX" +assert ps.get_pauli(0) == Pauli.x() +assert ps[0] == Pauli.x() +assert ps[-1] == Pauli.y() +``` + +```python +from cqlib.qis import PauliString + +# 乘法与对易性 +ps1 = PauliString.from_str("X") +ps2 = PauliString.from_str("Z") +assert str(ps1 * ps2) == "-iY" + +ps3 = PauliString.from_str("XZ") +ps4 = PauliString.from_str("YX") +assert str(ps3 * ps4) == "-ZY" + +ps5 = PauliString.from_str("XZI") +assert ps5.commutes_with(PauliString.from_str("ZXI")) is True +assert ps5.commutes_with(PauliString.from_str("YII")) is False +``` + +```python +import math + +from cqlib.qis import PauliString, Statevector + +# 期望值与方差 +sv = Statevector(1) +ps_z = PauliString.from_str("Z") +assert math.isclose(ps_z.expectation_statevector(sv), 1.0) + +sv.apply_h(0) +ps_x = PauliString.from_str("X") +assert math.isclose(ps_x.expectation_statevector(sv), 1.0) +assert math.isclose(ps_z.variance_statevector(sv), 1.0, abs_tol=1e-10) + +# 由概率分布求期望值 +assert math.isclose(ps_z.expectation({"0": 0.5, "1": 0.5}), 0.0) +``` + +```python +import numpy as np + +from cqlib.qis import PauliString + +# 矩阵形式,小端张量序:比特 0 是最低有效张量因子 +ps = PauliString.from_str("ZX") +matrix = ps.to_matrix() +assert matrix.shape == (4, 4) +assert matrix.dtype == np.complex128 +``` + +--- + +## Validation and error handling + +| Exception | When it occurs | +| --- | --- | +| `ValueError` | The string of `PauliString.from_str` is empty, contains an illegal character or lacks operators; the two operands of `commutes_with`, `*` or `*=` have inconsistent qubit counts. | +| `IndexError` | Qubit index out of range in `get_pauli`, `set_pauli` or `ps[i]`. | +| `OverflowError` | The exponent of `Phase(val)` is outside the acceptable range. | + +The input types of `expectation_statevector`, `expectation_density_matrix` and `variance_statevector` are given in [Statevector](1_statevector.md) and [DensityMatrix](2_density_matrix.md). diff --git a/docs/api/en/python/3_qis/7_hamiltonian.md b/docs/api/en/python/3_qis/7_hamiltonian.md new file mode 100644 index 00000000..9f27e7b2 --- /dev/null +++ b/docs/api/en/python/3_qis/7_hamiltonian.md @@ -0,0 +1,222 @@ +# Hamiltonian observable + +`cqlib.qis` + +`Hamiltonian` represents an operator as a linear combination of Pauli strings, $H = \sum_k c_k P_k$, where $c_k$ is a complex coefficient and $P_k$ is a multi-qubit Pauli string. It is a sparse representation of a $2^N \times 2^N$ matrix, used to describe system energy, to compute expectation values as an observable, and to generate time evolution circuits. + +This page also covers the `Observable` protocol, which specifies the members an observable must provide. + +## Import + +```python +from cqlib.qis import Hamiltonian, Observable +``` + +--- + +## Observable + +The observable protocol. It is a pure Python `typing.Protocol` defined by `cqlib.qis`, not a native class, and it provides no runtime checking. + +Definition: + +```python +from typing import Dict, List, Protocol, Tuple + + +class Observable(Protocol): + def expectation_statevector(self, sv: Statevector) -> float: ... + def expectation_density_matrix(self, dm: DensityMatrix) -> float: ... + def expectation_probs( + self, measurements: List[Tuple[PauliString, Dict[str, float]]] + ) -> float: ... + def variance_statevector(self, sv: Statevector) -> float: ... + @property + def num_qubits(self) -> int: ... +``` + +Implementing this protocol requires the following members: + +- `expectation_statevector(sv) -> float`: compute the expectation value $\langle \psi | O | \psi \rangle$ from a statevector. +- `expectation_density_matrix(dm) -> float`: compute the expectation value $\mathrm{Tr}(\rho O)$ from a density matrix. +- `expectation_probs(measurements) -> float`: compute the expectation value from measurement probabilities, where `measurements` is several `(PauliString, dict)` tuples. +- `variance_statevector(sv) -> float`: compute the variance from a statevector. +- `num_qubits -> int`: the number of qubits the observable acts on, used for the dimension check before computation. + +Both `Hamiltonian` and `PauliString` have all of the members above and can be passed directly as an observable to interfaces accepting `Observable`. + +--- + +## Hamiltonian + +### Hamiltonian(num_qubits) + +Parameters: + +- `num_qubits` (`int`): the number of qubits the operator acts on. The result is the zero operator on that number of qubits. + +### Hamiltonian.from_pauli(pauli) + +Construct from a single Pauli string, with a coefficient of `1.0`. + +Parameters: + +- `pauli` (`PauliString`): the Pauli string serving as the only term. + +Returns: + +- `Hamiltonian`: the operator representing $H = 1.0 \cdot P$. + +### Hamiltonian.from_list(terms) + +Construct from a list of `(PauliString, coefficient)` tuples. + +Parameters: + +- `terms` (`list[tuple[PauliString, float | int | complex | tuple[float, float]]]`): the list of terms. A coefficient can be a `float`, an `int`, a `complex`, or a `(real part, imaginary part)` pair. + +Returns: + +- `Hamiltonian`: the new operator. + +Raises: + +- `ValueError`: the Pauli strings of the terms have inconsistent qubit counts; or an element of the list is not a pair. + +### Attributes + +- `num_qubits -> int`: the number of qubits. +- `num_terms -> int`: the current number of terms, that is, the length of the `terms` list before `simplify()`. +- `terms -> list[tuple[PauliString, complex]]`: the list of terms, with coefficients given as Python complex numbers. + +### Methods + +- `add_term(op, coeff) -> None`: append a Pauli term and its coefficient. Coefficient forms are the same as in `from_list`. +- `simplify() -> None`: simplify. The internal phase of the Pauli strings is first absorbed into the complex coefficients, then terms with the same Pauli string are merged and terms with a coefficient close to zero are removed. +- `scale(factor) -> None`: scale all terms by a complex coefficient. Coefficient forms are the same as in `from_list`. +- `all_terms_commute() -> bool`: determine whether all Pauli terms commute pairwise. +- `to_matrix() -> numpy.ndarray`: return the dense matrix $H = \sum_k c_k P_k$, expanded in little-endian bit order and including the Pauli phases. The shape is $(2^N, 2^N)$ and the type is `complex128`; an empty operator gives a zero matrix. The space complexity is $O(4^N)$, which suits analysis and validation of small-scale systems. +- `to_trotter_circuit(time, steps, mode) -> Circuit`: generate the time evolution circuit given by the Trotter-Suzuki decomposition, approximating $U(t) = e^{-iHt}$ by a sequence of Pauli rotations. +- `to_evolution_circuit(time, steps, mode) -> Circuit`: generate the time evolution circuit. When all terms commute, an exact single-pass decomposition is used; when non-commuting terms exist, it falls back to the specified Trotter mode and number of steps. +- `copy() -> Hamiltonian`: return a copy. +- `expectation_statevector(sv) -> float`: compute the expectation value from a statevector. +- `expectation_density_matrix(dm) -> float`: compute the expectation value from a density matrix. +- `expectation_probs(measurements) -> float`: compute the expectation value from measurement probabilities. +- `variance_statevector(sv) -> float`: compute the variance from a statevector. + +### Other behavior + +- `+` and `+=` concatenate the term lists of two operators lazily and do not merge like terms automatically; call `simplify()` when merging is needed. +- Supports `==`. +- Does not define `hash` and is unhashable. +- `str` gives a readable form of the operator; `repr` has the form `Hamiltonian(num_qubits=2, num_terms=2)`. + +### Raises + +- `ValueError`: the two operators have inconsistent qubit counts (`+`, `+=`), the qubit count of a term is inconsistent with the operator (`from_list`, `add_term`), the coefficient type is not supported, the observable and the quantum state have inconsistent qubit counts (`expectation_*`, `variance_statevector`), or no usable measurement basis is found in the measurement probabilities (`expectation_probs`). +- `ValueError`: `to_trotter_circuit` and `to_evolution_circuit` raise when `steps` is `0`, the operator is empty, a coefficient is not Hermitian, or a Pauli string phase is not Hermitian. + +### Example + +```python +import math + +from cqlib.qis import Hamiltonian, PauliString, Statevector + +# 从空算符逐项构造 +h = Hamiltonian(2) +h.add_term(PauliString.from_str("ZZ"), 0.5) +h.add_term(PauliString.from_str("XX"), 0.5) +h.simplify() + +# 在 Bell 态上求期望值 +sv = Statevector(2) +sv.apply_h(0) +sv.apply_cx(0, 1) +assert math.isclose(h.expectation_statevector(sv), 1.0) +``` + +```python +import math + +from cqlib.qis import Hamiltonian, PauliString + +# 由项列表构造,系数支持 float、complex 与 (实部, 虚部) 二元组 +terms = [ + (PauliString.from_str("XX"), 0.5), + (PauliString.from_str("YY"), -0.3 + 0.0j), + (PauliString.from_str("ZZ"), (0.0, 0.2)), +] +h = Hamiltonian.from_list(terms) +assert h.num_qubits == 2 +assert h.num_terms == 3 +``` + +```python +import math + +from cqlib.qis import Hamiltonian, PauliString + +# 化简:合并同类项、删除近零项、把内部相位并入系数 +h = Hamiltonian(2) +h.add_term(PauliString.from_str("XX"), 0.5) +h.add_term(PauliString.from_str("XX"), 0.3) +h.add_term(PauliString.from_str("ZZ"), 1.0) +h.add_term(PauliString.from_str("ZZ"), -1.0) +h.add_term(PauliString.from_str("+iYY"), 0.5) + +assert h.num_terms == 5 +h.simplify() +assert h.num_terms == 2 + +xx_term = next(t for t in h.terms if str(t[0]) == "+XX") +assert math.isclose(xx_term[1].real, 0.8) +``` + +```python +from cqlib.qis import Hamiltonian, PauliString, TrotterMode + +# 时间演化线路 +h = Hamiltonian(2) +h.add_term(PauliString.from_str("ZZ"), 0.5) +h.add_term(PauliString.from_str("XX"), 0.3) + +circuit = h.to_trotter_circuit(1.0, 10, TrotterMode.first_order()) +assert circuit.num_qubits == 2 +assert len(circuit) > 0 +``` + +```python +from cqlib.qis import Hamiltonian, PauliString, TrotterMode + +# 所有项对易时 to_evolution_circuit 走精确分解 +h_commuting = Hamiltonian(2) +h_commuting.add_term(PauliString.from_str("ZZ"), 0.5) +h_commuting.add_term(PauliString.from_str("IZ"), 0.3) +assert h_commuting.all_terms_commute() is True + +exact = h_commuting.to_evolution_circuit(1.0, 1, TrotterMode.first_order()) +assert exact.num_qubits == 2 + +# 存在非对易项时回退到 Trotter +h_non_commuting = Hamiltonian(1) +h_non_commuting.add_term(PauliString.from_str("X"), 1.0) +h_non_commuting.add_term(PauliString.from_str("Z"), 1.0) +assert h_non_commuting.all_terms_commute() is False + +trotterized = h_non_commuting.to_evolution_circuit(0.5, 4, TrotterMode.second_order()) +assert trotterized.num_qubits == 1 +``` + +--- + +## Validation and error handling + +| Exception | When it occurs | +| --- | --- | +| `ValueError` | Inconsistent qubit counts: between the terms of `from_list`, between a term and the operator in `add_term`, between the two operators of `+` and `+=`, or between the observable and the quantum state. | +| `ValueError` | The coefficient is not a `float`, an `int`, a `complex`, or a `(real part, imaginary part)` pair. | +| `ValueError` | `to_trotter_circuit` and `to_evolution_circuit`: `steps` is `0`, the operator is empty, or a coefficient or a Pauli string phase is not Hermitian. | +| `ValueError` | `expectation_probs` cannot derive the target operator from the given measurement bases. | + +The input types of observable computation are given in [Statevector](1_statevector.md) and [DensityMatrix](2_density_matrix.md); the definition of Trotter modes is given in [Trotter evolution](8_evolution.md). diff --git a/docs/api/en/python/3_qis/8_evolution.md b/docs/api/en/python/3_qis/8_evolution.md new file mode 100644 index 00000000..e5979593 --- /dev/null +++ b/docs/api/en/python/3_qis/8_evolution.md @@ -0,0 +1,148 @@ +# Trotter evolution + +`cqlib.qis` + +`TrotterMode` describes how the time evolution operator $U(t) = e^{-iHt}$ is approximated by a sequence of Pauli rotations, and is the decomposition mode that must be specified when converting a `Hamiltonian` into an evolution circuit. This page covers `TrotterMode` itself, together with the two evolution entry points that consume it. + +## Import + +```python +from cqlib.qis import Hamiltonian, PauliString, TrotterMode +``` + +--- + +## TrotterMode + +A Trotter-Suzuki decomposition mode. There is no public constructor; the three modes are created by static methods. Equality is determined by mode and random seed. + +### Static methods + +- `TrotterMode.first_order()`: first-order Lie-Trotter decomposition. For $H = \sum_k c_k P_k$, a single step applies the terms one by one in order + $U(t) \approx \left[ \prod_k e^{-i c_k (t/n) P_k} \right]^n$, + with an error of order $O(t^2/n)$. +- `TrotterMode.second_order()`: second-order Strang symmetric decomposition. A single step applies the terms once in order and once in reverse order + $U(t) \approx \left[ \prod_k e^{-i c_k (t/2n) P_k} \prod_k e^{-i c_k (t/2n) P_k} \right]^n$, + with an error of order $O(t^3/n^2)$. +- `TrotterMode.randomized(seed)`: randomized first-order decomposition. The order of the Pauli terms is shuffled randomly within each Trotter step, which weakens systematic errors. + +Parameters: + +- `seed` (`int`): the random seed, an unsigned integer; the same seed gives the same result. + +Returns: + +- `TrotterMode`: the corresponding decomposition mode object. + +### Other behavior + +- Supports `==` and `hash`. Two `TrotterMode.randomized(s)` are equal only when the seeds are the same; `first_order()` and `second_order()` are each equal to their own new instances. +- `str` is `first-order`, `second-order` or `randomized (seed=N)`. +- `repr` is `TrotterMode.FirstOrder`, `TrotterMode.SecondOrder` or `TrotterMode.Randomized(seed=N)`. + +--- + +## Evolution entry points + +`TrotterMode` is consumed by the two evolution methods of `Hamiltonian`. Both return an abstract circuit and their parameters have the same meaning. + +### Hamiltonian.to_trotter_circuit(time, steps, mode) -> Circuit + +Generate a Trotterized time evolution circuit with the given mode and number of steps. + +Parameters: + +- `time` (`float`): the total evolution time $t$. +- `steps` (`int`): the number of Trotter steps $n$, which must be greater than `0`. +- `mode` (`TrotterMode`): the decomposition mode. + +Returns: + +- `Circuit`: a circuit approximating $U(t) = e^{-iHt}$. + +### Hamiltonian.to_evolution_circuit(time, steps, mode) -> Circuit + +Generate a time evolution circuit. When all terms commute, an exact single-pass decomposition $\prod_k e^{-i c_k t P_k}$ is used; when non-commuting terms exist, a Trotter approximation is made with the specified mode and number of steps. `steps` and `mode` take effect only in the non-commuting case, but `steps` must still be greater than `0`. + +Parameters: + +- `time` (`float`): the total evolution time $t$. +- `steps` (`int`): the number of Trotter steps, which must be greater than `0`. It takes part in the decomposition only when commutation does not hold. +- `mode` (`TrotterMode`): the decomposition mode. It takes part in the decomposition only when commutation does not hold. + +Returns: + +- `Circuit`: the exact or approximate evolution circuit. + +### Raises + +Both entry points raise `ValueError` in the following cases: + +- `steps` is `0`. +- The `Hamiltonian` is empty. +- A coefficient is not Hermitian (the imaginary part is outside the tolerance). +- A Pauli string phase is not Hermitian, that is, the phase is $\pm i$ rather than $\pm 1$. + +### Example + +```python +from cqlib.qis import TrotterMode + +# 三种模式 +mode = TrotterMode.first_order() +assert str(mode) == "first-order" +assert "FirstOrder" in repr(mode) + +assert str(TrotterMode.second_order()) == "second-order" + +mode_random = TrotterMode.randomized(42) +assert "seed=42" in repr(mode_random) +assert mode_random != TrotterMode.randomized(123) +assert mode_random == TrotterMode.randomized(42) +``` + +```python +from cqlib.qis import Hamiltonian, PauliString, TrotterMode + +h = Hamiltonian(2) +h.add_term(PauliString.from_str("ZZ"), 0.5) +h.add_term(PauliString.from_str("XX"), 0.3) + +first = h.to_trotter_circuit(1.0, 10, TrotterMode.first_order()) +second = h.to_trotter_circuit(1.0, 10, TrotterMode.second_order()) +randomized = h.to_trotter_circuit(1.0, 10, TrotterMode.randomized(42)) + +assert first.num_qubits == 2 +assert second.num_qubits == 2 +assert randomized.num_qubits == 2 +``` + +```python +from cqlib.qis import Hamiltonian, PauliString, TrotterMode + +# 对易项走精确分解,步数与模式不参与分解 +h_commuting = Hamiltonian(2) +h_commuting.add_term(PauliString.from_str("ZZ"), 0.5) +h_commuting.add_term(PauliString.from_str("IZ"), 0.3) + +exact = h_commuting.to_evolution_circuit(1.0, 1, TrotterMode.first_order()) +assert exact.num_qubits == 2 + +# 非对易项回退到 Trotter +h_non_commuting = Hamiltonian(1) +h_non_commuting.add_term(PauliString.from_str("X"), 1.0) +h_non_commuting.add_term(PauliString.from_str("Z"), 1.0) + +trotterized = h_non_commuting.to_evolution_circuit(0.5, 4, TrotterMode.second_order()) +assert trotterized.num_qubits == 1 +``` + +--- + +## Validation and error handling + +| Exception | When it occurs | +| --- | --- | +| `ValueError` | `steps` of an evolution entry point is `0`, the `Hamiltonian` is empty, or a coefficient or Pauli string phase is not Hermitian. | + +When a decomposition mode must be specified in the construction of a parameterized evolution circuit, use the circuit-layer evolution strategy interface, see [Ansatz](../0_circuit/11_ansatz.md). The complete interface of `Hamiltonian` is given in [Hamiltonian observable](7_hamiltonian.md). diff --git a/docs/api/en/python/3_qis/9_metrics_entropy.md b/docs/api/en/python/3_qis/9_metrics_entropy.md new file mode 100644 index 00000000..d7cf7246 --- /dev/null +++ b/docs/api/en/python/3_qis/9_metrics_entropy.md @@ -0,0 +1,536 @@ +# Metrics and Entropy + +`cqlib.qis.metrics` + +`cqlib.qis.entropy` + +`cqlib.qis.metrics` provides measures of quantum states: purity, fidelity, trace distance, von Neumann entropy, and partial transpose and logarithmic negativity. `cqlib.qis.entropy` provides entropy and entanglement measures: linear entropy, Rényi entropy, entanglement entropy, negativity, concurrence and entanglement of formation. Both submodules consist of functions, whose inputs are a `Statevector` or a `DensityMatrix`. + +## Import + +```python +from cqlib.qis import entropy, metrics +``` + +--- + +## `cqlib.qis.metrics` + +### `purity_pure(sv) -> float` + +Compute the purity of a pure state. + +Parameters: + +- `sv` (`Statevector`): the statevector. + +Returns: + +- `float`: the purity. A normalized pure state gives `1.0`. + +Raises: + +- None. + +Example: + +```python +import math + +from cqlib.qis import Statevector, metrics + +sv = Statevector(2) +assert math.isclose(metrics.purity_pure(sv), 1.0, abs_tol=1e-10) +``` + +### `purity_mixed(dm) -> float` + +Compute the purity of a mixed state. + +Parameters: + +- `dm` (`DensityMatrix`): the density matrix. + +Returns: + +- `float`: the purity $\mathrm{Tr}(\rho^2)$, with a range of $[1/2^N, 1.0]$; the maximally mixed state reaches the lower bound. + +Raises: + +- None. + +Example: + +```python +import math + +from cqlib.qis import DensityMatrix, metrics + +dm_pure = DensityMatrix(2) +assert math.isclose(metrics.purity_mixed(dm_pure), 1.0, abs_tol=1e-10) +``` + +### `state_fidelity_pure(sv1, sv2) -> float` + +Compute the fidelity between two pure states. + +Parameters: + +- `sv1` (`Statevector`): the first statevector. +- `sv2` (`Statevector`): the second statevector. + +Returns: + +- `float`: the fidelity $F(\psi, \phi) = |\langle \psi | \phi \rangle|^2$, with a range of `[0.0, 1.0]`. + +Raises: + +- `ValueError`: the two statevectors have inconsistent qubit counts. + +Example: + +```python +import math + +from cqlib.qis import Statevector, metrics + +sv1 = Statevector(1) # |0> +sv2 = Statevector(1) +sv2.apply_x(0) # |1> + +assert math.isclose(metrics.state_fidelity_pure(sv1, sv1), 1.0, abs_tol=1e-10) +assert math.isclose(metrics.state_fidelity_pure(sv1, sv2), 0.0, abs_tol=1e-10) +``` + +### `trace_distance_pure(sv1, sv2) -> float` + +Compute the trace distance between two pure states. + +Parameters: + +- `sv1` (`Statevector`): the first statevector. +- `sv2` (`Statevector`): the second statevector. + +Returns: + +- `float`: the trace distance $D(\psi, \phi) = \sqrt{1 - |\langle \psi | \phi \rangle|^2}$, with a range of `[0.0, 1.0]`. + +Raises: + +- `ValueError`: the two statevectors have inconsistent qubit counts. + +Example: + +```python +import math + +from cqlib.qis import Statevector, metrics + +sv1 = Statevector(1) # |0> +sv2 = Statevector(1) +sv2.apply_x(0) # |1> + +assert math.isclose(metrics.trace_distance_pure(sv1, sv1), 0.0, abs_tol=1e-10) +assert math.isclose(metrics.trace_distance_pure(sv1, sv2), 1.0, abs_tol=1e-10) +``` + +### `state_fidelity_pure_mixed(sv, dm) -> float` + +Compute the fidelity between a pure state and a mixed state. + +Parameters: + +- `sv` (`Statevector`): the pure state. +- `dm` (`DensityMatrix`): the mixed state. + +Returns: + +- `float`: the fidelity $F(\psi, \rho) = \langle \psi | \rho | \psi \rangle$, with a range of `[0.0, 1.0]`. + +Raises: + +- `ValueError`: the statevector and the density matrix have inconsistent qubit counts. + +Example: + +```python +import math + +import numpy as np + +from cqlib.qis import DensityMatrix, Statevector, metrics + +sv = Statevector(1) # |0> +dm = DensityMatrix(1) # |0><0| +assert math.isclose(metrics.state_fidelity_pure_mixed(sv, dm), 1.0, abs_tol=1e-10) + +# |0><0| 与 I/2 的保真度为 0.5 +mixed_data = np.array([0.5, 0, 0, 0.5], dtype=complex) +dm_mixed = DensityMatrix.from_density_matrix(1, mixed_data) +assert math.isclose(metrics.state_fidelity_pure_mixed(sv, dm_mixed), 0.5, abs_tol=1e-10) +``` + +### `entropy(dm) -> float` + +Compute the von Neumann entropy of a mixed state. + +Parameters: + +- `dm` (`DensityMatrix`): the density matrix. + +Returns: + +- `float`: the entropy, in bits (logarithm base 2). + +Raises: + +- `ValueError`: eigendecomposition failure. + +Example: + +```python +import math + +import numpy as np + +from cqlib.qis import DensityMatrix, metrics + +dm = DensityMatrix(2) +assert math.isclose(metrics.entropy(dm), 0.0, abs_tol=1e-10) + +# 最大混合单比特态 I/2 的熵为 1 比特 +mixed_data = np.array([0.5, 0, 0, 0.5], dtype=complex) +dm_mixed = DensityMatrix.from_density_matrix(1, mixed_data) +assert math.isclose(metrics.entropy(dm_mixed), 1.0, abs_tol=1e-10) +``` + +### `trace_distance_mixed(dm1, dm2) -> float` + +Compute the trace distance between two mixed states. + +Parameters: + +- `dm1` (`DensityMatrix`): the first density matrix. +- `dm2` (`DensityMatrix`): the second density matrix. + +Returns: + +- `float`: the trace distance $D(\rho, \sigma) = \frac{1}{2}\mathrm{Tr}|\rho - \sigma|$. + +Raises: + +- `ValueError`: the two density matrices have inconsistent qubit counts, or eigendecomposition failure. + +Example: + +```python +import math + +from cqlib.qis import DensityMatrix, metrics + +dm1 = DensityMatrix(1) +dm2 = DensityMatrix(1) +dm2.apply_x(0) + +assert math.isclose(metrics.trace_distance_mixed(dm1, dm1), 0.0, abs_tol=1e-10) +assert math.isclose(metrics.trace_distance_mixed(dm1, dm2), 1.0, abs_tol=1e-10) +``` + +### `state_fidelity_mixed(dm1, dm2) -> float` + +Compute the fidelity between two mixed states. + +Parameters: + +- `dm1` (`DensityMatrix`): the first density matrix. +- `dm2` (`DensityMatrix`): the second density matrix. + +Returns: + +- `float`: the fidelity $F(\rho, \sigma) = \left( \mathrm{Tr}\sqrt{\sqrt{\rho}\,\sigma\sqrt{\rho}} \right)^2$. + +Raises: + +- `ValueError`: the two density matrices have inconsistent qubit counts, or eigendecomposition failure. + +Example: + +```python +import math + +from cqlib.qis import DensityMatrix, metrics + +dm1 = DensityMatrix(1) +dm2 = DensityMatrix(1) +dm2.apply_x(0) + +assert math.isclose(metrics.state_fidelity_mixed(dm1, dm1), 1.0, abs_tol=1e-10) +assert math.isclose(metrics.state_fidelity_mixed(dm1, dm2), 0.0, abs_tol=1e-10) +``` + +### `partial_transpose(dm, target_qubits) -> DensityMatrix` + +Take the partial transpose of the given subsystem of a density matrix. + +Parameters: + +- `dm` (`DensityMatrix`): the input density matrix. +- `target_qubits` (`list[int]`): the qubit indices of the subsystem to transpose. + +Returns: + +- `DensityMatrix`: the density matrix after the partial transpose. The transpose preserves the trace but may no longer be positive semidefinite. + +Raises: + +- `IndexError`: a qubit index in `target_qubits` is out of range. + +Example: + +```python +import math + +from cqlib.qis import DensityMatrix, metrics + +# Bell 态 |Φ+> = (|00> + |11>) / √2 +dm = DensityMatrix(2) +dm.apply_h(0) +dm.apply_cx(0, 1) + +pt_dm = metrics.partial_transpose(dm, [0]) +data = pt_dm.data +assert math.isclose(data[0, 0].real, 0.5, abs_tol=1e-10) +assert math.isclose(data[3, 3].real, 0.5, abs_tol=1e-10) +``` + +### `logarithmic_negativity(dm, sys_a) -> float` + +Compute the logarithmic negativity of a bipartite quantum state. + +Parameters: + +- `dm` (`DensityMatrix`): the density matrix. +- `sys_a` (`list[int]`): the qubit indices of subsystem A. + +Returns: + +- `float`: the logarithmic negativity $\log_2 \|\rho^{T_A}\|_1$, which is not less than `0`. + +Raises: + +- `IndexError`: a qubit index in `sys_a` is out of range. +- `ValueError`: eigendecomposition failure. + +Example: + +```python +import math + +from cqlib.qis import DensityMatrix, metrics + +# Bell 态的对数负度为 1.0 +dm = DensityMatrix(2) +dm.apply_h(0) +dm.apply_cx(0, 1) +assert math.isclose(metrics.logarithmic_negativity(dm, [0]), 1.0, abs_tol=1e-10) + +# 可分态 |00> 的对数负度为 0.0 +dm_sep = DensityMatrix(2) +assert math.isclose(metrics.logarithmic_negativity(dm_sep, [0]), 0.0, abs_tol=1e-10) +``` + +--- + +## `cqlib.qis.entropy` + +### `linear_entropy(dm) -> float` + +Compute the linear entropy. The linear entropy is a computationally cheaper approximation of the von Neumann entropy, used to characterize the degree of mixing. + +Parameters: + +- `dm` (`DensityMatrix`): the density matrix. + +Returns: + +- `float`: the linear entropy $1 - \mathrm{Tr}(\rho^2)$, with a range of `[0, 1)`. + +Raises: + +- None. + +Example: + +```python +from cqlib.qis import DensityMatrix, entropy + +dm_pure = DensityMatrix(2) +assert abs(entropy.linear_entropy(dm_pure) - 0.0) < 1e-10 +``` + +### `renyi_entropy(dm, alpha) -> float` + +Compute the Rényi entropy of order `alpha`. + +Parameters: + +- `dm` (`DensityMatrix`): the density matrix. +- `alpha` (`float`): the order, which must be positive. A value of `1.0` falls back to the von Neumann entropy. + +Returns: + +- `float`: the Rényi entropy, in bits (logarithm base 2). + +Raises: + +- `ValueError`: `alpha` is not greater than zero, or eigendecomposition failure. + +Example: + +```python +from cqlib.qis import DensityMatrix, entropy + +dm = DensityMatrix(2) # 纯态 +assert abs(entropy.renyi_entropy(dm, 2.0) - 0.0) < 1e-10 +assert abs(entropy.renyi_entropy(dm, 1.0) - 0.0) < 1e-10 # 回退到冯·诺依曼熵 +``` + +### `entanglement_entropy_pure(sv, subsys_a) -> float` + +Compute the entanglement entropy of a bipartite pure state, that is, the von Neumann entropy of the reduced density matrix of subsystem A. + +Parameters: + +- `sv` (`Statevector`): the pure state. +- `subsys_a` (`list[int]`): the qubit indices of subsystem A. The remaining qubits form subsystem B. + +Returns: + +- `float`: the entanglement entropy, in bits. + +Raises: + +- `ValueError`: `subsys_a` is empty, contains all qubits, or contains duplicate indices. +- `IndexError`: a qubit index in `subsys_a` is out of range. + +Example: + +```python +from cqlib.qis import Statevector, entropy + +# Bell 态 |Φ+> = (|00> + |11>) / √2 +sv = Statevector(2) +sv.apply_h(0) +sv.apply_cx(0, 1) +assert abs(entropy.entanglement_entropy_pure(sv, [0]) - 1.0) < 1e-10 + +# 直积态 |00> 的纠缠熵为 0 +sv_prod = Statevector(2) +assert abs(entropy.entanglement_entropy_pure(sv_prod, [0]) - 0.0) < 1e-10 +``` + +### `negativity(dm, subsys_a) -> float` + +Compute the negativity, based on the positive partial transpose criterion. + +Parameters: + +- `dm` (`DensityMatrix`): the density matrix of a bipartite state. +- `subsys_a` (`list[int]`): the qubit indices of subsystem A. + +Returns: + +- `float`: the negativity, that is, the sum of the absolute values of the negative eigenvalues after the partial transpose, which is not less than `0`. + +Raises: + +- `IndexError`: a qubit index in `subsys_a` is out of range. +- `ValueError`: eigendecomposition failure. + +Example: + +```python +from cqlib.qis import DensityMatrix, entropy + +dm = DensityMatrix(2) +dm.apply_h(0) +dm.apply_cx(0, 1) + +# Bell 态的负度为 0.5 +assert abs(entropy.negativity(dm, [0]) - 0.5) < 1e-10 +``` + +### `concurrence(dm) -> float` + +Compute the concurrence of a two-qubit quantum state. + +Parameters: + +- `dm` (`DensityMatrix`): the density matrix of a two-qubit state. + +Returns: + +- `float`: the concurrence, with a range of `[0, 1]`. A separable state gives `0` and a maximally entangled state gives `1`. + +Raises: + +- `ValueError`: the state does not have 2 qubits, or the computation fails. + +Example: + +```python +from cqlib.qis import DensityMatrix, entropy + +dm = DensityMatrix(2) +dm.apply_h(0) +dm.apply_cx(0, 1) + +assert abs(entropy.concurrence(dm) - 1.0) < 1e-10 + +dm_sep = DensityMatrix(2) +assert abs(entropy.concurrence(dm_sep) - 0.0) < 1e-10 +``` + +### `entanglement_of_formation(dm) -> float` + +Compute the entanglement of formation of a two-qubit state. + +Parameters: + +- `dm` (`DensityMatrix`): the density matrix of a two-qubit state. + +Returns: + +- `float`: the entanglement of formation, in bits. + +Raises: + +- `ValueError`: the state does not have 2 qubits. + +Example: + +```python +from cqlib.qis import DensityMatrix, entropy + +dm = DensityMatrix(2) +dm.apply_h(0) +dm.apply_cx(0, 1) + +assert abs(entropy.entanglement_of_formation(dm) - 1.0) < 1e-10 + +dm_sep = DensityMatrix(2) +assert abs(entropy.entanglement_of_formation(dm_sep) - 0.0) < 1e-10 +``` + +--- + +## Validation and error handling + +| Exception | When it occurs | +| --- | --- | +| `ValueError` | The two quantum states being compared have inconsistent qubit counts; `alpha` of `renyi_entropy` is not greater than zero; the subsystem of `entanglement_entropy_pure` is empty, contains all qubits or contains duplicate indices; the state of `concurrence` or `entanglement_of_formation` is not a two-qubit state; eigendecomposition failure. | +| `IndexError` | Qubit index out of range in `partial_transpose`, `logarithmic_negativity`, `negativity` or `entanglement_entropy_pure`. | + +Note that `cqlib.qis.entropy` is a submodule name, while `cqlib.qis.metrics` contains a function of the same name `entropy`; the two are unrelated. + +The input types of the functions on this page are given in [Statevector](1_statevector.md) and [DensityMatrix](2_density_matrix.md). diff --git a/docs/api/en/python/4_compile/0_overview.md b/docs/api/en/python/4_compile/0_overview.md new file mode 100644 index 00000000..0987725b --- /dev/null +++ b/docs/api/en/python/4_compile/0_overview.md @@ -0,0 +1,230 @@ +# Compilation + +`cqlib.compile` + +`cqlib.compile` is the compilation pipeline module of Cqlib. Through a single end-to-end `compile()` entry point it covers the complete flow from a logical circuit to a circuit that satisfies the target constraints (a physical circuit under a device target), and it also splits each compilation stage into independently callable transform submodules for callers that need fine-grained control over the compilation process. + +## Overview + +Compilation is the process of rewriting a given input circuit to match a specific quantum device topology and to optimize it for the execution goal. Most circuits must go through a series of transforms before they are compatible with the target device and before the impact of noise on the results is reduced. The recommended entry point of Cqlib is the `compile()` function: a single call performs canonicalization, knowledge rule optimization, gate decomposition, optional layout and routing, target gate set translation and final validation. + +### Compilation pipeline + +`compile()` orchestrates the following stages in a fixed order (the stage names correspond to the `stage` field of each record in `CompileResult.steps`): + +1. **Target resolution and resource validation** (`pre_init`) — resolve the target constraint (`resolve.target`) and validate the ancilla resource policy (`validate.resources`). +2. **Input canonicalization** (`init`) — perform production-grade canonicalization on the input circuit (`canonicalize.input`). +3. **Definition expansion and pre-decomposition optimization** (`optimization`) — first expand user-defined gate definitions so that knowledge rules can see the operations inside composite gates; then perform pre-decomposition knowledge rule rewriting (`optimize.pre_decomposition`). +4. **Gate decomposition** (`optimization`) — matrix unitary gate synthesis, multi-controlled gate decomposition (requesting ancillas according to the resource policy), post-decomposition canonicalization, commutative cancellation (`optimize.commutative_cancellation`), two-qubit block resynthesis and single-qubit optimization. +5. **Post-decomposition optimization** (`optimization`) — knowledge rule rewriting iterated to a fixed point (`optimize.post_decomposition`). +6. **Routing gate set lowering** (`translation`) — lower gates unsupported by the SABRE gate-arity constraint (such as `CCX`) to operations acting on at most two qubits. +7. **Layout and routing** (`routing`) — under a device target, select the initial layout, insert SWAPs to satisfy the topology adjacency constraint (`route.sabre`), and perform cleanup and resynthesis after routing. +8. **Target gate set translation** (`translation`) — lower the circuit to the target gate set; a device target further lowers it to the native instruction set. +9. **Native optimization and final validation** (`optimization` / `validation`) — run fixed-point optimization on native instructions (`optimize.native_fixed_point`); a device target finally performs device validation (`validate.device`). + +Without a device target, the layout, routing and device validation steps are skipped, and the reason for the skip is recorded in `steps`. Every stage can be called separately, skipping `compile()`; see [Transform](2_transform.md) and the following pages. + +### Choosing the optimization strength + +`CompileMode.normal()` is conservative logical optimization using production default pass parameters; `CompileMode.enhanced()` uses a stronger staged workflow, a larger pass budget, and performs target-aware cleanup when a target constraint is present. For most use cases `normal` is sufficient; use `enhanced` when the compilation time budget is generous and a better post-routing cleanup result is desired. + +### Choosing a target constraint + +- `CompileTarget.logical()`: logical optimization only; the output is not bound to any hardware. Suitable for algorithm-level circuit simplification. +- `CompileTarget.basis([...])`: lower to an explicit standard gate basis; device topology is not involved. +- `CompileTarget.device(device)`: routing and lowering for a concrete device; the output must match the device's native capabilities, and final device validation is performed. +- `CompileTarget.topology_basis(device, [...])`: route on the device topology while lowering to an explicit gate basis; the device only provides capacity, layout and routing constraints and does not require the gate basis to match the device's native capabilities. + +### Reproducibility + +Layout and routing in compilation involve randomized heuristics. Passing the `seed` parameter (supported by `compile()`, `CompileTarget.device()` and `CompileTarget.topology_basis()`) yields deterministic results: the same circuit, the same configuration and the same seed produce the same cqlib result. Outputs are not guaranteed to be consistent across different cqlib versions; the seed only guarantees reproducibility within the same version. + +--- + +## Common entry points + +```python +from cqlib import Circuit +from cqlib.compile import CompileMode, compile +from cqlib.device import Device + +circuit = Circuit(3) +circuit.h(0) +circuit.cx(0, 2) + +device = Device.line("line-3", 3) + +result = compile( + circuit, + mode=CompileMode.enhanced(), + device=device, + target_basis=["H", "CX", "RZ"], + seed=42, +) + +print("changed:", result.changed) +for step in result.steps: + if step.changed and not step.skipped: + print(step.stage, step.name) +``` + +--- + +## Core concepts and terms + +| Term | Description | +| --- | --- | +| **Compilation pipeline** | The flow that progressively converts an abstract circuit into a circuit satisfying the target constraints, including stages such as logical optimization, decomposition, layout, routing and target gate set translation. | +| **Pass** | An independent transform step in the compilation pipeline, for example canonicalization, knowledge rule rewriting or routing. Most passes can either be orchestrated automatically by `compile()` or be called separately. | +| **Transform** | A reusable transform object, for example `Canonicalizer` or `KnowledgeRewriter`. Calling `run()` yields a result object; the input circuit stays unchanged. | +| **Target constraint** | The conditions the compilation output must satisfy, including logical space (no constraint), a standard gate basis (`CompileTarget.basis`), a physical device (`CompileTarget.device`), and device topology plus gate basis (`CompileTarget.topology_basis`). | +| **Initial layout** | The mapping from logical qubits to physical qubits before compilation starts. It can be provided by the caller or selected automatically by a layout algorithm. | +| **Routing** | The process of making all two-body interactions satisfy the adjacency constraint on a restricted topology by inserting movement operations such as SWAPs. | +| **Gate decomposition** | Expanding composite gates, custom unitary gates or multi-controlled gates into more elementary gate sequences. | +| **Knowledge rule** | A rule describing an adjacent operation pattern and its replacement form, used for pattern simplification, cancellation and hardware native gate matching. | +| **Ancilla** | An auxiliary qubit requested on demand during decomposition. A clean ancilla is required to be in `\|0>` when entering and leaving the transform, and a dirty ancilla is required to have its original state fully restored. | + +--- + +## `cqlib.compile` API Overview + +### End-to-end compilation entry point + +| Name | Description | +| --- | --- | +| [`compile`](1_compiler.md) | The recommended entry point; a single call performs configuration, execution and result reporting. | +| [`CompileConfig`](1_compiler.md) | Compilation configuration snapshot, containing the mode, the target constraint and the resource policy. | +| [`CompileMode`](1_compiler.md) | Optimization strength enumeration, including `normal` and `enhanced`. | +| [`CompileTarget`](1_compiler.md) | Target constraint enumeration, including logical, basis, device, and topology plus basis. | +| [`DeviceCompileTarget`](1_compiler.md) | Device target input, containing the device, an optional initial layout and a random seed. | +| [`CompileResult`](1_compiler.md) | Compilation result, containing the optimized circuit, the step-by-step report and the device layout metadata. | +| [`WorkflowStepReport`](1_compiler.md) | The execution record of a single workflow step. | +| [`DeviceCompilationMetadata`](1_compiler.md) | The initial and final layouts produced by device compilation. | +| [`CompilerWorkflow`](1_compiler.md) | Reusable workflow object that repeatedly runs compilation according to the configuration. | + +### Basic transform passes + +| Name | Description | +| --- | --- | +| [`TransformResult`](2_transform.md) | The generic transform result, containing the output circuit and the changed flag. | +| [`CircuitAnalysis`](2_transform.md) | Circuit feature snapshot, for example whether it contains measurements, control flow or composite gates. | +| [`Canonicalizer`](2_transform.md) / [`canonicalize_circuit`](2_transform.md) | Canonicalization pass that unifies instruction forms, removes no-ops and folds the global phase. | +| [`KnowledgeRewriter`](2_transform.md) / [`rewrite_circuit`](2_transform.md) | Pattern rewriting pass based on knowledge rules. | +| [`OptimizeOneQubitRuns`](2_transform.md) | Optimization of consecutive single-qubit gate runs. | +| [`CommutativeCancellation`](2_transform.md) | Self-inverse cancellation based on commutation. | +| [`LowerToRoutingBasis`](2_transform.md) / [`lower_to_routing_basis`](2_transform.md) | Routing gate set lowering, ensuring the routing stage only faces movable gates. | + +### Layout and routing + +| Name | Description | +| --- | --- | +| [`trivial_layout`](3_layout.md) / [`greedy_layout`](3_layout.md) / [`vf2_perfect_layout`](3_layout.md) / [`sabre_layout`](3_layout.md) | Four initial layout algorithms. | +| [`LayoutObjective`](3_layout.md) / [`LayoutScore`](3_layout.md) | Layout objective weights and scoring result. | +| [`CircuitLayoutAnalysis`](3_layout.md) / [`InteractionGraph`](3_layout.md) | Circuit interaction graph analysis, the input of the layout algorithms. | +| [`PhysicalLayoutGraph`](3_layout.md) / [`DistanceTable`](3_layout.md) | Device physical graph and shortest distance table. | +| [`route_with_layout`](4_routing.md) / [`route_sabre`](4_routing.md) | Transform-layer routing entry points. | +| [`RoutedCircuit`](4_routing.md) / [`SabreRouteResult`](4_routing.md) | Routing result objects. | +| [`SabreConfig`](5_sabre.md) / [`SabreHeuristicConfig`](5_sabre.md) / [`SabreVf2PrepassConfig`](5_sabre.md) | SABRE algorithm configuration. | +| [`sabre_route`](5_sabre.md) | SABRE routing given a known initial layout. | + +### Decomposition, resynthesis and target gate set + +| Name | Description | +| --- | --- | +| [`expand_definitions`](6_decompose_resynthesis.md) / [`decompose_unitaries`](6_decompose_resynthesis.md) / [`decompose_mc_gates`](6_decompose_resynthesis.md) | Definition expansion, unitary gate synthesis and multi-controlled gate decomposition. | +| [`UnitaryDecomposeConfig`](6_decompose_resynthesis.md) / [`McGateDecomposeConfig`](6_decompose_resynthesis.md) | Decomposition configurations. | +| [`ResynthesizeTwoQubitBlocks`](6_decompose_resynthesis.md) / [`resynthesize_two_qubit_blocks`](6_decompose_resynthesis.md) | Two-qubit block resynthesis. | +| [`TargetBasisLowerer`](6_decompose_resynthesis.md) / [`TargetBasisCostModel`](6_decompose_resynthesis.md) | Target gate set translation and cost evaluation. | +| [`DeviceLowerer`](6_decompose_resynthesis.md) | Device native instruction lowering. | + +### Knowledge rules and resource management + +| Name | Description | +| --- | --- | +| [`Rule`](7_knowledge.md) / [`RuleItem`](7_knowledge.md) / [`Condition`](7_knowledge.md) | Knowledge rule construction. | +| [`RuleLibrary`](7_knowledge.md) / [`RuleMetadata`](7_knowledge.md) / [`RuleKind`](7_knowledge.md) | Rule library validation, classification and queries. | +| [`loads`](7_knowledge.md) / [`load`](7_knowledge.md) / [`dumps`](7_knowledge.md) / [`dump`](7_knowledge.md) | Rule DSL reading and writing. | +| [`rule_matches_operations`](7_knowledge.md) / [`MatchBindings`](7_knowledge.md) | Rule structural matching. | +| [`ResourceManager`](8_resource.md) / [`ResourcePolicy`](8_resource.md) / [`ResourceLimits`](8_resource.md) | Ancilla resource planning and borrowing. | +| [`Commutation`](9_commutation.md) / [`CommutationChecker`](9_commutation.md) / [`check_commutation`](9_commutation.md) | Conservative commutation proofs. | + +--- + +## Quick examples + +### 1. End-to-end compilation + +```python +from cqlib import Circuit +from cqlib.compile import CompileMode, compile +from cqlib.device import Device + +circuit = Circuit(3) +circuit.h(0) +circuit.cx(0, 2) + +device = Device.line("line-3", 3) + +result = compile( + circuit, + mode=CompileMode.enhanced(), + device=device, + target_basis=["H", "CX", "RZ"], + seed=42, +) + +print("changed:", result.changed) +for step in result.steps: + if step.changed and not step.skipped: + print(step.stage, step.name) +``` + +### 2. Running a single transform + +```python +from cqlib import Circuit +from cqlib.compile.transform import canonicalize_circuit + +circuit = Circuit(1) +circuit.i(0) + +result = canonicalize_circuit(circuit) +assert result.circuit is not circuit +assert len(circuit.operations) == 1 +assert len(result.circuit.operations) == 0 +assert result.changed +``` + +### 3. SABRE routing + +```python +from cqlib import Circuit +from cqlib.compile.sabre import SabreConfig, sabre_route +from cqlib.device import Device, Layout + +circuit = Circuit(2) +circuit.cx(0, 1) + +result = sabre_route( + circuit, + Device.line("line-3", 3), + Layout.from_pairs([(0, 0), (1, 2)], physical_count=3), + SabreConfig(routing_trials=1, seed=7), +) +assert result.swap_count == 1 +``` + +--- + +## Validation and error handling + +The compilation process may fail at the configuration parsing, transform execution and device validation stages. Common exceptions include: + +| Exception | When it occurs | +| --- | --- | +| `CompilerConfigError` | The compilation configuration is invalid, for example `target` is provided together with `target_basis` / `device`, the mode name is unknown, or the target gate set is empty. | +| `CompilerTransformError` | A transform pass fails to execute, for example decomposition or routing cannot be completed under the given constraints. | +| `CompilerInternalError` | A compilation internal consistency check fails. | +| `CompilerError` | The common base class of the exceptions above. | + +For device-related errors see the device module documentation; for knowledge rule parsing errors see [Knowledge](7_knowledge.md); for insufficient resource errors see [Resource](8_resource.md). diff --git a/docs/api/en/python/4_compile/1_compiler.md b/docs/api/en/python/4_compile/1_compiler.md new file mode 100644 index 00000000..5a4410df --- /dev/null +++ b/docs/api/en/python/4_compile/1_compiler.md @@ -0,0 +1,302 @@ +# Compiler + +The end-to-end compilation entry points of `cqlib.compile`, including the `compile()` function, the compilation configuration object, the compilation result object and the reusable workflow object. + +## Import + +```python +from cqlib.compile import ( + CompileMode, + CompileTarget, + DeviceCompileTarget, + CompileConfig, + CompileResult, + WorkflowStepReport, + DeviceCompilationMetadata, + CompilerWorkflow, + compile, +) +``` + +--- + +## Functions + +### compile(circuit, *, mode=None, target=None, target_basis=None, device=None, initial_layout=None, resource_policy=None, seed=None) + +Run the configured compilation workflow and return the compilation result. The input circuit is not modified. + +The top-level package re-exports the same function under the name `compile_circuit`, to avoid clashing with the built-in name and the submodule name; `from cqlib import compile_circuit` and `from cqlib.compile import compile` yield the same function, and the two behave identically. + +Parameters: + +- `circuit` (`Circuit`): the circuit to compile. +- `mode` (`CompileMode | str | None`): the optimization strength. Accepts `CompileMode.normal()` / `CompileMode.enhanced()`, or the strings `"normal"` / `"enhanced"` (case-insensitive). Defaults to `CompileMode.normal()`. +- `target` (`CompileTarget | None`): an explicit target constraint. Mutually exclusive with `target_basis` and `device`; providing both raises `CompilerConfigError`. +- `target_basis` (`list[str | Instruction] | None`): a convenience form of the target standard gate basis, equivalent to `CompileTarget.basis(...)`. Entries are case-insensitive standard gate name strings or `Instruction` objects; multi-controlled gates have no string form and must be passed as an `Instruction`. +- `device` (`Device | None`): a convenience form of the target device. Provided alone it is equivalent to `CompileTarget.device(device, ...)`; provided together with `target_basis` it is equivalent to `CompileTarget.topology_basis(device, target_basis, ...)`. +- `initial_layout` (`Layout | None`): the logical-to-physical initial layout; takes effect only when `device` is provided. +- `resource_policy` (`ResourcePolicy | None`): the ancilla policy of the pre-layout decomposition stage, see [Resource](8_resource.md). +- `seed` (`int | None`): the deterministic random seed for device layout and routing; takes effect only when `device` is provided. + +Returns: + +- `CompileResult` + +Raises: + +- `CompilerConfigError`: the configuration is invalid (mutually exclusive `target` and convenience parameters, an unknown mode name, an empty target gate set, and so on). +- `CompilerTransformError`: a transform fails to execute. +- `CompilerInternalError`: an internal consistency check fails. + +Example: + +```python +from cqlib import Circuit +from cqlib.compile import CompileMode, compile +from cqlib.device import Device + +circuit = Circuit(3) +circuit.h(0) +circuit.cx(0, 2) + +device = Device.line("line-3", 3) + +result = compile( + circuit, + mode=CompileMode.enhanced(), + device=device, + target_basis=["H", "CX", "RZ"], + seed=42, +) + +print("changed:", result.changed) +for step in result.steps: + if step.changed and not step.skipped: + print(step.stage, step.name, step.reason) +``` + +--- + +## CompileMode + +Optimization strength enumeration. + +### Static methods + +- `CompileMode.normal()`: conservative logical optimization using production default pass parameters. +- `CompileMode.enhanced()`: a stronger staged workflow using a larger pass budget, performing target-aware cleanup when a target constraint is present. + +### Other behavior + +- `str(CompileMode.normal()) == "normal"`, `str(CompileMode.enhanced()) == "enhanced"`. +- Supports `==`, `hash`, `copy` and `deepcopy`. + +--- + +## CompileConfig + +Compilation configuration snapshot, immutable. + +### CompileConfig(*, mode=None, target=None, resource_policy=None) + +Parameters: + +- `mode` (`CompileMode | str | None`): defaults to `CompileMode.normal()`. +- `target` (`CompileTarget | None`): defaults to `CompileTarget.logical()`. +- `resource_policy` (`ResourcePolicy | None`): defaults to `ResourcePolicy()`. + +### Attributes + +- `mode -> CompileMode` +- `target -> CompileTarget` +- `resource_policy -> ResourcePolicy` + +Example: + +```python +from cqlib.compile import CompileConfig, CompileMode, CompileTarget + +config = CompileConfig(mode=CompileMode.enhanced(), target=CompileTarget.logical()) +assert config.mode == CompileMode.enhanced() +assert config.target.kind == "logical" +``` + +--- + +## CompileTarget + +Target constraint enumeration. Instances are constructed through static methods. + +### Static methods + +- `CompileTarget.logical()`: compile in the logical qubit space without target-related lowering. +- `CompileTarget.basis(instructions)`: lower to an explicit standard gate basis. + - `instructions` (`list[str | Instruction]`): a non-empty list of gate basis entries; the entry requirements are the same as those of `target_basis` in `compile()`. +- `CompileTarget.device(device, *, initial_layout=None, seed=None)`: routing and lowering for a concrete device; the output is required to match the device's native capabilities and final device validation is performed. `device` must already be configured with a native gate set (`device.native_gates` non-empty), otherwise construction fails; when only the topology and layout matter and the output is not required to land on the device native gate basis, use `topology_basis` instead. +- `CompileTarget.topology_basis(device, instructions, *, initial_layout=None, seed=None)`: route on the device topology while lowering to an explicit gate basis; the device is used only for capacity, layout and routing, the gate basis is not required to match the device's native capabilities, and therefore the device is not required to have a native gate set configured. + +### Attributes + +- `kind -> str`: the target category, taking the values `"logical"`, `"basis"`, `"device"`, `"topology_basis"`. + +Raises: + +- `CompilerConfigError`: the gate basis is empty, a gate basis entry is not a known standard gate, or the device used by a `device` target has no native gate set. + +Example: + +```python +from cqlib.circuit import Instruction, StandardGate +from cqlib.compile import CompileTarget +from cqlib.device import Device + +logical = CompileTarget.logical() +basis = CompileTarget.basis(["H", "CX", "RZ"]) + +device = Device.line("line-3", 3) +device.native_gates = [ + Instruction.from_standard_gate(StandardGate.H), + Instruction.from_standard_gate(StandardGate.CX), +] +device_target = CompileTarget.device(device, seed=42) + +topo_basis = CompileTarget.topology_basis(Device.line("line-3", 3), ["H", "CX", "RZ"]) +``` + +--- + +## DeviceCompileTarget + +Device compilation input snapshot. + +### DeviceCompileTarget(device, *, initial_layout=None, seed=None) + +Parameters: + +- `device` (`Device`): the device with ordered native capabilities that constrains the output. +- `initial_layout` (`Layout | None`): the logical-to-physical initial layout provided by the caller. +- `seed` (`int | None`): the deterministic random seed for device layout and routing. + +### Attributes + +- `device -> Device` +- `initial_layout -> Layout | None` +- `seed -> int | None` + +--- + +## CompileResult + +The object returned by `compile()` and `CompilerWorkflow.run()`. + +### Attributes + +- `circuit -> Circuit`: the optimized circuit. +- `changed -> bool`: whether any step changed the input representation. +- `mode -> CompileMode`: the mode used by this run. +- `steps -> list[WorkflowStepReport]`: the step-by-step reports in execution order. +- `device_metadata -> DeviceCompilationMetadata | None`: the physical layout data when routing on a device topology; `None` for a non-device target. + +### Methods + +- `step(name) -> WorkflowStepReport | None`: return the first step report whose name matches. +- `step_changed(name) -> bool`: whether the reports under that name contain a record that was not skipped and did change the circuit. + +### Other behavior + +- Supports `==`, `copy` and `deepcopy`. + +Example: + +```python +from cqlib import Circuit +from cqlib.compile import compile + +circuit = Circuit(2) +circuit.h(0) +circuit.cx(0, 1) + +result = compile(circuit) +assert result.circuit is not circuit +assert result.step_changed("canonicalize.input") or not result.changed +``` + +--- + +## WorkflowStepReport + +The execution record of a single workflow step. + +### Attributes + +- `stage -> str`: the coarse-grained stage name, for example `"pre_init"`, `"init"`, `"optimization"`, `"translation"`, `"routing"`, `"output"`, `"validation"`. +- `name -> str`: the step name within the workflow. +- `changed -> bool`: whether the step changed the circuit representation. +- `skipped -> bool`: whether the step was deliberately skipped. +- `reason -> str | None`: the skip or configuration explanation. + +### Other behavior + +- Supports `copy` and `deepcopy`. + +--- + +## DeviceCompilationMetadata + +The physical layout information produced by device compilation. + +### Attributes + +- `initial_layout -> Layout`: the logical-to-physical layout before routing starts. +- `final_layout -> Layout`: the logical-to-physical layout after all routing SWAPs. + +### Other behavior + +- Supports `==`, `copy` and `deepcopy`. + +--- + +## CompilerWorkflow + +Reusable compilation workflow object. + +### CompilerWorkflow(config=None) + +Parameters: + +- `config` (`CompileConfig | None`): the configuration snapshot; defaults to `CompileConfig()`. + +### Attributes + +- `config -> CompileConfig` + +### Methods + +- `run(circuit) -> CompileResult`: run the workflow according to the configuration without modifying the input circuit. + +Example: + +```python +from cqlib import Circuit +from cqlib.compile import CompileMode, CompilerWorkflow, CompileConfig + +workflow = CompilerWorkflow(CompileConfig(mode=CompileMode.enhanced())) + +for _ in range(3): + circuit = Circuit(2) + circuit.h(0) + circuit.cx(0, 1) + result = workflow.run(circuit) +``` + +--- + +## Exceptions + +- `CompilerError`: the compilation error base class. +- `CompilerConfigError`: configuration error. +- `CompilerTransformError`: transform execution error. +- `CompilerInternalError`: internal error. + +All of the exceptions above are exported from `cqlib.compile`. diff --git a/docs/api/en/python/4_compile/2_transform.md b/docs/api/en/python/4_compile/2_transform.md new file mode 100644 index 00000000..c13aafec --- /dev/null +++ b/docs/api/en/python/4_compile/2_transform.md @@ -0,0 +1,325 @@ +# Transform + +`cqlib.compile.transform` provides reusable compilation passes. Each pass returns a result object and does not modify the input circuit. The end-to-end `compile()` orchestrates these passes automatically; they can also be called directly when a stage needs to be observed or debugged on its own. + +## Import + +```python +from cqlib.compile.transform import ( + TransformResult, + CircuitAnalysis, + CanonicalizeConfig, + Canonicalizer, + CanonicalizeResult, + canonicalize_circuit, + RewriteMode, + RewriteConfig, + KnowledgeRewriter, + KnowledgeRewriteResult, + KnowledgeRewriteStats, + rewrite_circuit, + OptimizeOneQubitRuns, + CommutativeCancellation, + LowerToRoutingBasis, + lower_to_routing_basis, +) +``` + +--- + +## TransformResult + +The generic transform result object. + +### Attributes + +- `circuit -> Circuit`: the output circuit. +- `changed -> bool`: whether the pass changed the circuit. + +### Other behavior + +- Supports `==`, `copy` and `deepcopy`. + +--- + +## CircuitAnalysis + +Circuit feature snapshot, used by the workflow to decide which passes to execute. The full module path of this class is `cqlib.compile.transform.analysis`. + +### CircuitAnalysis.analyze(circuit) + +Static method that analyzes a circuit and returns the snapshot. + +### Attributes + +- `has_classical_data -> bool`: whether classical data is present. +- `has_classical_control -> bool`: whether classical control flow is present. +- `has_measurement -> bool`: whether measurements are present. +- `has_classical_values -> bool`: whether classical values (such as measurement results) are present. +- `has_classical_vars -> bool`: whether classical variables are present. +- `has_runtime_classical -> bool`: whether runtime classical semantics are present (a measurement value used by a later condition). +- `needs_classical_handle_preservation -> bool`: whether the transform must preserve classical handles. +- `has_circuit_gate_definitions -> bool`: whether circuit gate definitions are present. +- `has_unitary_circuit_definitions -> bool`: whether unitary circuit definitions are present. +- `has_unitary_gates -> bool`: whether custom unitary gates are present. +- `has_mc_gates -> bool`: whether multi-controlled gates are present. + +Example: + +```python +from cqlib import Circuit +from cqlib.compile.transform import CircuitAnalysis + +circuit = Circuit(2) +circuit.h(0) +m = circuit.measure(0) + +analysis = CircuitAnalysis.analyze(circuit) +assert analysis.has_measurement +``` + +--- + +## Canonicalizer / canonicalize_circuit + +Canonicalization pass that unifies instruction forms, folds the global phase and removes no-ops. + +### CanonicalizeConfig(*, round_limit=8, recurse_control_flow=true, fold_gphase=true, canonicalize_instruction_form=true, drop_noops=true, canonicalize_barriers=true) + +Parameters: + +- `round_limit` (`int`): the maximum number of canonicalization rounds. +- `recurse_control_flow` (`bool`): whether to recurse into control flow blocks. +- `fold_gphase` (`bool`): whether to fold the global phase. +- `canonicalize_instruction_form` (`bool`): whether to unify instruction forms. +- `drop_noops` (`bool`): whether to drop no-ops. +- `canonicalize_barriers` (`bool`): whether to canonicalize barriers. + +### Static methods + +- `CanonicalizeConfig.production()`: the production default configuration. + +### Attributes + +`round_limit`, `recurse_control_flow`, `fold_gphase`, `canonicalize_instruction_form`, `drop_noops`, `canonicalize_barriers`, corresponding one-to-one with the constructor parameters. + +### Canonicalizer(config=None) + +Parameters: + +- `config` (`CanonicalizeConfig | None`): defaults to `CanonicalizeConfig.production()`. + +### Static methods + +- `Canonicalizer.production()`: construct with the production default configuration. + +### Attributes and methods + +- `config -> CanonicalizeConfig` +- `run(circuit) -> CanonicalizeResult` + +### CanonicalizeResult + +Attributes: + +- `circuit -> Circuit` +- `changed -> bool` +- `rounds -> int`: the number of canonicalization rounds actually executed. + +### canonicalize_circuit(circuit, config=None) + +Functional entry point, equivalent to `Canonicalizer(config).run(circuit)`. + +Example: + +```python +from cqlib import Circuit +from cqlib.compile.transform import canonicalize_circuit + +circuit = Circuit(1) +circuit.i(0) + +result = canonicalize_circuit(circuit) +assert result.circuit is not circuit +assert len(circuit.operations) == 1 +assert len(result.circuit.operations) == 0 +assert result.changed +``` + +--- + +## KnowledgeRewriter / rewrite_circuit + +Pattern rewriting pass based on knowledge rules; it is the core of logical optimization in the compilation pipeline. + +### RewriteMode + +Rewrite mode enumeration: + +- `RewriteMode.optimize()`: conservative optimization; an accepted replacement must strictly improve the local cost. +- `RewriteMode.lowering()`: explicit lowering; decomposition and hardware native rules may expand locally. +- `RewriteMode.optimize().name -> "optimize"`, `RewriteMode.lowering().name -> "lowering"`. + +### RewriteConfig(*, max_rounds=8, max_window_ops=16, max_pattern_len=8, recurse_control_flow=true, skip_labeled_ops=true, enabled_kinds=None, mode=None, target_instructions=None) + +Parameters: + +- `max_rounds` (`int`): the maximum number of rewriting rounds. +- `max_window_ops` (`int`): the maximum number of operations in the sliding window. +- `max_pattern_len` (`int`): the maximum pattern length. +- `recurse_control_flow` (`bool`): whether to recurse into control flow blocks. +- `skip_labeled_ops` (`bool`): whether to skip labeled operations. +- `enabled_kinds` (`list[RuleKind] | None`): the enabled rule kinds; `None` means all. +- `mode` (`RewriteMode | None`): defaults to `RewriteMode.optimize()`. +- `target_instructions` (`list[str | Instruction] | None`): an optional target instruction basis, used for target-aware cost evaluation. + +### Static methods + +- `RewriteConfig.production()`: the production optimization configuration. +- `RewriteConfig.lowering()`: the lowering configuration. + +### Attributes + +`max_rounds`, `max_window_ops`, `max_pattern_len`, `recurse_control_flow`, `skip_labeled_ops`, `enabled_kinds`, `mode`, `target_instructions`, corresponding one-to-one with the constructor parameters. + +### KnowledgeRewriter(config=None) + +Parameters: + +- `config` (`RewriteConfig | None`): defaults to `RewriteConfig.production()`. + +### Static methods + +- `KnowledgeRewriter.production()`, `KnowledgeRewriter.lowering()`: construct with the corresponding preset configuration. + +### Attributes and methods + +- `config -> RewriteConfig` +- `run(circuit) -> KnowledgeRewriteResult` + +### KnowledgeRewriteResult + +Attributes: + +- `circuit -> Circuit` +- `changed -> bool` +- `stats -> KnowledgeRewriteStats` + +### KnowledgeRewriteStats + +Attributes: + +- `rounds_executed -> int`: the number of rounds actually executed. +- `rules_applied -> int`: the number of rule applications. +- `changed_sequences -> int`: the number of operation sequences that changed. +- `reached_fixpoint -> bool`: whether a fixed point was reached. + +### rewrite_circuit(circuit, config=None) + +Functional entry point, equivalent to `KnowledgeRewriter(config).run(circuit)`. + +Example: + +```python +from cqlib import Circuit +from cqlib.compile.transform import rewrite_circuit + +circuit = Circuit(2) +circuit.h(0) +circuit.h(0) +circuit.cx(0, 1) + +result = rewrite_circuit(circuit) +print(result.stats.rules_applied) +``` + +--- + +## OptimizeOneQubitRuns + +Optimization of consecutive single-qubit gate runs. The full module path of this class is `cqlib.compile.transform.one_qubit_optimization`. + +### Static methods + +- `OptimizeOneQubitRuns.logical()`: a target-neutral optimizer using the strict logical cost. +- `OptimizeOneQubitRuns.basis(target_basis)`: optimize by the exact cost after lowering to `target_basis`. `target_basis` is a list of case-insensitive standard gate name strings or `Instruction` objects (multi-controlled gates must use `Instruction`). + +### Attributes and methods + +- `policy -> str`: the currently accepted policy, taking the values `"logical"` or `"basis"`. +- `target_basis -> list[Instruction] | None`: the explicit target gate set; `None` for logical optimization. +- `run(circuit) -> TransformResult` + +Example: + +```python +from cqlib import Circuit, Parameter +from cqlib.compile.transform import OptimizeOneQubitRuns + +circuit = Circuit(1) +circuit.rx(0, Parameter("a")) +circuit.rx(0, Parameter("b")) + +result = OptimizeOneQubitRuns.logical().run(circuit) +``` + +--- + +## CommutativeCancellation + +Self-inverse cancellation pass based on commutation. The full module path of this class is `cqlib.compile.transform.commutative_cancellation`. + +### CommutativeCancellation() + +No-argument constructor. + +### Methods + +- `run(circuit) -> TransformResult` + +Example: + +```python +from cqlib import Circuit +from cqlib.compile.transform import CommutativeCancellation + +circuit = Circuit(2) +circuit.x(0) +circuit.cx(0, 1) +circuit.x(0) + +result = CommutativeCancellation().run(circuit) +``` + +--- + +## LowerToRoutingBasis / lower_to_routing_basis + +Routing gate set lowering pass, converting the circuit into a form suitable for the routing stage. + +### LowerToRoutingBasis(preferred_basis=None) + +Parameters: + +- `preferred_basis` (`list[Instruction] | None`): the preferred gate basis; `None` uses the default routing gate basis. + +### Methods + +- `run(circuit) -> TransformResult` + +### lower_to_routing_basis(circuit, preferred_basis=None) + +Functional entry point. + +Example: + +```python +from cqlib import Circuit +from cqlib.compile.transform import lower_to_routing_basis + +circuit = Circuit(2) +circuit.h(0) +circuit.cx(0, 1) + +result = lower_to_routing_basis(circuit) +``` diff --git a/docs/api/en/python/4_compile/3_layout.md b/docs/api/en/python/4_compile/3_layout.md new file mode 100644 index 00000000..86cddea0 --- /dev/null +++ b/docs/api/en/python/4_compile/3_layout.md @@ -0,0 +1,293 @@ +# Layout + +`cqlib.compile.transform.layout` provides initial layout algorithms and layout analysis tools. The layout algorithms map logical qubits to physical qubits and provide a starting point for the subsequent routing stage. + +## Import + +```python +from cqlib.compile.transform.layout import ( + trivial_layout, + greedy_layout, + vf2_perfect_layout, + sabre_layout, + analyze_circuit_for_layout, + prepare_sabre_circuit, + prepare_sabre_device_target, + trivial_layout_prepared, + greedy_layout_prepared, + vf2_perfect_layout_prepared, + sabre_layout_prepared, + LayoutObjective, + LayoutScore, + LayoutDiagnostics, + LayoutResult, + Vf2EdgeRequirement, + Vf2LayoutConfig, + Interaction, + InteractionGraph, + CircuitLayoutAnalysis, + DistanceTable, + PhysicalLayoutGraph, + PreparedSabreCircuit, + PreparedSabreTarget, +) +``` + +--- + +## Layout functions + +### trivial_layout(circuit, device, objective=None) + +Trivial layout: map logical qubits to physical qubits in index order. + +### greedy_layout(circuit, device, objective=None) + +Greedy layout: place logical qubits on physical qubits in order of interaction strength. + +### vf2_perfect_layout(circuit, device, objective=None, config=None) + +VF2 exact layout: produce a perfect layout when the circuit interaction graph can be embedded exactly into the device topology. + +Parameters: + +- `config` (`Vf2LayoutConfig | None`): the VF2 search configuration. + +### sabre_layout(circuit, device, objective=None, config=None) + +SABRE layout: refine a candidate layout using the SABRE heuristic. + +Parameters: + +- `config` (`SabreConfig | None`): the SABRE configuration, see [SABRE](5_sabre.md). + +Common parameters of the functions above: + +- `circuit` (`Circuit`): the circuit to lay out. +- `device` (`Device`): the target device. +- `objective` (`LayoutObjective | None`): the layout objective weights; defaults to `LayoutObjective.topology_only()`. + +Returns: + +- `LayoutResult` + +### Precomputed entry points + +For scenarios that lay out repeatedly, the circuit analysis or device preparation can be done once and the intermediate results reused: + +- `analyze_circuit_for_layout(circuit) -> CircuitLayoutAnalysis`: circuit interaction graph analysis. +- `prepare_sabre_circuit(circuit) -> PreparedSabreCircuit`: SABRE circuit preprocessing. +- `prepare_sabre_device_target(device) -> PreparedSabreTarget`: device physical graph preprocessing. +- `trivial_layout_prepared(analysis, physical, objective=None) -> LayoutResult` +- `greedy_layout_prepared(analysis, physical, objective=None) -> LayoutResult` +- `vf2_perfect_layout_prepared(analysis, physical, objective=None, config=None) -> LayoutResult` +- `sabre_layout_prepared(prepared, prepared_target, objective=None, config=None) -> LayoutResult` + +Here `analysis` is a `CircuitLayoutAnalysis`, `physical` is a `PhysicalLayoutGraph`, `prepared` is a `PreparedSabreCircuit`, and `prepared_target` is a `PreparedSabreTarget`. + +Example: + +```python +from cqlib import Circuit +from cqlib.compile.transform.layout import sabre_layout +from cqlib.device import Device + +circuit = Circuit(3) +circuit.cx(0, 2) + +device = Device.line("line-3", 3) +result = sabre_layout(circuit, device) +print(result.layout) +``` + +--- + +## LayoutObjective + +Layout objective weights, used to score candidate layouts. + +### LayoutObjective(*, distance_weight=1.0, direction_weight=1.0, two_qubit_error_weight=0.0, readout_error_weight=0.0) + +Parameters: + +- `distance_weight` (`float`): the weight of the shortest distance term. +- `direction_weight` (`float`): the weight of the direction term. +- `two_qubit_error_weight` (`float`): the weight of the two-qubit gate error rate term. +- `readout_error_weight` (`float`): the weight of the readout error rate term. + +### Static methods + +- `LayoutObjective.topology_only()`: pure topology objective. +- `LayoutObjective.fidelity_aware()`: the default fidelity-aware objective. +- `LayoutObjective.auto_from_device(device)`: choose the fidelity-aware objective when the device has usable calibration data, otherwise fall back to the topology objective. +- `LayoutObjective.auto_from_physical(physical)`: as above, acting on a prepared physical graph. +- `LayoutObjective.fidelity_required(device)`: the fidelity-aware objective; raises when the device has no usable calibration data. +- `LayoutObjective.fidelity_required_from_physical(physical)`: as above, acting on a prepared physical graph. + +### Attributes + +- `distance_weight`, `direction_weight`, `two_qubit_error_weight`, `readout_error_weight` (`float`), corresponding one-to-one with the constructor parameters. +- `uses_fidelity -> bool`: whether fidelity terms are used. + +### Methods + +- `score_layout(analysis, physical, layout) -> LayoutScore`: compute the score from a circuit analysis, a physical graph and a complete layout. + +### Other behavior + +- Supports `==`, `copy` and `deepcopy`. + +--- + +## LayoutScore + +Layout scoring result. + +### Attributes + +- `total -> float`: the total score. +- `distance -> float`: the distance term. +- `direction -> float`: the direction term. +- `two_qubit_error -> float`: the two-qubit error term. +- `readout_error -> float`: the readout error term. +- `used_fidelity -> bool`: whether fidelity terms were used. + +--- + +## LayoutDiagnostics + +Diagnostic information of the layout process. + +### Attributes + +- `is_perfect -> bool`: whether a perfect layout was found. +- `candidates_evaluated -> int`: the number of candidates evaluated. +- `used_fidelity -> bool`: whether fidelity terms were used. +- `notes -> list[str]`: diagnostic notes. + +--- + +## LayoutResult + +The object returned by the layout functions. + +### Attributes + +- `layout -> Layout`: the selected logical-to-physical layout. +- `score -> LayoutScore | None`: the scoring result, `None` when no scoring is performed. +- `diagnostics -> LayoutDiagnostics`: diagnostic information. + +--- + +## Vf2EdgeRequirement + +Edge requirements of the VF2 search. + +### Static methods + +- `Vf2EdgeRequirement.positive_interactions()`: require only positive interaction edges to match. +- `Vf2EdgeRequirement.all_interactions()`: require all interaction edges to match. + +--- + +## Vf2LayoutConfig + +VF2 search configuration. + +### Vf2LayoutConfig(*, candidate_limit=10, call_limit=None, edge_requirement=None) + +Parameters: + +- `candidate_limit` (`int`): the upper limit on the number of candidate layouts. +- `call_limit` (`int | None`): the upper limit on the number of search calls. +- `edge_requirement` (`Vf2EdgeRequirement | None`): the edge matching requirement. + +### Attributes + +`candidate_limit`, `call_limit`, `edge_requirement`, corresponding one-to-one with the constructor parameters. + +--- + +## Layout analysis objects + +### Interaction + +A logical qubit pair interaction. + +Attributes: + +- `left -> Qubit`, `right -> Qubit`: the logical qubits at the two ends of the interaction. +- `weight -> float`: the total interaction weight. +- `directed_weight_left_to_right -> float`: the directed weight from left to right. +- `directed_weight_right_to_left -> float`: the directed weight from right to left. +- `first_seen_order -> int`: the first-seen order. + +### InteractionGraph + +Circuit interaction graph. + +Attributes and methods: + +- `interactions -> list[Interaction]`: the interaction list. +- `is_empty() -> bool`: whether there are no interactions. +- `logical_activity() -> list[tuple[Qubit, float]]`: the total interaction weight of each logical qubit. + +### CircuitLayoutAnalysis + +Circuit layout analysis result, returned by `analyze_circuit_for_layout()`. + +Attributes: + +- `logical_qubits -> list[Qubit]`: the logical qubit list. +- `interactions -> InteractionGraph`: the interaction graph. + +### DistanceTable + +Shortest distance table of the physical graph. + +Attributes and methods: + +- `qubits -> list[Qubit]`: the physical qubit list. +- `distance(a, b) -> int | None`: the shortest distance between two physical qubits, `None` when unreachable. + +### PhysicalLayoutGraph + +Device physical layout graph, constructed by `PhysicalLayoutGraph.from_device(device)`. + +Attributes and methods: + +- `physical_qubits -> list[Qubit]` +- `distances -> DistanceTable` +- `has_fidelity_data -> bool`: whether fidelity data is present. +- `has_readout_error_data -> bool`: whether readout error rate data is present. +- `has_two_qubit_error_data -> bool`: whether two-qubit gate error rate data is present. +- `distance(a, b) -> int | None` +- `is_adjacent_undirected(a, b) -> bool` +- `readout_error(qubit) -> float | None` +- `supports_two_qubit_gate_directed(a, b) -> bool` +- `two_qubit_gate_error_directed(a, b) -> float | None` +- `supports_directed_coupling(a, b) -> bool` + +### PreparedSabreCircuit + +SABRE circuit preprocessing result, returned by `prepare_sabre_circuit()`. + +Attributes: + +- `analysis -> CircuitLayoutAnalysis` +- `logical_qubits -> list[Qubit]` + +### PreparedSabreTarget + +Device physical graph preprocessing result, returned by `prepare_sabre_device_target()`. + +Attributes: + +- `physical -> PhysicalLayoutGraph` + +--- + +## Raises + +- `CompilerConfigError`: the configuration is invalid, for example a fidelity objective used on a device without calibration data. +- `CompilerTransformError`: the layout cannot be completed, for example an interaction cannot be satisfied on the topology. diff --git a/docs/api/en/python/4_compile/4_routing.md b/docs/api/en/python/4_compile/4_routing.md new file mode 100644 index 00000000..3469e273 --- /dev/null +++ b/docs/api/en/python/4_compile/4_routing.md @@ -0,0 +1,117 @@ +# Routing + +`cqlib.compile.transform.routing` provides the transform-layer routing entry points. Routing inserts SWAPs on a restricted topology so that all two-body interactions in the circuit satisfy the device adjacency constraint. + +## Import + +```python +from cqlib.compile.transform.routing import ( + route_with_layout, + route_sabre, + RoutedCircuit, + SabreRouteResult, +) +``` + +--- + +## Functions + +### route_with_layout(circuit, device, initial_layout, config=None) + +Deterministic routing under a known initial layout. + +Parameters: + +- `circuit` (`Circuit`): the circuit to route. +- `device` (`Device`): the target device. +- `initial_layout` (`Layout`): the logical-to-physical initial layout. +- `config` (`SabreConfig | None`): the SABRE configuration; defaults to `SabreConfig()`. + +Returns: + +- `RoutedCircuit` + +### route_sabre(circuit, device, objective=None, config=None) + +SABRE routing with layout selection: first select the initial layout according to `objective`, then perform routing. + +Parameters: + +- `circuit` (`Circuit`): the circuit to route. +- `device` (`Device`): the target device. +- `objective` (`LayoutObjective | None`): the layout objective; defaults to `LayoutObjective.topology_only()`. +- `config` (`SabreConfig | None`): the SABRE configuration; defaults to `SabreConfig()`. + +Returns: + +- `SabreRouteResult` + +Example: + +```python +from cqlib import Circuit +from cqlib.compile.transform.routing import route_sabre +from cqlib.device import Device + +circuit = Circuit(3) +circuit.cx(0, 2) + +device = Device.line("line-3", 3) +result = route_sabre(circuit, device) +print("swap_count:", result.swap_count) +``` + +--- + +## RoutedCircuit + +The routing result under a known initial layout. + +### Attributes + +- `circuit -> Circuit`: the routed circuit. +- `initial_layout -> Layout`: the layout before routing starts. +- `final_layout -> Layout`: the layout after routing ends. +- `swap_count -> int`: the number of inserted SWAPs. +- `diagnostics -> SabreRoutingDiagnostics`: routing diagnostic information, see [SABRE](5_sabre.md). +- `changed -> bool`: whether routing changed the circuit. + +--- + +## SabreRouteResult + +The routing result with layout selection; it carries layout scoring on top of `RoutedCircuit`. + +### Attributes + +- `routed -> RoutedCircuit`: the routing result. +- `layout_score -> LayoutScore | None`: the score of the selected layout. +- `layout_diagnostics -> LayoutDiagnostics`: diagnostic information of the layout process. +- `circuit`, `initial_layout`, `final_layout`, `swap_count`, `diagnostics`, `changed`: the same as `RoutedCircuit`. + +Example: + +```python +from cqlib import Circuit +from cqlib.compile.transform.routing import route_sabre +from cqlib.device import Device + +circuit = Circuit(2) +circuit.cx(0, 1) + +device = Device.line("line-3", 3) +result = route_sabre(circuit, device) + +print("selected layout:", result.routed.initial_layout) +print("score:", result.layout_score.total if result.layout_score else None) +``` + +--- + +## Raises + +- `CompilerConfigError`: the SABRE configuration is invalid, for example `routing_trials` is zero. +- `CompilerTransformError`: routing fails, for example the initial layout is missing or unreachable, or the device capacity is insufficient. + +For details of the low-level `sabre_route` and `SabreConfig`, see [SABRE](5_sabre.md). diff --git a/docs/api/en/python/4_compile/5_sabre.md b/docs/api/en/python/4_compile/5_sabre.md new file mode 100644 index 00000000..58aa9707 --- /dev/null +++ b/docs/api/en/python/4_compile/5_sabre.md @@ -0,0 +1,193 @@ +# SABRE + +`cqlib.compile.sabre` provides the low-level entry point and configuration of SWAP routing. Use `sabre_route` when the initial layout is known and the circuit must satisfy the device topology constraint. Fixing the random seed makes the tie-breaking and selection of multiple trials reproducible. + +## Import + +```python +from cqlib.compile.sabre import ( + SabreConfig, + SabreHeuristicConfig, + SabreVf2PrepassConfig, + SabreRoutingResult, + SabreRoutingDiagnostics, + sabre_route, + normalize_initial_layout, + validate_reachable_interactions, +) +``` + +--- + +## sabre_route(circuit, device, initial_layout, config=None) + +SABRE routing under a known initial layout. + +Parameters: + +- `circuit` (`Circuit`): the circuit to route. +- `device` (`Device`): the target device. +- `initial_layout` (`Layout`): the logical-to-physical initial layout. +- `config` (`SabreConfig | None`): defaults to `SabreConfig()`. + +Returns: + +- `SabreRoutingResult` + +Example: + +```python +from cqlib import Circuit +from cqlib.compile.sabre import SabreConfig, sabre_route +from cqlib.device import Device, Layout + +circuit = Circuit(2) +circuit.cx(0, 1) + +result = sabre_route( + circuit, + Device.line("line-3", 3), + Layout.from_pairs([(0, 0), (1, 2)], physical_count=3), + SabreConfig(routing_trials=1, seed=7), +) +assert result.swap_count == 1 +``` + +--- + +## Helper functions + +### normalize_initial_layout(logical_qubits, device, initial_layout) + +Normalize a complete logical-to-physical layout against the qubits available on the device. + +Parameters: + +- `logical_qubits` (`list[Qubit]`): the logical qubit list of the circuit. +- `device` (`Device`): the target device. +- `initial_layout` (`Layout`): the initial layout to normalize. + +Returns: + +- `Layout` + +### validate_reachable_interactions(circuit, device, initial_layout) + +Validate native movement reachability without performing routing. + +Parameters: + +- `circuit` (`Circuit`): the circuit to check. +- `device` (`Device`): the target device. +- `initial_layout` (`Layout`): the initial layout. + +Returns: + +- `None`; raises `CompilerError` when something is unreachable. + +--- + +## SabreConfig + +Configuration shared by SABRE layout refinement and routing. + +### SabreConfig(*, layout_trials=10, layout_assignment_budget=1_000_000, vf2_prepass=SabreVf2PrepassConfig(candidate_limit=10, call_limit=1_000_000), refinement_iterations=1, routing_trials=1, seed=None, heuristic=None) + +Parameters: + +- `layout_trials` (`int`): the number of randomized initial layouts added beyond the deterministic and interaction-aware candidates. +- `layout_assignment_budget` (`int`): the maximum exploration of layout component assignment states; exhaustion reports budget exhaustion, which is distinct from proven infeasibility. +- `vf2_prepass` (`SabreVf2PrepassConfig | None`): the bounded VF2 prepass, used to add topology-perfect candidates; `None` disables the prepass. +- `refinement_iterations` (`int`): the number of forward/backward refinement rounds per layout trial. +- `routing_trials` (`int`): the number of complete routing trials performed per fully refined layout. The search returns the best route directly and does not perform routing for intermediate refinement states. +- `seed` (`int | None`): the deterministic random seed. The same seed produces the same cqlib result. +- `heuristic` (`SabreHeuristicConfig | None`): the swap selection heuristic configuration; defaults to `SabreHeuristicConfig()`. + +### Static methods + +- `SabreConfig.deterministic_seeded(seed)`: a compact deterministic configuration, suitable for tests and examples. All trial counts are small, the random seed is fixed, and swap attempts are bounded. + +### Attributes + +`layout_trials`, `layout_assignment_budget`, `vf2_prepass`, `refinement_iterations`, `routing_trials`, `seed`, `heuristic`, corresponding one-to-one with the constructor parameters. + +### Raises + +- `CompilerConfigError`: routing fields are invalid, for example `routing_trials` is zero (layout refinement fields do not affect routing validation). + +--- + +## SabreHeuristicConfig + +SABRE swap selection heuristic configuration. The main score combines the current front layer sum, the lookahead scaled by the active layer, and a multiplicative congestion term; when a native plan exists, candidates are decided within a narrow structural window using the exact native two-qubit cost. + +### SabreHeuristicConfig(*, basic_weight=1.0, lookahead_weights=None, decay_increment=0.002, decay_reset=10, attempt_limit=1000, best_epsilon=1e-10) + +Parameters: + +- `basic_weight` (`float`): the weight of the total distance of the current front layer. +- `lookahead_weights` (`list[float] | None`): the weight that scales the lookahead layer sum for each active layer; defaults to `[0.5, 0.25, 0.125, 0.0625, 0.03125]`. +- `decay_increment` (`float | None`): the increment of the congestion multiplier of a physical qubit after the heuristic uses it for a SWAP; `None` disables congestion control. +- `decay_reset` (`int`): the number of heuristic SWAP attempts before the decay value is reset. +- `attempt_limit` (`int`): the number of consecutive heuristic SWAPs allowed while no frontier node has been routed, beyond which the search falls back to a shortest-path escape. +- `best_epsilon` (`float`): the floating-point tolerance within which candidate SWAP scores are considered tied. + +### Attributes + +`basic_weight`, `lookahead_weights`, `decay_increment`, `decay_reset`, `attempt_limit`, `best_epsilon`, corresponding one-to-one with the constructor parameters. + +--- + +## SabreVf2PrepassConfig + +The bounded VF2 prepass used to seed SABRE layout candidates. + +### SabreVf2PrepassConfig(*, candidate_limit=10, call_limit=1_000_000) + +Parameters: + +- `candidate_limit` (`int`): the upper limit on the number of complete perfect mappings scored by VF2. +- `call_limit` (`int`): the upper limit on the number of partial mapping extensions attempted by VF2. + +### Attributes + +`candidate_limit`, `call_limit`, corresponding one-to-one with the constructor parameters. + +--- + +## SabreRoutingResult + +The object returned by `sabre_route`. + +### Attributes + +- `circuit -> Circuit`: the routed circuit. +- `initial_layout -> Layout`: the layout before routing starts. +- `final_layout -> Layout`: the layout after routing ends. +- `swap_count -> int`: the number of inserted SWAPs. +- `diagnostics -> SabreRoutingDiagnostics`: routing diagnostic information. + +--- + +## SabreRoutingDiagnostics + +Diagnostic information of the routing process. + +### Attributes + +- `trials_evaluated -> int`: the number of routing trials evaluated. +- `selected_trial_index -> int`: the index of the selected trial. +- `fallback_count -> int`: the number of fallbacks. +- `control_flow_blocks_routed -> int`: the number of control flow blocks routed. +- `two_qubit_depth -> int`: the two-qubit depth of the routed circuit. +- `operation_count -> int`: the total number of operations. +- `native_two_qubit_count -> int`: the number of native two-qubit gates. +- `native_two_qubit_depth -> int`: the native two-qubit depth. +- `native_total_depth -> int`: the native total depth. +- `native_operation_count -> int`: the total number of native operations. +- `unknown_loop_count -> int`: the number of unknown loops. +- `requirement_signature_count -> int`: the number of requirement signatures. +- `eager_pair_state_count -> int`: the number of eager pair states. +- `lazy_pair_l1_lookup_count -> int`: the number of lazy pair level-1 lookups. +- `lazy_pair_l1_hit_count -> int`: the number of lazy pair level-1 hits. +- `lazy_pair_l1_cached_count -> int`: the number of lazy pair level-1 cached entries. diff --git a/docs/api/en/python/4_compile/6_decompose_resynthesis.md b/docs/api/en/python/4_compile/6_decompose_resynthesis.md new file mode 100644 index 00000000..7d07baec --- /dev/null +++ b/docs/api/en/python/4_compile/6_decompose_resynthesis.md @@ -0,0 +1,570 @@ +# Decompose / Resynthesis + +`cqlib.compile.transform.decompose` + +Gate decomposition, two-qubit block resynthesis, target gate set translation and device lowering, located in the `decompose`, `resynthesis`, `target_basis` and `device_lowering` submodules of `cqlib.compile.transform`. + +Gate decomposition is layered according to the representations available for an operation: definition expansion handles gates that carry their own implementation circuit, numeric synthesis handles unitary gates with a fixed matrix, and multi-controlled gate decomposition handles controlled operations. Definition expansion runs before numeric synthesis, so that a unitary gate with an implementation circuit is already expanded before it enters matrix synthesis. + +## Import + +```python +from cqlib.compile.transform.decompose import ( + DecompositionRuleStats, + McGateDecomposeConfig, + TwoQubitUnitaryDecomposeBasis, + UnitaryDecomposeConfig, + decompose_mc_gates, + decompose_mc_gates_for_device, + decompose_mc_gates_with_rule_stats, + decompose_unitaries, + decompose_unitaries_with_rule_stats, + expand_definitions, + mc_gate, + unitary, +) +from cqlib.compile.transform.resynthesis import ( + ResynthesizeTwoQubitBlocks, + TwoQubitBlockResynthesisConfig, + resynthesize_two_qubit_blocks, +) +from cqlib.compile.transform.target_basis import ( + TargetBasisCost, + TargetBasisCostModel, + TargetBasisLowerer, + TargetBasisSignature, +) +from cqlib.compile.transform import DeviceLowerer +``` + +--- + +## Definition expansion + +### expand_definitions(circuit) + +Expand circuit gate definitions without modifying the input circuit. + +Parameters: + +- `circuit` (`Circuit`): the circuit to expand. + +Returns: + +- `TransformResult`; the `circuit` field is the expanded circuit, and `changed` indicates whether any rewriting occurred. + +--- + +## Unitary synthesis + +The circuit-level entry point is located in `cqlib.compile.transform.decompose`, and the corresponding numeric synthesis primitives are located in `cqlib.compile.transform.decompose.unitary`. + +### decompose_unitaries(circuit, config=None) + +Synthesize one-qubit and two-qubit unitary gates from matrices. + +Parameters: + +- `circuit` (`Circuit`): the circuit to synthesize. +- `config` (`UnitaryDecomposeConfig | None`): the decomposition configuration; defaults to `UnitaryDecomposeConfig()`. + +Returns: + +- `TransformResult` + +Raises: + +- `CompilerTransformError`: the `UnitaryGate` has no matrix representation, or the qubit count of the matrix does not match the qubit count of the gate. + +### decompose_unitaries_with_rule_stats(circuit, config=None) + +As above, and additionally returns the rule cache statistics within the pass. + +Returns: + +- `(TransformResult, DecompositionRuleStats)` + +### UnitaryDecomposeConfig(*, two_qubit_basis=None, target_basis=None, recurse_control_flow=True) + +Parameters: + +- `two_qubit_basis` (`TwoQubitUnitaryDecomposeBasis | None`): the two-qubit synthesis gate basis. +- `target_basis` (`list[str | Instruction] | None`): the target gate set constraint; entries take the same form as `target_basis` in `compile()`. +- `recurse_control_flow` (`bool`): whether to recurse into control flow blocks. + +`two_qubit_basis` and `target_basis` are mutually exclusive; providing both is rejected. + +Attributes: + +- `two_qubit_basis -> TwoQubitUnitaryDecomposeBasis | None` +- `target_basis -> list[Instruction] | None` +- `recurse_control_flow -> bool` + +Raises: + +- `CompilerConfigError`: `two_qubit_basis` and `target_basis` are both provided, or the target gate set is empty. + +### TwoQubitUnitaryDecomposeBasis + +Two-qubit synthesis gate basis, taking the value of one of the following static methods: + +- `TwoQubitUnitaryDecomposeBasis.pauli_rotations()`: local `U` gates plus `RXX`, `RYY`, `RZZ`. +- `TwoQubitUnitaryDecomposeBasis.cx()`: local `U` gates plus `CX`. +- `TwoQubitUnitaryDecomposeBasis.cy()`: local `U` gates plus `CY`. +- `TwoQubitUnitaryDecomposeBasis.cz()`: local `U` gates plus `CZ`. +- `TwoQubitUnitaryDecomposeBasis.rzz()`: local `U`, `H`, `RX` plus `RZZ`. + +### DecompositionRuleStats + +Rule cache statistics within the pass: + +- `hits -> int`: the number of cache hits. +- `misses -> int`: the number of cache misses. +- `inserts -> int`: the number of cache writes. + +--- + +## Numeric unitary synthesis + +`cqlib.compile.transform.decompose.unitary` provides direct synthesis from a matrix to a gate sequence, without circuit traversal. + +### synthesize_numeric_1q_unitary(matrix) + +Synthesize a numeric one-qubit unitary matrix into `U` gate angles and a global phase. + +Parameters: + +- `matrix` (`list[list[complex]]`): a 2×2 unitary matrix, convertible to a two-dimensional `complex128` array. + +Returns: + +- `OneQubitUnitaryDecomposition` + +Raises: + +- `CompilerConfigError`: the matrix is not 2×2. +- `TypeError`: the input cannot be converted to a two-dimensional array. + +### synthesize_numeric_2q_unitary(matrix, first, second, basis=None) + +Synthesize a numeric two-qubit unitary matrix into a gate sequence. + +Parameters: + +- `matrix` (`list[list[complex]]`): a 4×4 unitary matrix, convertible to a two-dimensional `complex128` array. +- `first`, `second` (`int | Qubit`): the acted-on qubits, which must be distinct. +- `basis` (`TwoQubitUnitaryDecomposeBasis | None`): the synthesis gate basis; defaults to `TwoQubitUnitaryDecomposeBasis.pauli_rotations()`. + +Returns: + +- `TwoQubitUnitarySynthesisResult` + +Raises: + +- `CompilerConfigError`: the matrix is not 4×4, or `first` and `second` point to the same qubit. + +### kak_decompose(matrix) + +Compute the canonical KAK decomposition of a numeric 4×4 unitary matrix. + +Parameters: + +- `matrix` (`list[list[complex]]`): a 4×4 unitary matrix, convertible to a two-dimensional `complex128` array. + +Returns: + +- `KakDecomposition` + +Raises: + +- `TypeError`: the input cannot be converted to a two-dimensional array. + +### OneQubitUnitaryDecomposition + +The `U` gate decomposition result of a one-qubit unitary matrix. + +Attributes: + +- `theta -> float`: the polar angle of the synthesized `U` gate. +- `phi -> float`: the first azimuthal angle of the synthesized `U` gate. +- `lambda_ -> float`: the second azimuthal angle of the synthesized `U` gate. +- `global_phase -> float`: the scalar phase multiplied onto the synthesized `U` gate. + +Other behavior: + +- Supports `copy` and `deepcopy`, and compares by value. + +### TwoQubitUnitarySynthesisResult + +The synthesis result of a two-qubit unitary matrix. + +Attributes: + +- `operations -> list[ValueOperation]`: the standard gate operation sequence implementing the unitary, exact up to `global_phase`. +- `global_phase -> float`: the scalar phase multiplied onto the operation sequence. + +Other behavior: + +- Supports `copy` and `deepcopy`. + +### KakDecomposition + +The canonical KAK decomposition of a two-qubit unitary matrix. + +Attributes: + +- `global_phase -> float`: the scalar phase multiplied onto the complete decomposition. +- `k1l -> numpy.ndarray`: the left local factor acting after the Cartan interaction. +- `k1r -> numpy.ndarray`: the right local factor acting after the Cartan interaction. +- `k2l -> numpy.ndarray`: the left local factor acting before the Cartan interaction. +- `k2r -> numpy.ndarray`: the right local factor acting before the Cartan interaction. +- `a -> float`: the canonical Pauli-XX interaction coordinate. +- `b -> float`: the canonical Pauli-YY interaction coordinate. +- `c -> float`: the canonical Pauli-ZZ interaction coordinate. + +Other behavior: + +- Each `k` factor is an independent copy; modifying the returned value does not affect the decomposition object. +- Supports `copy` and `deepcopy`. + +--- + +## Multi-controlled gate decomposition + +### decompose_mc_gates(circuit, config=None) + +Decompose multi-controlled gates using the configured ancilla resources. This entry point has no target device and therefore does not check device capacity; use `decompose_mc_gates_for_device` when capacity checking is needed. + +Parameters: + +- `circuit` (`Circuit`): the circuit to decompose. +- `config` (`McGateDecomposeConfig | None`): the decomposition configuration; defaults to `McGateDecomposeConfig()`. + +Returns: + +- `TransformResult` + +### decompose_mc_gates_with_rule_stats(circuit, config=None) + +As above, and additionally returns the rule cache statistics. + +Returns: + +- `(TransformResult, DecompositionRuleStats)` + +### decompose_mc_gates_for_device(circuit, device, resource_policy=None) + +Decompose multi-controlled gates under the device capacity constraint. This entry point constrains the logical qubit count of the circuit not to exceed the number of qubits available on the device, and does not check the coupling topology. + +Parameters: + +- `circuit` (`Circuit`): the circuit to decompose. +- `device` (`Device`): provides the available physical qubit capacity constraint. +- `resource_policy` (`ResourcePolicy | None`): the ancilla policy. + +Returns: + +- `TransformResult` + +Raises: + +- `CompilerConfigError`: the circuit width exceeds the number of qubits available on the device. + +### McGateDecomposeConfig(*, resource_policy=None, resource_limits=None) + +Parameters: + +- `resource_policy` (`ResourcePolicy | None`): the ancilla policy. +- `resource_limits` (`ResourceLimits | None`): the resource limits. + +Attributes: + +- `resource_policy -> ResourcePolicy` +- `resource_limits -> ResourceLimits` + +--- + +## Multi-controlled gate primitives + +`cqlib.compile.transform.decompose.mc_gate` provides exact synthesis primitives divided by gate family; they give the operation sequence directly, without circuit traversal, without selecting an algorithm and without requesting ancillas. **The normal compilation flow should use `decompose_mc_gates` or `decompose_mc_gates_for_device`**; call these primitives directly only when ancilla allocation must be controlled manually or a custom lowering strategy is being implemented. + +The primitives are grouped by gate family, and each group usually provides a "no ancilla", a "one or more clean ancillas", a "one or more dirty ancillas" and a fixed-depth variant for a single ancilla: + +| Gate family | Coverage | +| --- | --- | +| MCX | Multi-controlled X gates. | +| Multi-controlled SU(2) | Multi-controlled special unitary rotations, with the rotation axis given by `Su2RotationAxis`. | +| Pauli | The multi-controlled Pauli gate family. | +| RZZ | Multi-controlled RZZ. | +| Pauli rotation | Multi-controlled Pauli rotations. | +| Rotation | Multi-controlled `RX` / `RY` / `RZ` and their intrinsic controlled forms. | +| Phase | Multi-controlled `S` / `SDG` / `T` / `TDG` / `Phase`. | +| QCIS | Synthesis primitives aimed at the QCIS gate set. | +| Hadamard | Multi-controlled Hadamard. | +| SWAP | Multi-controlled SWAP. | +| FSIM | Multi-controlled FSIM. | +| Unitary | Multi-controlled general unitary gates. | + +Common parameters: + +- `controls` (`list[int | Qubit]`): the control qubits, which must be the complete set of expanded controls, including the controls intrinsic to the gate itself. +- `target`, or `first` and `second` for two-qubit primitives: the target qubits. +- `clean_ancillas` / `clean_ancilla`: clean ancillas, which must enter as `|0>` and be restored to `|0>`. +- `dirty_ancillas` / `dirty_ancilla`: dirty ancillas, which may enter in an arbitrary unknown state but must be restored exactly. + +All primitives return `list[ValueOperation]`, which can be assembled into a circuit with `Circuit.from_operations`. Extra ancillas beyond the consumed prefix are ignored; ancillas must be distinct from all control qubits and target qubits. For the complete list see the public surface of `cqlib.compile.transform.decompose.mc_gate`. + +--- + +## Two-qubit block resynthesis + +### resynthesize_two_qubit_blocks(circuit, config=None) + +Resynthesize two-qubit gate blocks; the functional entry point. + +Parameters: + +- `circuit` (`Circuit`): the circuit to resynthesize. +- `config` (`TwoQubitBlockResynthesisConfig | None`): the resynthesis configuration. + +Returns: + +- `TransformResult` + +### ResynthesizeTwoQubitBlocks(config=None) + +Reusable pass object. + +Parameters: + +- `config` (`TwoQubitBlockResynthesisConfig | None`): the resynthesis configuration. + +Attributes and methods: + +- `config -> TwoQubitBlockResynthesisConfig` +- `run(circuit) -> TransformResult` + +### TwoQubitBlockResynthesisConfig(*, two_qubit_basis=None, target_basis=None, enhanced=False, max_block_ops=None, max_crossed_ops=None, max_scan_span=None, skip_labeled_ops=True, recurse_control_flow=True, commutation=None) + +Parameters: + +- `two_qubit_basis` (`TwoQubitUnitaryDecomposeBasis | None`): the two-qubit synthesis gate basis. +- `target_basis` (`list[str | Instruction] | None`): the target gate set constraint. +- `enhanced` (`bool`): whether to use the enhanced budget, trading compilation time for better post-routing cleanup. +- `max_block_ops` (`int | None`): the maximum number of operations in a block. +- `max_crossed_ops` (`int | None`): the maximum number of crossed operations in a block; a crossed operation stays in place and must commute with the synthesized replacement. +- `max_scan_span` (`int | None`): the collection budget on one side of a two-qubit anchor. +- `skip_labeled_ops` (`bool`): whether to treat labeled operations as hard boundaries. +- `recurse_control_flow` (`bool`): whether to recurse into control flow blocks. +- `commutation` (`CommutationConfig | None`): the commutation check configuration. + +--- + +## Target gate set translation + +### TargetBasisLowerer(target_basis) + +Translate a circuit to a target gate set. + +Parameters: + +- `target_basis` (`list[str | Instruction]`): a non-empty target gate set; entries take the same form as `target_basis` in `compile()`. + +Attributes and methods: + +- `target_basis -> list[Instruction]` +- `run(circuit) -> TransformResult` + +### TargetBasisSignature + +Target gate set signature: + +- `TargetBasisSignature.from_standard_gates(gates)`: construct a signature from a list of standard gates. + +### TargetBasisCost + +Target gate set cost statistics: + +- `two_qubit_ops -> int`: the number of two-qubit gates. +- `depth -> int`: the depth. +- `total_ops -> int`: the total number of operations. +- `parameterized_ops -> int`: the number of parameterized operations. + +### TargetBasisCostModel + +Target gate set cost model: + +- `signature -> TargetBasisSignature` +- `target_basis -> list[Instruction]` +- `cost_of_fixed_operations(qubits, operations) -> TargetBasisCost`: compute the cost of a fixed operation set. `qubits` is a list of qubit indices or `Qubit` objects, and `operations` is a list of `ValueOperation`. + +--- + +## Device lowering + +### DeviceLowerer(device) + +Lower a circuit according to the device native instruction set. + +Parameters: + +- `device` (`Device`): the target device. + +Attributes and methods: + +- `device -> Device` +- `run(circuit) -> TransformResult` + +--- + +## Examples + +### 1. Definition expansion and unitary synthesis + +```python +import numpy as np + +from cqlib.circuit import Circuit, UnitaryGate +from cqlib.compile.transform.decompose import ( + decompose_unitaries_with_rule_stats, + expand_definitions, +) + +definition = Circuit(1) +definition.h(0) +circuit = Circuit(1) +circuit.append_circuit_gate(definition.to_gate("custom_h"), [0]) + +expanded = expand_definitions(circuit) +assert expanded.changed is True +assert [op.instruction.instruction.name for op in circuit.operations] == ["custom_h"] +assert [op.instruction.instruction.name for op in expanded.circuit.operations] == ["H"] + +matrix = np.array([[0, 1], [1, 0]], dtype=np.complex128) +gate = UnitaryGate("x_matrix", 1).with_matrix(matrix) +circuit = Circuit(2) +circuit.append_unitary_gate(gate, [0]) +circuit.append_unitary_gate(gate, [1]) + +result, stats = decompose_unitaries_with_rule_stats(circuit) +assert result.changed is True +assert all( + operation.instruction.instruction.name == "U" + for operation in result.circuit.operations +) +assert (stats.hits, stats.misses, stats.inserts) == (1, 1, 1) +``` + +### 2. Numeric unitary synthesis + +```python +import numpy as np + +from cqlib.circuit import Circuit, Qubit, UnitaryGate +from cqlib.compile.transform.decompose import TwoQubitUnitaryDecomposeBasis +from cqlib.compile.transform.decompose.unitary import ( + kak_decompose, + synthesize_numeric_1q_unitary, + synthesize_numeric_2q_unitary, +) + +source = ( + np.exp(0.37j) + * np.array([[1, 1], [1, -1]], dtype=np.complex128) + / np.sqrt(2) +) +decomposition = synthesize_numeric_1q_unitary(source) +circuit = Circuit(1) +circuit.u(0, decomposition.theta, decomposition.phi, decomposition.lambda_) +reconstructed = np.exp(1j * decomposition.global_phase) * circuit.to_matrix() +np.testing.assert_allclose(reconstructed, source, atol=1e-10) + +swap = np.array( + [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]], + dtype=np.complex128, +) +synthesis = synthesize_numeric_2q_unitary( + swap, 0, Qubit(1), TwoQubitUnitaryDecomposeBasis.cx() +) +expected = Circuit(2) +expected.append_unitary_gate(UnitaryGate("source", 2).with_matrix(swap), [0, 1]) +synthesized = Circuit.from_operations([Qubit(0), Qubit(1)], synthesis.operations) +synthesized.set_global_phase(synthesis.global_phase) +np.testing.assert_allclose( + synthesized.to_matrix(), expected.to_matrix(), atol=1e-8 +) + +kak = kak_decompose(swap) +assert kak.k1l.shape == (2, 2) +assert isinstance(kak.global_phase, float) +``` + +### 3. Multi-controlled gate decomposition + +```python +from cqlib.circuit import Circuit, MCGate, StandardGate +from cqlib.compile.resource import ResourceLimits, ResourcePolicy +from cqlib.compile.transform.decompose import ( + McGateDecomposeConfig, + decompose_mc_gates_with_rule_stats, +) + +circuit = Circuit(4) +gate = MCGate(3, StandardGate.X) +circuit.append_mc_gate(gate, [0, 1, 2, 3]) +circuit.append_mc_gate(gate, [0, 1, 2, 3]) + +config = McGateDecomposeConfig( + resource_policy=ResourcePolicy( + max_pre_layout_clean_ancillas=2, + allow_dirty_borrowing=True, + ), + resource_limits=ResourceLimits(max_total_qubits=8), +) +result, stats = decompose_mc_gates_with_rule_stats(circuit, config) +assert result.changed is True +assert all( + not operation.instruction.instruction.is_mcgate + for operation in result.circuit.operations +) +assert stats.inserts >= 1 +``` + +### 4. Two-qubit block resynthesis and target gate set translation + +```python +from cqlib.circuit import Circuit +from cqlib.compile.transform.decompose import TwoQubitUnitaryDecomposeBasis +from cqlib.compile.transform.resynthesis import ( + ResynthesizeTwoQubitBlocks, + TwoQubitBlockResynthesisConfig, + resynthesize_two_qubit_blocks, +) +from cqlib.compile.transform.target_basis import TargetBasisLowerer + +circuit = Circuit(2) +circuit.cx(0, 1) +circuit.cx(0, 1) +config = TwoQubitBlockResynthesisConfig( + two_qubit_basis=TwoQubitUnitaryDecomposeBasis.cx() +) + +result = resynthesize_two_qubit_blocks(circuit, config) +transformer = ResynthesizeTwoQubitBlocks(config) +assert transformer.config == config +assert result.changed is True +assert len(result.circuit.operations) == 0 + +lowerer = TargetBasisLowerer(["h", "CZ"]) +assert [instruction.name for instruction in lowerer.target_basis] == ["H", "CZ"] +``` + +--- + +## Raises + +- `CompilerTransformError`: decomposition or synthesis fails, for example the matrix is not unitary, the target gate set cannot cover the input operations, the number of ancillas is insufficient, or qubits are duplicated. +- `CompilerConfigError`: the configuration is invalid, for example an empty target gate set, `two_qubit_basis` and `target_basis` both provided, or the circuit width exceeding the device capacity. +- `ParameterError`: a gate parameter is not a finite value. +- `ValueError`: a primitive requiring exactly two ancillas received a different number. +- `ResourceError`: multi-controlled gate decomposition fails to request ancillas, see [Resource](8_resource.md). diff --git a/docs/api/en/python/4_compile/7_knowledge.md b/docs/api/en/python/4_compile/7_knowledge.md new file mode 100644 index 00000000..7f48daf8 --- /dev/null +++ b/docs/api/en/python/4_compile/7_knowledge.md @@ -0,0 +1,284 @@ +# Knowledge + +`cqlib.compile.knowledge` provides compilation knowledge rules, a validated rule library and structural matching capabilities. A knowledge rule describes an adjacent operation pattern, optional symbolic parameter conditions and the operation sequence that replaces the pattern; it is the source of the rules for knowledge rule rewriting and gate decomposition. + +## Import + +```python +from cqlib.compile.knowledge import ( + Rule, + RuleItem, + Condition, + RuleId, + RuleKind, + RuleMetadata, + RuleLibrary, + MatchBindings, + loads, + load, + dumps, + dump, + match_rule_item, + conditions_hold, + instantiate_target, + rule_matches_operations, +) +``` + +--- + +## Rule construction + +Qubit labels within a rule are non-negative integers and must be contiguous starting from zero. They are placeholders: label `0` may bind to any concrete `Qubit` during matching. + +### RuleItem + +A single operation in a rule pattern or replacement target. + +#### Static methods + +- `RuleItem.standard(gate, qubits)`: a standard gate item, preserving the parameters bound to the gate. `qubits` is the list of qubit labels within the rule. +- `RuleItem.mc_gate(gate, qubits)`: a multi-controlled gate item, preserving the bound parameters. + +#### Attributes and methods + +- `instruction -> Instruction`: the item instruction. +- `qubits -> list[int]`: the qubit labels within the rule. +- `params -> list[...]`: the parameters. +- `symbols() -> set[str]`: the symbols appearing in the item. +- `validate()`: validate the item; raises `ValueError` when it is invalid. +- `equivalent_to(other) -> bool`: item equivalence test. + +### Condition + +A symbolic parameter condition for rule matching. + +#### Static methods + +- `Condition.equal(lhs, rhs)`: the constraint `lhs == rhs`. +- `Condition.equal_mod(lhs, rhs, modulus)`: the constraint `lhs == rhs (mod modulus)`. + +#### Attributes + +- `kind -> str`: the condition kind, taking the values `"equal"` or `"equal_mod"`. +- `lhs`, `rhs`, `modulus`: the condition operands. + +#### Methods + +- `symbols() -> set[str]`: the symbols appearing in the condition. + +### Rule(name, operations, target, conditions=None) + +Parameters: + +- `name` (`str`): the rule name. +- `operations` (`list[RuleItem]`): the adjacent operation pattern to be matched. +- `target` (`list[RuleItem]`): the replacement target, which may be an empty list (meaning cancellation and deletion). +- `conditions` (`list[Condition] | None`): the symbolic parameter conditions. + +#### Attributes and methods + +- `name -> str` +- `operations -> list[RuleItem]` +- `conditions -> list[Condition]` +- `target -> list[RuleItem]` +- `num_qubits -> int`: the number of qubit labels involved in the rule. +- `validate()`: structural validation; raises `ValueError` when it is invalid. +- `verify() -> VerifyResult`: the rule validation result. +- `verify_by_sampling(num_bindings, tolerance) -> VerifyResult`: sampling validation; `num_bindings` must be greater than zero. +- `needs_sampling_fallback() -> bool`: whether a sampling fallback is needed. +- `free_symbols() -> set[str]` +- `operation_qubits() -> set[int]` +- `target_qubits() -> set[int]` + +#### VerifyResult + +Attributes: + +- `status -> str`: the validation status. +- `passed -> bool`: whether it passed. +- `num_bindings -> int`: the number of bindings validated. +- `reason -> str | None`: the reason it did not pass. + +Rule construction example: + +```python +from cqlib.circuit import StandardGate +from cqlib.compile.knowledge import Rule, RuleItem + +cancel_h = Rule( + "cancel_h", + operations=[ + RuleItem.standard(StandardGate.H, [0]), + RuleItem.standard(StandardGate.H, [0]), + ], + target=[], +) +cancel_h.validate() +assert cancel_h.verify().passed +``` + +--- + +## RuleKind + +Rule kinds: + +- `RuleKind.simplify()`: simplification. +- `RuleKind.cancel()`: cancellation. +- `RuleKind.merge()`: merging. +- `RuleKind.commute()`: commutation. +- `RuleKind.decompose()`: decomposition. +- `RuleKind.canonicalize()`: canonicalization. +- `RuleKind.hardware_native()`: hardware native. +- `RuleKind.other()`: other. + +Attributes and methods: + +- `name -> str`: the kind name. +- `label() -> str`: the kind label. + +--- + +## RuleLibrary + +Rule library, responsible for loading, validating, classifying and querying rules. + +### Construction and loading + +- `RuleLibrary()`: an empty rule library. +- `RuleLibrary.builtin()`: the built-in rule library. +- `RuleLibrary.from_rules(rules, kind)`: construct from a rule list; all rules are assigned to the `kind` category. +- `RuleLibrary.from_dsl(source, kind)`: parse and construct from a `.rule` DSL string. +- `RuleLibrary.from_dsl_file(path, kind)`: parse and construct from a `.rule` DSL file. + +### Modification + +- `add_rule(rule, kind) -> RuleId`: add a rule and return its ID. +- `extend_rules(rules, kind) -> list[RuleId]`: add rules in bulk. + +### Queries + +- `rules() -> list[Rule]` +- `get(id) -> Rule | None` +- `metadata(id) -> RuleMetadata | None` +- `id_by_name(name) -> RuleId | None` +- `get_by_name(name) -> Rule | None` +- `name in library -> bool` (`__contains__`) +- `len(library) -> int` +- `candidates_for_first_instruction(instruction) -> list[RuleId]`: filter candidate rules by first instruction. +- `rules_by_kind(kind) -> list[RuleId]` +- `filter_rule_ids_by_instruction_keys(op_instructions, target_instructions) -> list[RuleId]`: filter rule IDs by operation and target instruction keys. + +### RuleId + +- `index -> int`: the index within the library; supports `int(rule_id)`. + +### RuleMetadata + +Rule metadata: + +- `id -> RuleId` +- `kind -> RuleKind` +- `pattern_len -> int`: the pattern length. +- `rewrite_len -> int`: the replacement length. +- `qubit_count -> int`: the number of qubits involved. +- `first_instruction -> Instruction`: the first instruction. +- `cost_delta -> int`: the cost change. +- `has_conditions -> bool`: whether conditions are present. + +--- + +## DSL reading and writing + +### loads(source) -> list[Rule] + +Parse a rule list from a `.rule` DSL string. + +### load(path) -> list[Rule] + +Parse a rule list from a `.rule` DSL file. + +### dumps(rules) -> str + +Serialize a rule list to a DSL string. + +### dump(rules, path) + +Write a rule list to a DSL file. + +Example: + +```python +from cqlib.compile.knowledge import loads + +rule = loads(''' +rule cancel_x { + match { X 0, X 0 } + rewrite {} +} +''')[0] +rule.validate() +assert rule.verify().passed +``` + +--- + +## Structural matching + +### MatchBindings + +Match binding result. + +Attributes and methods: + +- `qubits() -> dict[int, Qubit]`: the binding from rule qubit labels to concrete qubits. +- `qubit(rule_qubit) -> Qubit | None`: the binding of a single label. +- `params() -> dict[str, Parameter]`: the symbolic parameter bindings. +- `param(symbol) -> Parameter | None`: the binding of a single symbol. + +### rule_matches_operations(rule, operations) -> MatchBindings | None + +Match a rule against an adjacent `ValueOperation` sequence; returns `None` when there is no match. + +### match_rule_item(item, operation, bindings) -> bool + +Match a single `RuleItem` against one `ValueOperation`, updating the bindings in place. + +### conditions_hold(conditions, bindings) -> bool + +Check whether the conditions hold under the given bindings. + +### instantiate_target(target, bindings) -> list[ValueOperation] + +Instantiate the replacement target with the bindings. + +Example: + +```python +from cqlib import Circuit, Qubit +from cqlib.circuit import StandardGate, ValueOperation +from cqlib.compile.knowledge import loads, rule_matches_operations + +rule = loads(''' +rule cancel_x { + match { X 0, X 0 } + rewrite {} +} +''')[0] + +operations = [ + ValueOperation.from_standard_gate(StandardGate.X, [Qubit(3)]), + ValueOperation.from_standard_gate(StandardGate.X, [Qubit(3)]), +] + +bindings = rule_matches_operations(rule, operations) +assert bindings is not None +assert bindings.qubit(0) == Qubit(3) +``` + +--- + +## Raises + +- `ValueError`: the rule structure is invalid, DSL parsing fails, rule library construction or validation fails (error messages carry prefixes such as `Rule library error: ...`), or the sampling validation parameters are invalid. diff --git a/docs/api/en/python/4_compile/8_resource.md b/docs/api/en/python/4_compile/8_resource.md new file mode 100644 index 00000000..24373fcd --- /dev/null +++ b/docs/api/en/python/4_compile/8_resource.md @@ -0,0 +1,149 @@ +# Resource + +`cqlib.compile.resource` provides the request, planning, borrowing and reclamation management of ancilla qubits. + +Clean resources and dirty resources are restoration contracts, not state simulation results. Clean resources must be in `|0>` when entering and leaving the transform; dirty resources may be in an unknown or entangled state, and their input state must be fully restored before the transform releases the lease. + +## Import + +```python +from cqlib.compile.resource import ( + AncillaRequirement, + ResourcePolicy, + ResourceLimits, + ResourceRequest, + ResourcePlan, + ResourceLease, + ResourceManager, + ResourceError, + ResourceUnavailableError, +) +``` + +--- + +## Resource contract objects + +### AncillaRequirement + +Resource kinds: + +- `AncillaRequirement.clean_zero()`: clean ancilla, in `|0>` when entering and leaving the transform. +- `AncillaRequirement.dirty()`: dirty ancilla, whose original state must be fully restored before release. + +Supports `==`, `hash`, `copy` and `deepcopy`. + +### ResourceRequest(requirement, count, *, excluded=None) + +Resource request: + +- `requirement` (`AncillaRequirement`): the resource kind. +- `count` (`int`): the requested count. +- `excluded` (`list[Qubit] | None`): the qubits that cannot be used. + +Attributes: + +- `requirement`, `count`, `excluded`, corresponding one-to-one with the constructor parameters. + +### ResourcePlan + +Request preview result, returned by `ResourceManager.preview()`; it does not actually occupy resources. + +Attributes: + +- `qubits -> list[Qubit]`: the planned qubits. +- `requirement -> AncillaRequirement`: the resource kind. +- `num_new_qubits -> int`: the number of new qubits the plan introduces. + +### ResourceLease + +An effective lease, returned by `ResourceManager.commit()`. + +Attributes: + +- `id -> int`: the lease ID. +- `qubits -> list[Qubit]`: the borrowed qubits. +- `requirement -> AncillaRequirement`: the resource kind. + +--- + +## ResourcePolicy / ResourceLimits + +### ResourcePolicy(*, max_pre_layout_clean_ancillas=0, allow_dirty_borrowing=false) + +Ancilla permissions of the pre-layout decomposition stage: + +- `max_pre_layout_clean_ancillas` (`int`): the upper limit on the logical clean ancillas allowed to be allocated. +- `allow_dirty_borrowing` (`bool`): whether borrowing dirty input qubits is allowed. + +A device target derives a hard capacity from the available physical qubits and is not restricted by this policy. + +### ResourceLimits(*, max_total_qubits=None) + +Resource limits: + +- `max_total_qubits` (`int | None`): the upper limit on the total number of qubits. + +--- + +## ResourceManager + +Resource manager, separating side-effect-free planning from actual modification. + +### ResourceManager.from_circuit(circuit, *, policy=None, limits=None) + +Static method that constructs a manager from a circuit. The manager must evolve in sync with the circuit. + +Parameters: + +- `circuit` (`Circuit`): the circuit tracked by the manager. +- `policy` (`ResourcePolicy | None`): defaults to `ResourcePolicy()`. +- `limits` (`ResourceLimits | None`): the resource limits. + +### Methods + +- `preview(request) -> ResourcePlan`: preview a request. Does not occupy resources and does not modify the circuit. +- `commit(circuit, plan) -> ResourceLease`: actually occupy resources according to the plan and modify the circuit. +- `release(lease)`: release a lease. +- `enter_post_layout(circuit)`: enter the post-layout stage. +- `verify_consistency(circuit)`: verify that the manager and the circuit are consistent. +- `verify_idle(circuit)`: verify that all leases have been released. + +### Usage conventions + +A transform that consumes resources uses `lease.qubits` while the lease is effective and must complete restoration before release (a clean ancilla returns to `|0>`, a dirty ancilla has its original state restored). Ordinary compilation only needs to pass a `ResourcePolicy` to `compile()`; the manager lifecycle is maintained internally by the compiler. + +Example: + +```python +from cqlib import Circuit +from cqlib.compile.resource import ( + AncillaRequirement, + ResourceManager, + ResourcePolicy, + ResourceRequest, +) + +circuit = Circuit(2) +manager = ResourceManager.from_circuit( + circuit, + policy=ResourcePolicy(max_pre_layout_clean_ancillas=1), +) + +request = ResourceRequest(AncillaRequirement.clean_zero(), 1) + +# 预览不占用资源、不改变线路 +plan = manager.preview(request) +lease = manager.commit(circuit, plan) + +# 消耗资源的变换在此使用 lease.qubits,之后须恢复 |0> +manager.release(lease) +manager.verify_idle(circuit) +``` + +--- + +## Raises + +- `ResourceError`: the resource management error base class. +- `ResourceUnavailableError`: insufficient resources or an invalid lease state. diff --git a/docs/api/en/python/4_compile/9_commutation.md b/docs/api/en/python/4_compile/9_commutation.md new file mode 100644 index 00000000..88342e0d --- /dev/null +++ b/docs/api/en/python/4_compile/9_commutation.md @@ -0,0 +1,100 @@ +# Commutation + +`cqlib.compile.commutation` provides conservative proofs for the exchange of quantum operations. This module proves whether the semantics of a circuit are preserved after two concrete `ValueOperation` objects are exchanged. Returning `None` means the configured proof sources cannot establish commutation; it does not prove that the two operations do not commute. + +## Import + +```python +from cqlib.compile.commutation import ( + Commutation, + CommutationConfig, + CommutationChecker, + check_commutation, + algebraic_commutation, +) +``` + +--- + +## Functions + +### check_commutation(lhs, rhs) -> Commutation | None + +Use the shared built-in checker to decide whether two self-contained operations commute. + +### algebraic_commutation(lhs, rhs) -> Commutation | None + +Decide commutation using only the symbolic algebra proof sources. + +Example: + +```python +from cqlib import Circuit, Qubit +from cqlib.circuit import StandardGate, ValueOperation +from cqlib.compile.commutation import check_commutation + +lhs = ValueOperation.from_standard_gate(StandardGate.H, [Qubit(0)]) +rhs = ValueOperation.from_standard_gate(StandardGate.X, [Qubit(1)]) + +proof = check_commutation(lhs, rhs) +assert proof is not None and proof.is_exact() +``` + +--- + +## Commutation + +Commutation proof result. + +### Static methods + +- `Commutation.exact()`: an exact commutation proof. +- `Commutation.up_to_global_phase(phase)`: a commutation proof up to a global phase. + +### Attributes and methods + +- `phase -> Parameter`: the phase carried by the proof. +- `is_exact() -> bool`: whether the commutation is exact. + +--- + +## CommutationConfig + +Commutation checker configuration. + +### CommutationConfig(*, enable_rule_oracle=true, enable_matrix_fallback=true, max_matrix_qubits=4) + +Parameters: + +- `enable_rule_oracle` (`bool`): enable the rule oracle (the commute rules from the knowledge library). +- `enable_matrix_fallback` (`bool`): enable the matrix fallback. +- `max_matrix_qubits` (`int`): the maximum number of qubits allowed for the matrix fallback. + +### Attributes + +`enable_rule_oracle`, `enable_matrix_fallback`, `max_matrix_qubits`, corresponding one-to-one with the constructor parameters. + +--- + +## CommutationChecker + +Configurable commutation checker. + +### Static methods + +- `CommutationChecker.builtin()`: built-in rules and the default configuration. +- `CommutationChecker.with_config(config)`: built-in rules and an explicit configuration. +- `CommutationChecker.from_library(library, config=None)`: use the commute rules of a loaded rule library (that is, the `RuleKind.commute()` category) in proofs. `config=None` uses the default configuration, consistent with `builtin()`. Raises `ValueError` when `enable_rule_oracle` is disabled; otherwise the rule library is silently ignored. + +### Attributes and methods + +- `config -> CommutationConfig`: a copy of the current configuration. +- `check(lhs, rhs) -> Commutation | None`: decide whether two self-contained operation values commute. + +--- + +## Notes + +- Proof results are conservative: `None` only means that a proof cannot be established, not that the operations do not commute. +- Operations related to classical control flow are outside the proof scope of this checker. +- Operations with symbolic parameters keep their symbolic parameters unchanged when participating in a proof. diff --git a/docs/api/en/python/5_visualization/0_overview.md b/docs/api/en/python/5_visualization/0_overview.md new file mode 100644 index 00000000..761cf2fc --- /dev/null +++ b/docs/api/en/python/5_visualization/0_overview.md @@ -0,0 +1,208 @@ +# Visualization + +`cqlib.visualization` + +`cqlib.visualization` renders quantum circuits, quantum states and measurement results into visualization output. It takes SVG as the primary output format: every rendering entry point generates SVG text in memory as its return value, and writes the same SVG to file when an output path is given; for scenarios that need to inspect a circuit in a terminal or a plain-text environment, text circuit diagrams are provided as well. + +`cqlib.visualization` mainly covers the following capabilities: + +- **Circuit rendering**: draw a circuit as an SVG circuit diagram or a text circuit diagram, for checking gate order, qubit connections, parameter values and folding effects. +- **Quantum state rendering**: present quantum state information as a single Bloch vector, a reduced Bloch vector per qubit, a state matrix and Pauli expectation values. +- **Measurement result rendering**: render the counts of an execution result as a counts histogram or a normalized probability distribution plot. +- **Unified output contract**: all rendering entry points share the SVG text return value and the output path writing convention. + +## Import + +```python +from cqlib.visualization import ( + draw_text, + draw_figure, + plot_histogram, + plot_distribution, + plot_bloch_vector, + plot_bloch_multivector, + plot_state_city, + plot_state_paulivec, +) +``` + +--- + +## Overview + +The rendering entry points fall into three groups by input object; the input types and output of each group are as follows: + +| Input | Rendering entry points | Output | +| --- | --- | --- | +| Circuit (`Circuit`) | `draw_text`, `draw_figure` | Text circuit diagram, SVG circuit diagram | +| Quantum state (`Statevector` or `DensityMatrix`) | `plot_bloch_vector`, `plot_bloch_multivector`, `plot_state_city`, `plot_state_paulivec` | SVG state plot | +| Execution result (`ExecutionResult`) | `plot_histogram`, `plot_distribution` | SVG counts histogram, SVG probability distribution plot | + +### Unified output contract + +All rendering entry points follow the same output flow: + +1. Call the rendering entry point to get the return value, which is the text content of the rendering output. Except for `draw_text`, which returns a text circuit diagram, the other entry points return SVG text, whose type is the `str` subclass `_InlineSvg`. +2. In a notebook frontend, using the return value as the last expression of a cell displays it inline; `_InlineSvg` accomplishes this through the SVG rich display protocol. See [Rendering output and file writing](5_render_to_file.md). +3. When `output_path` is passed, the same SVG is written to that path at the same time. When the extension is `.png` the same SVG is rasterized to PNG, and for other extensions the SVG text is written directly. + +### Grouping of options + +The option sets of the three groups of entry points are independent of each other, and all parameters after `*` in the signatures are keyword arguments: + +- **Circuit options**: `initial_state`, `reverse_bits`, `show_params` and `decompose_circuit_gates` are shared by the two circuit entry points; `fold` is provided only by `draw_figure`, and `line_width` only by `draw_text`. +- **State options**: `title`, `color`, `alpha`, `reverse_bits`, `figsize`; the four state rendering entry points have identical signatures. +- **Result options**: `figsize`, `color`, `number_to_keep`, `sort`, `target_string`, `legend`, `bar_labels`, `title`; the two result rendering entry points have identical signatures. + +An option of the same name has the same meaning across the three groups: `reverse_bits` controls whether the display of the computational basis qubit order is reversed, `figsize` controls the plot size, `title` controls the title text, and `output_path` controls whether the output is additionally written to file. + +--- + +## Common entry points + +```python +from cqlib import Circuit +from cqlib.qis.state import Statevector +from cqlib.visualization import draw_text, plot_bloch_multivector + +circuit = Circuit(2) +circuit.h(0) +circuit.cx(0, 1) + +print(draw_text(circuit)) + +state = Statevector(2) +state.apply_h(0) +state.apply_cx(0, 1) + +svg = plot_bloch_multivector(state) +assert svg.startswith("` at the start of each qubit line. +- `reverse_bits` (`bool`): whether to reverse the display order of the qubits. +- `show_params` (`bool`): whether to append the gate parameters after the gate label. +- `decompose_circuit_gates` (`bool`): whether to first expand circuit gates into their internal operations before drawing. + +All parameters after `line_width` are keyword arguments. + +Returns: + +- `str`: the text circuit diagram. This entry point forwards directly to the underlying implementation, and the return value is an ordinary string that does not provide inline display; use [SVG circuit diagrams](2_draw_figure.md) when the rendering result needs to be displayed inline. + +Raises: + +- `TypeError`: `circuit` is not a circuit object. +- `ValueError`: circuit preprocessing fails, or an operation in the circuit references a qubit that does not exist. + +--- + +## Rendering behavior + +- Each qubit line is drawn with box-drawing characters (such as `─`, `│`), and gates are annotated on the line in the form of character boxes. +- With `initial_state=True`, `|0>` is displayed at the start of the line, to emphasize the initial state assumption of the circuit. +- With `show_params=True`, rotation gates that carry parameters include the parameter values in their label; when disabled, only the gate name is displayed. +- With `reverse_bits=True`, the highest-numbered qubit is displayed at the top, making it easier to read in alignment with bitstrings written in bit order. +- With `decompose_circuit_gates=True`, circuit gates are first expanded into their internal operations, and the layer of circuit gates no longer appears in the text drawing. + +--- + +## Examples + +### 1. Render a text drawing of a circuit + +```python +from cqlib import Circuit +from cqlib.visualization import draw_text + +circuit = Circuit(2) +circuit.h(0) +circuit.cx(0, 1) +circuit.measure(0) +circuit.measure(1) + +text = draw_text(circuit) +assert "H" in text +``` + +### 2. Control folding and displayed content + +```python +from cqlib import Circuit +from cqlib.visualization import draw_text + +circuit = Circuit(3) +circuit.h(0) +circuit.rx(1, 0.25) +circuit.cx(0, 2) +circuit.barrier([0, 1, 2]) +circuit.swap(1, 2) +circuit.measure(0) +circuit.measure(1) +circuit.measure(2) + +text = draw_text( + circuit, + line_width=40, + initial_state=True, + reverse_bits=True, + show_params=False, +) +``` + +This example only renders a text circuit diagram and produces no file; to render the same circuit as an SVG that can be written to file, use [draw_figure](2_draw_figure.md). diff --git a/docs/api/en/python/5_visualization/2_draw_figure.md b/docs/api/en/python/5_visualization/2_draw_figure.md new file mode 100644 index 00000000..23b8842f --- /dev/null +++ b/docs/api/en/python/5_visualization/2_draw_figure.md @@ -0,0 +1,102 @@ +# SVG Circuit Diagrams + +`cqlib.visualization.draw_figure` + +`draw_figure` renders a circuit into an SVG circuit diagram. Compared with a text circuit diagram, it represents operations such as quantum gates, connection lines, barriers and measurements with graphical symbols, is suitable for presentation in documents, web pages and notebooks, and can be written to file directly through an output path. + +## Import + +```python +from cqlib.visualization import draw_figure +``` + +--- + +## Functions + +### draw_figure(circuit, *, fold=None, initial_state=False, reverse_bits=False, show_params=True, decompose_circuit_gates=False, output_path=None) + +Render an SVG circuit diagram of the circuit. + +Parameters: + +- `circuit` (`Circuit`): the circuit to render. +- `fold` (`int | None`): the maximum number of columns drawn per row; a negative value means no folding; when not passed, the default value of 18 is used. +- `initial_state` (`bool`): whether to display `|0>` in the qubit labels. +- `reverse_bits` (`bool`): whether to reverse the display order of the qubits. +- `show_params` (`bool`): whether to append the gate parameters after the gate label. +- `decompose_circuit_gates` (`bool`): whether to first expand circuit gates into their internal operations before drawing. +- `output_path` (`str | None`): the output path. When given, the same SVG is written to that path; when the extension is `.png` the SVG is rasterized to PNG, and for other extensions the SVG text is written directly. + +All parameters after `circuit` are keyword arguments. + +Returns: + +- `_InlineSvg`: SVG text, whose type is a subclass of `str`; it is displayed inline when used as the last expression of a cell in a notebook frontend. Whether or not `output_path` is given does not affect the content of the return value. + +Raises: + +- `TypeError`: `circuit` is not a circuit object. +- `ValueError`: circuit preprocessing fails, an operation references a qubit that does not exist, or PNG rasterization fails. +- `IOError`: writing to the path pointed at by `output_path` fails; `IOError` is `OSError`. + +--- + +## Rendering behavior + +- `fold` controls folding: the circuit is split by columns into multiple rows, each row not exceeding the given number of columns; a negative value means no folding, and the whole circuit is drawn on one row; when not passed, the default number of columns is used. +- With `initial_state=True`, qubit labels take the form `q2 |0>`; with `reverse_bits=True` the display order is reversed, and the highest-numbered qubit is displayed at the top. +- With `show_params=False`, parameter values do not appear in the gate labels. +- With `decompose_circuit_gates=True`, circuit gates are first expanded into their internal operations, and the layer of circuit gates no longer appears in the drawing. + +--- + +## Examples + +### 1. Render a circuit diagram with the default style + +```python +from cqlib import Circuit +from cqlib.visualization import draw_figure + +circuit = Circuit(2) +circuit.h(0) +circuit.cx(0, 1) +circuit.measure(0) +circuit.measure(1) + +svg = draw_figure(circuit, output_path="bell.svg") +assert svg._repr_svg_() == str(svg) +assert "sim" in svg +assert ">rest" in svg +``` + +### 3. Probability distribution plot + +```python +from cqlib.device import ExecutionResult +from cqlib.visualization import plot_distribution + +result = ExecutionResult.from_counts( + "vis-test", + [0], + 100, + 1, + {"0": 25, "1": 75}, +) + +svg = plot_distribution( + result, + figsize=(3.2, 2.4), + color=["#0f766e"], + legend=["probability"], + bar_labels=False, + title="Distribution options", + output_path="distribution.svg", +) +assert 'width="320"' in svg +assert 'height="240"' in svg +assert "#0f766e" in svg +assert ">probability" in svg +``` diff --git a/docs/api/en/python/5_visualization/5_render_to_file.md b/docs/api/en/python/5_visualization/5_render_to_file.md new file mode 100644 index 00000000..692dbb50 --- /dev/null +++ b/docs/api/en/python/5_visualization/5_render_to_file.md @@ -0,0 +1,108 @@ +# Rendering Output and File Writing + +`cqlib.visualization` + +This page explains the output convention shared by the rendering entry points of `cqlib.visualization`: the relationship between the SVG text return value and writing to file through `output_path`, the meaning of the two type aliases `FigureSize` and `QuantumState`, and the inline display behavior of the return value in a notebook. + +## Import + +```python +from cqlib.visualization import FigureSize, QuantumState +``` + +--- + +## Output path and return value + +Every rendering entry point first generates the output text in memory and then writes it to file as needed: + +- **Return value**: the text content of the rendering output. Except for `draw_text`, which returns a text circuit diagram, the other entry points return SVG text. +- **`output_path`**: an optional keyword argument. When given, the same SVG is written to that path. The write does not change the content of the return value; both come from the same rendering result. +- **Extension rules**: when the extension is `.png` (case-insensitive), the same SVG is rasterized into a PNG file; for other extensions (including `.svg` and no extension) the SVG text is written directly. + +```python +from cqlib import Circuit +from cqlib.visualization import draw_figure + +circuit = Circuit(2) +circuit.h(0) +circuit.cx(0, 1) + +# 只取返回值:SVG 文本留在内存中,不产生文件 +svg = draw_figure(circuit) + +# 同时落盘:返回值不变,同一份 SVG 另写入 "bell.svg" +svg = draw_figure(circuit, output_path="bell.svg") + +# 扩展名为 .png:返回值不变,同一份 SVG 另光栅化为 "bell.png" +svg = draw_figure(circuit, output_path="bell.png") +``` + +Raises: + +- `IOError`: writing to the path pointed at by `output_path` fails; `IOError` is `OSError`. +- `ValueError`: PNG rasterization fails, for example the SVG cannot be parsed or the target size exceeds the representable range. + +--- + +## FigureSize + +```python +FigureSize = tuple[float, float] +``` + +Plot size type alias, used by the `figsize` parameter of the rendering entry points. The two elements are the width and the height in inches. + +- When `figsize` is not passed, the default size of that plot family is used. +- At render time the inches are converted into the pixel size of the SVG canvas; the default sizes and conversion lower bounds differ between plot families, so the same values may yield different canvas sizes in different plot families. +- A too-small value is handled according to the lower bound of that plot family, so a degenerate canvas is never produced. + +```python +from cqlib.device import ExecutionResult +from cqlib.visualization import plot_distribution + +result = ExecutionResult.from_counts("vis-test", [0], 100, 1, {"0": 25, "1": 75}) + +svg = plot_distribution(result, figsize=(3.2, 2.4)) +assert 'width="320"' in svg +assert 'height="240"' in svg +``` + +--- + +## QuantumState + +```python +QuantumState = Statevector | DensityMatrix +``` + +The state type alias accepted by the state rendering entry points, taking the value `cqlib.qis.Statevector` or `cqlib.qis.DensityMatrix`. The two state types are interchangeable, and rendering the same quantum state with either input yields the same result; see [Quantum state visualization](3_state_plots.md) for details. + +When an object that is neither a `Statevector` nor a `DensityMatrix` is passed, the state rendering entry points report it as a `ValueError`. + +--- + +## Inline display + +The SVG return value of the rendering entry points is not an ordinary string but the `str` subclass `_InlineSvg`. It keeps all the behavior of a string while implementing the SVG rich display protocol: + +- `_repr_svg_()` returns the SVG text itself, on the basis of which a notebook frontend renders the return value directly as a graphic. +- Inline display is triggered only when the return value is used as the last expression of a cell; assigning the return value to a variable or storing it in a container does not display it automatically, and the variable needs to be evaluated explicitly. +- Since the return value is a subclass of `str`, it can take part directly in string comparison, concatenation and file writing. + +```python +from cqlib import Circuit +from cqlib.visualization import draw_figure + +circuit = Circuit(2) +circuit.h(0) +circuit.cx(0, 1) +circuit.measure(0) +circuit.measure(1) + +svg = draw_figure(circuit, output_path="bell.svg") +assert svg._repr_svg_() == str(svg) +assert " U (U† U)^level`, so that the circuit depth grows with the level; selective folding performs the same expansion only on operations whose instruction name matches the given gate set, leaving the other operations as they are. When the level is `0` the mitigation circuit is a copy of the input circuit. +- Virtual distillation copies the circuit `copies` times and inserts SWAPs qubit-wise, yielding a copy-swap circuit of `copies` times the width. The numerator and the denominator are executed on the same circuit, differing only in the observable passed to the callback: the denominator is `None` and the numerator is the `Hamiltonian` expanded by the number of copies. + +### Data flow + +```text +输入线路 + 观测量 + ↓ +构造缓解线路族(折叠线路 / copy-swap 线路) + ↓ +回调逐条估计,返回 (期望值, 方差) + ↓ +后处理:外推到零噪声点 / 分子分母求比值 + ↓ +MitigatedResult(expectation, variance) +``` + +The two post-processing procedures handle `variance` differently: zero-noise extrapolation uses only the expectation value sequence and the `variance` of the result is `None`; virtual distillation uses both the expectation values and the variances, and the `variance` of the result has a value. + +--- + +## Common entry points + +```python +from cqlib.circuit import Circuit +from cqlib.error_mitigation import ( + ErrorMitigation, + ExtrapolateMethod, + MitigationMethod, + ProcessArgs, + RunArgs, + ZneConfig, +) +from cqlib.qis import Hamiltonian, PauliString + +circuit = Circuit(1) +circuit.x(0) + +hamiltonian = Hamiltonian.from_list([(PauliString.from_str("Z"), 1.0)]) + + +def estimator(run_circuit, observable, shots): + # 替换为后端或模拟器的期望值估计 + return (len(run_circuit.operations) + 0.5, 0.0) + + +mitigation = ErrorMitigation( + circuit, + MitigationMethod.zne(ZneConfig([0, 1, 2])), +) + +mitigation.run(hamiltonian, RunArgs.zne(shots=256), estimator) +result = mitigation.get_mitigated( + ProcessArgs.zne(ExtrapolateMethod.polynomial(), degree=1) +) + +print("expectation:", result.expectation) +print("variance:", result.variance) +``` + +The call order is constrained by the object itself: each `ErrorMitigation` instance can be `run()` only once and `get_mitigated()` only once, and `run()` must be called before `get_mitigated()`. + +--- + +## Core concepts and terms + +| Term | Description | +| --- | --- | +| **Error mitigation** | Processing that reduces the systematic bias of an observable at the expectation value level; it does not change the unitary semantics of the circuit and is not equivalent to quantum error correction. | +| **Fold level** | The non-negative integer controlling the number of circuit expansions in zero-noise extrapolation, given by `fold_levels`. | +| **Noise factor** | The noise amplification factor derived from the fold level, satisfying `noise_factor = 2 * fold_level + 1`. | +| **Global folding** | The expansion `U -> U (U† U)^level` applied to the whole circuit when `gate_set` is `None`. | +| **Selective folding** | Folding applied only to operations whose instruction name matches `gate_set`, leaving the other operations as they are. | +| **Extrapolation method** | The fitting model that extrapolates expectation values at different noise factors to the zero-noise point, including the polynomial and exponential kinds. | +| **Copies** | The number of copies of the density matrix in virtual distillation, given by `copies`, with a minimum of 2. | +| **copy-swap circuit** | The mitigation circuit obtained by copying the input circuit `copies` times and inserting SWAPs qubit-wise. | +| **Numerator circuit** | The copy-swap circuit execution used to estimate `Tr(O ρ^M)`, with the observable being the `Hamiltonian` expanded by the number of copies. | +| **Denominator circuit** | The copy-swap circuit execution used to estimate `Tr(ρ^M)`, with the observable being `None`. | +| **estimator** | The callback provided by the caller, which executes one mitigation circuit and returns an `(expectation value, variance)` pair. | +| **Unified pipeline** | The sequential flow carried by `ErrorMitigation`: `run()` collects the estimates and `get_mitigated()` performs post-processing. | + +--- + +## `cqlib.error_mitigation` API Overview + +### Zero-noise extrapolation + +| Name | Description | +| --- | --- | +| [`ZNEMitigation`](1_zne.md) | Low-level zero-noise extrapolation helper class, responsible for folding, execution and extrapolation. | +| [`ZneConfig`](1_zne.md) | Zero-noise extrapolation configuration, containing the fold levels. | +| [`ExtrapolateMethod`](1_zne.md) | Extrapolation method, including `polynomial` and `exponential`. | +| [`Estimator`](1_zne.md) | Callback type alias, specifying three parameters and a two-element return. | + +### Virtual distillation + +| Name | Description | +| --- | --- | +| [`VirtualDistillation`](2_virtual_distillation.md) | Low-level virtual distillation helper class, responsible for constructing the copy-swap circuit and computing the ratio. | +| [`VirtualDistillationConfig`](2_virtual_distillation.md) | Virtual distillation configuration, containing the number of copies. | + +### Unified pipeline + +| Name | Description | +| --- | --- | +| [`ErrorMitigation`](3_unified.md) | Unified pipeline, completing one mitigation in the order `run()` → `get_mitigated()`. | +| [`MitigationMethod`](3_unified.md) | Mitigation method selection, including `zne` and `virtual_distillation`. | +| [`RunArgs`](3_unified.md) | Execution arguments, corresponding one-to-one with the selected method. | +| [`ProcessArgs`](3_unified.md) | Post-processing arguments, corresponding one-to-one with the selected method. | +| [`MitigatedResult`](3_unified.md) | Mitigation result, containing the expectation value and an optional variance. | +| [`ErrorMitigationError`](3_unified.md) | The base class of error mitigation exceptions. | + +--- + +## Quick examples + +### 1. Low-level zero-noise extrapolation + +```python +from cqlib.circuit import Circuit +from cqlib.error_mitigation import ExtrapolateMethod, ZNEMitigation +from cqlib.qis import Hamiltonian, PauliString + +circuit = Circuit(1) +circuit.x(0) + +hamiltonian = Hamiltonian.from_list([(PauliString.from_str("Z"), 1.0)]) + +zne = ZNEMitigation(circuit, [0, 1, 2]) +print(zne.fold_levels) # [0, 1, 2] +print(zne.noise_factors) # [1, 3, 5] + +folded = zne.fold_circuits() +print([len(item.operations) for item in folded]) # [1, 3, 5] + + +def estimator(run_circuit, observable, shots): + return (0.5 * len(run_circuit.operations), 0.0) + + +noisy = zne.run_em_sequence_with_shots(None, hamiltonian, 256, estimator) +print(noisy) # [0.5, 1.5, 2.5] + +mitigated = zne.extrapolate(noisy, ExtrapolateMethod.polynomial(), 1) +print(mitigated) # 0.0 +``` + +### 2. Low-level virtual distillation + +```python +from cqlib.circuit import Circuit +from cqlib.error_mitigation import VirtualDistillation +from cqlib.qis import Hamiltonian, PauliString + +circuit = Circuit(1) +circuit.x(0) + +hamiltonian = Hamiltonian.from_list([(PauliString.from_str("Z"), 1.0)]) + +vd = VirtualDistillation(circuit, 2) +copy_swap = vd.build_copy_swap_circuit() +print(copy_swap.width) # 2 + + +def estimator(run_circuit, observable, shots): + if observable is None: + return (2.0, 1.0) # 分母:Tr(ρ^M) + return (1.5, 0.25) # 分子:Tr(O ρ^M) + + +expectation, variance = vd.run_vd( + hamiltonian, + shots_numerator=3, + shots_denominator=2, + estimator=estimator, +) +print(expectation) # 0.75 +print(variance) # 0.203125 +``` + +### 3. Unified pipeline + +```python +from cqlib.circuit import Circuit +from cqlib.error_mitigation import ( + ErrorMitigation, + MitigationMethod, + VirtualDistillationConfig, + ProcessArgs, + RunArgs, +) +from cqlib.qis import Hamiltonian, PauliString + +circuit = Circuit(1) +circuit.x(0) + +hamiltonian = Hamiltonian.from_list([(PauliString.from_str("Z"), 1.0)]) + + +def estimator(run_circuit, observable, shots): + if observable is None: + return (2.0, 1.0) + return (1.5, 0.25) + + +mitigation = ErrorMitigation( + circuit, + MitigationMethod.virtual_distillation(VirtualDistillationConfig(2)), +) + +mitigation.run( + hamiltonian, + RunArgs.virtual_distillation(shots_numerator=3, shots_denominator=2), + estimator, +) +result = mitigation.get_mitigated(ProcessArgs.virtual_distillation()) + +print(result.expectation) # 0.75 +print(result.variance) # 0.203125 +``` + +--- + +## Validation and error handling + +Validation in the module falls into two kinds: configuration validation at construction time (fold levels, copies) and consistency validation at execution time (observable qubit count, pipeline call order, and whether parameters match the method). + +| Exception | When it occurs | +| --- | --- | +| `ErrorMitigationError` | The configuration or the call order is invalid, for example a negative fold level, fewer than 2 copies, an observable qubit count that does not match the circuit, calling `get_mitigated()` without `run()`, repeating `run()` or `get_mitigated()`, a `RunArgs` / `ProcessArgs` that does not match the actual method, or invalid extrapolation input (see [Zero-noise extrapolation](1_zne.md)). | +| `CircuitError` | Mitigation circuit construction fails, for example calling `fold_circuits()` on a `ZNEMitigation` with a negative fold level. | +| `ValueError` | An observable-related operation fails, for example an error while expanding the `Hamiltonian` by the number of copies. | +| `TypeError` | The `estimator` passed in is not callable. | + +`ErrorMitigationError` and `CircuitError` are both subclasses of `cqlib.circuit.CqlibError`. Exceptions raised inside the callback are not replaced; they are re-raised as they are after the mitigation flow returns. diff --git a/docs/api/en/python/6_error_mitigation/1_zne.md b/docs/api/en/python/6_error_mitigation/1_zne.md new file mode 100644 index 00000000..298c2242 --- /dev/null +++ b/docs/api/en/python/6_error_mitigation/1_zne.md @@ -0,0 +1,226 @@ +# Zero-Noise Extrapolation + +`cqlib.error_mitigation.zne` + +`cqlib.error_mitigation.zne` provides the low-level interface of zero-noise extrapolation: construct a mitigation circuit family according to the fold levels, estimate the expectation value of each circuit with a callback, and then extrapolate the results to the zero-noise point. This page covers `ZNEMitigation`, `ZneConfig`, `ExtrapolateMethod` and the callback type alias `Estimator`. + +## Import + +```python +from cqlib.error_mitigation.zne import ( + Estimator, + ExtrapolateMethod, + ZneConfig, + ZNEMitigation, +) +``` + +The symbols above are also re-exported by `cqlib.error_mitigation`. + +--- + +## Estimator + +Callback type alias, specifying how the mitigation methods call the estimation function provided by the caller. + +### `Estimator = Callable[[Circuit, Hamiltonian | None, int | None], tuple[float, float]]` + +Calling convention: pass the three arguments as positional arguments. + +| Parameter | Type | Meaning | +| --- | --- | --- | +| `circuit` | `Circuit` | The mitigation circuit to execute this time. Zero-noise extrapolation passes the circuit expanded at some fold level. | +| `hamiltonian` | `Hamiltonian \| None` | The observable to estimate. Zero-noise extrapolation always passes the configured `Hamiltonian`. | +| `shots` | `int \| None` | The number of shots for this execution. | + +Return convention: an `(expectation value, variance)` pair must be returned, and both elements are used as floating-point numbers. + +| Return position | Meaning | Used by zero-noise extrapolation | +| --- | --- | --- | +| The 1st element | The expectation value of that circuit on the observable. | Used; it forms the input sequence of the extrapolation. | +| The 2nd element | The variance of that expectation value. | Not used; it takes part in post-processing only in virtual distillation. | + +The value of `shots` depends on the calling path: + +- `run_em_sequence()` always passes `None`. +- `run_em_sequence_with_shots()` passes the given number of shots. +- `RunArgs.zne(shots=...)` of the unified pipeline passes that value; it is `None` when not provided. + +When `shots` is `None`, it means the callback decides the sampling method itself. The callback must be callable, otherwise a `TypeError` is raised before the flow starts. Exceptions raised inside the callback are re-raised as they are after the flow returns, and are not replaced by another exception type. + +The same alias is also defined in `cqlib.error_mitigation.virtual_distillation` and `cqlib.error_mitigation.unified`, with the same meaning in all three; the callback form is the same in [Virtual distillation](2_virtual_distillation.md) and the [Unified pipeline](3_unified.md), differing only in the circuit and the observable passed in. + +--- + +## ZNEMitigation + +Low-level helper class of zero-noise extrapolation; it holds a copy of the base circuit and is responsible for folding, execution and extrapolation. + +### ZNEMitigation(circuit, fold_levels) + +Parameters: + +- `circuit` (`Circuit`): the base circuit. It is copied on construction, and folding does not modify the circuit passed in. +- `fold_levels` (`Sequence[int]`): the fold level sequence, where each level corresponds to the noise factor `2 * level + 1`. No validation is performed on construction; a negative value fails only at folding time. + +### Attributes + +- `circuit -> Circuit`: a copy of the base circuit. +- `fold_levels -> list[int]`: the configured fold levels. +- `noise_factors -> list[int]`: the noise factors derived from the fold levels, satisfying `noise_factor = 2 * fold_level + 1`. + +### Methods + +- `fold_circuits(gate_set=None) -> list[Circuit]`: return one mitigation circuit per fold level, in the same order as `fold_levels` and with the same length as `fold_levels`. +- `run_em_sequence(gate_set, hamiltonian, estimator) -> list[float]`: execute the folded circuits one by one and return the list of expectation values. Equivalent to calling the next method with `shots=None`. +- `run_em_sequence_with_shots(gate_set, hamiltonian, shots, estimator) -> list[float]`: as above, and passes `shots` to the callback. +- `extrapolate(noisy_results, method, degree) -> float`: select the fitting method according to `method` and return the extrapolated value at the zero-noise point. +- `poly_extrapolate(noisy_results, degree) -> float`: extrapolate with a polynomial fit. +- `exp_extrapolate(noisy_results) -> float`: extrapolate with an exponential decay model fit. + +### Other behavior + +- Supports `copy` and `deepcopy`; a copy and the original object are independent of each other. +- Does not provide value equality comparison; two `ZNEMitigation` instances are equal only when they are the same object. +- `repr()` takes a form such as `ZNEMitigation(fold_levels=[0, 1, 2], noise_factors=[1, 3, 5])`. + +--- + +## Folding behavior + +The `gate_set` of `fold_circuits()` determines the folding scope: + +- `gate_set` is `None` (the default): global folding, rewriting the circuit as a whole to `U -> U (U† U)^level`. The number of expanded operations grows linearly with the level. +- `gate_set` is an instruction sequence: selective folding, performing the same expansion only on operations whose instruction name matches any one of those instructions, and leaving the other operations as they are. + +The instruction name is taken from the `Instruction` passed in, so it can first be constructed by standard gate name: + +The `zne` in the examples below is a `ZNEMitigation` instance. + +```python +from cqlib.circuit import Instruction, StandardGate + +gate_set = [Instruction.from_standard_gate(StandardGate.X)] +folded = zne.fold_circuits(gate_set) +``` + +When the fold level is `0` no expansion is performed and a copy of the base circuit is returned. When the level is negative, folding fails and raises `CircuitError`. + +--- + +## Execution conventions + +Both `run_em_sequence()` and `run_em_sequence_with_shots()` require the observable qubit count to match the width of the base circuit, otherwise they raise `ErrorMitigationError`. The `gate_set` of both must be passed explicitly: pass `None` for global folding and an instruction sequence for selective folding. + +Both methods call the callback once per fold level, and the returned list has the same length as `fold_levels`, with each list element being the 1st element of the pair returned by the callback: + +```python +noisy = zne.run_em_sequence(None, hamiltonian, estimator) +``` + +The `shots` passed by `run_em_sequence()` is always `None`; use `run_em_sequence_with_shots()` when the number of shots needs to be handed to the callback. + +--- + +## Extrapolation + +All three extrapolation methods take `noise_factors` as the independent variable and `noisy_results` as the observed values, and return the fitted value at an independent variable of `0`. + +### `extrapolate(noisy_results, method, degree) -> float` + +Select the fitting method according to `method`: + +- `ExtrapolateMethod.polynomial()`: uses `degree`, equivalent to `poly_extrapolate(noisy_results, degree)`. +- `ExtrapolateMethod.exponential()`: ignores `degree`, equivalent to `exp_extrapolate(noisy_results)`. + +Parameters: + +- `noisy_results` (`Sequence[float]`): the expectation values at the various fold levels; the length must be the same as that of `noise_factors`. +- `method` (`ExtrapolateMethod`): the fitting method. +- `degree` (`int`): the polynomial degree. This parameter does not take part in the computation for an exponential fit, but must still be passed. + +Returns: + +- `float`: the extrapolated value at the zero-noise point. + +### `poly_extrapolate(noisy_results, degree) -> float` + +Perform a least-squares fit of the given degree on the data points and return the constant term. `degree` must be strictly less than the number of noise factors; `noisy_results` must correspond one-to-one with the noise factors and must not be empty. + +### `exp_extrapolate(noisy_results) -> float` + +Perform a linear regression in log space according to `y(x) = A * exp(-x / tau)` and return the value `A` at the zero-noise point. Before the call, all `noisy_results` must be positive — log space requires non-negative values, and an entry equal to `0` is likewise rejected. + +Example: + +```python +from cqlib.circuit import Circuit +from cqlib.error_mitigation.zne import ZNEMitigation + +circuit = Circuit(1) +circuit.x(0) + +zne = ZNEMitigation(circuit, [0, 1, 2]) +print(zne.noise_factors) # [1, 3, 5] + +noisy_results = [2.75, 6.75, 10.75] +print(zne.poly_extrapolate(noisy_results, 1)) # 0.75 +``` + +The data points in the example above satisfy `y = 0.75 + 2x`, and the constant term of the first-order fit is the value at the zero-noise point. + +--- + +## ExtrapolateMethod + +Extrapolation method, constructed through static methods. An instance is an immutable value object and can be passed to `ZNEMitigation.extrapolate()` and to `ProcessArgs.zne()` of the unified pipeline. + +### Static methods + +- `ExtrapolateMethod.polynomial()`: polynomial fit, requiring a degree. +- `ExtrapolateMethod.exponential()`: exponential decay fit in log space, not using a degree. + +### Other behavior + +- `str()` returns `"polynomial"` or `"exponential"`; `repr()` returns `ExtrapolateMethod.polynomial()` or `ExtrapolateMethod.exponential()`. +- Supports `==`, `hash`, `copy` and `deepcopy`. + +--- + +## ZneConfig + +The configuration object of zero-noise extrapolation; it can be constructed independently of the low-level class and passed to the unified pipeline. + +### ZneConfig(fold_levels) + +Parameters: + +- `fold_levels` (`Sequence[int]`): the fold level sequence; negative values are not validated on construction. + +### Attributes + +- `fold_levels -> list[int]`: the configured fold levels. + +### Other behavior + +- Supports `==`, `copy` and `deepcopy`; does not provide `hash`. +- `repr()` takes a form such as `ZneConfig(fold_levels=[0, 1, 2])`. + +Example: + +```python +from cqlib.error_mitigation.zne import ZneConfig + +config = ZneConfig([0, 1, 2]) +print(config.fold_levels) # [0, 1, 2] +``` + +--- + +## Raises + +| Exception | When it occurs | +| --- | --- | +| `ErrorMitigationError` | The observable qubit count does not match the base circuit width; the extrapolation input is empty; the extrapolation input length does not match the number of noise factors; the polynomial degree is not less than the number of data points; the exponential extrapolation input contains a non-positive value; the normal equations or the regression system of the fit are singular. | +| `CircuitError` | The fold level is negative and constructing the folded circuit fails. | +| `TypeError` | The `estimator` passed in is not callable. | diff --git a/docs/api/en/python/6_error_mitigation/2_virtual_distillation.md b/docs/api/en/python/6_error_mitigation/2_virtual_distillation.md new file mode 100644 index 00000000..2d5ca07f --- /dev/null +++ b/docs/api/en/python/6_error_mitigation/2_virtual_distillation.md @@ -0,0 +1,164 @@ +# Virtual Distillation + +`cqlib.error_mitigation.virtual_distillation` + +`cqlib.error_mitigation.virtual_distillation` provides the low-level interface of virtual distillation: copy the base circuit several times and insert SWAPs to construct a copy-swap circuit, then estimate the numerator and the denominator separately, and finally compute the ratio to obtain the mitigated expectation value. This page covers `VirtualDistillation` and `VirtualDistillationConfig`. + +## Import + +```python +from cqlib.error_mitigation.virtual_distillation import ( + Estimator, + VirtualDistillationConfig, + VirtualDistillation, +) +``` + +`VirtualDistillation` and `VirtualDistillationConfig` are also re-exported by `cqlib.error_mitigation`; for the callback convention see `Estimator` in [Zero-noise extrapolation](1_zne.md). + +The ratio estimated by virtual distillation is `Tr(O ρ^M) / Tr(ρ^M)`, where `M` is the number of copies and `O` is the observable represented by the `Hamiltonian`. + +--- + +## VirtualDistillationConfig + +The configuration object of virtual distillation; it can be constructed independently of the low-level class and passed to the unified pipeline. + +### VirtualDistillationConfig(copies) + +Parameters: + +- `copies` (`int`): the number of copies of the density matrix. No validation is performed on construction; a value less than 2 fails when the configuration is actually used. + +### Attributes + +- `copies -> int`: the configured number of copies. + +### Other behavior + +- Supports `==`, `copy` and `deepcopy`; does not provide `hash`. +- `repr()` takes a form such as `VirtualDistillationConfig(copies=2)`. + +--- + +## VirtualDistillation + +Low-level helper class of virtual distillation; it holds a copy of the base circuit and is responsible for constructing the copy-swap circuit, executing the numerator and the denominator, and computing the ratio. + +### VirtualDistillation(circuit, copies) + +Parameters: + +- `circuit` (`Circuit`): the base circuit. It is copied on construction and the circuit passed in is not modified. +- `copies` (`int`): the number of copies, which must not be less than 2, otherwise `ErrorMitigationError` is raised. The number of copies determines the width of the mitigation circuit: the width of the copy-swap circuit is `copies` times the width of the base circuit. + +### Attributes + +- `copies -> int`: the currently configured number of copies. + +### Methods + +- `set_copies(copies)`: update the number of copies. Validation is completed before assignment, so the number of copies of the object stays unchanged when validation fails. +- `build_copy_swap_circuit() -> Circuit`: construct the copy-swap circuit. The base circuit is first decomposed, then copied one by one into their respective qubit ranges, and finally SWAPs are inserted qubit-wise. +- `run_denominator_circuit(shots, estimator) -> tuple[float, float]`: execute the denominator circuit and return the pair given by the callback. +- `run_numerator_circuit(hamiltonian, shots, estimator) -> tuple[float, float]`: execute the numerator circuit and return the pair given by the callback. +- `run_vd(hamiltonian, shots_numerator, shots_denominator, estimator) -> tuple[float, float]`: execute the numerator and the denominator in turn and compute the ratio, returning `(mitigated expectation value, variance)`. + +### Other behavior + +- Supports `copy` and `deepcopy`; a copy and the original object are independent of each other. +- Does not provide value equality comparison; two `VirtualDistillation` instances are equal only when they are the same object. +- `repr()` takes a form such as `VirtualDistillation(copies=2)`. + +--- + +## copy-swap circuit + +The output of `build_copy_swap_circuit()` consists of three parts: + +1. The base circuit is first decomposed, ensuring that a group of already expanded operations is copied. +2. The base circuit is copied `copies` times, with the `i`-th copy landing on the qubit range offset by `i` times the base width. +3. For the 1st copy and each of the other copies, SWAPs are inserted qubit-wise. SWAPs are inserted only between the 1st copy and each of the other copies; the other copies are not directly connected to one another. + +Therefore, when the number of copies is 2, each operation of the base circuit appears 2 times, plus the qubit-wise SWAPs; as the number of copies increases, the number of SWAPs grows linearly with the number of copies rather than as pairwise combinations of copies. + +```python +from cqlib.circuit import Circuit +from cqlib.error_mitigation.virtual_distillation import VirtualDistillation + +circuit = Circuit(1) +circuit.x(0) + +vd = VirtualDistillation(circuit, 2) +copy_swap = vd.build_copy_swap_circuit() +print(copy_swap.width) # 2 +``` + +--- + +## Execution conventions + +Virtual distillation has only one mitigation circuit; both the numerator and the denominator are executed on that circuit, differing in the observable and the number of shots passed to the callback: + +| Method | Circuit received by the callback | Observable received by the callback | Shots received by the callback | +| --- | --- | --- | --- | +| `run_denominator_circuit(shots, estimator)` | copy-swap circuit | `None` | `shots` | +| `run_numerator_circuit(hamiltonian, shots, estimator)` | copy-swap circuit | The `Hamiltonian` expanded by the number of copies | `shots` | +| `run_vd(...)` | copy-swap circuit | Numerator first, then denominator, as above | `shots_numerator` and `shots_denominator` respectively | + +The observable used by the numerator is obtained by expanding the `Hamiltonian` passed in: the original Pauli terms are kept on their respective qubits and the higher-order qubits are padded with `Z`, so that the observable qubit count matches the width of the copy-swap circuit. The expansion is performed inside the class, and the caller only needs to pass the observable acting on the base circuit. + +`run_vd()` completes two steps in turn: first execute the numerator with `shots_numerator`, then execute the denominator with `shots_denominator`, and then combine the results as follows: + +```text +期望值 = 分子期望值 / 分母期望值 +方差 = 分子方差 / 分母期望值^2 + + 分子期望值^2 * 分母方差 / 分母期望值^4 +``` + +When the denominator expectation value is `0` the ratio cannot be computed and `ErrorMitigationError` is raised. `run_vd()` requires the observable qubit count to match the base circuit width, and raises `ErrorMitigationError` before calling the callback when they do not match. + +`run_denominator_circuit()` and `run_numerator_circuit()` do not combine the results and return the `(expectation value, variance)` pair given by the callback directly, suiting scenarios that need to inspect the numerator and denominator results separately. + +Example: + +```python +from cqlib.circuit import Circuit +from cqlib.error_mitigation.virtual_distillation import VirtualDistillation +from cqlib.qis import Hamiltonian, PauliString + +circuit = Circuit(1) +circuit.x(0) + +hamiltonian = Hamiltonian.from_list([(PauliString.from_str("Z"), 1.0)]) + +vd = VirtualDistillation(circuit, 2) + + +def estimator(run_circuit, observable, shots): + if observable is None: + return (2.0, 1.0) # 分母:Tr(ρ^M) + return (1.5, 0.25) # 分子:Tr(O ρ^M) + + +expectation, variance = vd.run_vd( + hamiltonian, + shots_numerator=3, + shots_denominator=2, + estimator=estimator, +) +print(expectation) # 0.75 +print(variance) # 0.203125 +``` + +In the example above the numerator and the denominator are fixed values, so `0.75 = 1.5 / 2.0` and `0.203125 = 0.25 / 4 + 2.25 / 16`. + +--- + +## Raises + +| Exception | When it occurs | +| --- | --- | +| `ErrorMitigationError` | The number of copies is less than 2 (at construction time or in `set_copies()`); the observable qubit count of `run_vd()` does not match the base circuit width; the denominator expectation value is `0`. | +| `CircuitError` | Copy-swap circuit construction fails, or the qubit count after observable expansion does not match the mitigation circuit width. | +| `TypeError` | The `estimator` passed in is not callable. | diff --git a/docs/api/en/python/6_error_mitigation/3_unified.md b/docs/api/en/python/6_error_mitigation/3_unified.md new file mode 100644 index 00000000..cfc15745 --- /dev/null +++ b/docs/api/en/python/6_error_mitigation/3_unified.md @@ -0,0 +1,230 @@ +# Unified Pipeline + +`cqlib.error_mitigation.unified` + +`cqlib.error_mitigation.unified` provides a sequential unified pipeline: first execute the mitigation circuits according to the selected method and collect the callback results, then perform method-related post-processing. This page covers `ErrorMitigation` and its companions `MitigationMethod`, `RunArgs`, `ProcessArgs`, `MitigatedResult` and `ErrorMitigationError`. + +## Import + +```python +from cqlib.error_mitigation.unified import ( + Estimator, + ErrorMitigationError, + MitigationMethod, + RunArgs, + ProcessArgs, + MitigatedResult, + ErrorMitigation, +) +``` + +Except for `Estimator`, the symbols above are also re-exported by `cqlib.error_mitigation`. For the callback convention see `Estimator` in [Zero-noise extrapolation](1_zne.md). + +--- + +## MitigationMethod + +Mitigation method selection, constructed through static methods; it determines which method this instance uses when the `ErrorMitigation` is constructed. + +### Static methods + +- `MitigationMethod.zne(config)`: the configuration is zero-noise extrapolation. The parameter `config` (`ZneConfig`) gives the fold levels. +- `MitigationMethod.virtual_distillation(config)`: the configuration is virtual distillation. The parameter `config` (`VirtualDistillationConfig`) gives the number of copies. + +### Attributes + +- `method_type -> str`: the method identifier, taking the values `"zne"` or `"virtual_distillation"`. + +### Other behavior + +- Supports `==`, `copy` and `deepcopy`; does not provide `hash`. +- `repr()` takes a form such as `MitigationMethod.zne(ZneConfig(fold_levels=[0, 1, 2]))`. + +--- + +## RunArgs + +The run arguments required for one execution, corresponding one-to-one with the selected method. When the method is `zne`, only `RunArgs.zne(...)` can be used, otherwise `run()` raises `ErrorMitigationError`. + +### Static methods + +- `RunArgs.zne(gate_set=None, shots=None)`: + - `gate_set` (`Sequence[Instruction] | None`): the gate set for selective folding. Defaults to `None`, meaning global folding. + - `shots` (`int | None`): the number of shots passed to the callback. Defaults to `None`. +- `RunArgs.virtual_distillation(shots_numerator, shots_denominator)`: + - `shots_numerator` (`int`): the number of shots for the numerator. + - `shots_denominator` (`int`): the number of shots for the denominator. + +### Attributes + +- `method_type -> str`: the method identifier. + +### Other behavior + +- Supports `==`, `copy` and `deepcopy`; does not provide `hash`. When comparing `zne` arguments, the elements of `gate_set` are compared by instruction name rather than by object identity. +- `repr()` takes a form such as `RunArgs.zne(gate_set=None, shots=Some(256))`. + +Example: + +```python +from cqlib.circuit import Instruction, StandardGate +from cqlib.error_mitigation.unified import RunArgs + +global_folding = RunArgs.zne(shots=256) +selective_folding = RunArgs.zne( + gate_set=[Instruction.from_standard_gate(StandardGate.X)], + shots=256, +) + +vd_args = RunArgs.virtual_distillation(shots_numerator=3, shots_denominator=2) +``` + +--- + +## ProcessArgs + +Post-processing arguments, corresponding one-to-one with the selected method, passed at `get_mitigated()` time. + +### Static methods + +- `ProcessArgs.zne(method, degree=None)`: + - `method` (`ExtrapolateMethod`): the extrapolation method. + - `degree` (`int | None`): the polynomial degree. Defaults to `None`, in which case `min(number of data points - 1, 1)` is used. This parameter does not take part in the computation for an exponential fit. +- `ProcessArgs.virtual_distillation()`: virtual distillation has no additional post-processing arguments. + +### Attributes + +- `method_type -> str`: the method identifier. + +### Other behavior + +- Supports `==`, `copy` and `deepcopy`; does not provide `hash`. +- `repr()` takes a form such as `ProcessArgs.zne(method=polynomial, degree=Some(1))`. + +--- + +## MitigatedResult + +Mitigation result, returned by `get_mitigated()`; it cannot be constructed directly. The attributes are read-only. + +### Attributes + +- `expectation -> float`: the mitigated expectation value. +- `variance -> float | None`: the mitigated variance. The result of zero-noise extrapolation is `None`; the result of virtual distillation has a value. + +### Other behavior + +- Supports `==`, `copy` and `deepcopy`; does not provide `hash`. +- `repr()` takes a form such as `MitigatedResult(expectation=0.5, variance=None)`. + +--- + +## ErrorMitigation + +Unified pipeline, completing one mitigation in the order `run()` → `get_mitigated()`. + +### ErrorMitigation(circuit, method) + +Parameters: + +- `circuit` (`Circuit`): the base circuit. It is copied on construction. +- `method` (`MitigationMethod`): the mitigation method. + +The method configuration is validated at construction time: when the fold levels of zero-noise extrapolation contain a negative value, or the number of copies of virtual distillation is less than 2, `ErrorMitigationError` is raised. + +### Methods + +- `run(hamiltonian, run_args, estimator) -> None`: execute the mitigation circuits corresponding to the method according to `run_args`, and record the callback results. +- `get_mitigated(process_args) -> MitigatedResult`: perform post-processing on the recorded results and return the mitigation result. + +### Other behavior + +- Supports `copy` and `deepcopy`; a copy carries the current state. +- Does not provide value equality comparison. +- `repr()` is fixed as `ErrorMitigation()`. + +### State machine + +Each instance completes one mitigation only, and both the call order and the argument matching are constrained by the instance itself: + +| Current state | Call | Result | +| --- | --- | --- | +| Constructed | `run()` | Executes and transitions to the run state. | +| Constructed | `get_mitigated()` | Raises `ErrorMitigationError`, requiring `run()` to be completed first. | +| Run | `get_mitigated()` | Returns the result and transitions to the mitigated state. | +| Run | `run()` | Raises `ErrorMitigationError`; repeating `run()` is not allowed. | +| Mitigated | `run()` or `get_mitigated()` | Raises `ErrorMitigationError`; calling again is not allowed. | + +When the `RunArgs` passed to `run()` do not match the method given at construction, `ErrorMitigationError` is raised; in that case the callback is not called and the state does not change. When the `ProcessArgs` passed to `get_mitigated()` do not match the method, `ErrorMitigationError` is likewise raised. + +### Execution conventions + +Zero-noise extrapolation: `run()` constructs the mitigation circuits according to the configured fold levels, calls the callback once per circuit, with the observable being the `Hamiltonian` passed in and the number of shots taken from `RunArgs.zne(shots=...)`. Only the 1st element of the pair returned by the callback is used, and the variance is discarded. `get_mitigated()` fits the extrapolation input, and the `variance` of the result is `None`. + +Virtual distillation: `run()` first constructs the copy-swap circuit, then calls the callback twice on the same circuit — the numerator first, then the denominator. The numerator receives the observable expanded by the number of copies and `shots_numerator`, and the denominator receives `None` and `shots_denominator`. `get_mitigated()` computes the ratio according to the combination formula of [Virtual distillation](2_virtual_distillation.md), and the `variance` of the result has a value. + +### Example + +```python +from cqlib.circuit import Circuit +from cqlib.error_mitigation.unified import ( + ErrorMitigation, + MitigationMethod, + ProcessArgs, + RunArgs, +) +from cqlib.error_mitigation.zne import ExtrapolateMethod, ZneConfig +from cqlib.qis import Hamiltonian, PauliString + +circuit = Circuit(1) +circuit.x(0) + +hamiltonian = Hamiltonian.from_list([(PauliString.from_str("Z"), 1.0)]) + + +def estimator(run_circuit, observable, shots): + return (len(run_circuit.operations) + 0.5, 0.0) + + +mitigation = ErrorMitigation( + circuit, + MitigationMethod.zne(ZneConfig([0, 1, 2])), +) + +mitigation.run(hamiltonian, RunArgs.zne(shots=256), estimator) +result = mitigation.get_mitigated( + ProcessArgs.zne(ExtrapolateMethod.polynomial(), degree=1) +) + +print(result.expectation) # 0.5 +print(result.variance) # None +``` + +In the example above the operation counts of the folded circuits are `[1, 3, 5]` in order, the callback gives `[1.5, 3.5, 5.5]`, and the zero-noise point value of the first-order fit is `0.5`. + +--- + +## ErrorMitigationError + +The base class of error mitigation exceptions, inheriting from `cqlib.circuit.CqlibError`. + +| Exception | When it occurs | +| --- | --- | +| `ErrorMitigationError` | See the situations listed in the table below. | +| `CircuitError` | Folded circuit or copy-swap circuit construction fails. | +| `ValueError` | An observable-related operation fails, for example an error while expanding the `Hamiltonian` by the number of copies. | +| `TypeError` | The `estimator` passed in is not callable. | + +The common situations covered by `ErrorMitigationError`: + +| When it occurs | Description | +| --- | --- | +| The fold levels contain a negative value at construction | Reported at `ErrorMitigation()` construction time; `ZneConfig` itself does not raise. | +| The number of copies is less than 2 at construction | Reported at `ErrorMitigation()` construction time. | +| The observable qubit count does not match the base circuit width | Reported in `run()`; the callback is not called. | +| `RunArgs` do not match the actual method | Reported in `run()`; the callback is not called. | +| `ProcessArgs` do not match the actual method | Reported in `get_mitigated()`. | +| `get_mitigated()` is called without `run()` | `run()` must be completed first. | +| `run()` or `get_mitigated()` is repeated | Each may be called only once per instance. | +| The extrapolation input is invalid or the fit fails | See [Zero-noise extrapolation](1_zne.md). | +| The denominator expectation value of virtual distillation is `0` | The ratio cannot be computed; see [Virtual distillation](2_virtual_distillation.md). | diff --git a/docs/api/en/rust/0_circuit/0_overview.md b/docs/api/en/rust/0_circuit/0_overview.md new file mode 100644 index 00000000..981f026c --- /dev/null +++ b/docs/api/en/rust/0_circuit/0_overview.md @@ -0,0 +1,250 @@ +# Quantum Circuit + +`cqlib_core::circuit` + +`cqlib_core::circuit` is the quantum circuit intermediate representation module in the Cqlib Rust core. It provides type-safe circuit construction, qubit management, parameter expressions, gate definitions, classical data, structured control flow, control flow graphs, matrix conversion and parameterized circuit templates. + +## Overview + +`cqlib_core::circuit` mainly provides the following capabilities: + +- **Quantum circuit construction**: create a set of logical qubits through `Circuit`, and append standard gates, custom gates, composite gates, measurement, reset, barrier and other operations. +- **Parameterized expressions**: represent gate angles, global phases and variational circuit parameters through `Parameter`, with support for expression parsing, evaluation, simplification, substitution, differentiation and equivalence checking. +- **Operation-level IR**: represent instructions and operations in a circuit through `Instruction`, `Operation`, `ValueInstruction` and `ValueOperation`, with the construction-layer IR separated from the storage-layer IR. +- **Gate definition system**: provide gate definition types such as `StandardGate`, `UnitaryGate`, `MCGate`, `CircuitGate` and `FrozenCircuit`, covering standard gates, custom unitary gates, multi-controlled gates and sub-circuit composite gates. +- **Classical data and control flow**: describe measurement results, classical variables and structured control flow through types such as `ClassicalType`, `ClassicalExpr` and `ClassicalControlOp`. +- **Control flow graph analysis**: provide a control flow graph view for compiler passes through types such as `CircuitCFG`, `BasicBlock` and `Terminator`, and provide a directed acyclic graph view of the circuit through `CircuitDag`. +- **Matrix and symbolic matrix conversion**: convert small purely quantum circuits into numeric matrices, or into symbolic matrices that retain parameters, with support for global phase equivalence checking. +- **Parameterized circuit templates**: provide common templates such as VQE, QAOA, feature map and Hamiltonian evolution through the `ansatz` module. + +--- + +## Common entry points + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; + +let mut circuit = Circuit::new(2); +circuit.h(Qubit::new(0))?; +circuit.cx(Qubit::new(0), Qubit::new(1))?; + +assert_eq!(circuit.qubits().len(), 2); +assert_eq!(circuit.operations().len(), 2); + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +--- + +## API navigation + +| Category | Page | Main objects | Description | +| --- | --- | --- | --- | +| Circuit container | [Circuit](1_circuit.md) | `Circuit` | Circuit construction, gate methods, parameter binding, inversion, decomposition, composition and matrix conversion. | +| Qubit | [Qubit](2_qubit.md) | `Qubit`, `QubitError` | `u32` logical qubit handle and checked integer conversion. | +| Parameter system | [Parameter](3_parameter.md) | `Parameter`, `ParameterError`, `EvalError` | Symbolic expressions, evaluation, differentiation, substitution and simplification. | +| Operation IR | [Operation / Instruction](4_operation_instruction.md) | `Instruction`, `Operation`, `ValueInstruction`, `ValueOperation` | Storage-layer IR and construction-layer IR. | +| Standard gates | [Standard Gates](5_gate_standard.md) | `StandardGate` | Native gate enum, gate metadata, matrices and inverse gates. | +| Custom unitary gates | [Unitary Gates](6_gate_unitary.md) | `UnitaryGate`, `UnitaryMatrix` | Numeric matrix gates, symbolic matrix gates and circuit-backed gates. | +| Multi-controlled gates | [Multi-Controlled Gates](7_gate_mc_gate.md) | `MCGate` | Add control qubits in front of a standard gate to express multi-controlled gate semantics. | +| Sub-circuit gates | [Circuit Gates](8_gate_circuit_gate.md) | `FrozenCircuit`, `CircuitGate` | Freeze a circuit and wrap it as a reusable composite gate. | +| Classical data and control flow | [Classical / Control Flow](9_classical_control_flow.md) | `ClassicalType`, `ClassicalExpr`, `ClassicalControlOp` | Measurement, classical data, expressions and structured control flow. | +| Symbolic matrix | [Symbolic Matrix](10_symbolic_matrix.md) | `SymbolicComplex`, `SymbolicMatrix` | Dense symbolic matrices that retain `Parameter`, and equivalence checking. | +| Ansatz | [Ansatz](11_ansatz.md) | `Ansatz`, `TwoLocal`, `QAOAAnsatz`, `PauliEvolutionAnsatz` | Parameterized circuit templates. | +| Control flow graph | [CFG](12_cfg.md) | `CircuitCFG`, `BasicBlock`, `Terminator` | Circuit control flow graph view, analysis and reconstruction. | +| Circuit DAG | [Circuit DAG](14_circuit_dag.md) | `CircuitDag`, `DagNode`, `DagWire` | Circuit directed acyclic graph view, dependency queries and graph structure reconstruction. | +| Matrix conversion | [Circuit To Matrix](13_circuit_to_matrix.md) | `circuit_to_matrix`, `Circuit::to_matrix` | Numeric matrix conversion, qubit ordering and global phase. | + +--- + +## Core concepts and terms + +| Term | Description | +| --- | --- | +| **Abstract circuit** | A quantum circuit oriented to algorithm description; it represents a quantum program with logical qubits and high-level operations, and is not bound directly to a specific hardware topology or native gate set. | +| **Physical circuit** | A circuit that has completed layout, routing and gate set conversion according to the target constraints, and usually contains only the physical qubit connections and native operations supported by the backend. | +| **Qubit** | The basic logical unit of quantum information, represented by `Qubit`, with a `u32` handle internally. | +| **Instruction** | The type of an operation in a circuit, represented by `Instruction`; it can be a quantum gate, or `Barrier`, `Measure`, `Reset`, `Delay`, a classical data operation or a control flow structure. | +| **Operation** | A concrete application of an instruction in a circuit, including the instruction type, the acted-on qubits, the parameter list and an optional label. | +| **Gate definition** | A gate description separated from a concrete application, represented by types such as `StandardGate`, `UnitaryGate`, `MCGate` and `CircuitGate`. | +| **Parameter** | A construction-time symbolic expression, represented by `Parameter`, which can later be bound to a numeric value through `assign_parameters()`. | +| **Global phase** | The overall phase factor of a circuit. It usually does not affect individual measurement probabilities, but it still matters in circuit composition, controlled operations, matrix equivalence checking and compilation rewriting. | +| **Ansatz** | A parameterized circuit template provided by the `ansatz` module, commonly used in variational algorithms and quantum machine learning. | + +### Comparison of the two IR layers + +The circuit IR in the Rust core is divided into a construction layer and a storage layer. Understanding this distinction matters when writing importers, serialization tools and compiler passes. + +| Layer | Operation type | Instruction type | Parameter type | Applicable scenarios | +| --- | --- | --- | --- | --- | +| Construction-layer IR | `ValueOperation` | `ValueInstruction` | `ParameterValue` | External construction, serialization, importers and compiler pass output. | +| Storage-layer IR | `Operation` | `Instruction` | `CircuitParam` | Compact internal storage inside `Circuit`, parameter residency and runtime validation. | + +The construction-layer IR is self-contained: parameters are kept as `ParameterValue` and can be passed across circuits or written to external formats directly. The storage-layer IR is more compact, and a parameter may reference the internal parameter table of the owning `Circuit` in the form of `CircuitParam::Index`, so it must be interpreted together with the concrete circuit context. + +The typical conversion relationships are as follows: + +```text +ValueOperation + └── append / from_operations + └── Operation stored in Circuit + +Operation + └── Circuit::index(i) + └── ValueOperation +``` + +--- + +## Minimal example: static circuit + +The following example creates a two-qubit Bell circuit and adds an `H` gate and a `CX` gate in turn. + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; + +let mut circuit = Circuit::new(2); + +circuit.h(Qubit::new(0))?; +circuit.cx(Qubit::new(0), Qubit::new(1))?; + +assert_eq!(circuit.operations().len(), 2); + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +`Circuit::new(2)` creates the logical qubits `Qubit(0)` and `Qubit(1)`. Most high-level gate methods return `Result<(), CircuitError>`, so that errors such as a non-existent qubit, a parameter count mismatch or a gate arity mismatch are exposed as early as possible when the operation is appended. + +--- + +## Minimal example: parameterized circuit + +A parameterized circuit allows gate angles to be expressed as symbolic expressions. After construction, concrete values can be bound through `assign_parameters()`, and matrix conversion or subsequent compilation can continue. + +```rust +use cqlib_core::circuit::{Circuit, Parameter, Qubit}; +use std::collections::HashMap; + +let theta = Parameter::symbol("theta"); + +let mut circuit = Circuit::new(1); +circuit.rx(Qubit::new(0), theta)?; + +let mut bindings = HashMap::new(); +bindings.insert("theta", std::f64::consts::FRAC_PI_2); + +let bound = circuit.assign_parameters(&Some(bindings))?; +let matrix = bound.to_matrix(None)?; + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +`assign_parameters()` returns a new circuit object, and the original parameterized circuit can still be reused as a template. For VQE, QAOA and parameter sweep tasks, keep the template circuit and generate a bound circuit for each parameter set. + +--- + +## Minimal example: classical data and control flow + +The Rust core supports representing measurement, classical expressions and structured control flow in a circuit. The following example measures qubit `0` and conditionally applies an `X` gate to qubit `1` according to the measurement result. + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; + +let mut circuit = Circuit::new(2); + +let m = circuit.measure(Qubit::new(0))?; + +circuit.if_(m.expr().to_bool()?, |body| { + body.x(Qubit::new(1))?; + Ok(()) +})?; + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +Such circuits contain runtime classical data and control flow structures, and usually no longer have a single fixed unconditional unitary matrix representation. Therefore `to_matrix()` should not be called directly on a circuit that contains measurement or control flow, unless a purely quantum sub-circuit has been extracted from it. + +Whether a specific backend supports such control flow structures must be confirmed against the target compiler, IR and execution backend. + +--- + +## Minimal example: creating a circuit from the construction-layer IR + +When a circuit comes from an external format, serialized data or compiler pass output, first build a list of `ValueOperation` and then produce the circuit through `Circuit::from_operations()`. + +```rust +use cqlib_core::circuit::{ + Circuit, + ParameterValue, + Qubit, + StandardGate, + ValueOperation, +}; + +let ops = vec![ + ValueOperation::from_standard(StandardGate::H, [Qubit::new(0)], []), + ValueOperation::from_standard( + StandardGate::RZ, + [Qubit::new(0)], + [ParameterValue::from("theta")], + ), +]; + +let circuit = Circuit::from_operations( + vec![Qubit::new(0)], + ops, + None, + None, +)?; + +assert!(circuit.symbols().contains("theta")); + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +`Circuit::from_operations()` makes the construction-layer parameters resident in the circuit's internal parameter table, and validates qubits, parameter counts, classical handles and control flow structures. + +--- + +## Types and parameters + +| Name | Rust representation | Where it is used | Description | +| --- | --- | --- | --- | +| Logical qubit | `Qubit` | Circuits, gate operations, matrix ordering | Stores a `u32` number internally and is a logical identifier. | +| Parameter expression | `Parameter` | Gate angles, global phase, symbolic matrices | A construction-time symbolic expression. | +| Construction-layer parameter | `ParameterValue` | `ValueOperation` | Either a fixed numeric value or a complete `Parameter`. | +| Storage-layer parameter | `CircuitParam` | `Operation` | Either a fixed numeric value or an index into the circuit parameter table. | +| Classical type | `ClassicalType` | Classical variables, measurement values, expressions | Includes `Bit`, `Bool`, `UInt` and `BitVec`. | +| Classical expression | `ClassicalExpr` | Control flow conditions, comparisons, selections | A runtime classical-side expression AST. | + +Controlled gates usually follow the order "control qubits first, target qubit last". For example, when a multi-controlled X gate is applied to `[q0, q1, q2]`, `q0` and `q1` are usually the control qubits and `q2` is the target qubit. + +--- + +## Errors and return values + +The Rust API does not use exceptions. Fallible interfaces usually return `Result<_, CircuitError>`, `Result<_, ParameterError>` or a related error type. Callers should propagate errors with `?`, or handle them according to the concrete error type. + +| Error type | Description | +| --- | --- | +| `CircuitError` | Circuit construction, validation, matrix conversion, inversion, control flow and gate definition errors. | +| `ParameterError` | Errors in parameter expression parsing, simplification, substitution, differentiation or symbolic evaluation. | +| `EvalError` | Errors in numeric evaluation of parameters. | +| `QubitError` | A negative number or an out-of-range value when converting an integer to `Qubit`. | + +Common triggering scenarios include: + +- An operation references a qubit that does not exist in the circuit; +- The same qubit is used more than once in a single operation; +- The number of qubits a gate acts on does not match the gate definition; +- The parameter count does not match the gate definition; +- A fixed parameter is `NaN` or infinite; +- A numeric matrix is requested for a circuit that contains unbound parameters; +- A matrix or inversion is requested for measurement, reset, delay or control flow; +- A classical value in control flow is used outside its scope. + +For circuits that are externally imported, generated automatically by a program or produced by compiler passes, call the following before entering subsequent flows: + +```rust +circuit.validate()?; +``` diff --git a/docs/api/en/rust/0_circuit/10_symbolic_matrix.md b/docs/api/en/rust/0_circuit/10_symbolic_matrix.md new file mode 100644 index 00000000..11076d4d --- /dev/null +++ b/docs/api/en/rust/0_circuit/10_symbolic_matrix.md @@ -0,0 +1,271 @@ +# Symbolic Matrix + +`cqlib_core::circuit::symbolic_matrix` + +```rust +use cqlib_core::circuit::symbolic_matrix::{ + SymbolicComplex, + SymbolicMatrix, + circuit_to_symbolic_matrix, + evaluate_symbolic_matrix, + circuits_equivalent, +}; +``` + +The `symbolic_matrix` module provides symbolic capability for circuit matrix conversion. Unlike the numeric matrix conversion function `circuit_to_matrix()`, symbolic matrix conversion retains the `Parameter` expressions in a circuit as far as possible, so that matrix elements can contain unbound symbolic parameters. + +--- + +## Core concepts + +| Type / Function | Description | +| --- | --- | +| `SymbolicComplex` | A symbolic complex number whose real and imaginary parts are both `Parameter` expressions. | +| `SymbolicMatrix` | A dense matrix composed of `SymbolicComplex`, usually understood as `Array2`. | +| `circuit_to_symbolic_matrix()` | Convert a purely quantum circuit into a matrix that retains symbolic parameters. | +| `evaluate_symbolic_matrix()` | Bind parameters for a symbolic matrix and evaluate it into a numeric complex matrix. | +| `symbolic_matrices_equivalent()` | Determine whether two symbolic matrices can be proven to differ only by a global phase. | +| `circuits_equivalent()` | Determine whether two circuits can be proven to be equivalent up to a global phase in the matrix sense. | + +--- + +## `SymbolicComplex` + +`SymbolicComplex` represents a symbolic complex number whose real and imaginary parts are both `Parameter` expressions. It can represent an ordinary complex constant, or a complex expression containing symbolic variables, such as `exp(iθ)`. + +### Common constructors + +| Method | Description | +| --- | --- | +| `new(re, im)` | Create a symbolic complex number from real and imaginary part expressions. | +| `zero()` | Create `0 + 0i`. | +| `one()` | Create `1 + 0i`. | +| `i()` | Create `0 + 1i`. | +| `from_real(value)` | Create a purely real symbolic complex number from a real number or a real expression. | +| `from_complex(value)` | Create a symbolic complex constant from a `Complex64`. | +| `exp_i(theta)` | Create `cos(theta) + i sin(theta)`, that is, `e^{i theta}`. | + +```rust +use cqlib_core::circuit::Parameter; +use cqlib_core::circuit::symbolic_matrix::SymbolicComplex; + +let theta = Parameter::symbol("theta"); + +let zero = SymbolicComplex::zero(); +let one = SymbolicComplex::one(); +let imag = SymbolicComplex::i(); +let phase = SymbolicComplex::exp_i(theta); +``` + +Among these, `exp_i(theta)` is commonly used to define matrix elements in phase gates, diagonal gates and symbolic quantum evolution. + +### Common methods + +| Method | Description | +| --- | --- | +| `evaluate(bindings)` | Bind symbolic parameters and evaluate into a `Complex64`. | +| `simplify()` | Simplify the real and imaginary part expressions. | +| `replace(symbol, value)` | Replace a symbol appearing in the complex number. | +| `is_zero_exact()` | Determine whether it is syntactically exactly zero. | +| `is_one_exact()` | Determine whether it is syntactically exactly one. | +| `simplifies_to_zero()` | Determine whether it is zero after simplification. | + +```rust +use cqlib_core::circuit::Parameter; +use cqlib_core::circuit::symbolic_matrix::SymbolicComplex; +use std::collections::HashMap; + +let theta = Parameter::symbol("theta"); +let value = SymbolicComplex::exp_i(theta); + +let mut bindings = HashMap::new(); +bindings.insert("theta", 0.0); + +let numeric = value.evaluate(&Some(bindings))?; +assert_eq!(numeric, num_complex::Complex64::new(1.0, 0.0)); + +# Ok::<(), cqlib_core::circuit::error::ParameterError>(()) +``` + +--- + +## `SymbolicMatrix` + +`SymbolicMatrix` is a dense matrix composed of `SymbolicComplex`. It can represent a quantum gate matrix or circuit matrix with symbolic parameters. + +```rust +use cqlib_core::circuit::symbolic_matrix::{ + SymbolicComplex, + SymbolicMatrix, +}; + +let matrix = SymbolicMatrix::from_shape_vec( + (2, 2), + vec![ + SymbolicComplex::zero(), + SymbolicComplex::i(), + SymbolicComplex::i(), + SymbolicComplex::zero(), + ], +).expect("valid 2x2 symbolic matrix"); +``` + +When using a symbolic matrix, ensure that the matrix dimension matches the number of qubits of the target gate or circuit. For example, a single-qubit gate matrix should be `2 × 2`, and a two-qubit gate matrix should be `4 × 4`. + +--- + +## Matrix utility functions + +The `symbolic_matrix` module provides a set of common utility functions for construction, substitution, evaluation, conversion and equivalence checking. + +| Function | Description | +| --- | --- | +| `symbolic_eye(dim)` | Create a `dim × dim` symbolic identity matrix. | +| `substitute_symbolic_matrix(matrix, replacements)` | Perform simultaneous substitution of the symbols in a matrix. | +| `evaluate_symbolic_matrix(matrix, bindings)` | Evaluate a symbolic matrix into an `Array2`. | +| `standard_gate_symbolic_matrix(gate, params)` | Generate the symbolic matrix of a standard gate. | +| `circuit_to_symbolic_matrix(circuit, qubits_order)` | Convert a circuit into a symbolic unitary matrix. | +| `symbolic_matrices_equivalent(lhs, rhs)` | Determine whether two symbolic matrices can be proven to differ only by a global phase. | +| `circuits_equivalent(lhs, rhs, qubits_order)` | Determine whether two circuits can be proven to be equivalent up to a global phase. | + +Besides the functions above, the module also exports a set of low-level functions that write gate matrices into a symbolic matrix in place, mainly used for block-wise matrix construction and custom gate implementations. + +| Function | Description | +| --- | --- | +| `apply_standard_gate_to_matrix(matrix, gate, bits, params)` | Apply a standard gate to the given bits in place. | +| `apply_gate_to_matrix(matrix, gate, bits)` | Apply a symbolic gate matrix to the given bits in place. | +| `apply_gate_to_matrix_num(matrix, gate, bits)` | Apply a numeric gate matrix to the given bits in place. | +| `apply_single_qubit_gate(matrix, gate, bit)` / `apply_single_qubit_gate_num(matrix, gate, bit)` | Apply a single-qubit gate in place; the two functions take a symbolic gate matrix and a numeric gate matrix respectively. | +| `apply_two_qubit_gate(matrix, gate, b0, b1)` / `apply_two_qubit_gate_num(matrix, gate, b0, b1)` | Apply a two-qubit gate in place. | +| `apply_general_gate(matrix, gate, bits)` / `apply_general_gate_num(matrix, gate, bits)` | Apply a gate on an arbitrary number of bits in place. | +| `apply_symbolic_permutation_gate(matrix, permutation, bits)` / `apply_numeric_permutation_gate(matrix, permutation, bits)` | Apply a permutation-type gate in place. | +| `apply_symbolic_diagonal_gate(matrix, diagonal, bits)` / `apply_numeric_diagonal_gate(matrix, diagonal, bits)` | Apply a diagonal-type gate in place. | + +--- + +## Circuit symbolic matrix conversion + +```rust +pub fn circuit_to_symbolic_matrix( + circuit: &Circuit, + qubits_order: Option<&[usize]>, +) -> Result +``` + +`circuit_to_symbolic_matrix()` converts a quantum circuit into a symbolic matrix that retains `Parameter` expressions. + +```rust +use cqlib_core::circuit::{Circuit, Parameter, Qubit}; +use cqlib_core::circuit::symbolic_matrix::{ + circuit_to_symbolic_matrix, + evaluate_symbolic_matrix, +}; +use std::collections::HashMap; + +let theta = Parameter::symbol("theta"); + +let mut circuit = Circuit::new(1); +circuit.rx(Qubit::new(0), theta)?; + +let symbolic = circuit_to_symbolic_matrix(&circuit, None)?; + +let mut bindings = HashMap::new(); +bindings.insert("theta", std::f64::consts::FRAC_PI_2); + +let numeric = evaluate_symbolic_matrix(&symbolic, &Some(bindings))?; +assert_eq!(numeric.shape(), &[2, 2]); + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +In the example above, the parameter of `RX(theta)` is not evaluated to a number during symbolic matrix construction, but is kept as a symbolic expression. Later, `evaluate_symbolic_matrix()` can be used to bind a concrete value for `theta` and obtain a numeric matrix. + +--- + +## Qubit order convention + +`circuit_to_symbolic_matrix(circuit, qubits_order)` needs to determine the correspondence between matrix basis state indices and circuit qubits. The order convention is as follows: + +- `qubits_order = None`: sort by qubit number by default; +- `qubits_order = Some(order)`: use the order explicitly given by the caller; +- The explicit order must match the qubit set in the circuit exactly, and must not omit, duplicate or contain unknown qubits; +- The first qubit in the order corresponds to the least-significant bit of the basis index. + +For example, if the qubit numbers in the circuit are `0` and `2`, the explicit order can be written as `[0, 2]` or `[2, 0]`, and the two give matrix representations under different axis orders. + +--- + +## Symbolic matrix evaluation + +```rust +pub fn evaluate_symbolic_matrix( + matrix: &SymbolicMatrix, + bindings: &Option>, +) -> Result, ParameterError> +``` + +`evaluate_symbolic_matrix()` converts a symbolic matrix into a numeric complex matrix. It performs parameter binding and evaluation on each `SymbolicComplex` element separately. + +```rust +use cqlib_core::circuit::Parameter; +use cqlib_core::circuit::symbolic_matrix::{ + SymbolicComplex, + SymbolicMatrix, + evaluate_symbolic_matrix, +}; +use std::collections::HashMap; + +let theta = Parameter::symbol("theta"); + +let matrix = SymbolicMatrix::from_shape_vec( + (2, 2), + vec![ + SymbolicComplex::one(), + SymbolicComplex::zero(), + SymbolicComplex::zero(), + SymbolicComplex::exp_i(theta), + ], +).expect("valid symbolic matrix"); + +let mut bindings = HashMap::new(); +bindings.insert("theta", std::f64::consts::PI); + +let numeric = evaluate_symbolic_matrix(&matrix, &Some(bindings))?; +assert_eq!(numeric.shape(), &[2, 2]); + +# Ok::<(), cqlib_core::circuit::error::ParameterError>(()) +``` + +--- + +## Symbolic substitution + +`substitute_symbolic_matrix(matrix, replacements)` performs simultaneous substitution of the symbols in a matrix. It suits scenarios such as parameter renaming, composite gate expansion, template parameter substitution and custom symbolic gate binding. + +Typical scenarios include: + +- Replacing an inner parameter `theta` of a sub-circuit with an outer parameter `alpha`; +- Renaming parameters in a compiler pass to avoid symbol conflicts; +- Instantiating a generic symbolic matrix as another parameterized gate definition. + +--- + +## Equivalence checking + +The `symbolic_matrix` module provides equivalence checking interfaces in the sense of global phase. + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::circuit::symbolic_matrix::circuits_equivalent; + +let mut lhs = Circuit::new(1); +lhs.x(Qubit::new(0))?; +lhs.set_global_phase(std::f64::consts::PI.into()); + +let mut rhs = Circuit::new(1); +rhs.x(Qubit::new(0))?; + +assert!(circuits_equivalent(&lhs, &rhs, None)?); + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` diff --git a/docs/api/en/rust/0_circuit/11_ansatz.md b/docs/api/en/rust/0_circuit/11_ansatz.md new file mode 100644 index 00000000..b4da4e64 --- /dev/null +++ b/docs/api/en/rust/0_circuit/11_ansatz.md @@ -0,0 +1,339 @@ +# Ansatz + +`cqlib_core::circuit::ansatz` + +```rust +use cqlib_core::circuit::ansatz::{ + Ansatz, + EntanglementTopology, + TwoLocal, + BasicEntanglerLayers, + StronglyEntanglingLayers, + AngleEncoding, + BasisEncoding, + ZFeatureMap, + IQPFeatureMap, + ZZFeatureMap, + PauliFeatureMap, + QAOAAnsatz, + EvolutionStrategy, + PauliEvolutionAnsatz, + real_amplitudes, + efficient_su2, + zz_feature_map, + pauli_feature_map, +}; +``` + +The `ansatz` module provides a set of reusable parameterized quantum circuit templates, mainly for scenarios such as variational quantum algorithms, quantum machine learning, combinatorial optimization and Hamiltonian time evolution. With these templates, a `Circuit` with a clear structure and consistent parameter naming can be generated from a small amount of configuration, avoiding repeated hand-written construction of common circuit structures in algorithm development. + +--- + +## `Ansatz` trait + +The `Ansatz` trait defines the unified interface of ansatz templates. Templates implementing this trait all support configuration validation, circuit construction, parameter count query and qubit count query. + +```rust +pub trait Ansatz { + fn validate(&self) -> Result<(), CircuitError> { ... } + fn build_circuit(&self, prefix: &str) -> Result; + fn num_parameters(&self) -> usize; + fn num_qubits(&self) -> usize; +} +``` + +| Method | Returns | Description | +| --- | --- | --- | +| `validate()` | `Result<(), CircuitError>` | Check whether the template configuration is valid, for example whether the qubit count, gate arity, topology and Hamiltonian meet the requirements. | +| `build_circuit(prefix)` | `Result` | Generate a parameterized circuit from the current template configuration. | +| `num_parameters()` | `usize` | Return the number of independent parameters required by the circuit generated by this template. | +| `num_qubits()` | `usize` | Return the number of qubits this template acts on. | + +```rust +use cqlib_core::circuit::ansatz::{Ansatz, TwoLocal}; + +let ansatz = TwoLocal::new(3).reps(2); + +ansatz.validate()?; + +let circuit = ansatz.build_circuit("theta")?; + +assert_eq!(ansatz.num_qubits(), 3); +assert_eq!(circuit.symbols().len(), ansatz.num_parameters()); + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +--- + +## Builder style and parameter prefix + +Templates in the `ansatz` module are usually configured in builder style. Configuration methods return the configured template object, so that the number of repetition layers, rotation gates, entanglement gates, entanglement topology or evolution strategy can be specified step by step through chained calls. + +```rust +use cqlib_core::circuit::StandardGate; +use cqlib_core::circuit::ansatz::{Ansatz, EntanglementTopology, TwoLocal}; + +let ansatz = TwoLocal::new(4) + .reps(2) + .rotation_gates(vec![StandardGate::RY, StandardGate::RZ]) + .entanglement_gate(StandardGate::CX) + .entanglement(EntanglementTopology::Linear); + +let circuit = ansatz.build_circuit("theta")?; + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +The `prefix` in `build_circuit(prefix)` is used to generate symbolic parameter names in the circuit. For example, common templates generate parameter names such as `{prefix}_0` and `{prefix}_1`; the QAOA template generates parameter names with `gamma` / `beta` semantics; Hamiltonian evolution templates usually use a time parameter name. + +When combining multiple ansatz templates in the same model, use a different prefix for each module to avoid symbol name conflicts. For example: + +```text +encoder_0, encoder_1, ... +ansatz_0, ansatz_1, ... +qaoa_gamma_0, qaoa_beta_0, ... +``` + +--- + +## `EntanglementTopology` + +```rust +pub enum EntanglementTopology { + Linear, + Circular, + Full, + Custom(Vec<(usize, usize)>), +} +``` + +`EntanglementTopology` describes the entanglement connectivity in multi-qubit templates. It determines between which qubits a two-qubit gate or a many-body Pauli evolution term is added in each layer. + +| Topology | Description | +| --- | --- | +| `Linear` | Nearest-neighbor chain connectivity, for example `(0, 1), (1, 2), ...`. | +| `Circular` | Adds a connection between the first and last qubits on top of `Linear`. | +| `Full` | Fully connected topology, where any two qubits can be connected. | +| `Custom(Vec<(usize, usize)>)` | Connection pairs explicitly specified by the user. | + +The common methods are as follows: + +| Method | Description | +| --- | --- | +| `generate_pairs(num_qubits)` | Generate two-qubit connection pairs from the topology. | +| `generate_k_tuples(k, num_qubits)` | Generate k-local groups of acted-on qubits from the topology. | + +```rust +use cqlib_core::circuit::ansatz::EntanglementTopology; + +let topology = EntanglementTopology::Linear; +let pairs = topology.generate_pairs(4); + +assert_eq!(pairs, vec![(0, 1), (1, 2), (2, 3)]); +``` + +--- + +## `TwoLocal` + +`TwoLocal` is a common hardware-friendly ansatz. It consists of alternating single-qubit rotation layers and multi-qubit entanglement layers, and suits VQE, quantum machine learning and general variational circuit construction. + +```rust +use cqlib_core::circuit::StandardGate; +use cqlib_core::circuit::ansatz::{Ansatz, EntanglementTopology, TwoLocal}; + +let ansatz = TwoLocal::new(3) + .reps(2) + .rotation_gates(vec![StandardGate::RY, StandardGate::RZ]) + .entanglement_gate(StandardGate::CX) + .entanglement(EntanglementTopology::Linear); + +let circuit = ansatz.build_circuit("theta")?; + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +The configuration methods are as follows: + +| Method | Description | +| --- | --- | +| `reps(reps)` | Set the number of repetition layers. | +| `rotation_gates(gates)` | Set the single-qubit parameterized rotation gates used in each layer. | +| `entanglement_gate(gate)` | Set the two-qubit gate used in the entanglement layer. | +| `entanglement(topology)` | Set the entanglement topology. | +| `skip_final_rotation_layer(skip)` | Set whether to skip the final rotation layer. | + +When using `TwoLocal`, ensure that the rotation gates are single-qubit parameterized gates and that the entanglement gate is a suitable multi-qubit gate. If a gate arity does not match the template requirements, `validate()` or `build_circuit()` returns an error. + +--- + +## Feature Maps + +A feature map maps classical data into quantum circuit parameters, and is a common component in quantum machine learning and quantum kernel methods. + +| Type | Description | +| --- | --- | +| `AngleEncoding` | Map each input feature to a single-qubit rotation angle. | +| `BasisEncoding` | Prepare a computational basis state according to a bitstring, without introducing continuous parameters. | +| `ZFeatureMap` | First-order Pauli-Z feature map. | +| `IQPFeatureMap` | IQP-style diagonal feature map. | +| `ZZFeatureMap` | Second-order ZZ feature map, commonly used in quantum kernel methods. | +| `PauliFeatureMap` | Feature map supporting arbitrary Pauli string templates. | + +```rust +use cqlib_core::circuit::ansatz::{Ansatz, EntanglementTopology, ZZFeatureMap}; + +let fm = ZZFeatureMap::new(3) + .reps(2) + .entanglement(EntanglementTopology::Full); + +let circuit = fm.build_circuit("x")?; + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +--- + +## Convenience constructors + +For the most frequently used templates, the `ansatz` module provides a set of convenience constructors that create instances directly by template name. They still return the template objects listed above, and configuration can be adjusted further with builder methods. + +| Function | Returns | Description | +| --- | --- | --- | +| `real_amplitudes(num_qubits, reps, entanglement)` | `TwoLocal` | A template consisting of alternating `RY` rotation layers and entanglement layers. | +| `efficient_su2(num_qubits, reps, entanglement)` | `TwoLocal` | A template consisting of alternating `RY` and `RZ` rotation layers and entanglement layers. | +| `zz_feature_map(num_qubits, reps, entanglement)` | `ZZFeatureMap` | Second-order ZZ feature map. | +| `pauli_feature_map(num_qubits, reps, paulis, entanglement)` | `PauliFeatureMap` | A feature map constructed from the given Pauli string templates. | + +Here `paulis` is a list of `(PauliString, String)`, where the former specifies the interaction type and the latter is the label of that Pauli term. + +```rust +use cqlib_core::circuit::ansatz::{ + efficient_su2, + real_amplitudes, + zz_feature_map, + Ansatz, + EntanglementTopology, +}; + +let ansatz = real_amplitudes(3, 2, EntanglementTopology::Linear); + +assert_eq!(ansatz.num_qubits(), 3); +assert_eq!(ansatz.num_parameters(), 9); + +let su2 = efficient_su2(2, 1, EntanglementTopology::Full); + +assert_eq!(su2.num_parameters(), 8); + +let fm = zz_feature_map(3, 1, EntanglementTopology::Full); + +assert_eq!(fm.num_parameters(), 3); +``` + +--- + +## Layer templates + +Besides the general `TwoLocal`, the `ansatz` module also provides common layer templates for quickly constructing highly regular trainable circuits. + +### 1. `BasicEntanglerLayers` + +`BasicEntanglerLayers` usually consists of single-qubit rotation layers and entanglement layers with a fixed pattern, and suits training circuits with a simple structure and a controllable parameter count. + +```rust +use cqlib_core::circuit::StandardGate; +use cqlib_core::circuit::ansatz::{Ansatz, BasicEntanglerLayers}; + +let ansatz = BasicEntanglerLayers::new(4) + .reps(3) + .rotation_gate(StandardGate::RY) + .entanglement_gate(StandardGate::CX); + +let circuit = ansatz.build_circuit("w")?; + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +### 2. `StronglyEntanglingLayers` + +`StronglyEntanglingLayers` usually uses stronger single-qubit rotations and a ranged entanglement pattern, and suits variational models that require stronger expressivity. + +```rust +use cqlib_core::circuit::StandardGate; +use cqlib_core::circuit::ansatz::{Ansatz, StronglyEntanglingLayers}; + +let ansatz = StronglyEntanglingLayers::new(4) + .reps(3) + .entanglement_gate(StandardGate::CX) + .ranges(vec![1, 2]); + +let circuit = ansatz.build_circuit("w")?; + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +--- + +## `QAOAAnsatz` + +`QAOAAnsatz` constructs the parameterized circuit of the Quantum Approximate Optimization Algorithm (QAOA). In circuit structure, QAOA alternately applies the time evolution of the cost Hamiltonian and the mixer Hamiltonian. + +The typical form can be understood as: + +```text +U(β, γ) = ∏_l exp(-i β_l H_M) exp(-i γ_l H_C) +``` + +Where: + +- `H_C` is the problem Hamiltonian, also called the cost operator; +- `H_M` is the mixer Hamiltonian, usually an X mixer by default; +- `p = reps` is the number of QAOA layers; +- Each layer usually contains one `γ_l` and one `β_l` parameter, so `num_parameters()` is usually `2 * reps`. + +The configuration methods are as follows: + +| Method | Description | +| --- | --- | +| `new(cost_operator)` | Create a QAOA ansatz from the cost Hamiltonian. | +| `reps(reps)` | Set the number of QAOA layers `p`. | +| `mixer(mixer_operator)` | Set a custom mixer Hamiltonian. | +| `initial_state(circuit)` | Set the initial state circuit. | +| `evolution_strategy(strategy)` | Set the Hamiltonian evolution strategy. | + +When using QAOA, ensure that the cost Hamiltonian, the mixer Hamiltonian and the initial state circuit have the same number of qubits. If the dimensions do not match, an error is returned when the circuit is built. + +--- + +## Hamiltonian Evolution + +Hamiltonian evolution templates construct parameterized time evolution circuits of the form `exp(-i H t)`. Such templates are commonly used to simulate the evolution of quantum systems, to build QAOA sub-modules, or to validate the gate-level decomposition of a Pauli Hamiltonian. + +### 1. `EvolutionStrategy` + +`EvolutionStrategy` specifies how a Hamiltonian evolution is converted into a sequence of quantum gates. + +| Variant | Description | +| --- | --- | +| `Exact` | Use exact Pauli rotation decomposition for a pairwise commuting Hamiltonian; if the terms do not commute, an error is returned at build time. | +| `Auto { steps }` | Choose the strategy automatically: use exact when the terms commute, and first-order Trotter with `steps` steps when they do not. | +| `Trotter { mode, steps }` | Explicitly use the given Trotter-Suzuki decomposition mode and number of steps. | + +### 2. `EvolutionInfo` + +`EvolutionInfo` describes the evolution strategy actually used and Hamiltonian structure information, and is usually returned by `PauliEvolutionAnsatz::evolution_info()`. + +Common fields include: + +- `is_exact` +- `steps` +- `trotter_mode` +- `all_terms_commute` +- `num_terms` + +This information can be used to debug the evolution decomposition, record experiment configurations, or state in a report whether an evolution circuit is an exact decomposition. + +### 3. `PauliEvolutionAnsatz` + +`PauliEvolutionAnsatz` constructs the time evolution circuit of a Pauli Hamiltonian. The circuit generated by this template usually contains only one time parameter. The default parameter name may be `{prefix}_t`, and it can also be specified through `with_time_param_name()`, for example `tau`. diff --git a/docs/api/en/rust/0_circuit/12_cfg.md b/docs/api/en/rust/0_circuit/12_cfg.md new file mode 100644 index 00000000..d9a8e178 --- /dev/null +++ b/docs/api/en/rust/0_circuit/12_cfg.md @@ -0,0 +1,381 @@ +# CircuitCFG + +`cqlib_core::circuit::CircuitCFG` + +```rust +use cqlib_core::circuit::CircuitCFG; +use cqlib_core::circuit::cfg::{ + BasicBlock, + ControlFlowRegion, + FlowEdge, + OperationMetadata, + SwitchRegionCase, + Terminator, +}; +``` + +`CircuitCFG` is the analysis view used in the Rust core to represent a quantum circuit control flow graph (Control Flow Graph, CFG). Unlike `Circuit`, which is oriented to users and structured circuit construction, `CircuitCFG` is oriented more toward internal compiler use, for analyzing, rewriting and validating circuits that contain structured control flow. + +--- + +## Core concepts + +| Type | Description | +| --- | --- | +| `CircuitCFG` | The control flow graph view of a circuit, containing basic blocks, control flow edges, the entry block, qubits, the classical data tables, the symbol table, the parameter table and the global phase. | +| `BasicBlock` | A basic block that holds a sequence of sequentially executed `Operation`s; in a valid CFG it must carry exactly one terminator. | +| `Terminator` | The control transfer description at the end of a basic block, for example a sequential jump, a branch or a loop exit. | +| `FlowEdge` | The edge type in the control flow graph, used to describe the execution flow relationship between blocks. | +| `ControlFlowRegion` | Structured control flow region metadata, used to record the structured region corresponding to a branch or a loop. | +| `OperationMetadata` | Operation-level metadata, recording the qubits, parameters and label the operation acts on. | +| `SwitchRegionCase` | Case metadata in a `switch` control flow region, recording the case value and the corresponding entry block. | + +Among these types, only `CircuitCFG` can be imported from the `cqlib_core::circuit` module root; the remaining types live in the `cqlib_core::circuit::cfg` submodule. The symbol table, the parameter table and the global phase are kept internally by the CFG and have no public access interface, but they are restored together when `to_circuit()` is called. + +--- + +## `BasicBlock` + +`BasicBlock` represents a sequence of sequentially executed operations. + +The common methods are as follows: + +| Method | Description | +| --- | --- | +| `new()` | Create an empty basic block. | +| `with_label(label)` | Consume itself and return a new basic block with a label; requires chaining or rebinding. | +| `push_operation(op)` | Append one operation to the end of the basic block. | +| `extend_operations(ops)` | Append operations in bulk. | +| `set_terminator(terminator)` | Set the terminator of the basic block. | +| `is_empty()` | Determine whether the basic block has neither operations nor a terminator. | +| `has_terminator()` | Determine whether the basic block already has a terminator set. | +| `len()` | Return the number of operations in the basic block. | +| `label()` | Read the basic block label. | +| `terminator()` | Read the basic block terminator. | +| `operations()` | Read the slice of operations in the basic block. | + +In addition, `BasicBlock` implements `Default`, whose behavior is equivalent to `BasicBlock::new()`. + +--- + +## `Terminator` + +```rust +pub enum Terminator { + Branch(ClassicalExpr), + ForLoop { + var: ClassicalVar, + start: ClassicalExpr, + stop: ClassicalExpr, + step: ClassicalExpr, + }, + Switch(ClassicalExpr), + Jump(NodeIndex), + Break(NodeIndex), + Continue(NodeIndex), + Return, +} +``` + +| Variant | Description | +| --- | --- | +| `Branch(condition)` | Conditional branch; the condition must be a `Bool` expression, and the outgoing edge types are `FlowEdge::TrueBranch` and `FlowEdge::FalseBranch` respectively. | +| `ForLoop { var, start, stop, step }` | Half-open interval loop; the four expressions must be `UInt` of the same bit width. | +| `Switch(target)` | Multi-way selection; the target must be a `UInt` expression, and the number of outgoing edges must equal the number of cases plus one. | +| `Jump(target)` | Unconditional jump to the target basic block; the outgoing edge type is `FlowEdge::Unconditional`. | +| `Break(target)` | Break out of a loop or a `switch`; the outgoing edge type is `FlowEdge::Break`. | +| `Continue(target)` | Enter the next loop iteration; the outgoing edge type is `FlowEdge::Continue`. | +| `Return` | End the current circuit branch; no outgoing edge is allowed. | + +In a valid CFG, every basic block must have exactly one terminator, and its outgoing edge count and outgoing edge types must agree with the terminator. For a linear circuit constructed by `from_circuit()`, the entry block terminator is `Terminator::Return`. + +--- + +## `FlowEdge` + +| Variant | Description | +| --- | --- | +| `TrueBranch` | The edge taken when the condition is true. | +| `FalseBranch` | The edge taken when the condition is false. | +| `Unconditional` | An unconditional jump edge. | +| `Case(value)` | The edge taken when `value` is matched in a `switch`. | +| `DefaultCase` | The edge of the `switch` default branch; it exists even when the source `switch` has no default body. | +| `Break` | The `break` exit edge. | +| `Continue` | The `continue` edge. | + +--- + +## Creating `CircuitCFG` + +`CircuitCFG` provides the following creation interfaces: + +```rust +pub fn new(num_qubits: usize) -> Self +pub fn from_qubits(qubits: Vec) -> Self +pub fn from_circuit(circuit: &Circuit) -> Result +``` + +| Interface | Description | +| --- | --- | +| `CircuitCFG::new(num_qubits)` | Create an empty CFG containing consecutive logical qubits; no entry block is set yet, so it must be edited before it can pass `validate()`. | +| `CircuitCFG::from_qubits(qubits)` | Create an empty CFG from the specified set of logical qubits, suitable for sparse logical numbering; duplicate qubits are removed and insertion order is preserved. | +| `CircuitCFG::from_circuit(circuit)` | Construct a CFG from an existing structured `Circuit`; this is the most common entry point. `validate()` is called internally once before construction finishes, and control flow that cannot be structured returns an error directly. | + +`CircuitCFG` also implements `TryFrom<&Circuit>`, whose behavior is identical to `from_circuit()`; the reverse `TryFrom<&CircuitCFG> for Circuit` is identical to `to_circuit()`. The error type of both is `CircuitError`. + +```rust +use cqlib_core::circuit::{Circuit, CircuitCFG, Qubit}; + +let mut circuit = Circuit::new(2); +circuit.h(Qubit::new(0))?; + +let cfg = CircuitCFG::from_circuit(&circuit)?; + +assert_eq!(cfg.num_qubits(), 2); + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +--- + +## Graph editing interfaces + +`CircuitCFG` provides basic graph editing capabilities, used to add basic blocks, add edges, set the entry block and maintain structured control flow region metadata. + +| Method | Description | +| --- | --- | +| `add_block(block)` | Add a `BasicBlock` and return the corresponding `NodeIndex`. | +| `add_edge(source, target, flow)` | Add a control flow edge between `source` and `target`, returning the edge index on success; if either endpoint does not exist, the graph is not modified and `None` is returned. | +| `entry_block()` | Read the entry basic block of the current CFG; return `None` when it is not set. | +| `set_entry_block(index)` | Set the entry basic block of the CFG; this interface does not check whether the index is valid, and validity is decided by `validate()`. | +| `set_control_flow_region(branch_block, region)` | Set structured region metadata for a branch or control flow entry block. | +| `control_flow_region(branch_block)` | Read the structured control flow region metadata associated with a basic block; return `None` when there is no record. | +| `is_loop_header(block)` | Determine whether a basic block is a loop header; only the entry block of a `While` or `For` region returns `true`, and a `Switch` region entry does not count as a loop header. | +| `block_mut(index)` | Mutably read the specified basic block; return `None` when the index does not exist. | +| `outgoing_edges(source)` | Iterate over the outgoing edges of the specified basic block, yielding `(NodeIndex, FlowEdge)` pairs. | + +--- + +## Query interfaces + +`CircuitCFG` provides the following query interfaces, used to traverse the graph structure and read basic circuit information. + +| Method | Description | +| --- | --- | +| `blocks()` | Iterate over the basic blocks in the CFG, yielding `(NodeIndex, &BasicBlock)` pairs. | +| `num_blocks()` | Return the number of basic blocks. | +| `num_qubits()` | Return the number of qubits. | +| `qubits()` | Return the list of logical qubits in insertion order, as an owned `Vec`. | +| `classical_vars()` | Return the classical variable type table. | +| `classical_values()` | Return the classical value type table. | + +--- + +## Validation and reconstruction + +`CircuitCFG` provides two key interfaces for checking and restoring the graph structure: + +```rust +pub fn validate(&self) -> Result<(), CircuitError> +pub fn to_circuit(&self) -> Result +``` + +### `validate()` + +`validate()` checks whether the CFG structure is self-consistent. Typical checks include: + +- whether a valid entry block exists; +- whether the basic blocks referenced by edges exist; +- whether every basic block has a terminator, whether the terminator matches the outgoing edge count and outgoing edge types, and whether `Return` has no outgoing edge; +- whether unexpanded `Instruction::ClassicalControl` remains inside basic blocks; +- whether there are unreachable or unconsumed basic blocks, that is, whether all basic blocks are reachable from the entry block; +- whether the control flow region metadata is consistent with the graph structure; +- whether loop headers, loop back edges and exit edges satisfy the constraints; +- whether the condition of `Branch` is a `Bool` expression; +- whether the target of `Switch` is a `UInt` expression, and whether the number of outgoing edges equals the number of cases plus one; +- whether the scopes and dependencies of classical variables and values can be interpreted correctly; +- whether the `break` / `continue` terminators each have exactly one outgoing edge of the same type pointing to the target block. + +Whether `break` and `continue` appear inside a valid loop or `switch` region is decided by `from_circuit()` at construction time; `validate()` only checks the correspondence between terminators and outgoing edges. + +### `to_circuit()` + +`to_circuit()` restores the CFG back into a structured `Circuit`. It requires that the CFG is not only a valid graph, but also maps back to the structured control flow forms supported by Cqlib. + +If the graph structure of the CFG has been damaged, or some control flow regions cannot be mapped back to structured constructs such as `if`, `while`, `for` and `switch`, `to_circuit()` returns an error. The method calls `validate()` once internally first, so the constraints of the previous section also apply here. + +The recommended workflow is as follows: + +```rust +use cqlib_core::circuit::{Circuit, CircuitCFG, Qubit}; + +let mut circuit = Circuit::new(2); +circuit.h(Qubit::new(0))?; +circuit.cx(Qubit::new(0), Qubit::new(1))?; + +let cfg = CircuitCFG::from_circuit(&circuit)?; + +// 在这里执行 CFG 分析或转换 pass + +cfg.validate()?; +let new_circuit = cfg.to_circuit()?; + +assert_eq!(new_circuit.num_qubits(), 2); + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +--- + +## `ControlFlowRegion` metadata + +`ControlFlowRegion` records the boundaries and semantics of structured control flow regions. For constructs such as `if`, `while`, `for` and `switch`, CFG edges alone are not enough to fully recover the original structured semantics; region metadata is also needed to describe which basic blocks belong to the same control flow structure, where the entry and the exit are, and how switch cases correspond. + +```rust +pub enum ControlFlowRegion { + If { + then_entry: NodeIndex, + else_entry: NodeIndex, + merge_block: NodeIndex, + has_else: bool, + outer: OperationMetadata, + }, + While { + body_entry: NodeIndex, + exit_block: NodeIndex, + outer: OperationMetadata, + }, + For { + body_entry: NodeIndex, + exit_block: NodeIndex, + outer: OperationMetadata, + }, + Switch { + cases: Vec, + default_entry: NodeIndex, + merge_block: NodeIndex, + has_default: bool, + outer: OperationMetadata, + }, +} +``` + +| Variant | Field | Description | +| --- | --- | --- | +| `If` | `then_entry`, `else_entry`, `merge_block`, `has_else`, `outer` | The entry block of the conditional branch region, the `else` branch entry block, the merge block, and whether the source circuit has an `else` branch. | +| `While` | `body_entry`, `exit_block`, `outer` | The `while` loop body entry block and exit block. | +| `For` | `body_entry`, `exit_block`, `outer` | The `for` loop body entry block and exit block. | +| `Switch` | `cases`, `default_entry`, `merge_block`, `has_default`, `outer` | The metadata of each case, the default branch entry block, the merge block, and whether the source `switch` has a default branch. | + +Here `outer` is the operation metadata that records the control flow structure itself, used to restore the expanded control flow operation during reconstruction. The related types are as follows: + +```rust +pub struct OperationMetadata { + pub qubits: SmallVec<[Qubit; 3]>, + pub params: SmallVec<[CircuitParam; 1]>, + pub label: Option>, +} + +pub struct SwitchRegionCase { + pub value: u128, + pub entry: NodeIndex, +} +``` + +| Field | Description | +| --- | --- | +| `OperationMetadata::qubits` | The qubits the control flow operation acts on. | +| `OperationMetadata::params` | The parameters of the control flow operation. | +| `OperationMetadata::label` | The label of the control flow operation. | +| `SwitchRegionCase::value` | The `UInt` value the case matches. | +| `SwitchRegionCase::entry` | The entry block of the branch corresponding to the case. | + +These metadata have no separate access interfaces, so a pass must maintain them itself when modifying the graph structure; otherwise parameters or labels may be lost when `to_circuit()` reconstructs the circuit. + +Therefore, the following points require special attention when writing a CFG pass: + +- if the branch structure is modified, the corresponding `ControlFlowRegion` should be updated as well; +- if basic blocks are deleted or merged, check whether the region metadata still references the old block; +- if loop edges are adjusted, check whether the loop header and the region boundaries are still correct; +- if switch cases are modified, metadata such as `SwitchRegionCase` should be maintained as well; +- if only local optimization is performed on the operations inside a basic block, the region metadata usually does not need to be modified. + +Keeping the region metadata consistent with the graph edges is the key to whether `to_circuit()` can successfully reconstruct a structured circuit. + +--- + +## Typical workflow + +### 1. Entering the CFG from a structured circuit + +```rust +use cqlib_core::circuit::{Circuit, CircuitCFG, Qubit}; + +let mut circuit = Circuit::new(2); +circuit.h(Qubit::new(0))?; +circuit.cx(Qubit::new(0), Qubit::new(1))?; + +let cfg = CircuitCFG::from_circuit(&circuit)?; + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +### 2. Iterating over basic blocks + +```rust +use cqlib_core::circuit::{Circuit, CircuitCFG, Qubit}; + +let mut circuit = Circuit::new(2); +circuit.h(Qubit::new(0))?; + +let cfg = CircuitCFG::from_circuit(&circuit)?; + +for (node, block) in cfg.blocks() { + for _op in block.operations() { + // 分析每条 Operation + } + + if block.has_terminator() { + // 分析控制转移 + } +} + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +### 3. Validating and reconstructing after modification + +```rust +use cqlib_core::circuit::cfg::{BasicBlock, Terminator}; +use cqlib_core::circuit::{Circuit, CircuitCFG}; + +let mut cfg = CircuitCFG::new(1); + +let mut block = BasicBlock::new().with_label("entry"); +block.set_terminator(Terminator::Return); +let entry = cfg.add_block(block); +cfg.set_entry_block(entry); + +cfg.validate()?; +let circuit = cfg.to_circuit()?; + +assert_eq!(circuit.num_qubits(), 1); + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +--- + +## Applicable compiler pass types + +`CircuitCFG` is suitable for analysis and transformation tasks that require an explicit control flow structure, for example: + +| Pass type | Description | +| --- | --- | +| Control flow reachability analysis | Check unreachable basic blocks, branch paths and loop structures. | +| Local gate optimization within a branch | Perform gate merging, gate cancellation or local replacement inside each basic block. | +| Loop body resource estimation | Count the gate count, depth or measurement usage inside a loop body. | +| Classical data scope checking | Analyze whether `ClassicalValue` is used out of scope. | +| Dynamic circuit validity checking | Check whether control flow, measurement and classical data satisfy backend constraints. | +| Backend control flow lowering | Convert structured control flow into the form supported by the target backend. | +| Branch expansion or staticization | Simplify control flow into linear operations when the condition can be determined statically. | diff --git a/docs/api/en/rust/0_circuit/13_circuit_to_matrix.md b/docs/api/en/rust/0_circuit/13_circuit_to_matrix.md new file mode 100644 index 00000000..1cd7feaf --- /dev/null +++ b/docs/api/en/rust/0_circuit/13_circuit_to_matrix.md @@ -0,0 +1,141 @@ +# Circuit To Matrix + +- `cqlib_core::circuit::circuit_to_matrix` +- `cqlib_core::circuit::Circuit::to_matrix` +- `cqlib_core::circuit::symbolic_matrix::circuit_to_symbolic_matrix` + +```rust +use cqlib_core::circuit::{Circuit, Qubit, circuit_to_matrix}; +``` + +The interfaces related to `Circuit To Matrix` convert a quantum circuit into a matrix representation. The Rust core provides two categories of matrix conversion: + +- Numeric matrix conversion: convert a circuit whose parameters are all bound into an `Array2`; +- Symbolic matrix conversion: retain the `Parameter` expressions in the circuit and generate a `SymbolicMatrix`. + +--- + +## Numeric matrix conversion + +```rust +pub fn circuit_to_matrix( + circuit: &Circuit, + qubits_order: Option<&[usize]>, +) -> Result, CircuitError> +``` + +`circuit_to_matrix()` is the functional matrix conversion entry point, which converts a circuit into a numeric complex matrix. `Circuit::to_matrix()` is the corresponding method-style convenience interface, and the two have the same semantics. + +```rust +use cqlib_core::circuit::{Circuit, Qubit, circuit_to_matrix}; + +let mut circuit = Circuit::new(2); +circuit.h(Qubit::new(0))?; +circuit.cx(Qubit::new(0), Qubit::new(1))?; + +let matrix = circuit_to_matrix(&circuit, None)?; +assert_eq!(matrix.shape(), &[4, 4]); + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +In the example above, the circuit contains 2 qubits, so the matrix dimension is `4 × 4`. In general, if a circuit contains `n` qubits, the returned matrix shape is: + +```text +(2^n, 2^n) +``` + +--- + +## `Circuit::to_matrix()` method-style entry point + +Besides the functional interface, `to_matrix()` can also be called directly on a circuit object: + +```rust +let matrix = circuit.to_matrix(None)?; +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +--- + +## Qubit order + +`qubits_order` specifies the arrangement order of qubits in the matrix basis state indices. It must be a complete permutation of the qubit numbers in the circuit. + +The rules are as follows: + +- `None`: sort by qubit number by default; +- `Some(&[...])`: use the explicitly specified qubit order; +- The explicit order must match the qubit set of the circuit exactly, and must not omit, duplicate or contain unknown qubits; +- The first qubit in the order corresponds to the least-significant bit of the basis index, that is, the little-endian convention. + +```rust +let matrix_default = circuit_to_matrix(&circuit, None)?; +let matrix_reordered = circuit_to_matrix(&circuit, Some(&[1, 0]))?; + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +--- + +## Parameterized circuits + +A numeric matrix requires all symbolic parameters to be bound to concrete values. If unbound symbolic parameters remain in the circuit, matrix conversion cannot produce a definite complex matrix. + +```rust +use cqlib_core::circuit::{Circuit, Parameter, Qubit, circuit_to_matrix}; +use std::collections::HashMap; + +let mut circuit = Circuit::new(1); +circuit.rx(Qubit::new(0), Parameter::symbol("theta"))?; + +let mut bindings = HashMap::new(); +bindings.insert("theta", 0.5); + +let bound = circuit.assign_parameters(&Some(bindings))?; +let matrix = circuit_to_matrix(&bound, None)?; + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +--- + +## Symbolic matrix conversion + +```rust +use cqlib_core::circuit::{Circuit, Parameter, Qubit}; +use cqlib_core::circuit::symbolic_matrix::circuit_to_symbolic_matrix; + +let mut circuit = Circuit::new(1); +circuit.rx(Qubit::new(0), Parameter::symbol("theta"))?; + +let symbolic = circuit_to_symbolic_matrix(&circuit, None)?; + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +Symbolic matrix conversion retains the `Parameter` expressions in the circuit and returns a `SymbolicMatrix`. It can then be evaluated repeatedly under different parameter values through `evaluate_symbolic_matrix()`. + +--- + +## Global phase + +Both numeric and symbolic matrix conversion include the global phase of the circuit. If the circuit global phase is `theta` and the circuit matrix without considering the global phase is `U`, the matrix conversion returns: + +```text +exp(i * theta) * U +``` + +```rust +use cqlib_core::circuit::{Circuit, Qubit, circuit_to_matrix}; + +let mut circuit = Circuit::new(1); +circuit.x(Qubit::new(0))?; +circuit.set_global_phase(std::f64::consts::PI.into()); + +let matrix = circuit_to_matrix(&circuit, None)?; + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +In the example above, the operation part of the circuit corresponds to the `X` gate and the global phase is `π`, so the overall matrix is equivalent to `-X`. diff --git a/docs/api/en/rust/0_circuit/14_circuit_dag.md b/docs/api/en/rust/0_circuit/14_circuit_dag.md new file mode 100644 index 00000000..ea12f4ea --- /dev/null +++ b/docs/api/en/rust/0_circuit/14_circuit_dag.md @@ -0,0 +1,741 @@ +# Circuit DAG + +`cqlib_core::circuit` + +```rust +use cqlib_core::circuit::{CircuitDag, DagControlFlow, DagNode, DagSwitchCase, DagWire}; +``` + +`CircuitDag` is the algorithmic view of a circuit at the operation dependency level: nodes represent operations, and edges represent the precedence between two operations that arises from sharing the same resource. The shared resources include qubits, mutable classical variables, immutable classical values, and a global order used to carry operations that have no concrete data resource. This view does not change the storage structure of the circuit itself, and is mainly used for dependency analysis, layered scheduling and subgraph replacement. + +This page covers `CircuitDag`, `DagWire`, `DagNode`, `DagControlFlow` and `DagSwitchCase`. + +--- + +## Core concepts + +| Term | Description | +| --- | --- | +| `CircuitDag` | The operation-level dependency graph of a circuit; besides operation nodes, each wire also has input and output sentinel nodes. | +| `DagWire` | The resource an edge carries; its value is a qubit, a classical variable, a classical value or the global order. | +| `DagNode` | The node kind, divided into input sentinel, output sentinel and operation. | +| `DagControlFlow` | The recursive DAG payload carried by a structured control flow operation. | +| `DagSwitchCase` | A single exact integer match branch in a `switch`. | + +--- + +## CircuitDag + +### Construction + +```rust +pub fn from_circuit(circuit: &Circuit) -> Result +``` + +`CircuitDag::from_circuit()` constructs a dependency graph from an existing circuit, copying the circuit's qubits, parameter table, symbol names, classical variable and classical value type tables, and the global phase. `validate()` is called automatically after construction finishes. + +```rust +use cqlib_core::circuit::{Circuit, CircuitDag, Qubit}; + +let mut circuit = Circuit::new(2); +circuit.h(Qubit::new(0))?; +circuit.cx(Qubit::new(0), Qubit::new(1))?; + +let dag = CircuitDag::from_circuit(&circuit)?; + +assert_eq!(dag.num_qubits(), 2); +assert_eq!(dag.num_ops(), 2); +assert_eq!(dag.depth()?, 2); + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +```rust +pub fn from_operations( + qubits: impl IntoIterator, + operations: &[Operation], +) -> Result +``` + +`CircuitDag::from_operations()` constructs a dependency graph from a self-contained slice of operations, and is the narrow entry point of this view for local pass analysis. It accepts no parameter table, classical declarations or global phase: every operation must be valid with respect to exactly the given qubit set, and must not depend on any circuit-level metadata. When the operation flow contains indexed parameters, classical variables, classical values or control flow bodies, `from_circuit()` should be used instead; an analysis pass that intentionally ignores these resources should normalize them first, for example by resolving parameters or by replacing non-quantum operations with dependency barriers. + +An error is returned when duplicate qubits appear in `qubits`, when an operation references a qubit outside the set, or when the operation slice is not self-contained. + +```rust +use cqlib_core::circuit::{Circuit, CircuitDag, Qubit}; + +let mut circuit = Circuit::new(2); +circuit.h(Qubit::new(0))?; +circuit.cx(Qubit::new(0), Qubit::new(1))?; + +let dag = CircuitDag::from_operations(circuit.qubits(), circuit.operations())?; + +assert_eq!(dag.num_ops(), 2); +assert_eq!(dag.parameters().len(), 0); + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +### Exporting back to a circuit + +```rust +pub fn to_circuit(&self) -> Result +``` + +`to_circuit()` reconstructs the circuit in the deterministic topological order of the DAG, and restores the qubit set, the classical type tables and the global phase. An error is returned when the graph contains a cycle, when an invalid parameter index is referenced, or when the reconstructed operation sequence is rejected by `Circuit::from_operations()`. + +```rust +use cqlib_core::circuit::{Circuit, CircuitDag, ParameterValue, Qubit}; + +let mut circuit = Circuit::new(1); +circuit.rx(Qubit::new(0), ParameterValue::from("theta"))?; + +let dag = CircuitDag::from_circuit(&circuit)?; +let recovered = dag.to_circuit()?; + +assert_eq!(recovered.operations().len(), 1); +assert!(dag.symbols().contains("theta")); + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +### Validation + +```rust +pub fn validate(&self) -> Result<(), CircuitError> +``` + +`validate()` checks the consistency of the graph and the metadata, including: whether the graph contains a cycle, whether the input and output sentinels of each wire are valid, whether operation node numbers are unique and consistent with the order index, whether the qubits and parameter indices referenced by operations are valid, whether classical variables and classical values belong to the current circuit, whether control flow payloads match the owning operation, and whether edge resources are valid. + +A DAG that was imported externally, generated automatically or modified manually should be validated explicitly before entering later stages: + +```rust +use cqlib_core::circuit::{Circuit, CircuitDag, Qubit}; + +let mut circuit = Circuit::new(2); +circuit.h(Qubit::new(0))?; +circuit.cx(Qubit::new(0), Qubit::new(1))?; +circuit.measure(Qubit::new(1))?; + +let dag = CircuitDag::from_circuit(&circuit)?; +dag.validate()?; + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +### Type conversion + +`TryFrom` conversions between the DAG and the circuit are also provided, with semantics identical to `from_circuit()` and `to_circuit()`. + +| Implementation | Description | +| --- | --- | +| `impl TryFrom<&Circuit> for CircuitDag` | Equivalent to `CircuitDag::from_circuit()`. | +| `impl TryFrom<&CircuitDag> for Circuit` | Equivalent to `CircuitDag::to_circuit()`. | + +```rust +use cqlib_core::circuit::{Circuit, CircuitDag, Qubit}; + +let mut circuit = Circuit::new(2); +circuit.h(Qubit::new(0))?; +circuit.cx(Qubit::new(0), Qubit::new(1))?; + +let dag = CircuitDag::try_from(&circuit)?; +let recovered = Circuit::try_from(&dag)?; +assert_eq!(recovered, circuit); + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +### Metadata and statistics + +| Method | Returns | Description | +| --- | --- | --- | +| `num_qubits()` | `usize` | The number of qubits. | +| `num_ops()` | `usize` | The number of operation nodes. | +| `is_empty()` | `bool` | Whether there are no operation nodes. | +| `qubits()` | `Vec` | Return the qubits in insertion order. | +| `parameters()` | `&IndexSet` | The parameter table. | +| `symbols()` | `&IndexSet` | The set of free symbol names. | +| `classical_vars()` | `&[ClassicalType]` | The mutable classical variable type table. | +| `classical_values()` | `&[ClassicalType]` | The immutable classical value type table. | +| `add_parameter(param)` | `(usize, bool)` | Make a parameter resident in the parameter table, returning its index in the table and whether it was newly inserted. | + +The behavior of `add_parameter()` is identical to `Circuit::add_parameter()`: insert the parameter when it is not present, and register all of its symbol names. + +```rust +use cqlib_core::circuit::{Circuit, ClassicalType, CircuitDag, Qubit}; + +let mut circuit = Circuit::new(1); +let _flag = circuit.var(ClassicalType::Bool); +let _value = circuit.measure(Qubit::new(0))?; + +let dag = CircuitDag::from_circuit(&circuit)?; + +assert_eq!(dag.classical_vars(), circuit.classical_vars()); +assert_eq!(dag.classical_values(), circuit.classical_values()); + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +### Attributes + +- Implements `Debug` and `Clone`. + +--- + +## DagWire + +```rust +pub enum DagWire { + Qubit(Qubit), + ClassicalVar(ClassicalVar), + ClassicalValue(ClassicalValue), + GlobalOrder, +} +``` + +`DagWire` is the resource an edge carries. It is also the identity of a wire: each wire corresponds to one input sentinel node and one output sentinel node in the graph. + +| Variant | Description | +| --- | --- | +| `DagWire::Qubit(Qubit)` | A qubit wire. | +| `DagWire::ClassicalVar(ClassicalVar)` | A mutable classical variable wire, written by `store`. | +| `DagWire::ClassicalValue(ClassicalValue)` | An immutable classical value wire, usually produced by measurement. | +| `DagWire::GlobalOrder` | The global order, used to carry a stable order between operations that have no concrete data resource. | + +`DagWire` implements `Debug`, `Clone`, `Copy`, `PartialEq`, `Eq` and `Hash`. + +### Wire queries + +| Method | Returns | Description | +| --- | --- | --- | +| `wires()` | `impl Iterator` | Iterate over all wires materialized in the current DAG. | +| `has_wire(wire)` | `bool` | Determine whether a wire is already materialized in the current DAG. | +| `is_wire_idle(wire)` | `Result` | Determine whether a wire has no operation on it. | +| `wire_in(wire)` | `Option` | Return the input sentinel node of the wire. | +| `wire_out(wire)` | `Option` | Return the output sentinel node of the wire. | +| `nodes_on_wire(wire)` | `Result, CircuitError>` | Return the operation nodes on the wire in wire order. | + +`is_wire_idle()` and `nodes_on_wire()` return an error when the wire is not a valid resource of the current DAG, or when the timeline on the wire is broken or forked. + +```rust +use cqlib_core::circuit::{Circuit, CircuitDag, DagWire, Qubit}; + +let mut circuit = Circuit::new(1); +circuit.h(Qubit::new(0))?; +circuit.x(Qubit::new(0))?; +circuit.z(Qubit::new(0))?; + +let dag = CircuitDag::from_circuit(&circuit)?; +let nodes = dag.nodes_on_wire(DagWire::Qubit(Qubit::new(0)))?; + +assert_eq!(nodes.len(), 3); +assert_eq!(dag.num_ops(), 3); + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +Qubit wires are all materialized at construction time, whereas classical variable wires and classical value wires are materialized only when referenced by an operation. The unwritten classical variable wire below is neither in `wires()` nor involved in any dependency; `DagWire::GlobalOrder` always exists, and is used to carry operations that do not involve any concrete data resource. + +```rust +use cqlib_core::circuit::{Circuit, CircuitDag, ClassicalType, DagWire, Qubit}; + +let mut circuit = Circuit::new(2); +let unused_var = circuit.var(ClassicalType::Bool); +circuit.h(Qubit::new(1))?; + +let dag = CircuitDag::from_circuit(&circuit)?; + +assert!(dag.has_wire(DagWire::Qubit(Qubit::new(0)))); +assert!(!dag.has_wire(DagWire::ClassicalVar(unused_var))); +assert!(dag.is_wire_idle(DagWire::Qubit(Qubit::new(0)))?); + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +--- + +## DagNode + +```rust +pub enum DagNode { + WireIn(DagWire), + WireOut(DagWire), + Operation { + operation: Operation, + order: usize, + }, +} +``` + +`DagNode` is the node payload in the graph. Each operation node stores a storage-layer operation and a source order position, which is used for deterministic traversal. + +| Variant | Description | +| --- | --- | +| `DagNode::WireIn(DagWire)` | The input sentinel of a wire. | +| `DagNode::WireOut(DagWire)` | The output sentinel of a wire. | +| `DagNode::Operation { operation, order }` | An operation, where `order` is its source order position. | + +The node kind can be queried with `node_kind()`, which returns `"wire_in"`, `"wire_out"` or `"operation"`, and returns `None` when the node does not exist. + +--- + +## Node identifiers + +The node identifier type is `NodeIndex`, provided by `rustworkx_core::petgraph::prelude::NodeIndex`, and its `usize` value can be retrieved with `node.index()`. Node identifiers are obtained from the following entry points: + +| How obtained | Description | +| --- | --- | +| `op_nodes()` | Iterate over all operation nodes in source order. | +| `topological_op_nodes()` | Return all operation nodes in topological order. | +| `front_layer()`, `layers()`, `node_layers()` | The nodes in the layering results. | +| `wire_in()` / `wire_out()` | The input and output sentinel nodes of a wire. | +| `apply_operation_back()` and other mutating methods | Return the new node identifier. | + +| Method | Returns | Description | +| --- | --- | --- | +| `operation(node)` | `Option<&Operation>` | Return the storage-layer operation corresponding to the node; `None` when the node is not an operation node. | +| `is_operation(node)` | `bool` | Determine whether the node is an operation node. | +| `node_kind(node)` | `Option<&'static str>` | Return the node kind. | +| `control_flow(node)` | `Option<&DagControlFlow>` | Return the control flow payload carried by the node; `None` when there is none. | +| `op_nodes()` | `impl Iterator` | Iterate over operation nodes in source order. | +| `topological_op_nodes()` | `Result, CircuitError>` | Return operation nodes in deterministic topological order. | + +```rust +use cqlib_core::circuit::{Circuit, CircuitDag, DagWire, Instruction, Qubit, StandardGate}; + +let mut circuit = Circuit::new(1); +circuit.h(Qubit::new(0))?; + +let dag = CircuitDag::from_circuit(&circuit)?; +let node = dag.topological_op_nodes()?[0]; +let operation = dag.operation(node).expect("operation node"); + +assert!(matches!( + operation.instruction, + Instruction::Standard(StandardGate::H) +)); +assert_eq!(operation.qubits[0], Qubit::new(0)); + +let wire_in = dag.wire_in(DagWire::Qubit(Qubit::new(0))).expect("qubit wire"); +assert!(dag.operation(wire_in).is_none()); + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +--- + +## Graph traversal + +Predecessor and successor queries come in three levels: all neighbors regardless of resource, neighbors restricted to a single wire, and neighbors classified by quantum or classical resource. + +| Method | Returns | Description | +| --- | --- | --- | +| `predecessors(node)` | `impl Iterator` | Iterate over all predecessor nodes, which may include sentinel nodes. | +| `successors(node)` | `impl Iterator` | Iterate over all successor nodes, which may include sentinel nodes. | +| `predecessors_on_wire(node, wire)` | `Result, CircuitError>` | Operation predecessors connected only through the specified wire. | +| `successors_on_wire(node, wire)` | `Result, CircuitError>` | Operation successors connected only through the specified wire. | +| `quantum_predecessors(node)` | `Result, CircuitError>` | Operation predecessors connected through qubit wires. | +| `quantum_successors(node)` | `Result, CircuitError>` | Operation successors connected through qubit wires. | +| `classical_predecessors(node)` | `Result, CircuitError>` | Operation predecessors connected through classical variable wires or classical value wires. | +| `classical_successors(node)` | `Result, CircuitError>` | Operation successors connected through classical variable wires or classical value wires. | + +The neighbors returned by `predecessors()` and `successors()` may include wire sentinel nodes, and `operation(node).is_some()` can be used to filter them out when needed. The remaining methods return only operation nodes, with the results arranged in topological order. An error is returned when a node identifier does not exist, or when a wire is not a valid resource of the current DAG. + +```rust +use cqlib_core::circuit::{Circuit, CircuitDag, DagWire, Qubit}; + +let mut circuit = Circuit::new(2); +circuit.h(Qubit::new(0))?; +circuit.x(Qubit::new(1))?; +circuit.cx(Qubit::new(0), Qubit::new(1))?; +circuit.z(Qubit::new(0))?; +circuit.h(Qubit::new(1))?; + +let dag = CircuitDag::from_circuit(&circuit)?; +let nodes = dag.topological_op_nodes()?; +let (h0, x1, cx, z0, h1) = (nodes[0], nodes[1], nodes[2], nodes[3], nodes[4]); + +assert_eq!( + dag.predecessors_on_wire(cx, DagWire::Qubit(Qubit::new(0)))?, + vec![h0] +); +assert_eq!( + dag.successors_on_wire(cx, DagWire::Qubit(Qubit::new(1)))?, + vec![h1] +); +assert_eq!(dag.quantum_predecessors(cx)?, vec![h0, x1]); +assert_eq!(dag.quantum_successors(cx)?, vec![z0, h1]); + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +Dependencies on classical resources use the same set of interfaces: + +```rust +use cqlib_core::circuit::{Circuit, CircuitDag, ClassicalExpr, ClassicalType, Qubit}; + +let mut circuit = Circuit::new(1); +let measured = circuit.measure(Qubit::new(0))?; +let dest = circuit.var(ClassicalType::Bool); +circuit.store(dest, ClassicalExpr::bit_to_bool(measured.expr())?)?; + +let dag = CircuitDag::from_circuit(&circuit)?; +let nodes = dag.topological_op_nodes()?; +let (measure, store) = (nodes[0], nodes[1]); + +assert_eq!(dag.classical_predecessors(store)?, vec![measure]); +assert!(dag.quantum_predecessors(store)?.is_empty()); + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +--- + +## Layers and depth + +| Method | Returns | Description | +| --- | --- | --- | +| `front_layer()` | `Result, CircuitError>` | The operation nodes with no operation predecessors, that is, the earliest executable layer. | +| `layers()` | `Result>, CircuitError>` | The operation nodes layered by earliest scheduling. | +| `node_layers()` | `Result, CircuitError>` | The mapping from each operation node to its layer number. | +| `depth()` | `Result` | The dependency depth, that is, the number of layers. | + +`layers()` and `node_layers()` use the same set of layer numbers: the layer number of a layer is the maximum layer number among all of its operation predecessors plus one. `depth()` equals the maximum layer number plus one, and is `0` for an empty graph. An error is returned when the graph contains a cycle, or when the layer numbers of operation predecessors are inconsistent with the topological order. + +```rust +use cqlib_core::circuit::{Circuit, CircuitDag, Qubit}; + +let mut circuit = Circuit::new(2); +circuit.h(Qubit::new(0))?; +circuit.x(Qubit::new(1))?; +circuit.cx(Qubit::new(0), Qubit::new(1))?; + +let dag = CircuitDag::from_circuit(&circuit)?; +let layers = dag.layers()?; + +assert_eq!(layers.len(), 2); +assert_eq!(layers[0].len(), 2); +assert_eq!(layers[1].len(), 1); +assert_eq!(dag.front_layer()?.len(), 2); +assert_eq!(dag.depth()?, 2); + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +The mapping returned by `node_layers()` agrees with `layers()`, and can be used to look up the layer number by node. + +```rust +use cqlib_core::circuit::{Circuit, CircuitDag, Qubit}; + +let mut circuit = Circuit::new(2); +circuit.h(Qubit::new(0))?; +circuit.x(Qubit::new(1))?; +circuit.cx(Qubit::new(0), Qubit::new(1))?; + +let dag = CircuitDag::from_circuit(&circuit)?; +let node_layers = dag.node_layers()?; + +assert_eq!(node_layers.len(), dag.num_ops()); +for (layer_index, layer) in dag.layers()?.iter().enumerate() { + for node in layer { + assert_eq!(node_layers.get(node).copied(), Some(layer_index)); + } +} + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +--- + +## Control flow and measurement + +When a circuit contains structured control flow, the control flow operation nodes additionally carry a recursive DAG payload. For the construction interfaces of control flow itself and the low-level IR, see [Classical Data / Control Flow](9_classical_control_flow.md). + +| Method | Returns | Description | +| --- | --- | --- | +| `has_control_flow()` | `bool` | Whether structured control flow exists among the top-level operations. | +| `has_nested_control_flow()` | `bool` | Whether a control flow body still contains structured control flow inside it. | +| `has_measurement()` | `bool` | Whether the top-level operations contain measurement directly or recursively. | +| `control_flow(node)` | `Option<&DagControlFlow>` | Return the control flow payload carried by the node. | + +Both `has_control_flow()` and `has_measurement()` traverse only the top-level operation nodes; the difference is that `has_measurement()` recurses inside each operation, so measurements inside control flow bodies are counted as well. `has_nested_control_flow()` goes deeper into control flow bodies to determine whether there is still control flow inside them. + +### DagControlFlow + +```rust +pub enum DagControlFlow { + If { + then_body: Box, + else_body: Option>, + }, + While { body: Box }, + For { body: Box }, + Switch { + cases: Vec, + default: Option>, + }, + Break, + Continue, +} +``` + +| Variant | Description | +| --- | --- | +| `If { then_body, else_body }` | The `if` construct; `else_body` is `None` when that branch is absent. | +| `While { body }` | A `while` loop. | +| `For { body }` | A half-open interval loop based on a `UInt` variable. | +| `Switch { cases, default }` | A `switch` multi-way selection. | +| `Break` | A break jump, with no subgraph. | +| `Continue` | A continue jump, with no subgraph. | + +Each branch body is itself a `CircuitDag`, so interfaces such as `num_ops()`, `topological_op_nodes()` and `control_flow()` can be called on it for recursive analysis. + +```rust +use cqlib_core::circuit::{Circuit, CircuitDag, ClassicalExpr, DagControlFlow, Qubit}; + +let mut circuit = Circuit::new(2); +let measured = circuit.measure(Qubit::new(0))?; +let condition = ClassicalExpr::bit_to_bool(measured.expr())?; +circuit.if_(condition, |body| { + body.x(Qubit::new(1))?; + Ok(()) +})?; + +let dag = CircuitDag::from_circuit(&circuit)?; +let control_node = dag.topological_op_nodes()?[1]; + +match dag.control_flow(control_node).expect("if payload") { + DagControlFlow::If { + then_body, + else_body, + } => { + assert_eq!(then_body.num_ops(), 1); + assert!(else_body.is_none()); + } + _ => panic!("expected if control-flow payload"), +} + +assert!(dag.has_control_flow()); +assert!(!dag.has_nested_control_flow()); + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +Loop bodies are obtained in the same way. The `For` variant stores only the loop body, not the loop variable, the bounds or the step; that information remains in the `ClassicalControlOp::For` of the owning operation. + +```rust +use cqlib_core::circuit::{Circuit, CircuitDag, ClassicalExpr, ClassicalType, DagControlFlow, Qubit}; + +let mut circuit = Circuit::new(1); +let counter = circuit.var(ClassicalType::uint(8).expect("valid width")); +circuit.for_uint( + counter, + ClassicalExpr::uint_literal(8, 0)?, + ClassicalExpr::uint_literal(8, 3)?, + ClassicalExpr::uint_literal(8, 1)?, + |body, _| { + body.h(Qubit::new(0))?; + Ok(()) + }, +)?; + +let dag = CircuitDag::from_circuit(&circuit)?; +let control_node = dag + .topological_op_nodes()? + .into_iter() + .find(|node| dag.control_flow(*node).is_some()) + .expect("control-flow node"); + +match dag.control_flow(control_node).expect("for payload") { + DagControlFlow::For { body } => { + assert_eq!(body.num_ops(), 1); + } + _ => panic!("expected for control-flow payload"), +} + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +### DagSwitchCase + +```rust +pub struct DagSwitchCase { + pub value: u128, + pub body: Box, +} +``` + +| Field | Type | Description | +| --- | --- | --- | +| `value` | `u128` | The integer value this branch matches. | +| `body` | `Box` | The control flow body executed when this branch is hit. | + +```rust +use cqlib_core::circuit::{Circuit, ClassicalType, CircuitDag, DagControlFlow, Qubit}; + +let mut circuit = Circuit::new(1); +let state = circuit.var(ClassicalType::uint(2).expect("valid width")); +circuit.switch(state.expr(), |cases| { + cases.value(0, |body| { + body.x(Qubit::new(0))?; + Ok(()) + })?; + cases.value(1, |body| { + body.z(Qubit::new(0))?; + Ok(()) + })?; + cases.default(|body| { + body.h(Qubit::new(0))?; + Ok(()) + })?; + Ok(()) +})?; + +let dag = CircuitDag::from_circuit(&circuit)?; +let control_node = dag + .topological_op_nodes()? + .into_iter() + .find(|node| dag.control_flow(*node).is_some()) + .expect("switch node"); + +match dag.control_flow(control_node).expect("switch payload") { + DagControlFlow::Switch { cases, default } => { + assert_eq!(cases.len(), 2); + assert_eq!(cases[0].value, 0); + assert_eq!(cases[0].body.num_ops(), 1); + assert!(default.is_some()); + } + _ => panic!("expected switch control-flow payload"), +} + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +--- + +## Operation counts and runs + +| Method | Returns | Description | +| --- | --- | --- | +| `operation_count_by_name()` | `IndexMap` | Count top-level operations by instruction name. | +| `operation_count_by_name_recursive()` | `IndexMap` | Count operations by instruction name at the top level and inside each control flow body. | +| `collect_runs_on_wire(wire, predicate)` | `Result>, CircuitError>` | Collect the runs of consecutive operations accepted by `predicate` on the specified wire. | +| `collect_1q_runs()` | `Result>, CircuitError>` | Collect runs of consecutive single-qubit gates on each qubit wire. | +| `collect_2q_runs()` | `Result>, CircuitError>` | Collect runs of consecutive two-qubit gates on each qubit wire. | + +The counting results preserve insertion order, with instruction names as keys. Runs are collected on a wire in source order: an operation for which `predicate` returns `false` breaks the current run. + +`collect_runs_on_wire()` targets concrete qubit wires and classical wires; `DagWire::GlobalOrder` is also accepted, but it only orders operations that have no concrete data resource and is usually not suitable as the basis for dividing optimization runs. + +The internal criterion of `collect_1q_runs()` and `collect_2q_runs()` is: the operation acts on one or two qubits, and the instruction is a standard gate, a multi-controlled gate, a custom unitary gate or a composite gate. Non-gate operations, measurement, reset, barrier and multi-qubit operations interrupt a run. The same two-qubit operation may appear in the runs of both of its qubit wires. + +```rust +use cqlib_core::circuit::{Circuit, CircuitDag, DagWire, Instruction, Qubit}; + +let mut circuit = Circuit::new(2); +circuit.h(Qubit::new(0))?; +circuit.x(Qubit::new(0))?; +circuit.cx(Qubit::new(0), Qubit::new(1))?; +circuit.h(Qubit::new(0))?; +circuit.reset(Qubit::new(0))?; +circuit.z(Qubit::new(0))?; +circuit.x(Qubit::new(1))?; + +let dag = CircuitDag::from_circuit(&circuit)?; +let q0_runs = dag.collect_runs_on_wire(DagWire::Qubit(Qubit::new(0)), |operation| { + matches!(operation.instruction, Instruction::Standard(_)) && operation.qubits.len() == 1 +})?; + +assert_eq!( + q0_runs.iter().map(Vec::len).collect::>(), + vec![2, 1, 1] +); + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +--- + +## Modifying the graph + +| Method | Returns | Description | +| --- | --- | --- | +| `apply_operation_back(operation)` | `Result` | Append a storage-layer operation to the back of the DAG. | +| `apply_value_operation_back(operation)` | `Result` | Lower first, then append a construction-layer operation to the back of the DAG. | +| `apply_operation_front(operation)` | `Result` | Append a storage-layer operation to the front of the DAG. | +| `apply_value_operation_front(operation)` | `Result` | Lower first, then append a construction-layer operation to the front of the DAG. | +| `remove_op_node(node)` | `Result` | Remove an operation node and return the removed operation. | +| `substitute_node(node, operation)` | `Result<(), CircuitError>` | Replace the specified node with a storage-layer operation. | +| `substitute_value_node(node, operation)` | `Result<(), CircuitError>` | Lower first, then replace the specified node with a construction-layer operation. | +| `substitute_node_with_dag(node, replacement)` | `Result<(), CircuitError>` | Replace the specified node with another `CircuitDag`. | + +`apply_value_operation_*` and `substitute_value_node()` make the symbolic parameters in a `ValueOperation` resident in the DAG parameter table, so `symbols()` and `parameters()` are updated accordingly after the call. An error is returned if a non-finite numeric parameter appears during lowering or a parameter index is invalid. + +```rust +use cqlib_core::circuit::{Circuit, CircuitDag, Parameter, ParameterValue, Qubit, StandardGate, ValueOperation}; + +let mut circuit = Circuit::new(1); +circuit.h(Qubit::new(0))?; +let mut dag = CircuitDag::from_circuit(&circuit)?; +let theta = Parameter::symbol("theta"); + +let rx = ValueOperation::from_standard( + StandardGate::RX, + [Qubit::new(0)], + [ParameterValue::Param(theta.clone())], +); + +let front = dag.apply_value_operation_front(rx)?; + +assert!(dag.symbols().contains("theta")); +assert_eq!(dag.num_ops(), 2); + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +`substitute_node_with_dag()` first validates the replacement DAG, then checks whether the resources read or written after the replacement fall within the resource scope of the replaced node. When a control flow node is replaced, the replacement must contain exactly one control flow operation of the same kind. An error is returned if any check fails. + +```rust +use cqlib_core::circuit::{Circuit, CircuitDag, Qubit}; + +let mut circuit = Circuit::new(1); +circuit.x(Qubit::new(0))?; +let mut dag = CircuitDag::from_circuit(&circuit)?; + +let mut replacement = Circuit::new(1); +replacement.h(Qubit::new(0))?; +let replacement_dag = CircuitDag::from_circuit(&replacement)?; + +let node = dag.topological_op_nodes()?[0]; +dag.substitute_node_with_dag(node, replacement_dag)?; + +assert_eq!(dag.num_ops(), 1); + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +--- + +## Typical usage + +Compiler passes and routing algorithms usually perform dependency analysis on the DAG: `front_layer()` and `node_layers()` decide which operations are already ready, `predecessors()` and `successors()` determine whether a precedence constraint exists between two operations, run-collection interfaces such as `collect_1q_runs()` locate gate sequences that can be processed as a whole, and the `substitute_node()` family of interfaces writes transformation results back into the graph. + +`from_operations()` is suitable for a local view inside a pass: when a segment of operations has already had its parameters resolved and involves only quantum resources, it can be used to construct a lightweight dependency graph that carries no circuit-level metadata. + +--- + +## Error handling + +Most interfaces of `CircuitDag` return `Result<_, CircuitError>`. The common error sources are as follows: + +| Error | When it occurs | +| --- | --- | +| `CircuitError::InvalidDag` | The graph contains a cycle, operation node numbers are duplicated or inconsistent with the order index, a wire sentinel is invalid, the timeline on a wire is broken or forked, a node identifier does not exist, a control flow payload does not match the owning operation, or the resource scope or control flow shape of the replacement DAG is invalid. | +| `CircuitError::QubitNotFound` | An operation references a qubit that is not in the DAG qubit set. | +| `CircuitError::DuplicateQubits` | The `qubits` of `from_operations()` contains duplicate qubits. | +| `CircuitError::ForeignClassicalHandle` | An operation or wire references a classical variable or classical value belonging to another circuit. | +| `CircuitError::InvalidParameterIndex` | An operation references a parameter index outside the range of the parameter table, or the global phase parameter cannot be resolved. | +| `CircuitError::InvalidParameterValue` | A non-finite numeric parameter appears while lowering a construction-layer operation. | +| `CircuitError::InvalidOperation` | An invalid control flow representation is nested inside a construction-layer instruction. | +| `CircuitError::DuplicateClassicalValueDefinition` | The same immutable classical value has more than one producer. | diff --git a/docs/api/en/rust/0_circuit/1_circuit.md b/docs/api/en/rust/0_circuit/1_circuit.md new file mode 100644 index 00000000..fdf245d8 --- /dev/null +++ b/docs/api/en/rust/0_circuit/1_circuit.md @@ -0,0 +1,332 @@ +# Circuit + +`cqlib_core::circuit::Circuit` + +```rust +use cqlib_core::circuit::{Circuit, Parameter, Qubit}; +``` + +`Circuit` is the main quantum circuit container in the Rust core. It holds the core structural information of a quantum program, including the logical qubit set, the operation sequence, the parameter table, classical variables and classical values, control flow scopes and the global phase. + +--- + +## Creating a circuit + +### `Circuit::new` + +```rust +pub fn new(num_qubits: usize) -> Self +``` + +`Circuit::new(num_qubits)` creates an empty circuit containing consecutive logical qubits. Qubit numbering starts at `0`, from `Qubit::new(0)` to `Qubit::new(num_qubits - 1)`. + +```rust +use cqlib_core::circuit::Circuit; + +let circuit = Circuit::new(3); +assert_eq!(circuit.num_qubits(), 3); +assert_eq!(circuit.width(), 3); +``` + +### `Circuit::from_qubits` + +```rust +pub fn from_qubits(qubits: Vec) -> Result +``` + +`Circuit::from_qubits()` creates a circuit from an explicitly specified set of logical qubits. This interface allows sparse logical numbering, for example `Qubit::new(2)` and `Qubit::new(5)`. + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; + +let circuit = Circuit::from_qubits(vec![Qubit::new(2), Qubit::new(5)])?; +assert_eq!(circuit.width(), 2); + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +### `Circuit::from_operations` + +```rust +pub fn from_operations( + qubits: Vec, + operations: impl IntoIterator, + classical_vars: Option>, + classical_values: Option>, +) -> Result +``` + +`Circuit::from_operations()` creates a circuit from the construction-layer IR. + +--- + +## Basic properties + +`Circuit` provides a set of read-only access methods for inspecting the basic structural information of a circuit. + +| Method | Returns | Description | +| --- | --- | --- | +| `id()` | `CircuitId` | The classical handle identity of the current circuit, used to distinguish classical variables and values in different circuits. | +| `width()` | `usize` | Circuit width, that is, the number of qubits. | +| `num_qubits()` | `usize` | The number of qubits, with the same meaning as `width()`. | +| `qubits()` | `Vec` | Return the logical qubits in the circuit in insertion order. | +| `operations()` | `&[Operation]` | Return the internal storage-layer operation sequence. | +| `parameters()` | `&IndexSet` | Return the set of resident parameter expressions. | +| `symbols()` | `&IndexSet` | Return the set of free symbol names appearing in the circuit. | +| `used_symbols()` | `IndexSet` | Return the set of symbol names actually referenced by the executable IR. | +| `uses_symbol(symbol)` | `bool` | Determine whether the given symbol is referenced by the executable IR. | +| `classical_vars()` | `&[ClassicalType]` | Return the table of allocated mutable classical variable types. | +| `classical_values()` | `&[ClassicalType]` | Return the table of immutable classical value types, usually produced by measurement. | +| `global_phase()` | `Parameter` | Return the parameter expression of the circuit global phase. | +| `global_phase_param()` | `&CircuitParam` | Return the global phase parameter in its internal storage form. | + +Example: + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; + +let q0 = Qubit::new(0); +let q1 = Qubit::new(1); + +let mut c = Circuit::new(2); +c.h(q0)?; +c.cx(q0, q1)?; + +assert_eq!(c.operations().len(), 2); +assert_eq!(c.num_qubits(), 2); + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +--- + +## Qubit and parameter management + +`Circuit` maintains a qubit set and a parameter table internally. Qubits describe what an operation acts on, and the parameter table manages the symbolic parameters and parameter expressions in the circuit in a unified way. + +| Method | Description | +| --- | --- | +| `add_qubits(new_qubits)` | Append new logical qubits to the circuit; return an error if a duplicate qubit is included. | +| `add_parameter(param)` | Insert a parameter into the circuit parameter table and return `(index, is_new)`. | +| `resolve_parameter(param)` | Restore an internal `CircuitParam` to a `Parameter`. | +| `parameter_value(param)` | Convert an internal `CircuitParam` into a construction-layer `ParameterValue`. | +| `map_param(param)` | Map a `Parameter` into the circuit parameter table and return the internal `CircuitParam`. | +| `set_global_phase(phase)` | Set the circuit global phase. | + +--- + +## Appending operations + +### `append` + +```rust +pub fn append( + &mut self, + instruction: Instruction, + qubits: Q, + params: P, + label: Option<&str>, +) -> Result<(), CircuitError> +where + Q: IntoIterator, + Q::Item: Into, + P: IntoIterator +``` + +`append()` is the generic entry point for appending storage-layer instructions, used to append an arbitrary `Instruction` to a circuit. The call must explicitly provide the instruction, the acted-on qubits, the parameter list and an optional label. + +The method performs several checks, including: + +- whether the number of qubits the instruction acts on matches; +- whether the parameter count agrees with the instruction definition; +- whether the qubits belong to the current circuit; +- whether the same qubit is referenced more than once in a single operation; +- whether a fixed numeric parameter is finite, that is, not `NaN` or infinite; +- whether the parameter expressions can be mapped correctly into the circuit parameter table. + +### `append_value_operation` + +```rust +pub fn append_value_operation(&mut self, operation: ValueOperation) -> Result<(), CircuitError> +``` + +`append_value_operation()` appends a self-contained construction-layer operation. A `ValueOperation` already contains the instruction, qubits, parameters and label information, so this interface is particularly suitable for IR import, deserialization and compiler pass output. + +### `index` + +```rust +pub fn index(&self, i: usize) -> Result +``` + +`index(i)` reads the `i`-th operation and restores the internal storage-layer parameter representation to a construction-layer `ParameterValue`. + +--- + +## Standard gate convenience methods + +`Circuit` provides a set of convenience methods for common standard gates. All convenience methods modify the current circuit and return `Result<(), CircuitError>`. Users can handle possible errors with `?` or an explicit `match`. + +### Single-qubit fixed gates + +| Method | Standard gate | +| --- | --- | +| `i(qubit)` | `StandardGate::I` | +| `h(qubit)` | `StandardGate::H` | +| `x(qubit)` | `StandardGate::X` | +| `y(qubit)` | `StandardGate::Y` | +| `z(qubit)` | `StandardGate::Z` | +| `s(qubit)` | `StandardGate::S` | +| `sdg(qubit)` | `StandardGate::SDG` | +| `t(qubit)` | `StandardGate::T` | +| `tdg(qubit)` | `StandardGate::TDG` | +| `x2p(qubit)` | `StandardGate::X2P` | +| `x2m(qubit)` | `StandardGate::X2M` | +| `y2p(qubit)` | `StandardGate::Y2P` | +| `y2m(qubit)` | `StandardGate::Y2M` | + +### Parameterized single-qubit gates + +| Method | Description | +| --- | --- | +| `rx(qubit, theta)` | Rotation around the X axis. | +| `ry(qubit, theta)` | Rotation around the Y axis. | +| `rz(qubit, theta)` | Rotation around the Z axis. | +| `phase(qubit, lambda)` | Phase gate. | +| `u(qubit, theta, phi, lambda)` | Generic single-qubit gate. | +| `xy(qubit, theta)` | Gate of the XY interaction family. | +| `xy2p(qubit, theta)` | Positive half-angle XY gate. | +| `xy2m(qubit, theta)` | Negative half-angle XY gate. | +| `rxy(qubit, theta, phi)` | Rotation around an arbitrary axis in the XY plane. | + +### Multi-qubit gates + +| Method | Description | +| --- | --- | +| `cx(control, target)` | controlled-X, also known as CNOT. | +| `cy(control, target)` | controlled-Y. | +| `cz(control, target)` | controlled-Z. | +| `swap(a, b)` | Exchange the states of two qubits. | +| `ccx(control1, control2, target)` | Toffoli gate. | +| `rxx(a, b, theta)` | XX rotation. | +| `ryy(a, b, theta)` | YY rotation. | +| `rzz(a, b, theta)` | ZZ rotation. | +| `rzx(a, b, theta)` | ZX rotation. | +| `crx(control, target, theta)` | controlled-RX. | +| `cry(control, target, theta)` | controlled-RY. | +| `crz(control, target, theta)` | controlled-RZ. | +| `fsim(a, b, theta, phi)` | fSim gate. | + +Example: + +```rust +use cqlib_core::circuit::{Circuit, Parameter, Qubit}; + +let q0 = Qubit::new(0); +let q1 = Qubit::new(1); +let theta = Parameter::symbol("theta"); + +let mut c = Circuit::new(2); +c.h(q0)?; +c.cx(q0, q1)?; +c.rzz(q0, q1, theta)?; + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +--- + +## Other quantum operations + +Besides the standard gate convenience methods, `Circuit` also provides entry points for barrier, reset, delay, multi-controlled gates, custom unitary gates and composite gates. + +| Method | Description | +| --- | --- | +| `barrier(qubits)` | Insert a barrier, which constrains the compiler or scheduler not to reorder operations on the affected qubits across this boundary. | +| `reset(qubit)` | Reset the qubit to `\|0>`. | +| `delay(qubit, delay)` | Insert idle time on the given qubit. | +| `multi_control(instruction, controls, targets, params)` | Construct a multi-controlled standard gate. | +| `unitary(gate, qubits)` | Append a `UnitaryGate` without extra positional parameters. | +| `unitary_with_params(gate, qubits, params)` | Append a `UnitaryGate` with positional parameters. | +| `circuit_gate(gate, qubits, params)` | Append a `CircuitGate`. | + +--- + +## Classical data and control flow entry points + +`Circuit` also provides interfaces related to classical data and structured control flow. They are used to construct program structures such as conditional branches, loops and multi-way selections. + +| Method | Description | +| --- | --- | +| `var(ty)` | Allocate a classical variable. | +| `store(target, value)` | Write a classical expression into a classical variable. | +| `measure(qubit)` | Measure one qubit. | +| `measure_bits(qubits)` | Measure multiple qubits. | +| `measure_into(qubit, target)` | Measure and write into an existing classical variable. | +| `measure_bits_into(qubits, target)` | Measure multiple qubits and write into an existing classical variable. | +| `if_()` / `if_else()` | Conditional branch. | +| `while_()` | `while` loop. | +| `for_uint()` | Half-open interval loop based on `UInt`. | +| `switch()` | Multi-way selection. | +| `append_control(op)` | Append a low-level control flow object. | +| `break_loop()` / `continue_loop()` | Control flow jumps. | + +--- + +## Analysis and transformation + +`Circuit` provides various circuit analysis and structural transformation methods. + +| Method | Description | +| --- | --- | +| `depth(recurse)` | Compute the circuit depth. If the circuit contains control flow and `recurse = false`, `ControlFlowPresent` is usually returned. | +| `remove_operation(index)` | Remove a single top-level operation and return the removed operation. | +| `remove_operations(indices)` | Remove multiple top-level operations and return the removed operations in ascending order of their original indices. | +| `validate()` | Validate classical handles, control flow scopes, data dependencies and circuit structure invariants. | +| `inverse()` | Return the inverse of the current circuit. Return an error if it contains non-invertible operations. | +| `decompose()` | Expand composite gates defined by circuits. | +| `to_gate(name)` | Convert the circuit into an `Instruction::CircuitGate` for reuse as a composite gate. | +| `to_matrix(qubits_order)` | Return the dense numeric matrix of a small purely unitary circuit. | +| `assign_parameters(bindings)` | Bind symbolic parameters and return a new circuit. | +| `compose(other, qubits_map)` | Append another circuit to the current circuit, with an optional qubit mapping. | + +--- + +## Parameter binding + +```rust +use cqlib_core::circuit::{Circuit, Parameter, Qubit}; +use std::collections::HashMap; + +let theta = Parameter::symbol("theta"); + +let mut c = Circuit::new(1); +c.rx(Qubit::new(0), theta)?; + +let mut bindings = HashMap::new(); +bindings.insert("theta", 0.5); + +let bound = c.assign_parameters(&Some(bindings))?; + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +`assign_parameters()` replaces the symbolic parameters in a circuit with concrete numeric values. + +--- + +## Circuit composition + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; + +let mut lhs = Circuit::new(3); +let mut rhs = Circuit::new(2); + +rhs.cx(Qubit::new(0), Qubit::new(1))?; + +lhs.compose(&rhs, Some(&[Qubit::new(1), Qubit::new(2)]))?; + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +`compose(other, qubits_map)` appends another circuit to the end of the current circuit. When `qubits_map` is passed, the qubits of the right-hand circuit are mapped, in the order of `other.qubits()`, to the target qubits in the current circuit. diff --git a/docs/api/en/rust/0_circuit/2_qubit.md b/docs/api/en/rust/0_circuit/2_qubit.md new file mode 100644 index 00000000..09097cc9 --- /dev/null +++ b/docs/api/en/rust/0_circuit/2_qubit.md @@ -0,0 +1,134 @@ +# Qubit + +`cqlib_core::circuit::Qubit` + +```rust +use cqlib_core::circuit::Qubit; +``` + +`Qubit` is a lightweight handle representing a logical qubit in the Rust core. It stores a `u32` number internally, used to identify a logical qubit stably in quantum circuits, compilation IR, mapping tables and set structures. + +--- + +## Creating a `Qubit` + +```rust +pub const fn new(id: u32) -> Self +``` + +`Qubit::new(id)` creates a logical qubit from a `u32` number. Because the input type is already an unsigned 32-bit integer, the constructor itself cannot fail, and it can be used in a constant context. + +```rust +use cqlib_core::circuit::Qubit; + +let q0 = Qubit::new(0); +let q5 = Qubit::new(5); +``` +--- + +## Accessing the number + +`Qubit` provides two commonly used number access interfaces: + +| Method | Returns | Description | +| --- | --- | --- | +| `id()` | `u32` | Return the raw internal number. | +| `index()` | `usize` | Convert the number to `usize`, which is convenient in scenarios that require an array index type. | + +```rust +use cqlib_core::circuit::Qubit; + +let q = Qubit::new(12); + +assert_eq!(q.id(), 12); +assert_eq!(q.index(), 12usize); +assert_eq!(format!("{q}"), "Q12"); +``` + +--- + +## Integer conversion + +`Qubit` supports direct conversion from unsigned integer types that cannot overflow. For example, `u8`, `u16` and `u32` can be converted into `Qubit` through `From`. + +```rust +use cqlib_core::circuit::Qubit; + +let a: Qubit = 0u8.into(); +let b: Qubit = 10u16.into(); +let c: Qubit = 20u32.into(); + +assert_eq!(a, Qubit::new(0)); +assert_eq!(b, Qubit::new(10)); +assert_eq!(c, Qubit::new(20)); +``` + +For integer types that may be negative or may exceed the `u32` range, use `TryFrom`: the signed types `i8`, `i16`, `i32` and `isize`, and the unsigned type `usize`. On 64-bit systems `usize` and `isize` can represent values beyond `u32`; a failed conversion is not silently truncated, but returns an error. + +```rust +use cqlib_core::circuit::{Qubit, QubitError}; + +assert_eq!(Qubit::try_from(3i32).unwrap(), Qubit::new(3)); + +assert!(matches!( + Qubit::try_from(-1i32), + Err(QubitError::NegativeIndex(-1)) +)); + +assert!(matches!( + Qubit::try_from(usize::MAX), + Err(QubitError::IndexTooLarge(_)) +)); +``` + +Common conversion errors are as follows: + +| Error | Description | +| --- | --- | +| `QubitError::NegativeIndex(i128)` | The input is a signed integer whose value is negative. | +| `QubitError::IndexTooLarge(u128)` | The input value cannot be represented as `u32`. | + +--- + +## Comparison, ordering and hashing + +Both comparison and hashing of `Qubit` are based on the internal number. That is, two `Qubit` values with the same number are treated as the same logical qubit handle. + +```rust +use std::collections::{BTreeSet, HashMap}; +use cqlib_core::circuit::Qubit; + +assert_eq!(Qubit::new(0), Qubit::new(0)); +assert!(Qubit::new(0) < Qubit::new(1)); + +let mut map = HashMap::new(); +map.insert(Qubit::new(0), "ancilla"); +assert_eq!(map.get(&Qubit::new(0)), Some(&"ancilla")); + +let mut set = BTreeSet::new(); +set.insert(Qubit::new(2)); +set.insert(Qubit::new(0)); +set.insert(Qubit::new(1)); + +let ordered: Vec<_> = set.into_iter().collect(); +assert_eq!(ordered, vec![Qubit::new(0), Qubit::new(1), Qubit::new(2)]); +``` + +--- + +## Logical numbering and storage position + +The number of a `Qubit` is a logical identifier. In a circuit, the actual order of qubits is determined by the qubit list stored in `Circuit`; in a matrix or statevector, the axis order is likewise determined by the qubit ordering convention of the relevant conversion interface. + +For example, the following circuit contains only two qubits, but their logical numbers are `10` and `20` respectively: + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; + +let circuit = Circuit::from_qubits(vec![Qubit::new(10), Qubit::new(20)])?; + +assert_eq!(circuit.num_qubits(), 2); +assert_eq!(circuit.qubits(), vec![Qubit::new(10), Qubit::new(20)]); + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` diff --git a/docs/api/en/rust/0_circuit/3_parameter.md b/docs/api/en/rust/0_circuit/3_parameter.md new file mode 100644 index 00000000..35289f04 --- /dev/null +++ b/docs/api/en/rust/0_circuit/3_parameter.md @@ -0,0 +1,204 @@ +# Parameter + +`cqlib_core::circuit::Parameter` + +```rust +use cqlib_core::circuit::Parameter; +``` + +`Parameter` is the core type representing circuit parameter expressions in the Rust core. It can represent a definite numeric constant, a symbolic variable, or an expression composed of arithmetic operations and mathematical functions. + +--- + +## Creating parameters + +`Parameter` supports creation from symbol names, numeric constants, expression strings and built-in mathematical constants. + +| Interface | Description | +| --- | --- | +| `Parameter::symbol(name)` | Create a symbolic variable. | +| `Parameter::pi()` | Create the mathematical constant `π`. | +| `Parameter::e()` | Create the natural constant `e`. | +| `Parameter::new(expr)` | Create a parameter from an underlying expression object, usually for internal or advanced scenarios. | +| `as_expr()` | Read a reference to the underlying expression object. | +| `into_expr()` | Consume the parameter and take out the underlying expression object. | +| `str::parse::()` | Parse a parameter expression from a string. | +| `Parameter::from(f64)` | Create a numeric constant from a floating-point number. | +| `Parameter::default()` | Create the default parameter representing the integer `0`. | + +Numeric constants also support conversion from `f32`, `u32` and `i32`. `NaN`, positive infinity and negative infinity of `f64` and `f32` are not finite values, and construction fails directly when they are passed in, so filter them out before the call. + +```rust +use cqlib_core::circuit::Parameter; + +let theta = Parameter::symbol("theta"); +let pi = Parameter::pi(); +let numeric = Parameter::from(0.25_f64); + +let expr: Parameter = "2 * theta + pi / 2".parse()?; + +# Ok::<(), cqlib_core::circuit::error::ParameterError>(()) +``` + +In the example above, `theta` is a free symbol, `pi` and `numeric` are constant parameters, and `expr` is a composite expression obtained by parsing a string. An expression string is parsed as a mathematical expression; if the syntax is invalid or it contains unsupported structures, `ParameterError` is returned instead of silently treating it as an ordinary symbol. + +--- + +## Arithmetic operations + +`Parameter` supports common arithmetic operations: + +- Addition: `+` +- Subtraction: `-` +- Multiplication: `*` +- Division: `/` +- Unary negation: `-expr` + +```rust +use cqlib_core::circuit::Parameter; + +let theta = Parameter::symbol("theta"); +let phi = Parameter::symbol("phi"); + +let expr = theta.clone() * 2.0 + phi.clone() / 2.0 - Parameter::pi(); +let neg = -phi; +``` + +Every operation produces a new parameter expression and does not modify the original `Parameter`. Therefore the same symbolic parameter can safely be reused in multiple expressions. + +The operations above support four ownership combinations: `Parameter` with `Parameter`, `&Parameter` with `&Parameter`, `Parameter` with `&Parameter`, and `&Parameter` with `Parameter`. Mixed use of `Parameter` with `f64`, `f32`, `i32` and `u32` is also supported in both directions. Unary negation is available for both `Parameter` and `&Parameter`. + +--- + +## Mathematical functions + +Besides the basic arithmetic operations, `Parameter` also provides a set of common mathematical functions for constructing more complex parameter expressions. + +| Method | Description | +| --- | --- | +| `abs()` / `sqrt()` | Absolute value and square root. | +| `exp()` / `ln()` / `log(base)` | Exponential function, natural logarithm and logarithm with a given base. | +| `sin()` / `cos()` / `tan()` | Trigonometric functions. | +| `asin()` / `acos()` / `atan()` | Inverse trigonometric functions. | +| `sinh()` / `cosh()` / `tanh()` | Hyperbolic functions. | +| `floor()` / `ceil()` / `round()` | Rounding-related functions. | +| `pow(exp)` | Exponentiation. | + +--- + +## Evaluation + +```rust +pub fn evaluate( + &self, + bindings: &Option>, +) -> Result +``` + +`evaluate()` computes a parameter expression into a concrete floating-point result. For an expression containing free symbols, supply the value of each symbol through `bindings`; evaluation succeeds only after all free symbols in the expression are bound. + +--- + +## Symbol information and state checks + +`Parameter` provides a set of interfaces for inspecting the free symbols, constant properties and special numeric states of an expression. + +| Method | Returns | Description | +| --- | --- | --- | +| `get_symbols()` | `HashSet` | Return the set of free symbols in the expression. | +| `as_symbol()` | `Option` | When the expression is exactly a single symbol, return that symbol name. | +| `is_constant()` | `bool` | Determine whether the expression contains no free symbols. | +| `is_exact_zero()` | `Result` | Determine whether the expression is exactly 0. | +| `is_zero()` | `bool` | Determine whether it equals 0 when it can be evaluated. | +| `is_one()` | `bool` | Determine whether it equals 1 when it can be evaluated. | + +--- + +## Simplification and canonicalization + +| Method | Returns | Description | +| --- | --- | --- | +| `simplify()` | `Result` | Perform algebraic simplification on the expression. | +| `canonicalized()` | `Result` | Return the canonical form used for parameter residency, comparison and deduplication. | + +```rust +use cqlib_core::circuit::Parameter; + +let x = Parameter::symbol("x"); +let expr: Parameter = x.clone() + 0.0; + +let simplified = expr.simplify()?; +let canonical = simplified.canonicalized()?; + +# Ok::<(), cqlib_core::circuit::error::ParameterError>(()) +``` + +--- + +## Replacement and substitution + +`Parameter` supports replacing a symbol in an expression with another parameter expression. + +| Method | Description | +| --- | --- | +| `replace(symbol, param)` | Replace a single symbol. | +| `substitute_many(bindings)` | Replace multiple symbols at once. | + +```rust +use cqlib_core::circuit::Parameter; + +let x = Parameter::symbol("x"); +let y = Parameter::symbol("y"); + +let expr: Parameter = x + 2.0; +let replaced = expr.replace("x", y.clone() * 3.0); +``` + +--- + +## Differentiation + +```rust +pub fn derivative(&self, var: &str) -> Result +``` + +`derivative(var)` performs symbolic differentiation with respect to the given symbol variable and returns a new `Parameter` expression. + +```rust +use cqlib_core::circuit::Parameter; + +let theta = Parameter::symbol("theta"); +let expr = theta.clone().sin() + theta.clone() * theta; + +let dtheta = expr.derivative("theta")?; + +# Ok::<(), cqlib_core::circuit::error::ParameterError>(()) +``` + +--- + +## Equivalence checking + +`Parameter` implements `PartialEq`, `Eq` and `Hash`, all three based on the structure of the underlying expression AST: `x + y` and `y + x` differ in structure and are therefore not equal. To compare by mathematical semantics, use the conservative equivalence interfaces below. + +| Method | Description | +| --- | --- | +| `provably_equal(other, tolerance)` | Conservatively determine whether two expressions are equal. | +| `provably_equal_modulo(other, modulus, tolerance)` | Perform a conservative equality check modulo a given period. | + +`provably_equal_modulo()` is a conservative decision: it returns `true` only when the difference of the two expressions can be evaluated to a concrete value and that difference is an integer multiple of the modulus (within tolerance). If the difference still contains unbound symbols, it returns `false` even when the two expressions mathematically differ by an integer multiple of the period. For example, `theta + pi` and `theta - pi` differ by `2 * pi`, but the difference contains the free symbol `theta`, so the interface cannot prove the two equal. + +```rust +use cqlib_core::circuit::Parameter; + +let angle = Parameter::from(0.5); +let equivalent = Parameter::from(0.5 + 2.0 * std::f64::consts::PI); + +assert!(angle.provably_equal_modulo( + &equivalent, + &Parameter::from(2.0 * std::f64::consts::PI), + 1e-12, +)); + +# Ok::<(), cqlib_core::circuit::error::ParameterError>(()) +``` diff --git a/docs/api/en/rust/0_circuit/4_operation_instruction.md b/docs/api/en/rust/0_circuit/4_operation_instruction.md new file mode 100644 index 00000000..b76256c2 --- /dev/null +++ b/docs/api/en/rust/0_circuit/4_operation_instruction.md @@ -0,0 +1,422 @@ +# Operation / Instruction + +Module paths: + +- `cqlib_core::circuit::Instruction` +- `cqlib_core::circuit::Operation` +- `cqlib_core::circuit::ValueInstruction` +- `cqlib_core::circuit::ValueOperation` +- `cqlib_core::circuit::Directive` +- `cqlib_core::circuit::ClassicalDataOp` + +Together these types form the operation-level intermediate representation in the `cqlib_core::circuit` module. They describe every operation in a circuit, including quantum gates, non-unitary instructions, classical data operations, structured control flow and special instructions such as delay. + +Semantically, these types fall into two layers: + +- Storage-layer IR: used for efficient internal storage in `Circuit`, and dependent on the circuit's internal parameter table; +- Construction-layer IR: used for import, export, serialization, compiler pass output and testing, where operations are as self-contained as possible. + +--- + +## IR layers + +| Type | Layer | Description | +| --- | --- | --- | +| `Instruction` | Storage-layer IR | Describes "which instruction is executed"; it can be a standard gate, a multi-controlled gate, a custom unitary gate, a sub-circuit gate, a directive, a classical data operation, classical control flow or `Delay`. | +| `Operation` | Storage-layer IR | Represents one operation inside a circuit, composed of `Instruction + qubits + CircuitParam + label`. Its parameters may reference the parameter table of the owning `Circuit`. | +| `ValueInstruction` | Construction-layer IR | Can wrap an ordinary `Instruction`, or a construction-layer classical control flow operation. | +| `ValueOperation` | Construction-layer IR | Represents a self-contained operation, composed of `ValueInstruction + qubits + ParameterValue + label`, suitable for passing between circuits, serialization and import. | + +`Circuit::operations()` returns the internal storage-layer operation sequence: + +```rust +pub fn operations(&self) -> &[Operation] +``` + +To read self-contained operations that can be detached from the circuit's internal parameter table, use: + +```rust +pub fn index(&self, i: usize) -> Result +``` + +--- + +## `Instruction` + +`Instruction` describes what an operation "does". It describes only the instruction type and the gate definition itself; it does not include which qubits the instruction acts on, nor the parameter values carried by a particular operation instance. + +```rust +pub enum Instruction { + Standard(StandardGate), + McGate(Box), + UnitaryGate(Box), + CircuitGate(Box), + Directive(Directive), + ClassicalData(ClassicalDataOp), + ClassicalControl(ClassicalControlOp), + Delay, +} +``` + +### 1. Variant descriptions + +| Variant | Description | +| --- | --- | +| `Standard(StandardGate)` | A built-in Cqlib standard quantum gate, for example `H`, `CX`, `RZ`. | +| `McGate(Box)` | A multi-controlled standard gate, obtained by adding several control qubits to a base standard gate. | +| `UnitaryGate(Box)` | A user-defined unitary gate, which can be defined by a matrix, a symbolic matrix or a circuit. | +| `CircuitGate(Box)` | A composite gate defined by a frozen circuit. | +| `Directive(Directive)` | A non-unitary instruction, for example `Barrier`, `Measure`, `Reset`. | +| `ClassicalData(ClassicalDataOp)` | A classical data operation, for example `store` or writing a measurement result. | +| `ClassicalControl(ClassicalControlOp)` | Structured control flow, for example `if`, `while`, `for`, `switch`. | +| `Delay` | A delay or idle-time instruction, usually used for scheduling or hardware timing semantics. | + +In ordinary hand-written circuits, users usually do not need to construct `Instruction` directly. However, when writing compiler passes, IR importers, serialization tools or low-level tests, working with `Instruction` explicitly gives more precise control over the circuit structure. + +### 2. Stable metadata + +`Instruction` provides a set of stable identifiers that do not depend on `Debug` output and do not change with the display format, suitable for use in compiler passes, serialization and cache keys. + +| Method | Returns | Description | +| --- | --- | --- | +| `name()` | `String` | The concrete operation name or the user-defined gate label. Unlike `instruction_type()`, it can distinguish different gates within the same category. | +| `instruction_type()` | `&'static str` | The instruction category name: `"standard"`, `"mcgate"`, `"unitary"`, `"circuit"`, `"directive"`, `"classical_data"`, `"classical_control"`, `"delay"`. | + +The value of `name()` for each variant: standard gates take the gate name, multi-controlled gates take the multi-controlled gate name, unitary gates take the label, composite gates take the gate name, directives and classical operations take their own operation names, and `Delay` is always `"delay"`. + +Category checks use a set of constant predicates, all evaluated at compile time: + +| Predicate | Description | +| --- | --- | +| `is_standard()` | Whether the instruction is a standard gate instruction. | +| `is_mcgate()` | Whether the instruction is a multi-controlled gate instruction. | +| `is_unitary()` | Whether the instruction is a user-defined unitary gate instruction. | +| `is_circuit_gate()` | Whether the instruction is a composite gate instruction. | +| `is_directive()` | Whether the instruction is a non-unitary instruction. | +| `is_classical_data()` | Whether the instruction is a classical data operation. | +| `is_classical_control()` | Whether the instruction is structured control flow. | +| `is_delay()` | Whether the instruction is a delay instruction. | +| `is_quantum_gate()` | Whether the instruction is a unitary quantum gate, that is, one of the first four categories. | + +### 3. Structural equality + +`Instruction` implements `PartialEq`. Two instructions are equal if and only if their variants and internal definitions agree structurally; the comparison does not involve qubit binding or parameter values. + +### 4. Common methods + +| Method | Description | +| --- | --- | +| `has_measurement()` | Determine whether the current instruction or its recursive control flow body contains a measurement. | +| `reads_value(value)` | Determine whether the current instruction reads the given `ClassicalValue`. | +| `gate_arity()` | Return the number of qubits and the number of parameters the gate requires; non-gate instructions usually return `None`. | +| `matrix(params)` | Try to return the numeric matrix; non-unitary instructions or instructions without a supported matrix representation return `None`. | +| `inverse(params)` | Try to return the inverse instruction and inverse parameters; non-invertible instructions return `None`. | +| `control(num_new_ctrls)` | Try to promote the current instruction to a controlled instruction. | + +### 5. `From` conversions + +`Instruction` supports conversion from several underlying types, which makes it convenient to create instructions quickly when building IR or in compiler passes. + +Common conversions include: + +- `StandardGate` +- `Directive` +- `ClassicalControlOp` +- `ClassicalDataOp` + +```rust +use cqlib_core::circuit::{Instruction, StandardGate}; + +let inst: Instruction = StandardGate::H.into(); +``` + +--- + +## `Directive` + +`Directive` represents a special circuit instruction that is not an ordinary unitary gate. + +```rust +pub enum Directive { + Barrier, + Measure, + Reset, +} +``` + +| Variant | Description | +| --- | --- | +| `Barrier` | A compilation or scheduling barrier that prevents operations on the affected qubits from being reordered across this boundary. | +| `Measure` | Computational basis measurement; it produces a classical result and is a non-unitary operation. | +| `Reset` | Reset the qubit to `\|0>`, which usually destroys the coherence of the original quantum state. | + +`Directive` also provides a stable operation name interface: + +```rust +pub const fn name(self) -> &'static str +``` + +It returns `"barrier"`, `"measure"` and `"reset"` respectively. + +The public inversion interface of `Directive` is as follows: + +```rust +pub fn inverse(&self) -> Option +``` + +`Barrier` can be regarded as its own inverse; `Measure` and `Reset` are not invertible, so `None` is returned. + +```rust +use cqlib_core::circuit::Directive; + +assert_eq!(Directive::Barrier.inverse(), Some(Directive::Barrier)); +assert_eq!(Directive::Measure.inverse(), None); +assert_eq!(Directive::Reset.inverse(), None); +``` + +Note that `Directive` usually has no ordinary unitary matrix representation. A circuit containing `Measure` or `Reset` cannot be treated as a purely quantum gate circuit and passed directly to the matrix conversion interface. + +--- + +## `ClassicalDataOp` + +`ClassicalDataOp` represents classical data operations in a circuit, mainly used to describe writes to classical variables and the production of measurement results. + +```rust +pub enum ClassicalDataOp { + Store { target: ClassicalVar, value: ClassicalExpr }, + MeasureBit { result: ClassicalValue }, + MeasureBits { result: ClassicalValue }, +} +``` + +| Variant | Description | +| --- | --- | +| `Store { target, value }` | Write a classical expression into a mutable classical variable. | +| `MeasureBit { result }` | Single-qubit measurement produces one classical value. | +| `MeasureBits { result }` | Multi-qubit measurement produces a bit vector classical value. | + +The common access methods are as follows: + +| Method | Returns | Description | +| --- | --- | --- | +| `target()` | `Option` | Return the target variable when the operation is `Store`. | +| `result()` | `Option` | Return the result value when the operation produces a measurement result. | +| `value()` | `Option<&ClassicalExpr>` | Return the written expression when the operation is `Store`. | + +Ordinary users usually construct these operations through high-level `Circuit` methods, for example: + +- `Circuit::store()` +- `Circuit::measure()` +- `Circuit::measure_bits()` +- `Circuit::measure_into()` +- `Circuit::measure_bits_into()` + +--- + +## `Operation` + +`Operation` is the storage-layer operation representation used internally by `Circuit`. It records one concrete application of an instruction in the current circuit. + +```rust +pub struct Operation { + pub instruction: Instruction, + pub qubits: SmallVec<[Qubit; 3]>, + pub params: SmallVec<[CircuitParam; 1]>, + pub label: Option>, +} +``` + +Field descriptions: + +| Field | Description | +| --- | --- | +| `instruction` | The instruction type executed by the current operation. | +| `qubits` | The logical qubits the current operation acts on. | +| `params` | The parameters carried by the current operation, represented with `CircuitParam`. | +| `label` | An optional metadata label that does not change the mathematical semantics of the operation. | + +### `Operation::standard_gate` + +```rust +pub fn standard_gate(&self) -> Option +``` + +`Operation::standard_gate()` returns the corresponding standard gate definition when the operation is a standard gate, and `None` otherwise. + +### `Operation::matrix` + +```rust +pub fn matrix(&self) -> Result>, CircuitError> +``` + +`Operation::matrix()` computes the numeric matrix of a single operation. + +--- + +## `ValueInstruction` + +`ValueInstruction` is the construction-layer instruction representation. It can wrap an ordinary storage-layer `Instruction`, or a construction-layer classical control flow object. + +```rust +pub enum ValueInstruction { + Instruction(Instruction), + ClassicalControl(ValueClassicalControlOp), +} +``` + +The common methods are as follows: + +| Method | Description | +| --- | --- | +| `from_instruction(inst)` | Wrap an ordinary `Instruction` as a `ValueInstruction`. | +| `is_classical_control()` | Determine whether it is construction-layer control flow. | +| `is_instruction()` | Determine whether it is an ordinary instruction. | +| `name()` | Return the concrete operation name or the user-defined gate label. | +| `instruction_type()` | Return the stable category name; construction-layer control flow is always `"classical_control"`, and the rest is delegated to the inner `Instruction`. | +| `is_standard()` / `is_mcgate()` / `is_unitary()` / `is_circuit_gate()` | Determine whether the instruction is a standard gate, a multi-controlled gate, a custom unitary gate or a composite gate respectively. | +| `is_directive()` / `is_classical_data()` / `is_delay()` | Determine whether the instruction is a non-unitary instruction, a classical data operation or a delay instruction respectively. | +| `standard_gate()` | Return the corresponding `StandardGate` when the instruction is a standard gate. | +| `directive()` | Return the corresponding `Directive` when the instruction is a non-unitary instruction. | +| `as_instruction()` | Read a reference to the inner `Instruction`. | +| `into_instruction()` | Consume the object and take out the inner `Instruction`. | + +--- + +## `ValueOperation` + +`ValueOperation` is the construction-layer self-contained operation representation, suitable for passing between circuits and for use as a boundary object for serialization, importers and compiler output. + +```rust +pub struct ValueOperation { + pub instruction: ValueInstruction, + pub qubits: SmallVec<[Qubit; 3]>, + pub params: SmallVec<[ParameterValue; 1]>, + pub label: Option>, +} +``` + +Field descriptions: + +| Field | Description | +| --- | --- | +| `instruction` | The construction-layer instruction, which can represent an ordinary instruction or control flow. | +| `qubits` | The list of acted-on qubits. | +| `params` | Self-contained parameters represented with `ParameterValue`, which can be a fixed value or a complete `Parameter`. | +| `label` | An optional label used only as metadata. | + +### `ValueOperation::from_standard` + +```rust +pub fn from_standard( + gate: StandardGate, + qubits: impl IntoIterator, + params: impl IntoIterator, +) -> Self +``` + +```rust +use cqlib_core::circuit::{ParameterValue, Qubit, StandardGate, ValueOperation}; + +let op = ValueOperation::from_standard( + StandardGate::RX, + [Qubit::new(0)], + [ParameterValue::from(0.5_f64)], +); +``` + +--- + +## `ParameterValue` and `CircuitParam` + +Operation parameters use different representations in the construction layer and the storage layer. + +| Type | Description | +| --- | --- | +| `ParameterValue` | The construction-layer parameter representation; it can be a fixed numeric value or a complete `Parameter` expression. | +| `CircuitParam` | The storage-layer parameter representation; it can be a fixed numeric value or an index into the parameter table of the owning `Circuit`. | + +Common conversion examples: + +```rust +use cqlib_core::circuit::{Parameter, ParameterValue}; + +let fixed = ParameterValue::from(0.5_f64); +let symbolic = ParameterValue::from(Parameter::symbol("theta")); +let also_symbolic = ParameterValue::from("phi"); +``` + +--- + +## Building and importing a circuit manually + +`ValueOperation` is often used to build circuits from external formats, or as the intermediate result of compiler pass output. The following example shows how to build an operation sequence manually and create a circuit through `Circuit::from_operations()`. + +```rust +use cqlib_core::circuit::{ + Circuit, ParameterValue, Qubit, StandardGate, ValueOperation, +}; + +let ops = vec![ + ValueOperation::from_standard(StandardGate::H, [Qubit::new(0)], []), + ValueOperation::from_standard(StandardGate::CX, [Qubit::new(0), Qubit::new(1)], []), + ValueOperation::from_standard( + StandardGate::RZ, + [Qubit::new(1)], + [ParameterValue::from("theta")], + ), +]; + +let circuit = Circuit::from_operations( + vec![Qubit::new(0), Qubit::new(1)], + ops, + None, + None, +)?; + +assert!(circuit.symbols().contains("theta")); + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +--- + +## The `label` tag + +`label` is optional metadata attached to an operation instance, used to record debugging information, the import source position, calibration tags, visualization names or compiler pass markers. + +```rust +use cqlib_core::circuit::{ParameterValue, Qubit, StandardGate, ValueOperation}; + +let mut op = ValueOperation::from_standard( + StandardGate::RZ, + [Qubit::new(0)], + [ParameterValue::from(0.25_f64)], +); + +op.label = Some("calibrated-rz".into()); +``` + +In compilation, whether to keep, modify or discard a `label` should be treated as a metadata handling policy, not as part of circuit semantics. + +--- + +## Validation boundaries + +`Circuit::append()` and `Circuit::from_operations()` check for common structural errors when appending or importing, including: + +- whether the number of qubits matches the instruction arity; +- whether the parameter count matches the instruction definition; +- whether the qubits in an operation are duplicated; +- whether the qubits in an operation belong to the current circuit; +- whether fixed parameters are finite values; +- whether classical data handles belong to the current circuit; +- whether scopes, jumps and classical value reads in control flow satisfy the constraints. + +For automatically generated IR, call the following before entering later compiler stages or backend execution: + +```rust +circuit.validate()?; +``` + +This detects IR construction problems as early as possible and avoids errors being deferred to matrix conversion, device mapping or actual backend execution. diff --git a/docs/api/en/rust/0_circuit/5_gate_standard.md b/docs/api/en/rust/0_circuit/5_gate_standard.md new file mode 100644 index 00000000..20eb3300 --- /dev/null +++ b/docs/api/en/rust/0_circuit/5_gate_standard.md @@ -0,0 +1,278 @@ +# StandardGate + +`cqlib_core::circuit::StandardGate` + +```rust +use cqlib_core::circuit::StandardGate; +``` + +`StandardGate` is the enum type in the Rust core used to represent the native standard quantum gates of Cqlib. It covers a basic gate set including common single-qubit gates, parameterized rotation gates, multi-qubit gates, controlled gates, two-body Pauli rotation gates, the fSim gate and the global phase marker. + +--- + +## Enum variants + +```rust +pub enum StandardGate { + I, + H, + RX, + RXX, + RXY, + RY, + RYY, + RZ, + RZX, + RZZ, + S, + SDG, + SWAP, + T, + TDG, + U, + X, + XY, + X2P, + X2M, + XY2P, + XY2M, + Y, + Y2P, + Y2M, + Z, + Phase, + GPhase, + CX, + CCX, + CY, + CZ, + CRX, + CRY, + CRZ, + FSIM, +} +``` + +--- + +## Gate metadata + +`StandardGate` provides a set of metadata methods for querying information such as the number of qubits a gate acts on, the number of built-in control qubits, the number of parameters and whether the gate is diagonal. + +| Method | Returns | Description | +| --- | --- | --- | +| `name()` | `&'static str` | Return the stable operation name of the gate, for example `"H"`, `"RX"`, `"CX"`. | +| `all()` | `&'static [StandardGate]` | Return all standard gate enum values, commonly used for generating gate tables, tests or target gate set checks. | +| `num_qubits()` | `usize` | Return the total number of qubits the gate acts on. | +| `num_ctrl_qubits()` | `usize` | Return the number of control qubits built into the gate. | +| `num_params()` | `usize` | Return the number of parameters the gate requires. | +| `is_diagonal()` | `bool` | Determine whether the gate is diagonal in the computational basis. | + +```rust +use cqlib_core::circuit::StandardGate; + +assert_eq!(StandardGate::H.num_qubits(), 1); +assert_eq!(StandardGate::CX.num_ctrl_qubits(), 1); +assert_eq!(StandardGate::RZ.num_params(), 1); +assert!(StandardGate::RZ.is_diagonal()); +``` + +--- + +## Gate categories + +### 1. Single-qubit fixed gates + +Single-qubit fixed gates require no extra parameters and are commonly used for state preparation, basis change, phase correction and Clifford-related optimization. + +| Gate | Description | +| --- | --- | +| `I` | Identity gate, which leaves the quantum state unchanged. | +| `H` | Hadamard gate, used to convert between the computational basis and the superposition basis. | +| `X` / `Y` / `Z` | Pauli gates, corresponding to bit flip, bit flip with a phase and phase flip respectively. | +| `S` / `SDG` | The `S` gate and its inverse gate, both belonging to the Clifford phase gates. | +| `T` / `TDG` | The `T` gate and its inverse gate, commonly used to extend a gate set beyond Clifford gates. | +| `X2P` / `X2M` | Positive/negative half-angle rotation gates in the X direction. | +| `Y2P` / `Y2M` | Positive/negative half-angle rotation gates in the Y direction. | + +### 2. Parameterized single-qubit gates + +Parameterized single-qubit gates control rotation or phase change through angle parameters, and are commonly used in variational circuits, parameter sweeps and hardware calibration. + +| Gate | Number of parameters | Description | +| --- | --- | --- | +| `RX` | 1 | X-axis rotation, usually adopting the `exp(-i θ X / 2)` convention. | +| `RY` | 1 | Y-axis rotation, usually adopting the `exp(-i θ Y / 2)` convention. | +| `RZ` | 1 | Z-axis rotation, usually adopting the `exp(-i θ Z / 2)` convention. | +| `RXY` | 2 | Rotation around an arbitrary axis in the XY plane. | +| `U` | 3 | General single-qubit gate. | +| `Phase` | 1 | Phase gate, which applies a phase to the `\|1>` component. | +| `XY` | 1 | Single-qubit parameterized gate of the XY interaction family. | +| `XY2P` / `XY2M` | 1 | Positive/negative half-angle XY gates. | + +### 3. Multi-qubit gates + +Multi-qubit gates establish correlations between qubits and are core operations in entangled state preparation, quantum algorithms and compilation mapping. + +| Gate | Number of parameters | Description | +| --- | --- | --- | +| `CX` | 0 | controlled-X, also known as CNOT. | +| `CY` | 0 | controlled-Y. | +| `CZ` | 0 | controlled-Z. | +| `CCX` | 0 | Toffoli gate, with two control qubits and one target qubit. | +| `SWAP` | 0 | Exchange the states of two qubits. | +| `RXX` / `RYY` / `RZZ` / `RZX` | 1 | Two-qubit Pauli rotation gates. | +| `CRX` / `CRY` / `CRZ` | 1 | Singly controlled parameterized rotation gates. | +| `FSIM` | 2 | fSim gate, commonly used to describe two-qubit interactions in specific hardware. | + +### 4. Global phase gate + +`GPhase` is a zero-qubit global phase marker with a parameter count of 1. It represents a global phase factor and usually does not act on any concrete qubit. + +--- + +## Matrix interface + +```rust +pub fn matrix( + &self, + params: &[f64], +) -> Result>>, CircuitError> +``` + +`matrix()` returns the local numeric matrix of the standard gate. `params` must provide parameters according to `self.num_params()`; when any one of them is `NaN` or infinite, `InvalidParameterValue` is returned; passing fewer parameters than `num_params()` is a call error. + +```rust +use cqlib_core::circuit::StandardGate; + +let h = StandardGate::H.matrix(&[])?; +let rx = StandardGate::RX.matrix(&[std::f64::consts::PI / 2.0])?; + +assert_eq!(h.shape(), &[2, 2]); +assert_eq!(rx.shape(), &[2, 2]); + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +--- + +## Mathematical conventions + +Rotation gates in Cqlib use the half-angle exponential definition common in quantum computing. For example: + +```text +RX(θ) = exp(-i θ X / 2) +RY(θ) = exp(-i θ Y / 2) +RZ(θ) = exp(-i θ Z / 2) +``` + +Two-body Pauli rotation gates use a similar convention: + +```text +RXX(θ) = exp(-i θ X⊗X / 2) +RYY(θ) = exp(-i θ Y⊗Y / 2) +RZZ(θ) = exp(-i θ Z⊗Z / 2) +RZX(θ) = exp(-i θ Z⊗X / 2) +``` + +`Phase(λ)` applies a phase to the `|1>` component; `GPhase(λ)` represents a global phase factor. + +--- + +## Inverse gate interface + +```rust +pub fn inverse( + &self, + params: &[Parameter], +) -> Option<(StandardGate, SmallVec<[Parameter; 3]>)> +``` + +`inverse()` returns the inverse gate enum of the current standard gate together with the transformed parameter list. Every standard gate has a representable inverse gate, so this interface always returns `Some` for a standard gate. `params` must supply enough parameters according to `num_params()`; an insufficient number of parameters is a call error. + +Common inverse gate relations are as follows: + +| Gate | Inverse | +| --- | --- | +| `H` | `H` | +| `X` / `Y` / `Z` | The gate itself | +| `S` | `SDG` | +| `SDG` | `S` | +| `T` | `TDG` | +| `TDG` | `T` | +| `RX(θ)` | `RX(-θ)` | +| `RY(θ)` | `RY(-θ)` | +| `RZ(θ)` | `RZ(-θ)` | +| `CX` / `CY` / `CZ` | The gate itself | +| `SWAP` | The gate itself | +| `CCX` | The gate itself | +| `X2P` / `X2M` | Mutually inverse | +| `Y2P` / `Y2M` | Mutually inverse | +| `XY2P` / `XY2M` | Mutually inverse, with parameters unchanged | +| `Phase(λ)` / `GPhase(λ)` | Parameters are negated | +| `RXX` / `RYY` / `RZZ` / `RZX` | Parameters are negated | +| `RXY(θ, φ)` | `RXY(-θ, φ)`, with `φ` unchanged | +| `FSIM(θ, φ)` | `FSIM(-θ, -φ)` | +| `XY(θ)` | `XY(π + θ)` | +| `U(θ, φ, λ)` | `U(-θ, -λ, -φ)`, with the last two parameters swapped | + +```rust +use cqlib_core::circuit::{Parameter, StandardGate}; + +let theta = Parameter::symbol("theta"); +let inverse = StandardGate::RX.inverse(&[theta.clone()]).unwrap(); + +assert_eq!(inverse.0, StandardGate::RX); + +# Ok::<(), cqlib_core::circuit::error::ParameterError>(()) +``` + +--- + +## `StandardGate` and `Circuit` + +For ordinary hand-written circuits on the Rust side, the convenience methods provided by `Circuit` are usually preferred, for example `h()`, `cx()` and `rz()`. These methods automatically construct the corresponding standard gate instruction and check the number of qubits, the number of parameters and qubit ownership. + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; + +let mut c = Circuit::new(1); +c.rx(Qubit::new(0), "theta")?; + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +When writing importers, compiler passes or low-level tests, a `StandardGate` can also be converted explicitly into an `Instruction` and then appended to a circuit through `Circuit::append()`. + +```rust +use cqlib_core::circuit::{Circuit, ParameterValue, Qubit, StandardGate}; + +let mut c = Circuit::new(1); + +c.append( + StandardGate::RX.into(), + [Qubit::new(0)], + [ParameterValue::from("theta")], + None, +)?; + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +--- + +## `StandardGate` and `MCGate` + +Standard gates such as `CX`, `CY`, `CZ`, `CCX`, `CRX`, `CRY` and `CRZ` can be regarded as built-in enum forms of common controlled gates. For these gates, compilers and matrix implementations can use the optimized standard gate path directly. + +When more control qubits are needed, or control qubits are to be added to an arbitrary base standard gate, `MCGate` should be used. For example, multi-controlled X gates, multi-controlled phase gates and controlled custom standard gates are usually better represented through `MCGate`. + +| Standard gate | Can be understood as | +| --- | --- | +| `CX` | Singly controlled `X` | +| `CY` | Singly controlled `Y` | +| `CZ` | Singly controlled `Z` | +| `CCX` | Doubly controlled `X` | +| `CRX(θ)` | Singly controlled `RX(θ)` | +| `CRY(θ)` | Singly controlled `RY(θ)` | +| `CRZ(θ)` | Singly controlled `RZ(θ)` | diff --git a/docs/api/en/rust/0_circuit/6_gate_unitary.md b/docs/api/en/rust/0_circuit/6_gate_unitary.md new file mode 100644 index 00000000..c156a899 --- /dev/null +++ b/docs/api/en/rust/0_circuit/6_gate_unitary.md @@ -0,0 +1,245 @@ +# UnitaryGate + +- `cqlib_core::circuit::UnitaryGate` +- `cqlib_core::circuit::gate::unitary_gate::UnitaryMatrix` + +```rust +use cqlib_core::circuit::UnitaryGate; +``` + +`UnitaryGate` is used in the Rust core to define user-defined unitary gates outside the standard gate set. It suits gates that have clear unitary matrix semantics but do not belong to the `StandardGate` enum, for example a custom oracle, a hardware calibration gate, a black-box unitary matrix generated by an external tool, or a parameterized custom gate whose symbolic matrix definition needs to be retained. + +--- + +## Constructor + +```rust +pub fn new(label: &str, num_qubits: u16, num_params: u16) -> Self +``` + +`UnitaryGate::new(label, num_qubits, num_params)` creates a custom unitary gate object to which no concrete definition has been attached yet. + +| Parameter | Description | +| --- | --- | +| `label` | The readable name of the gate, usually used for debugging, visualization and IR output. | +| `num_qubits` | The number of qubits the gate acts on. | +| `num_params` | The number of positional parameters to pass in each time the gate is applied. | + +```rust +use cqlib_core::circuit::UnitaryGate; + +let gate = UnitaryGate::new("Oracle", 2, 0); + +assert_eq!(gate.label(), "Oracle"); +assert_eq!(gate.num_qubits(), 2); +assert_eq!(gate.num_params(), 0); +``` + +Note that every call to `UnitaryGate::new()` generates a new UUID. + +--- + +## Attributes and query interfaces + +`UnitaryGate` provides a set of query methods for reading the gate metadata and the definition already attached. + +| Method | Returns | Description | +| --- | --- | --- | +| `label()` | `&str` | Return the readable name of the gate. | +| `num_qubits()` | `u16` | Return the number of qubits the gate acts on. | +| `num_params()` | `u16` | Return the number of positional parameters required each time the gate is applied. | +| `matrix()` | `Option<&Array2>>` | Return the numeric matrix definition; if no numeric matrix definition was used, return `None`. | +| `symbolic_matrix()` | `Option<&SymbolicMatrix>` | Return the symbolic matrix definition. | +| `matrix_params()` | `Option<&[String]>` | Return the order of the symbolic matrix parameter names. | +| `matrix_repr()` | `Option<&UnitaryMatrix>` | Return the raw matrix representation. | +| `circuit()` | `&Option>` | Return the circuit-backed definition. | + +--- + +## `UnitaryMatrix` + +`UnitaryMatrix` is the raw matrix representation enum of `UnitaryGate`, readable through `matrix_repr()`. + +```rust +pub enum UnitaryMatrix { + Numeric(Array2>), + Symbolic(SymbolicMatrix), +} +``` + +| Variant | Description | +| --- | --- | +| `Numeric(Array2>)` | A numeric matrix, applicable only to a non-parameterized unitary gate definition. | +| `Symbolic(SymbolicMatrix)` | A symbolic matrix whose free symbols are mapped positionally in the order of the gate's `matrix_params`. | + +--- + +## Numeric matrix definition + +```rust +pub fn with_matrix(self, mat: Array2>) -> Result +``` + +`with_matrix()` attaches a fixed numeric matrix definition to a custom gate. The matrix must be a complex square matrix with the shape: + +```text +2^num_qubits × 2^num_qubits +``` + +In addition, when a numeric matrix definition is used, `num_params` must be `0`, because the matrix of the gate is already fully determined and no extra positional parameter needs to be passed in when the gate is applied. Matrix elements must be finite values, and the matrix must be unitary, that is, `U†U` is the identity matrix within tolerance; otherwise an error is returned. + +```rust +use cqlib_core::circuit::UnitaryGate; +use ndarray::array; +use num_complex::Complex64; + +let x = array![ + [Complex64::new(0.0, 0.0), Complex64::new(1.0, 0.0)], + [Complex64::new(1.0, 0.0), Complex64::new(0.0, 0.0)], +]; + +let gate = UnitaryGate::new("CustomX", 1, 0).with_matrix(x)?; + +assert!(gate.matrix().is_some()); + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +--- + +## Symbolic matrix definition + +```rust +pub fn with_symbolic_matrix( + self, + params: I, + matrix: SymbolicMatrix, +) -> Result +where + I: IntoIterator, + S: Into, +``` + +`with_symbolic_matrix()` attaches a symbolic matrix definition to a custom gate. Elements of the symbolic matrix may contain `Parameter` expressions, which suits parameterized custom unitary gates. + +Here, `params` specifies how the positional parameters passed in when the gate is applied are mapped to the variable names in the symbolic matrix. That is, `params` is not merely a descriptive list; it defines the parameter binding order. When `matrix_for_params()` is called, or the gate is appended to a circuit, the `i`-th parameter passed in is bound to the symbol name at `params[i]`. The length of `params` must equal `num_params`, and every free symbol appearing in the symbolic matrix must already be declared in `params`. + +```rust +use cqlib_core::circuit::{Parameter, UnitaryGate}; +use cqlib_core::circuit::symbolic_matrix::{SymbolicComplex, SymbolicMatrix}; + +let theta = Parameter::symbol("theta"); + +let matrix = SymbolicMatrix::from_shape_vec( + (2, 2), + vec![ + SymbolicComplex::one(), + SymbolicComplex::zero(), + SymbolicComplex::zero(), + SymbolicComplex::exp_i(theta), + ], +).expect("valid 2x2 symbolic matrix"); + +let gate = UnitaryGate::new("PhaseLike", 1, 1) + .with_symbolic_matrix(["theta"], matrix)?; + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +In the example above, the symbolic matrix contains the variable `theta`, and `with_symbolic_matrix(["theta"], matrix)` declares that the gate has exactly one positional parameter when it is applied, and that this parameter is bound to `theta` in the matrix. + +--- + +## Circuit-backed definition + +```rust +pub fn with_circuit(self, circuit: Arc) -> Result +``` + +`with_circuit()` uses a `FrozenCircuit` as the definition of a `UnitaryGate`. When the behavior of a gate can be described by a sub-circuit, this form can be used to retain the inner circuit structure. When the matrix is requested, Cqlib binds the inner circuit according to the parameters passed in when the gate is applied, and calls `circuit_to_matrix()` to compute the matrix. + +```rust +use cqlib_core::circuit::{Circuit, Qubit, UnitaryGate}; +use cqlib_core::circuit::gate::FrozenCircuit; +use std::sync::Arc; + +let mut inner = Circuit::new(2); +inner.h(Qubit::new(0))?; +inner.cx(Qubit::new(0), Qubit::new(1))?; + +let gate = UnitaryGate::new("BellPrep", 2, 0) + .with_circuit(Arc::new(FrozenCircuit::new(inner)))?; + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +--- + +## Computing a concrete matrix + +```rust +pub fn matrix_for_params( + &self, + params: &[f64], +) -> Result>>, CircuitError> +``` + +`matrix_for_params(params)` obtains the concrete numeric matrix of a `UnitaryGate` once the positional parameters are given. The method dispatches automatically according to the definition form of the gate: + +| Definition form | Matrix computation method | +| --- | --- | +| Numeric matrix | Return the saved matrix directly, usually in borrowed form. | +| Symbolic matrix | Bind `params` to the symbol names in the order given by `matrix_params()`, then evaluate the `SymbolicMatrix`. | +| Circuit-backed definition | Bind parameters in the order of the inner circuit's `used_symbols()`, then call `circuit_to_matrix()` on the inner circuit. | + +--- + +## Appending to `Circuit` + +A `UnitaryGate` can be added to a circuit through the custom unitary gate appending interfaces provided by `Circuit`. + +```rust +circuit.unitary(gate, qubits)?; +circuit.unitary_with_params(gate, qubits, params)?; +``` + +- `unitary(gate, qubits)` applies to a custom gate without positional parameters; +- `unitary_with_params(gate, qubits, params)` applies to a custom gate with positional parameters. + +```rust +use cqlib_core::circuit::{Circuit, Qubit, UnitaryGate}; +use ndarray::array; +use num_complex::Complex64; + +let x = array![ + [Complex64::new(0.0, 0.0), Complex64::new(1.0, 0.0)], + [Complex64::new(1.0, 0.0), Complex64::new(0.0, 0.0)], +]; + +let gate = UnitaryGate::new("CustomX", 1, 0).with_matrix(x)?; + +let mut circuit = Circuit::new(1); +circuit.unitary(gate, vec![Qubit::new(0)])?; + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +--- + +## Comparison of definition forms + +| Definition form | Parameter support | Preserves circuit structure | Matrix source | Typical use | +| --- | --- | --- | --- | --- | +| `with_matrix()` | Does not support application parameters, `num_params` must be 0 | No | Save `Array2` directly | Fixed black-box matrices, hardware calibration gates, test oracles. | +| `with_symbolic_matrix()` | Supports `num_params` positional parameters | No | Evaluate the `SymbolicMatrix` | Parameterized custom matrix gates, symbolic phase gates. | +| `with_circuit()` | Supports symbol binding of the inner circuit | Yes | Bind parameters to the inner circuit and then call `circuit_to_matrix()` | Carry a circuit definition in the form of a custom unitary gate. | + +--- + +## Differences from `StandardGate` and `CircuitGate` + +| Type | Suitable scenarios | Characteristics | +| --- | --- | --- | +| `StandardGate` | Standard gates natively supported by Cqlib | Lightweight, optimizable and with clear gate semantics, suitable as a compilation basis. | +| `CircuitGate` | Wrapping a sub-circuit as a reusable composite gate | Preserves module boundaries, which facilitates decomposition and structural analysis. | +| `UnitaryGate` | Custom unitary gates outside the standard gate set | Can be defined by a matrix, a symbolic matrix or a frozen circuit, suitable for black-box or special unitary gates. | diff --git a/docs/api/en/rust/0_circuit/7_gate_mc_gate.md b/docs/api/en/rust/0_circuit/7_gate_mc_gate.md new file mode 100644 index 00000000..29b6f1c6 --- /dev/null +++ b/docs/api/en/rust/0_circuit/7_gate_mc_gate.md @@ -0,0 +1,155 @@ +# MCGate + +`cqlib_core::circuit::MCGate` + +```rust +use cqlib_core::circuit::{MCGate, StandardGate}; +``` + +`MCGate` represents a multi-controlled gate obtained by "adding several control qubits in front of a standard gate". It takes a `StandardGate` as the base gate and adds new control qubits in front of the qubits the base gate originally acts on. The base gate acts on the target qubits only when all control qubits satisfy the control condition. + +--- + +## Constructor + +```rust +pub fn new(num_controls: u8, gate: StandardGate) -> Self +``` + +`MCGate::new(num_controls, gate)` adds `num_controls` control qubits in front of the base standard gate `gate`. + +| Parameter | Description | +| --- | --- | +| `num_controls` | The number of added control qubits. | +| `gate` | The base standard gate being controlled. | + +```rust +use cqlib_core::circuit::{MCGate, StandardGate}; + +let ccx = MCGate::new(2, StandardGate::X); +let mch = MCGate::new(3, StandardGate::H); +``` + +In the example above, `MCGate::new(2, StandardGate::X)` represents a doubly controlled `X` gate whose semantics are equivalent to the common Toffoli gate; `MCGate::new(3, StandardGate::H)` represents a triply controlled `H` gate. + +--- + +## Attributes and methods + +`MCGate` provides a set of metadata methods for querying the number of control qubits, the total number of qubits it acts on, the number of parameters and the base gate type. + +| Method | Returns | Description | +| --- | --- | --- | +| `num_ctrl_qubits()` | `usize` | Return the total number of control qubits, including the added control qubits and the control qubits built into the base gate. | +| `num_qubits()` | `usize` | Return the total number of qubits the multi-controlled gate acts on. | +| `num_params()` | `usize` | Return the number of parameters the base gate requires. | +| `base_gate()` | `&StandardGate` | Return the base standard gate being controlled. | +| `name()` | `String` | Return the stable operation name: the base gate name when no control qubit is added, otherwise `C-`. | + +```rust +use cqlib_core::circuit::{MCGate, StandardGate}; + +let gate = MCGate::new(1, StandardGate::CX); + +assert_eq!(gate.num_ctrl_qubits(), 2); // 新增 1 个控制位 + CX 自带 1 个控制位 +assert_eq!(gate.num_qubits(), 3); +assert_eq!(gate.num_params(), 0); +assert_eq!(*gate.base_gate(), StandardGate::CX); +``` + +--- + +## Order of control and target qubits + +When an `MCGate` is applied, the qubit order has clear semantics: + +```text +[new_control_0, new_control_1, ..., base_gate_qubit_0, base_gate_qubit_1, ...] +``` + +That is, the added control qubits always come first, followed by the qubits the base gate itself requires. If the base gate itself already carries control qubits, the control qubits inside the base gate keep the original order of that standard gate. + +For example, `MCGate::new(2, StandardGate::X)` acts on three qubits: + +```text +[control_0, control_1, target] +``` + +The first two are the added control qubits, and the last one is the target qubit of the `X` gate. + +And `MCGate::new(1, StandardGate::CX)` acts on three qubits: + +```text +[new_control, cx_control, cx_target] +``` + +The first one is the added control qubit, and the second and third are the control qubit and the target qubit of the original `CX` gate respectively. + +--- + +## Matrix + +```rust +pub fn matrix( + &self, + params: &[f64], +) -> Result>>, CircuitError> +``` + +`matrix(params)` returns the local numeric matrix of the multi-controlled gate. The parameter list `params` corresponds to the parameters of the base gate, and its length must agree with `self.num_params()`. + +```rust +use cqlib_core::circuit::{MCGate, StandardGate}; + +let gate = MCGate::new(1, StandardGate::RZ); + +let matrix = gate.matrix(&[std::f64::consts::PI / 2.0])?; +assert_eq!(matrix.shape(), &[4, 4]); + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +In the example above, the base gate is the single-qubit parameterized gate `RZ`; after one control qubit is added, the result is a two-qubit controlled `RZ` gate, so the matrix shape is `(4, 4)`. + +In general, if an `MCGate` acts on `n` qubits, its matrix shape is: + +```text +2^n × 2^n +``` + +--- + +## Inverse gate + +```rust +pub fn inverse(&self, params: &[Parameter]) -> Option<(MCGate, SmallVec<[Parameter; 3]>)> +``` + +`inverse(params)` returns the inverse gate of the current multi-controlled gate and the transformed parameter list. The inverse of a multi-controlled gate is equivalent to "taking the inverse of the base gate and then adding the same control qubits". + +```rust +use cqlib_core::circuit::{MCGate, StandardGate}; + +let gate = MCGate::new(1, StandardGate::S); + +let (inverse, params) = gate.inverse(&[]).unwrap(); + +assert_eq!(*inverse.base_gate(), StandardGate::SDG); +assert!(params.is_empty()); +``` + +--- + +## `MCGate` and `StandardGate` + +Some common controlled gates already exist as standard enum values of `StandardGate`. They can be regarded as special cases of `MCGate`. + +| Standard gate | Equivalent form | +| --- | --- | +| `StandardGate::CX` | `MCGate::new(1, StandardGate::X)` | +| `StandardGate::CY` | `MCGate::new(1, StandardGate::Y)` | +| `StandardGate::CZ` | `MCGate::new(1, StandardGate::Z)` | +| `StandardGate::CCX` | `MCGate::new(2, StandardGate::X)` | +| `StandardGate::CRX` | `MCGate::new(1, StandardGate::RX)` | +| `StandardGate::CRY` | `MCGate::new(1, StandardGate::RY)` | +| `StandardGate::CRZ` | `MCGate::new(1, StandardGate::RZ)` | diff --git a/docs/api/en/rust/0_circuit/8_gate_circuit_gate.md b/docs/api/en/rust/0_circuit/8_gate_circuit_gate.md new file mode 100644 index 00000000..868dcaa9 --- /dev/null +++ b/docs/api/en/rust/0_circuit/8_gate_circuit_gate.md @@ -0,0 +1,264 @@ +# CircuitGate / FrozenCircuit + +- `cqlib_core::circuit::CircuitGate` +- `cqlib_core::circuit::gate::FrozenCircuit` + +```rust +use cqlib_core::circuit::{Circuit, CircuitGate}; +use cqlib_core::circuit::gate::FrozenCircuit; +``` + +`FrozenCircuit` and `CircuitGate` are used in the Rust core to express "a composite gate defined by a circuit". `FrozenCircuit` is an immutable circuit definition, holding the structure and operation sequence of a sub-circuit; `CircuitGate` is a reusable gate object built from that definition, and can be appended to other circuits like an ordinary gate. + +--- + +## `FrozenCircuit` + +`FrozenCircuit` is an immutable circuit definition, used to hold a `Circuit` that has already been constructed. After creation, the inner circuit is used as a gate definition and should no longer be modified externally. + +```rust +pub fn new(circuit: Circuit) -> Self +pub fn circuit(&self) -> &Circuit +pub fn used_symbols(&self) -> &IndexSet +pub fn symbolic_matrix(&self) -> Result, CircuitError> +``` + +### Creating an immutable circuit definition + +`FrozenCircuit::new(circuit)` moves the passed-in `Circuit` and stores it as an immutable definition. Because the passed-in circuit is moved, the caller can no longer modify this circuit through the original variable, which prevents the composite gate definition from changing while it is reused. + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::circuit::gate::FrozenCircuit; + +let mut inner = Circuit::new(2); +inner.h(Qubit::new(0))?; +inner.cx(Qubit::new(0), Qubit::new(1))?; + +let frozen = FrozenCircuit::new(inner); + +assert_eq!(frozen.circuit().num_qubits(), 2); + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +### Reading the inner circuit + +```rust +pub fn circuit(&self) -> &Circuit +``` + +`circuit()` returns a reference to the inner immutable circuit. This interface is suitable for inspecting the number of qubits, the operation sequence or the symbolic parameters in the definition, or for performing compilation analysis. + +### Symbolic matrix cache + +```rust +pub fn symbolic_matrix(&self) -> Result, CircuitError> +``` + +`symbolic_matrix()` computes and returns the symbolic matrix of the inner circuit. The method usually computes the symbolic matrix on the first call and caches the result; later calls reuse the cached result, so as to reduce repeated computation overhead. + +--- + +## `CircuitGate::new` + +```rust +pub fn new(name: impl Into, circuit: FrozenCircuit) -> Result +``` + +`CircuitGate::new(name, circuit)` creates a composite gate definition from a `FrozenCircuit`. The method automatically uses the free symbols in the frozen circuit as the call signature. + +```rust +use cqlib_core::circuit::{Circuit, CircuitGate, Qubit}; +use cqlib_core::circuit::gate::FrozenCircuit; + +let mut inner = Circuit::new(1); +inner.h(Qubit::new(0))?; + +let gate = CircuitGate::new("HBlock", FrozenCircuit::new(inner))?; + +assert_eq!(gate.name(), "HBlock"); +assert_eq!(gate.num_qubits(), 1); + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +In the example above, `HBlock` is a composite gate defined by a single-qubit `H` gate circuit. Because the inner circuit has no symbolic parameters, the number of parameters of the composite gate is 0. + +--- + +## `CircuitGate::with_signature` + +```rust +pub fn with_signature( + name: impl Into, + circuit: FrozenCircuit, + params: impl IntoIterator, +) -> Result +``` + +`with_signature()` is used to declare the call parameter signature of a composite gate explicitly. + +--- + +## Attributes and methods + +`CircuitGate` provides the following common query interfaces: + +| Method | Returns | Description | +| --- | --- | --- | +| `name()` | `&str` | Return the composite gate name. | +| `num_qubits()` | `usize` | Return the number of qubits required when the composite gate is applied. | +| `num_params()` | `usize` | Return the number of positional parameters in the call signature. | +| `signature_params()` | `&IndexSet` | Return the parameter list in the composite gate call signature; the order carries binding semantics. | +| `used_symbols()` | `&IndexSet` | Return the set of symbols actually referenced by the inner circuit. | +| `symbols()` | `IndexSet` | Return a clone of `used_symbols()`. | +| `circuit()` | `Arc` | Return the frozen circuit definition of the composite gate. | +| `symbolic_matrix()` | `Result, CircuitError>` | Return the symbolic matrix of the inner definition, usually reusing the cache. | + +--- + +## Parameter signature and binding semantics + +Parameter binding of `CircuitGate` uses positional parameter semantics. That is, the `i`-th parameter passed in when the composite gate is called replaces the inner symbol corresponding to `signature_params()[i]`. + +For example, if the signature is: + +```text +["theta", "phi"] +``` + +then the call passes in: + +```text +[alpha, beta] +``` + +which means: + +```text +theta -> alpha +phi -> beta +``` + +This substitution is performed simultaneously, not step by step in order. Therefore, for swap scenarios such as `a -> b` and `b -> a`, no intermediate conflict arises from the substitution order. + +--- + +## Parameterized composite gate example + +The example below constructs a sub-circuit with symbolic parameters and wraps it as a `CircuitGate`. When the outer circuit calls this composite gate, new parameter expressions can be passed in to replace the inner symbols. + +```rust +use cqlib_core::circuit::{Circuit, CircuitGate, Parameter, ParameterValue, Qubit}; +use cqlib_core::circuit::gate::FrozenCircuit; + +let theta = Parameter::symbol("theta"); + +let mut inner = Circuit::new(1); +inner.rx(Qubit::new(0), theta)?; + +let gate = CircuitGate::new("RxBlock", FrozenCircuit::new(inner))?; + +let mut outer = Circuit::new(1); +outer.circuit_gate( + gate, + vec![Qubit::new(0)], + vec![ParameterValue::from("alpha")], +)?; + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +In this example, the outer call parameter `alpha` replaces the inner symbol `theta` in the composite gate definition. If the inner circuit contains several symbols, the number and order of the parameters passed in when calling must agree with `signature_params()`. + +--- + +## Inverse gate + +```rust +pub fn inverse(&self) -> Result +``` + +`inverse()` returns a new `CircuitGate` whose underlying circuit is the result of inverting the original frozen circuit operation by operation and arranging the operations in reverse order. By default, the new gate name has the suffix `_dg` appended to the original name, and the original parameter signature is kept. + +```rust +let inv = gate.inverse()?; +assert_eq!(inv.name(), "HBlock_dg"); + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +--- + +## Appending to `Circuit` + +A `CircuitGate` can be appended to an outer circuit through `Circuit::circuit_gate()`. + +```rust +circuit.circuit_gate(gate, qubits, params)?; +``` + +Here: + +- `gate` is the composite gate to apply; +- `qubits` is the list of qubits the gate actually acts on in the outer circuit; +- `params` is the list of positional parameters provided by the outer call. + +```rust +use cqlib_core::circuit::{Circuit, CircuitGate, ParameterValue, Qubit}; +use cqlib_core::circuit::gate::FrozenCircuit; + +let mut inner = Circuit::new(1); +inner.h(Qubit::new(0))?; + +let gate = CircuitGate::new("HBlock", FrozenCircuit::new(inner))?; + +let mut outer = Circuit::new(1); + +outer.circuit_gate( + gate, + vec![Qubit::new(0)], + Vec::::new(), +)?; + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +--- + +## Decomposition and matrix analysis + +Because `CircuitGate` keeps the inner circuit structure, it can be expanded when needed or used for matrix analysis. + +A typical flow includes: + +- Expand the composite gate into the inner base operations with `Circuit::decompose()`; +- Obtain the symbolic matrix of the inner definition with `CircuitGate::symbolic_matrix()`; +- Validate the overall matrix behavior in the outer circuit with `Circuit::to_matrix()`; +- Generate the corresponding inverse gate with `inverse()`. + +Compared with a black-box matrix gate, the advantage of `CircuitGate` is that it keeps the operation structure of the sub-circuit, so it is better suited to compiler analysis, gate decomposition, resource counting and visualization. + +--- + +## Difference from `UnitaryGate` + +| Type | Suitable scenario | Characteristics | +| --- | --- | --- | +| `CircuitGate` | A sub-circuit is reused as a composite gate | Keeps the circuit structure; it can be decomposed and inverted, and its symbolic matrix can be cached. | +| `UnitaryGate::with_matrix()` | Only a black-box numeric matrix is of interest | Does not keep the inner circuit structure; suitable for a fixed custom matrix. | +| `UnitaryGate::with_symbolic_matrix()` | Only a black-box symbolic matrix is of interest | Suitable for a parameterized custom matrix gate. | +| `UnitaryGate::with_circuit()` | A circuit-backed definition is to be kept in the form of a custom unitary | Keeps the circuit-backed definition, but semantically it is closer to a custom unitary gate. | + +--- + +## Signature and internal symbols + +`signature_params()` and `used_symbols()` are used to distinguish the external call interface of a composite gate from its internal implementation details. + +| Term | Description | +| --- | --- | +| `signature_params()` | The list of positional parameters that must be provided in order when calling. | +| `used_symbols()` | The set of free symbols actually referenced by the inner circuit. | +| `num_params()` | Equal to the number of signature parameters, not necessarily equal to the number of symbols actually used inside. | diff --git a/docs/api/en/rust/0_circuit/9_classical_control_flow.md b/docs/api/en/rust/0_circuit/9_classical_control_flow.md new file mode 100644 index 00000000..c375d859 --- /dev/null +++ b/docs/api/en/rust/0_circuit/9_classical_control_flow.md @@ -0,0 +1,416 @@ +# Classical Data / Control Flow + +- `cqlib_core::circuit::ClassicalType` +- `cqlib_core::circuit::ClassicalVar` +- `cqlib_core::circuit::ClassicalValue` +- `cqlib_core::circuit::Measurement` +- `cqlib_core::circuit::ClassicalExpr` +- `cqlib_core::circuit::ClassicalControlOp` +- `cqlib_core::circuit::{IfOp, WhileOp, ForOp, SwitchOp, ControlBody}` + +```rust +use cqlib_core::circuit::{ClassicalType, ClassicalVar, ClassicalValue, Measurement, ClassicalExpr, ClassicalControlOp}; +use cqlib_core::circuit::{IfOp, WhileOp, ForOp, SwitchOp, ControlBody}; +``` + +This page covers the API in the Rust core related to classical data and structured control flow. These types are used to represent measurement results, classical variables and classical expressions in a circuit, as well as control flow structures such as `if`, `while`, `for` and `switch` based on classical conditions. + +--- + +## Core concepts + +| Term | Description | +| --- | --- | +| `ClassicalType` | The static type of classical data, including `Bit`, `Bool`, `UInt(width)` and `BitVec(width)`. | +| `ClassicalVar` | A mutable classical variable handle, created by `Circuit::var()` and updatable through `store()`. | +| `ClassicalValue` | An immutable classical value handle, usually produced by measurement, with SSA-style semantics. | +| `Measurement` | A measurement receipt, recording the measurement result value and the order of the measured qubits. | +| `ClassicalExpr` | A typed classical expression AST, used for variable reads, value reads, logical operations, comparisons, type conversions and bit operations. | +| `ClassicalControlOp` | The storage-layer structured control flow IR, including `if`, `while`, `for`, `switch`, `break` and `continue`. | +| `ValueClassicalControlOp` | The construction-layer control flow IR, suitable for serialization, importers and `Circuit::from_operations()`. | +| `ControlBody` / `ValueControlBody` | Control flow bodies, holding the storage-layer or the construction-layer operation sequence respectively. | + +--- + +## `ClassicalType` + +```rust +pub enum ClassicalType { + Bit, + Bool, + UInt(u32), + BitVec(u32), +} +``` + +`ClassicalType` describes the static type of classical data. Through explicit types, Cqlib can check at construction time whether the types of expressions, variables, control flow conditions and measurement results match. + +| Constructor | Description | +| --- | --- | +| `ClassicalType::Bit` | A single bit, usually representing a single-qubit measurement result, with the value 0 or 1. | +| `ClassicalType::Bool` | A logical Boolean value, used for conditions such as `if` and `while`. | +| `ClassicalType::uint(width)` | Create an unsigned integer type with the given bit width; return `None` when the width is invalid. | +| `ClassicalType::bit_vec(width)` | Create a bit vector type with the given bit width. | + +The common methods are as follows: + +| Method | Description | +| --- | --- | +| `width()` | Return the bit width of the type. | +| `zero_literal()` | Return the typed 0 literal expression of this type. | +| `one_literal()` | Return the typed 1 literal expression of this type. | +| `measurement_width()` | Return the corresponding measurement width when this type can serve as a measurement result type. | + +```rust +use cqlib_core::circuit::ClassicalType; + +let bit_ty = ClassicalType::Bit; +let bool_ty = ClassicalType::Bool; +let u8_ty = ClassicalType::uint(8).expect("valid uint width"); +let bv4_ty = ClassicalType::bit_vec(4).expect("valid bit vector width"); + +assert_eq!(bit_ty.width(), 1); +assert_eq!(bool_ty.width(), 1); +assert_eq!(u8_ty.width(), 8); +assert_eq!(bv4_ty.width(), 4); +``` + +In control flow, `Bit` and `Bool` should be clearly distinguished: `Bit` is closer to a low-level bit value, while `Bool` is closer to a logical decision. To use a single-bit measurement result as a condition, it usually needs to be converted to a `Bool` expression first. + +--- + +## `ClassicalVar` and `ClassicalValue` + +`ClassicalVar` and `ClassicalValue` are both classical data handles, but their semantics differ. + +| Type | Creation | Description | +| --- | --- | --- | +| `ClassicalVar` | `Circuit::var(ty)` | A mutable classical storage handle, updatable through `Circuit::store()`. | +| `ClassicalValue` | `Circuit::measure()` / `Circuit::measure_bits()` | An immutable classical value handle, usually produced by measurement, with SSA style. | + +Both provide the following basic interfaces: + +- `index()` +- `circuit_id()` +- `ty()` +- `expr()` + +Among them, `ClassicalVar` also provides `id()`, used to represent a stable identity composed of a `CircuitId` and a variable index. + +```rust +use cqlib_core::circuit::{Circuit, ClassicalType}; + +let mut circuit = Circuit::new(1); + +let flag = circuit.var(ClassicalType::Bool); +let flag_expr = flag.expr(); + +assert_eq!(flag.ty(), ClassicalType::Bool); +assert_eq!(flag.circuit_id(), circuit.id()); +``` + +`ClassicalVar` is suitable for representing state that can be updated in control flow, such as loop flags, counters or branch state. `ClassicalValue` usually represents an immutable result produced by one measurement. Because it has SSA-style semantics, it is usually required to be defined before use, and it must not illegally escape the scope in which it is defined. + +--- + +## `Measurement` + +`Measurement` is the receipt object returned by a measurement operation, recording the measurement result value and the order of the measured qubits. + +| Method | Description | +| --- | --- | +| `value()` | Return the `ClassicalValue` produced by the measurement. | +| `expr()` | Return the `ClassicalExpr` that reads the measurement result. | +| `qubits()` | Return the order of the measured qubits. | +| `width()` | Return the number of measured bits. | +| `ty()` | Return the measurement result type. | +| `check_qubits(num_qubits)` | Check whether the measured qubits are within the given state range. | +| `project(full)` | Project the result corresponding to this measurement out of a complete `Outcome`. | +| `project_basis(basis)` | Project the result corresponding to this measurement from a computational basis index. | + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; + +let mut circuit = Circuit::new(2); + +let m = circuit.measure(Qubit::new(0))?; +assert_eq!(m.width(), 1); + +let expr = m.expr(); + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +For a single-qubit measurement, the result is usually of type `Bit`; for a multi-qubit measurement, the result is usually `BitVec(width)`. To use a measurement result in a Boolean condition, call `to_bool()` or the corresponding type conversion interface as needed. + +--- + +## `ClassicalExpr` + +`ClassicalExpr` is a typed, side-effect-free classical expression AST. It represents classical-side computation logic, such as reading variables, reading measurement values, constructing literals, performing logical operations, comparisons, type conversions, conditional selection and bit operations. + +### Creating expressions + +| Method | Description | +| --- | --- | +| `ClassicalExpr::var(var)` | Read a mutable classical variable. | +| `ClassicalExpr::value(value)` | Read an immutable classical value. | +| `bool_literal(value)` | Create a `Bool` literal. | +| `bit_literal(value)` | Create a `Bit` literal. | +| `uint_literal(width, value)` | Create a `UInt` literal with the given bit width. | +| `bit_vec_literal(width, value)` | Create a `BitVec` literal with the given bit width. | + +```rust +use cqlib_core::circuit::ClassicalExpr; + +let truth = ClassicalExpr::bool_literal(true); +let bit_one = ClassicalExpr::bit_literal(true); +let u3 = ClassicalExpr::uint_literal(3, 5)?; +let bits = ClassicalExpr::bit_vec_literal(4, 0b1010)?; + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +### Logical and bit operations + +| Method | Description | +| --- | --- | +| `try_not(expr)` | Construct a NOT expression. | +| `try_and(lhs, rhs)` | Construct an AND expression. | +| `try_or(lhs, rhs)` | Construct an OR expression. | +| `try_xor(lhs, rhs)` | Construct an XOR expression. | + +Expressions also implement convenience operators such as `!`, `&`, `|` and `^`. For library code, importers or compiler passes, the `try_*` family of methods is preferable, because they can return type errors explicitly and avoid panics or hard-to-locate problems caused by type mismatches. + +### Type conversion + +| Method | Description | +| --- | --- | +| `bit_to_bool(expr)` | Convert `Bit` to `Bool`. | +| `bit_vec_to_uint(expr)` | Convert `BitVec` to `UInt`. | +| `to_bool(self)` | Convert `Bit` or `UInt` to `Bool`. | +| `to_uint(self)` | Convert `Bit` or `BitVec` to `UInt`. | + +A common use is to convert a measured `Bit` into `Bool` and then use it as an `if` or `while` condition. + +### Comparison and selection + +| Method | Description | +| --- | --- | +| `eq(lhs, rhs)` | Construct an equality comparison. | +| `ne(lhs, rhs)` | Construct an inequality comparison. | +| `lt(lhs, rhs)` | Construct a less-than comparison. | +| `le(lhs, rhs)` | Construct a less-than-or-equal comparison. | +| `gt(lhs, rhs)` | Construct a greater-than comparison. | +| `ge(lhs, rhs)` | Construct a greater-than-or-equal comparison. | +| `select(condition, then_expr, else_expr)` | Construct a ternary conditional selection expression. | + +A comparison result is usually a `Bool` expression and can be used directly as the condition of `if_`, `if_else` or `while_`. When constructing a comparison, the types of the expressions on the two sides should be compatible, for example a magnitude comparison between `UInt` expressions of the same bit width. + +### Bit extraction and concatenation + +| Method | Description | +| --- | --- | +| `extract_bit(value, index)` | Extract a single bit from a `UInt` or `BitVec`. | +| `extract_bits(value, offset, width)` | Extract a contiguous bit range. | +| `concat(parts)` | Concatenate several `BitVec` expressions. | +| `pack_bits(bits)` | Pack several `Bit` expressions into a `BitVec`. | + +### Introspection and simplification + +| Method | Description | +| --- | --- | +| `ty()` | Return the static type of the expression. | +| `kind()` | Return the underlying AST node. | +| `vars()` | Return the set of all mutable classical variables read by the expression. | +| `values()` | Return the set of all immutable classical values read by the expression. | +| `collect_vars(out)` | Write the classical variables read into the given set. | +| `collect_values(out)` | Write the classical values read into the given set. | +| `remap_classical_ids(var_map, value_map)` | Remap the classical handles referenced by the expression according to the mapping tables. | +| `simplified()` | Return the expression after structural simplification. | +| `is_bool_true()` / `is_bool_false()` | Determine whether it is a `Bool` literal. | +| `is_bit_true()` / `is_bit_false()` | Determine whether it is a `Bit` literal. | + +--- + +## Expression AST types + +The underlying nodes of `ClassicalExpr` are described by the following types, all of which are visible at the root of the `cqlib_core::circuit` module. + +```rust +pub enum ClassicalUnaryOp { Not } +pub enum ClassicalBinaryOp { And, Or, Xor } +pub enum ClassicalCompareOp { Eq, Ne, Lt, Le, Gt, Ge } +pub enum ClassicalCast { BitToBool, BitVecToUInt } +``` + +```rust +pub enum ClassicalExprKind { + Var(ClassicalVar), + Value(ClassicalValue), + BoolLiteral(bool), + BitLiteral(bool), + UIntLiteral { width: NonZeroU32, value: u128 }, + BitVecLiteral { width: NonZeroU32, value: u128 }, + Unary { op: ClassicalUnaryOp, expr: ClassicalExpr }, + Binary { op: ClassicalBinaryOp, lhs: ClassicalExpr, rhs: ClassicalExpr }, + Compare { op: ClassicalCompareOp, lhs: ClassicalExpr, rhs: ClassicalExpr }, + Cast { cast: ClassicalCast, expr: ClassicalExpr }, + Select { condition: ClassicalExpr, then_expr: ClassicalExpr, else_expr: ClassicalExpr }, + ExtractBit { value: ClassicalExpr, index: u32 }, + ExtractBits { value: ClassicalExpr, offset: u32, width: NonZeroU32 }, + Concat { parts: Box<[ClassicalExpr]> }, + PackBits { bits: Box<[ClassicalExpr]> }, +} +``` + +| Type | Description | +| --- | --- | +| `ClassicalExprKind` | The expression AST node enum, covering variable reads, literals, operations, comparisons, type conversions and bit operations. | +| `ClassicalExprNode` | An AST node, carrying the static type of the expression together with the node content. | +| `ClassicalUnaryOp` / `ClassicalBinaryOp` | Unary and binary logical operation operators. | +| `ClassicalCompareOp` | Comparison operators. | +| `ClassicalCast` | Explicit type conversion operators. | + +--- + +## `Circuit` high-level control flow API + +`Circuit` provides closure-style control flow construction interfaces, used to construct structured control flow in a fairly natural Rust code style. + +```rust +circuit.if_(condition, body) +circuit.if_else(condition, then_body, else_body) +circuit.while_(condition, body) +circuit.for_uint(var, start, stop, step, body) +circuit.switch(target, build) +circuit.break_loop() +circuit.continue_loop() +``` + +### 1. `if_` + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; + +let mut circuit = Circuit::new(2); +let m = circuit.measure(Qubit::new(0))?; + +circuit.if_(m.expr().to_bool()?, |body| { + body.x(Qubit::new(1))?; + Ok(()) +})?; + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +`if_` constructs a conditional structure without an `else` branch. The condition must be a `Bool` expression, and the body callback constructs the circuit fragment executed when the condition holds. + +### 2. `if_else` + +```rust +circuit.if_else( + m.expr().to_bool()?, + |then_body| { + then_body.x(Qubit::new(1))?; + Ok(()) + }, + |else_body| { + else_body.z(Qubit::new(1))?; + Ok(()) + }, +)?; + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +`if_else` constructs a two-way conditional branch. Both branches are committed as part of the same control flow structure; if construction of either branch fails, the whole control flow construction is rolled back. + +### 3. `while_` / `for_uint` / `switch` + +| Method | Description | +| --- | --- | +| `while_(condition, body)` | Construct a runtime `while` loop based on a `Bool` condition. | +| `for_uint(var, start, stop, step, body)` | Construct a half-open interval loop based on a `UInt` variable. | +| `switch(target, build)` | Construct a multi-way selection based on exact `UInt` matching. | +| `break_loop()` | Exit the nearest loop or the control flow body that permits the jump. | +| `continue_loop()` | Jump to the next iteration of the nearest loop. | + +`for_uint` requires the loop variable, the start, the stop and the step to be `UInt` expressions of the same bit width; the target of `switch` must be a `UInt`, and case values must be representable in that bit width. + +### 4. `switch` and `SwitchBuilder` + +The callback parameter of `switch(target, build)` is a `SwitchBuilder`, used to register each case and the optional default branch in order. + +| Method | Description | +| --- | --- | +| `value(value, body)` | Register one exact-match case branch. | +| `default(body)` | Register the default branch; registering it repeatedly returns an error. | + +```rust +use cqlib_core::circuit::{Circuit, ClassicalType, Qubit}; + +let mut circuit = Circuit::new(2); +let state = circuit.var(ClassicalType::uint(2).expect("valid uint width")); + +circuit.switch(state.expr(), |case| { + case.value(0, |body| { + body.x(Qubit::new(0))?; + Ok(()) + })?; + case.value(1, |body| { + body.h(Qubit::new(1))?; + Ok(()) + })?; + case.default(|body| { + body.z(Qubit::new(0))?; + Ok(()) + })?; + Ok(()) +})?; + +# Ok::<(), cqlib_core::circuit::CircuitError>(()) +``` + +--- + +## Low-level control flow IR + +Besides the high-level closure-style API, the Rust core also provides a low-level control flow IR, suitable for importers, deserializers, compiler passes and low-level tests. + +| Type | Description | +| --- | --- | +| `ControlBody` | The storage-layer control flow body, containing `Vec`. | +| `ValueControlBody` | The construction-layer control flow body, containing `Vec`. | +| `IfOp` | The conditional branch structure. | +| `WhileOp` | The `while` loop structure. | +| `ForOp` | The `UInt` half-open interval loop structure. | +| `SwitchOp` / `SwitchCase` | The `switch` multi-way branch structure. | +| `ClassicalControlOp` | The storage-layer control flow enum, containing the control flow operations above. | +| `ValueClassicalControlOp` | The construction-layer control flow enum, suitable for self-contained IR. | + +`Circuit::append_control(op)` can append a storage-layer `ClassicalControlOp`. For self-contained import scenarios, `ValueClassicalControlOp` and `ValueOperation` can also be combined and handed to `Circuit::from_operations()`. + +--- + +## Validation rules + +`Circuit::validate()` and the related append procedures check the consistency of classical data and control flow structures. Typical checks include: + +- `ClassicalVar` and `ClassicalValue` must belong to the current `CircuitId`; +- an immutable `ClassicalValue` must be defined before it is read; +- a value defined inside a control flow scope must not escape to an outer scope; +- `break` and `continue` may appear only inside a valid control flow body; +- after a control transfer, ordinary operations of the same body cannot continue to be appended; +- the conditions of `if` and `while` must be `Bool` expressions; +- the variable, the start, the stop and the step of `for_uint` must be `UInt` of the same bit width; +- the `switch` target must be a `UInt`, and case values must be representable in that bit width; +- the variables and values involved in a classical expression must satisfy type compatibility requirements. + +For circuits that are externally imported, generated automatically by a program or constructed by hand at the low-level IR layer, an explicit call is recommended before entering later compiler stages or backend execution: + +```rust +circuit.validate()?; +``` + +This detects handle ownership, type mismatches, scope escapes and invalid control flow jump positions as early as possible. diff --git a/docs/api/en/rust/0_overview.md b/docs/api/en/rust/0_overview.md new file mode 100644 index 00000000..c4f187c9 --- /dev/null +++ b/docs/api/en/rust/0_overview.md @@ -0,0 +1,83 @@ +# Rust API Overview + +## Welcome to the Cqlib Rust API reference! + +## Documentation navigation + +### Quantum Circuit (`cqlib_core::circuit`) + +- [Overview](0_circuit/0_overview.md) +- [Circuit](0_circuit/1_circuit.md) +- [Qubit](0_circuit/2_qubit.md) +- [Parameters](0_circuit/3_parameter.md) +- [Operations / Instructions](0_circuit/4_operation_instruction.md) +- [Standard Gates](0_circuit/5_gate_standard.md) +- [Unitary Gates](0_circuit/6_gate_unitary.md) +- [Multi-Controlled Gates](0_circuit/7_gate_mc_gate.md) +- [Circuit Gates](0_circuit/8_gate_circuit_gate.md) +- [Classical / Control Flow](0_circuit/9_classical_control_flow.md) +- [Symbolic Matrix](0_circuit/10_symbolic_matrix.md) +- [Ansatz](0_circuit/11_ansatz.md) +- [CFG](0_circuit/12_cfg.md) +- [Circuit to Matrix](0_circuit/13_circuit_to_matrix.md) +- [CircuitDag](0_circuit/14_circuit_dag.md) + +### Intermediate Representation (`cqlib_core::ir`) + +- [Overview](1_ir/0_overview.md) +- [QCIS](1_ir/1_qcis.md) +- [OpenQASM 2.0](1_ir/2_qasm2.md) +- [OpenQASM 3.0](1_ir/3_qasm3.md) + +### Device (`cqlib_core::device`) + +- [Overview](2_device/0_overview.md) +- [Topology](2_device/1_topology.md) +- [Device and noise properties](2_device/2_properties_device.md) +- [Layout](2_device/3_layout.md) +- [Noise](2_device/4_noise.md) +- [Result](2_device/5_result.md) +- [Qubit Identifiers](2_device/6_qubits.md) + +### Quantum Information (`cqlib_core::qis`) + +- [Overview](3_qis/0_overview.md) +- [Statevector](3_qis/1_statevector.md) +- [DensityMatrix](3_qis/2_density_matrix.md) +- [DensityMatrixNoise](3_qis/3_density_matrix_noise.md) +- [Stabilizer](3_qis/4_stabilizer.md) +- [ClassicalState](3_qis/5_classical_state.md) +- [Pauli](3_qis/6_pauli.md) +- [Hamiltonian](3_qis/7_hamiltonian.md) +- [Evolution](3_qis/8_evolution.md) +- [Metrics / Entropy](3_qis/9_metrics_entropy.md) + +### Compilation (`cqlib_core::compile`) + +- [Overview](4_compile/0_overview.md) +- [Compiler](4_compile/1_compiler.md) +- [Transform](4_compile/2_transform.md) +- [Layout](4_compile/3_layout.md) +- [Routing](4_compile/4_routing.md) +- [SABRE](4_compile/5_sabre.md) +- [Decompose / Resynthesis](4_compile/6_decompose_resynthesis.md) +- [Knowledge](4_compile/7_knowledge.md) +- [Resource](4_compile/8_resource.md) +- [Commutation](4_compile/9_commutation.md) + +### Visualization (`cqlib_core::visualization`) + +- [Overview](5_visualization/0_overview.md) +- [Text circuit diagrams](5_visualization/1_draw_text.md) +- [SVG circuit diagrams](5_visualization/2_draw_figure.md) +- [Quantum state visualization](5_visualization/3_state_plots.md) +- [Measurement result visualization](5_visualization/4_result_plots.md) +- [Write to file and inline display](5_visualization/5_render_to_file.md) +- [Visual IR](5_visualization/6_visual_ir.md) + +### Error Mitigation (`cqlib_core::error_mitigation`) + +- [Overview](6_error_mitigation/0_overview.md) +- [Zero-noise extrapolation (ZNE)](6_error_mitigation/1_zne.md) +- [Virtual distillation](6_error_mitigation/2_virtual_distillation.md) +- [Unified pipeline](6_error_mitigation/3_unified.md) diff --git a/docs/api/en/rust/1_ir/0_overview.md b/docs/api/en/rust/1_ir/0_overview.md new file mode 100644 index 00000000..5206b700 --- /dev/null +++ b/docs/api/en/rust/1_ir/0_overview.md @@ -0,0 +1,187 @@ +# Intermediate Representation + +`cqlib_core::ir` + +`cqlib_core::ir` is the intermediate representation module of Cqlib, providing two-way conversion between `Circuit` and three circuit text formats: QCIS, OpenQASM 2.0 and OpenQASM 3.0. The crate root exports four function aliases for each format; inside each format module the code is organized into separate files for parsing and dumping, and each holds an independent error type. + +## Overview + +The intermediate representation addresses the text form of circuits: a circuit travels between files, tools and services as text, while inside Cqlib it is uniformly represented by `Circuit`. Every format module under `cqlib_core::ir` provides both directions, "text in, `Circuit` out" and "`Circuit` in, text out", and the conversion does not modify the `Circuit` passed in. + +### Crate root aliases + +The crate root re-exports the four entry points of each format as `_`; they are the same items as the functions of the same name inside the format modules: + +| Action | QCIS | OpenQASM 2.0 | OpenQASM 3.0 | +| --- | --- | --- | --- | +| String → `Circuit` | `qcis_loads` | `qasm2_loads` | `qasm3_loads` | +| File → `Circuit` | `qcis_load` | `qasm2_load` | `qasm3_load` | +| `Circuit` → String | `qcis_dumps` | `qasm2_dumps` | `qasm3_dumps` | +| `Circuit` → File | `qcis_dump` | `qasm2_dump` | `qasm3_dump` | + +### Format modules + +Each format module (`cqlib_core::ir::qcis`, `cqlib_core::ir::qasm2`, `cqlib_core::ir::qasm3`) consists of two submodules, `load` and `dump`, which forward the functions of the same name and additionally provide `from_str` / `from_path` and `to_string` / `to_path`. `qasm2::ast` is a public abstract syntax tree module, where the parse output can be inspected separately. + +### Error types + +Parsing and dumping of each format has its own independent error enum; the crate provides no unified wrapper: + +| Format | Parse error | Dump error | +| --- | --- | --- | +| QCIS | `qcis::load::QcisParseError` | `qcis::dump::QcisDumpError` | +| OpenQASM 2.0 | `qasm2::load::QasmParseError` | `qasm2::dump::QasmDumpError` | +| OpenQASM 3.0 | `qasm3::load::Qasm3ParseError` | `qasm3::dump::Qasm3DumpError` | + +When I/O fails, the file entry points return an `IoError` variant carrying the original `std::io::Error`; the original error can be retrieved layer by layer with `Error::source`. + +### Round-trip conversion and boundaries + +The circuit obtained from `loads(dumps(circuit))` is equivalent to the input circuit in its operation sequence; this is the round-trip conversion. The expressive power of the text formats is limited, however: + +- QCIS accepts only gates within its native gate set; if the circuit contains other gates, they must first be decomposed into that gate set before dumping. Classical storage (`store`) and classical control are outside the expressive scope of QCIS; measurement and barriers can be expressed, and symbolic gate parameters are written out verbatim as symbolic text. +- When dumping fails, the enum indicates the specific reason, for example an unsupported gate, a classical operation that cannot be expressed, or an invalid delay tick. + +### Relation to compilation + +Dumping is an optional write-to-file form for compilation output: once a circuit has been lowered to the target gate set, it can be written out in the corresponding format. Format differences appear only in the gate set and the text header; the interface on the `Circuit` side stays unchanged, so a circuit can also be read in one format and written out in another, completing a format conversion. + +--- + +## Common entry points + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::ir::{qcis_dumps, qcis_loads}; + +let mut circuit = Circuit::new(2); +circuit.h(Qubit::new(0)).unwrap(); +circuit.cz(Qubit::new(0), Qubit::new(1)).unwrap(); + +let qcis = qcis_dumps(&circuit).unwrap(); +let parsed = qcis_loads(&qcis).unwrap(); + +assert_eq!(parsed.num_qubits(), 2); +``` + +--- + +## Core concepts and terms + +| Term | Description | +| --- | --- | +| **Intermediate representation** | The representation layer between `Circuit` and circuit text formats, provided by the format modules under `cqlib_core::ir`. | +| **Parsing** | The direction from text to `Circuit`, corresponding to `loads` (string) and `load` (file). | +| **Dumping** | The direction from `Circuit` to text, corresponding to `dumps` (string) and `dump` (file). | +| **Round-trip conversion** | Dumping and then parsing back to an equivalent circuit, that is `loads(dumps(circuit))`. | +| **Format conversion** | Reading in one format and writing out another, for example writing OpenQASM 2.0 text as QCIS text. | +| **Native gate set** | The gate set defined by the text format itself; on dumping, every gate in the circuit must fall within that set. | + +--- + +## `cqlib_core::ir` API overview + +### Crate root aliases + +| Name | Description | +| --- | --- | +| [`qcis_loads`](1_qcis.md) / [`qcis_load`](1_qcis.md) | Parse a `Circuit` from a QCIS string or file. | +| [`qcis_dumps`](1_qcis.md) / [`qcis_dump`](1_qcis.md) | Dump a `Circuit` to a QCIS string or file. | +| [`qasm2_loads`](2_qasm2.md) / [`qasm2_load`](2_qasm2.md) | Parse a `Circuit` from an OpenQASM 2.0 string or file. | +| [`qasm2_dumps`](2_qasm2.md) / [`qasm2_dump`](2_qasm2.md) | Dump a `Circuit` to an OpenQASM 2.0 string or file. | +| [`qasm3_loads`](3_qasm3.md) / [`qasm3_load`](3_qasm3.md) | Parse a `Circuit` from an OpenQASM 3.0 string or file. | +| [`qasm3_dumps`](3_qasm3.md) / [`qasm3_dump`](3_qasm3.md) | Dump a `Circuit` to an OpenQASM 3.0 string or file. | + +### Format modules + +| Name | Description | +| --- | --- | +| [`cqlib_core::ir::qcis`](1_qcis.md) | The QCIS format, with `load` / `dump` submodules and auxiliary entry points such as `from_str` and `to_string`. | +| [`cqlib_core::ir::qasm2`](2_qasm2.md) | The OpenQASM 2.0 format, which additionally has a public `ast` submodule. | +| [`cqlib_core::ir::qasm3`](3_qasm3.md) | The OpenQASM 3.0 format. | + +### Error types + +| Name | Description | +| --- | --- | +| [`QcisParseError`](1_qcis.md) / [`QcisDumpError`](1_qcis.md) | QCIS parse and dump errors. | +| [`QasmParseError`](2_qasm2.md) / [`QasmDumpError`](2_qasm2.md) | OpenQASM 2.0 parse and dump errors. | +| [`Qasm3ParseError`](3_qasm3.md) / [`Qasm3DumpError`](3_qasm3.md) | OpenQASM 3.0 parse and dump errors. | + +--- + +## Quick examples + +### 1. Parse QCIS text and write it back + +```rust +use cqlib_core::ir::{qcis_dumps, qcis_loads}; + +let qcis = r#" + RX Q0 1.0 + RY Q1 pi/2 + X Q0 + CZ Q0 Q1 + "#; + +let circuit = qcis_loads(qcis).unwrap(); +assert_eq!(circuit.num_qubits(), 2); +``` + +### 2. The exact text of a dump result + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::ir::qcis_dumps; + +let mut c = Circuit::new(2); +let q0 = Qubit::new(0); +let q1 = Qubit::new(1); + +c.x(q0).unwrap(); +c.y(q1).unwrap(); +c.z(q0).unwrap(); +c.h(q1).unwrap(); + +let qcis = qcis_dumps(&c).unwrap(); +assert_eq!(qcis, "X Q0\nY Q1\nZ Q0\nH Q1\n"); +``` + +### 3. Write to a file and inspect the error source + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::ir::qcis::dump::QcisDumpError; +use cqlib_core::ir::qcis_dump; +use std::error::Error; + +let mut c = Circuit::new(1); +c.h(Qubit::new(0)).unwrap(); + +let path = std::env::temp_dir().join("cqlib_ir_overview.qcis"); +qcis_dump(&c, &path).unwrap(); + +let missing = path.join("out.qcis"); +let error = qcis_dump(&c, missing).unwrap_err(); +assert!(matches!(error, QcisDumpError::IoError(_))); +assert!(error.source().is_some()); +``` + +--- + +## Validation and error handling + +Parse errors and dump errors are defined separately; file failures are represented by the `IoError` variant, and the remaining variants are content-level causes. + +| Error | When it occurs | +| --- | --- | +| `QcisParseError::IoError` | `qcis_load` failed to read the file. | +| `QcisParseError::{InvalidQubitFormat, InvalidQubitId, QubitCountMismatch, ParameterCountMismatch, MissingParameter, InvalidParameter, UnknownGate, EmptyLine}` | Qubit notation in the QCIS text, an unknown gate, a mismatch in gate arity or parameter count, and empty lines or no valid content. | +| `QcisDumpError::{UnsupportedGate, UnsupportedClassicalData, UnsupportedClassicalControl, SymbolicParameter, InvalidDelayParameter}` | A gate, classical data, classical control, symbolic parameter or invalid delay parameter that cannot be expressed when dumping QCIS. | +| `QasmParseError::{ParseError, ConversionError, UndefinedQubit, UndefinedRegister, UndefinedGate, ReservedGateName, InvalidArgument, MismatchedQubitCount, MismatchedParameterCount, RecursionLimitExceeded, EvaluationError, CircularGateDependency, UnsupportedVersion, DuplicateDeclaration, IncludeCycle, UnsupportedOpaqueGate}` | OpenQASM 2.0 syntax, semantic, gate definition and version declaration problems. | +| `QasmDumpError::{FormatError, MeasureInGateNotAllowed, ResetInGateNotAllowed, InvalidQubitIndex, UnsupportedClassicalType, UnsupportedClassicalData, UnsupportedClassicalControl, ConflictingGateDefinition}` | Invalid generated content, measurement or reset inside a gate definition, an invalid qubit index, and classical types or duplicate custom gate names that cannot be expressed when dumping OpenQASM 2.0. | +| `Qasm3ParseError::{ParseError, SemanticError, ConversionError, UnsupportedFeature, UndefinedSymbol, UndefinedGate, TypeError, InvalidArgument, MismatchedQubitCount, MismatchedParameterCount, RecursionLimitExceeded, CircularGateDependency}` | OpenQASM 3.0 syntax, semantics, symbol resolution, type checking and lowering failures. | +| `Qasm3DumpError::{FormatError, UnsupportedInstruction, UnsupportedClassicalData, UnsupportedClassicalControl, MeasureInGateNotAllowed, ConflictingGateDefinition}` | Instructions, classical data, classical control and duplicate custom gate names that cannot be expressed when dumping OpenQASM 3.0. | +| `*DumpError::IoError` / `*ParseError::IoError` | File write or read failure; the original I/O error can be retrieved with `Error::source`. | + +Before dumping, the circuit must already fall within the gate set of the target format; otherwise perform gate decomposition first and then dump. diff --git a/docs/api/en/rust/1_ir/1_qcis.md b/docs/api/en/rust/1_ir/1_qcis.md new file mode 100644 index 00000000..4afe947f --- /dev/null +++ b/docs/api/en/rust/1_ir/1_qcis.md @@ -0,0 +1,97 @@ +# QCIS API + +`cqlib_core::ir` provides QCIS import and export interfaces for converting between `Circuit` and QCIS text or files. + +## Import + +```rust +use cqlib_core::ir::{qcis_load, qcis_loads, qcis_dump, qcis_dumps}; +``` + +## Interface overview + +- `qcis_load(path) -> Result` +- `qcis_loads(qcis: &str) -> Result` +- `qcis_dump(circuit: &Circuit, path) -> Result<(), QcisDumpError>` +- `qcis_dumps(circuit: &Circuit) -> Result` + +The path parameter of the file entry points is generic over `P: AsRef`; `&str`, `PathBuf` and `&Path` can all be passed directly. + +### Entry points inside the format module + +The `load` and `dump` submodules under `cqlib_core::ir::qcis` forward the functions of the same name and re-export a set of equivalent Rust-style entry points at module level: + +- `cqlib_core::ir::qcis::loads` / `cqlib_core::ir::qcis::from_str` +- `cqlib_core::ir::qcis::load` / `cqlib_core::ir::qcis::from_path` +- `cqlib_core::ir::qcis::dumps` / `cqlib_core::ir::qcis::to_string` +- `cqlib_core::ir::qcis::dump` / `cqlib_core::ir::qcis::to_path` + +## Parsing (load / loads) + +### `qcis_loads` + +Parse a `Circuit` from a QCIS string; returns `QcisParseError` on failure. + +Error types: + +- `IoError`: filesystem or I/O failure, never produced by the string entry point. +- `InvalidQubitFormat`: the qubit notation is not `Q`. +- `InvalidQubitId`: the number after `Q` cannot be parsed. +- `QubitCountMismatch`: the number of qubits of a gate does not match the specification. +- `ParameterCountMismatch`: the number of parameters of a gate does not match the specification. +- `MissingParameter`: the parameter expression is empty or cannot be parsed. +- `InvalidParameter`: the delay tick is not a fixed non-negative integer. +- `UnknownGate`: the gate name is not in the QCIS instruction set. +- `EmptyLine`: empty line or no valid content. + +### `qcis_load` + +Read from a file and parse, returning `Result`: a file read failure is `IoError`, and a content parse failure is the corresponding variant. + +## Dumping (dump / dumps) + +### `qcis_dumps` + +Dump a circuit to a QCIS string; returns `QcisDumpError` on failure. + +Error types: + +- `UnsupportedGate`: the circuit contains gates outside the QCIS native gate set, for example the standard identity gate, global phase, multi-controlled gates, custom gates and unitary gates. +- `UnsupportedClassicalData`: classical storage (`store`) cannot be expressed. +- `UnsupportedClassicalControl`: classical control flow cannot be expressed. +- `SymbolicParameter`: the parameter index does not resolve to a corresponding parameter, or the delay tick cannot be evaluated. +- `InvalidDelayParameter`: the tick of a delay instruction is not a non-negative integer. +- `IoError`: failed to write to the buffer. + +`M` (measurement) and `B` (barrier) can be dumped; `Reset` cannot. + +### `qcis_dump` + +Dump and write to a file, returning `Result<(), QcisDumpError>`; a file write failure is `IoError`. + +## QCIS native gate restrictions + +QCIS parsing and dumping support only the native gate set: + +- Single-qubit gates: `H`, `S`, `SD`, `T`, `TD`, `X`, `X2P`, `X2M`, `Y`, `Y2P`, `Y2M`, `Z` +- Parameterized single-qubit gates: `RX`, `RXY`, `RY`, `RZ`, `U`, `XY`, `XY2P`, `XY2M`, `PHASE` +- Multi-qubit gates: `RXX`, `RYY`, `RZX`, `RZZ`, `SWAP`, `CX`, `CCX`, `CY`, `CZ`, `CRX`, `CRY`, `CRZ`, `FSIM` + +Besides gates, QCIS text also expresses measurement (`M`), barrier (`B`) and delay (`I Qn t`, where `t` is the number of delay ticks in units of 0.5 ns and must be a non-negative integer). + +When parsing, `SDG`, `TDG` and `Barrier` are accepted as equivalent spellings of `SD`, `TD` and `B` respectively; when dumping, they are uniformly written as `SD`, `TD` and `B`. The standard identity gate and global phase are not in the native gate set, and `I` in QCIS always means delay. When an inexpressible gate is encountered, decompose it into the QCIS gate set first and then dump. + +## Minimal example + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::ir::{qcis_dumps, qcis_loads}; + +let mut c = Circuit::new(2); +c.h(Qubit::new(0)).unwrap(); +c.cz(Qubit::new(0), Qubit::new(1)).unwrap(); + +let text = qcis_dumps(&c).unwrap(); +let c2 = qcis_loads(&text).unwrap(); +assert_eq!(c2.num_qubits(), 2); +``` diff --git a/docs/api/en/rust/1_ir/2_qasm2.md b/docs/api/en/rust/1_ir/2_qasm2.md new file mode 100644 index 00000000..1c5051ba --- /dev/null +++ b/docs/api/en/rust/1_ir/2_qasm2.md @@ -0,0 +1,117 @@ +# OpenQASM 2.0 API + +`cqlib_core::ir` provides OpenQASM 2.0 import and export interfaces. + +## Import + +```rust +use cqlib_core::ir::{qasm2_load, qasm2_loads, qasm2_dump, qasm2_dumps}; +``` + +## Interface overview + +- `qasm2_load(path) -> Result` +- `qasm2_loads(source: &str) -> Result` +- `qasm2_dump(circuit: &Circuit, path) -> Result<(), QasmDumpError>` +- `qasm2_dumps(circuit: &Circuit) -> Result` + +The path parameter of the file entry points is generic over `P: AsRef`; `&str`, `PathBuf` and `&Path` can all be passed directly. + +### Entry points inside the format module + +The `load` and `dump` submodules under `cqlib_core::ir::qasm2` forward the functions of the same name and re-export a set of equivalent Rust-style entry points at module level: + +- `cqlib_core::ir::qasm2::loads` / `cqlib_core::ir::qasm2::from_str` +- `cqlib_core::ir::qasm2::load` / `cqlib_core::ir::qasm2::from_path` +- `cqlib_core::ir::qasm2::dumps` / `cqlib_core::ir::qasm2::to_string` +- `cqlib_core::ir::qasm2::dump` / `cqlib_core::ir::qasm2::to_path` + +`cqlib_core::ir::qasm2::ast` is a public abstract syntax tree module, where the statement, expression and parameter-reference types of the parse output are defined. + +## Parsing (load / loads) + +### `qasm2_loads` + +Parse a circuit from a QASM string. + +### `qasm2_load` + +Read from a file and parse a circuit. + +Both return `QasmParseError` on failure; the variants include: + +- `IoError`: filesystem or I/O failure. +- `ParseError`: syntax error. +- `ConversionError`: conversion from the AST to `Circuit` failed. +- `UndefinedQubit`: an undefined quantum register or qubit is referenced. +- `UndefinedRegister`: an undefined classical register is referenced. +- `UndefinedGate`: an undefined gate is referenced. +- `ReservedGateName`: an attempt to redefine a gate name declared by qelib1. +- `InvalidArgument`: the parameter format or usage is invalid. +- `MismatchedQubitCount` / `MismatchedParameterCount`: the number of qubits or parameters of a gate call does not match. +- `RecursionLimitExceeded` / `CircularGateDependency`: gate expansion is too deep or gates depend on each other in a cycle. +- `EvaluationError`: evaluation of a parameter expression failed. +- `UnsupportedVersion`: the version declaration is not 2.0. +- `DuplicateDeclaration`: duplicate declaration or naming conflict. +- `IncludeCycle`: `include` forms a cycle. +- `UnsupportedOpaqueGate`: an `opaque` gate is called and cannot be represented in `Circuit`. + +## Dumping (dump / dumps) + +### `qasm2_dumps` + +Dump a circuit to a QASM string; returns `QasmDumpError` on failure. The output contains an auto-generated comment, the `OPENQASM 2.0;` and `include "qelib1.inc";` header, and `qreg` / `creg` declarations. + +Error types: + +- `FormatError`: content generation failed. +- `MeasureInGateNotAllowed` / `ResetInGateNotAllowed`: measurement or reset appears inside a `gate` definition body. +- `InvalidQubitIndex`: the qubit index is invalid. +- `UnsupportedClassicalType`: the classical type cannot be represented as a `creg`. +- `UnsupportedClassicalData` / `UnsupportedClassicalControl`: a classical data operation or classical control cannot be expressed. +- `ConflictingGateDefinition`: different custom gates use the same OpenQASM gate name. +- `IoError`: write failure. + +### `qasm2_dump` + +Write the dump result to a file, returning `Result<(), QasmDumpError>`; a file write failure is `IoError`. + +## Supported features (overview) + +According to the current implementation, QASM2 import and export support: + +- Standard gates and parameterized gates, including `ch/cu1/cu3` and `sx/sxdg/crx/cry/crz/rxx/ryy/rzz/fsim` +- Instructions such as measurement, barrier and reset +- `qreg`, `creg` and `opaque` declarations, and the conditional statement `if (creg == integer) qop;` +- Export of `CircuitGate` definitions; a unitary gate that carries only a matrix is dumped as an `opaque` declaration +- Extension gates not covered directly are emitted as `gate` definitions (for example `crx/cry/rzz/rxx/ryy/rzx`) +- `include "qelib1.inc";` is treated as a built-in include; the file need not exist locally + +## Limitations + +Dumping makes trade-offs according to the expressive power of OpenQASM 2.0; the following constructs raise an error: + +- `else` branches, loops, `switch` and nested control flow in classical control +- Classical types that cannot be represented as `creg`, for example `bool` and unsigned integers +- Measurement and reset inside a `gate` definition body +- Bit-wise conditions (for example `if (c[0] == 1)`) and conditional barriers + +On the loading side, `loads` does not resolve external files brought in by `include`; only `load` resolves them relative to the directory of the file. `rzx` appears only in the extension gate definitions on the dumping side and is not accepted on loading; global phase is not written as a statement but emitted only as a comment, and is not restored on reload. + +## Minimal example + +```rust +use cqlib_core::ir::{qasm2_dumps, qasm2_loads}; + +let qasm = r#" +OPENQASM 2.0; +include "qelib1.inc"; +qreg q[2]; +h q[0]; +cx q[0],q[1]; +"#; + +let c = qasm2_loads(qasm).unwrap(); +let out = qasm2_dumps(&c).unwrap(); +assert!(out.contains("OPENQASM 2.0;")); +``` diff --git a/docs/api/en/rust/1_ir/3_qasm3.md b/docs/api/en/rust/1_ir/3_qasm3.md new file mode 100644 index 00000000..8bc15b95 --- /dev/null +++ b/docs/api/en/rust/1_ir/3_qasm3.md @@ -0,0 +1,163 @@ +# OpenQASM 3.0 API + +`cqlib_core::ir::qasm3` provides OpenQASM 3.0 import and export interfaces for converting between `Circuit` and OpenQASM 3.0 text or files. + +## Import + +```rust +use cqlib_core::ir::{qasm3_load, qasm3_loads, qasm3_dump, qasm3_dumps}; +``` + +## Interface overview + +- `qasm3_load(path: impl AsRef) -> Result` +- `qasm3_loads(source: &str) -> Result` +- `qasm3_dump(circuit: &Circuit, path: impl AsRef) -> Result<(), Qasm3DumpError>` +- `qasm3_dumps(circuit: &Circuit) -> Result` + +The `qasm3` module additionally provides aliases with identical behavior: `from_path`, `from_str`, `to_path`, `to_string`. + +## Parsing (load / loads) + +### `qasm3_loads` + +Parse a circuit from an OpenQASM 3.0 string. + +### `qasm3_load` + +Read from a file and parse a circuit. A relative-path `include` is resolved from the directory of the input file. + +Both return `Qasm3ParseError` on failure; the common variants: + +- `IoError` +- `ParseError` +- `SemanticError` +- `ConversionError` +- `UnsupportedFeature` +- `UndefinedSymbol` +- `UndefinedGate` +- `TypeError` +- `InvalidArgument` +- `MismatchedQubitCount` +- `MismatchedParameterCount` +- `RecursionLimitExceeded` +- `CircularGateDependency` + +Among them, `IoError` retains the underlying I/O error, which can be read through `source()`. + +## Dumping (dump / dumps) + +### `qasm3_dumps` + +Dump a circuit to an OpenQASM 3.0 string. The output consists, in order, of `OPENQASM 3.0;`, `include "stdgates.inc";`, extension gate definitions, custom gate definitions, quantum and classical declarations, and the operations of the main circuit. + +### `qasm3_dump` + +Write the dump result to a file, returning `Result<(), Qasm3DumpError>`. + +Both return `Qasm3DumpError` on failure; the common variants: + +- `IoError`: file read or write failure +- `FormatError`: formatting failure +- `UnsupportedInstruction`: instructions that cannot be represented, such as `delay`, `XY`, `RXY` +- `UnsupportedClassicalData`: classical storage that cannot be represented +- `UnsupportedClassicalControl`: control flow that cannot be represented, such as `while`, `for`, `break` / `continue` +- `MeasureInGateNotAllowed`: measurement is contained in a gate definition body +- `ConflictingGateDefinition`: conflicting signatures of gate definitions with the same name + +## Supported features (overview) + +According to the current implementation, OpenQASM 3.0 import and export support: + +- The header `OPENQASM 3;` and `OPENQASM 3.0;`, and `include "stdgates.inc";` (a built-in standard library is used; no file on disk is required) +- Scalar qubit and one-dimensional qubit register declarations +- Classical declarations `bit`, `bit[n]`, `bool`, `uint[n]`, and angle and float input declarations +- Standard gates mapped to `StandardGate` and Cqlib extension gates +- Custom gate definitions and calls +- Measurement, `reset`, `barrier`, global phase +- Control flow `if` / `else`, static `for`, constant-branch `switch` + +## Differences from the OpenQASM 2.0 interface + +Built on the same `Circuit` representation, the main differences between this interface and the OpenQASM 2.0 interface: + +| Aspect | OpenQASM 2.0 interface | OpenQASM 3.0 interface | +|---|---|---| +| Header and standard library | `OPENQASM 2.0;`, `include "qelib1.inc";` | `OPENQASM 3;` or `OPENQASM 3.0;`, `include "stdgates.inc";` | +| Quantum declarations | `qreg q[2];` | `qubit[2] q;`, and a scalar `qubit q;` is supported | +| Classical declarations | `creg c[2];` | `bit`, `bit[n]`, `bool`, `uint[n]` | +| Measurement | `measure q[0] -> c[0];` | `c[0] = measure q[0];`, `c = measure q;` | +| Control flow | `if (c == 1) ...` followed by a single statement | `if` / `else` blocks, static `for`, constant-branch `switch` | +| Global phase | Given only as a comment when dumping; cannot be loaded | `gphase(theta);`, can be loaded and dumped | +| Input declarations | None | Angle and float `input` declarations | + +## Limitations + +The following OpenQASM 3.0 features are not supported in the current implementation; a clear error is given when they are encountered, with no partial lowering: + +- Timing and pulses: `delay`, `box`, `cal`, `defcal` +- Subroutines and external calls: `def` subroutines, `extern` +- Hardware-related: hardware qubits, `output` declarations, and `input` declarations other than angle and float +- Other statements: `alias`, `pragma`, annotations, legacy declarations +- Gate modifiers: `negctrl`, `pow`; `ctrl` and `inv` are available only when a corresponding implementation exists for the target gate +- Some standard gates: `ch`, `cp`, `cu`, `cswap` +- Control flow: a lowering path for `while` exists in the implementation, but a loop with a runtime condition is currently rejected by the semantic check; `for` supports only the statically expanded form with a constant range +- Complex classical arithmetic, complex lvalue slicing, multi-dimensional indexing +- Qubit declarations inside a scope + +The dumping side has additional constraints: + +- `delay`, matrix-only `UnitaryGate`, an ordinary assignment that writes to a scalar `bit` variable, and measurement inside a gate definition body are not dumped +- Control flow such as `while`, `for`, `break` and `continue` is not dumped +- The two standard gates `XY` and `RXY` are not dumped + +## Round-trip behavior + +The dump result can be loaded again by this interface. Canonicalization rules: + +- The header is fixed to `OPENQASM 3.0;` and `include "stdgates.inc";`, regardless of how the header was written in the input. +- Top-level qubits are uniformly dumped as `qubit[n] q`; the single-qubit case is likewise written as `qubit[1] q`. +- User classical variables are named `c0`, `c1`, … and immutable measurement values are named `v0`, `v1`, … +- When a measurement is immediately followed by a compatible store, it is folded into the assignment form, for example `c0 = measure q;`. +- A standalone measurement is written into an auto-generated `bit[k] meas` register, for example `meas[0] = measure q[0];`. +- A non-contiguous or reordered register measurement is split into bit-wise assignments, for example `c0[0] = measure q[2];`. +- Gates already provided by `stdgates.inc` are called directly; an extension gate generates a `gate` definition once before the main circuit, and its name is still used at the call site. +- Global phase is dumped as a `gphase(...);` statement and enters the global phase of the circuit after reloading. +- Auto-generated names avoid the gate names and register names already present in the circuit. + +## Minimal example + +```rust +use cqlib_core::ir::{qasm3_dumps, qasm3_loads}; + +let qasm = r#" +OPENQASM 3; +include "stdgates.inc"; +qubit[2] q; +h q[0]; +cx q[0], q[1]; +"#; + +let c = qasm3_loads(qasm).unwrap(); +let out = qasm3_dumps(&c).unwrap(); +assert!(out.contains("OPENQASM 3.0;")); +``` + +Extension gate definitions and round trip: + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::ir::{qasm3_dumps, qasm3_loads}; + +let q0 = Qubit::new(0); +let q1 = Qubit::new(1); +let mut circuit = Circuit::new(2); +circuit.x2p(q0).unwrap(); +circuit.rzz(q0, q1, 0.25).unwrap(); + +let qasm = qasm3_dumps(&circuit).unwrap(); +assert!(qasm.contains("gate x2p q { rx(pi/2) q; }")); + +let round_trip = qasm3_loads(&qasm).unwrap(); +assert_eq!(round_trip.operations().len(), 2); +``` diff --git a/docs/api/en/rust/2_device/0_overview.md b/docs/api/en/rust/2_device/0_overview.md new file mode 100644 index 00000000..fb8a8c7e --- /dev/null +++ b/docs/api/en/rust/2_device/0_overview.md @@ -0,0 +1,248 @@ +# Device + +`cqlib_core::device` + +`cqlib_core::device` describes the static information of a quantum device and the state of task execution: the topology formed by qubits and couplings, the calibration properties of qubits and edges, the layout from logical qubits to physical qubits, the noise model, and execution results and status. + +## Overview + +The device module splits hardware-related data into several mutually independent groups of objects: topology describes only connectivity, properties describe only calibration data, layout describes only the mapping, and the noise model describes only channels. Compilation and execution each take what they need, without being coupled to one another. + +### Topology + +`Topology` consists of a physical qubit list and a coupling list and maintains a directed graph internally; `graph()` retrieves a reference to the underlying graph. Couplings are directed: a coupling on `(control, target)` does not mean the reverse direction has one too, so connectivity queries fall into two kinds, `successors` / `predecessors` (directed) and `neighbors_undirected` (undirected), and degree queries are likewise split into `out_degree` / `in_degree`. A coupling may carry a name label, used for identification only. + +### Device and properties + +`Device` layers device-level defaults and local overrides on top of a topology, and records the device name, registered qubits, unusable qubits, native gate set and calibration time. Most device-level configuration items have two forms: `with_*` consumes and returns `self`, for chained construction, while `set_*` modifies in place. Qubit-level properties are described by `QubitProp` (`t1`, `t2`, `frequency`, readout error and readout confusion probabilities), and edge-level properties by `EdgeProp`; both hold a set of `InstructionProp` (instruction, error rate and optional duration). + +When querying the properties of a qubit, the local value takes precedence and the device-level default is used as a fallback when nothing is set: `get_t1` first looks at the `t1` of that qubit and returns `default_t1` if none is available. The device also provides factory methods such as `line`, `line_from_qubits`, `bidirectional_line`, `ring`, `star`, `grid` and `from_edges`, which directly generate a topology and a qubit set in common shapes. + +### Native instruction capability + +The device-level `native_gates` is the global default capability; if a qubit or a directed edge has configured its own instruction list through `with_native_instruction` / `set_native_instruction`, that list is a complete override of the capability there and does not write back to the default. Calibration data only attaches error rates and durations to supported instructions; it does not turn an unsupported instruction into a supported one. Native capability is validated before being written, and a rejected update leaves the original capability unchanged. `validate_operation`, `validate_value_operation` and `validate_circuit` use the same set of rules to check whether a circuit can be executed on the device. + +### Layout + +`Layout` maintains the mapping from logical qubits to physical qubits; a physical qubit that carries no logical qubit is a vacant physical qubit. The mapping can be read as a whole through `l2p_map` / `p2l_map` or queried for a single qubit; `swap_physical` swaps the logical qubits carried at two physical positions, which is the basic operation for moving logical qubits during routing. + +### Noise model + +`NoiseModel` registers noise channels by qubit and operation: readout error is represented by `ReadoutError`, single-qubit channels are given by variants of `SingleQubitNoise` (bit flip, phase flip, Pauli, depolarizing, amplitude damping, phase damping), and two-qubit channels are given by `TwoQubitNoise` (depolarizing, independent channel combination, correlated Pauli). A channel can be converted into Kraus operators with `to_kraus`, for use in density matrix simulation. Queries specify the gate and qubit combination with `OperationKey`. + +### Execution result + +`ExecutionResult` records the lifecycle of one task: after construction with `new` it is in `Status::Queued`, after `start` it enters the running state, `finish` / `fail` / `cancel` end the task, and `from_counts` can construct an already completed result directly from measurement counts. A measurement result is represented by `Outcome`, which packs the bitstring into 64-bit blocks and supports both position-wise bit access and restoration of the bitstring; `counts` and `probabilities` are the measurement count and the normalized probability of each outcome respectively. + +--- + +## Common entry points + +```rust +use std::collections::HashSet; + +use cqlib_core::device::{Device, PhysicalQubit, QubitProp, Topology}; + +let q0 = PhysicalQubit::new(0); +let q1 = PhysicalQubit::new(1); +let topology = Topology::new(vec![q0, q1], vec![(q0, q1, "CX".to_string())]).unwrap(); + +let mut device = Device::new("demo".to_string(), HashSet::from_iter([q0, q1]), topology) + .unwrap() + .with_default_t1(40.0); + +device.add_qubit_properties(q0, QubitProp::new(0.02).with_t1(60.0)).unwrap(); + +assert_eq!(device.get_t1(q0), Some(60.0)); // 局部属性 +assert_eq!(device.get_t1(q1), Some(40.0)); // 回退到设备级默认值 +``` + +--- + +## Core concepts and terms + +| Term | Description | +| --- | --- | +| **Physical qubit** | A physical position on the device, represented by `PhysicalQubit`. | +| **Logical qubit** | A wire in a circuit, represented by `LogicalQubit`; even with the same index it is distinguished from a physical qubit. | +| **Topology** | The directed graph formed by physical qubits and the couplings between them, represented by `Topology`. | +| **Coupling** | A connection between two physical qubits over which a two-qubit gate can be executed. | +| **Directed coupling** | A coupling with a direction: a coupling on `(control, target)` does not imply that the reverse direction has one too. | +| **Native instruction** | An instruction that can be executed directly on the device; the device-level set is the default capability, and qubits and edges can each override it. | +| **Local override** | The native instruction list of a qubit or edge; once non-empty it completely replaces the device-level default capability and does not write back to the default. | +| **Default fallback** | When querying a qubit property, the device-level default is returned if that qubit has no local value. | +| **Initial layout** | The initial mapping from logical qubits to physical qubits, represented by `Layout`. | +| **Vacant physical qubit** | A physical qubit that currently carries no logical qubit. | +| **Noise channel** | A set of operators describing one kind of noise action, convertible into Kraus operators with `to_kraus`. | +| **Readout error** | The probability of reading `\|0>` as 1 or `\|1>` as 0 during measurement, given by the two fields `p_0_given_1` and `p_1_given_0` of `ReadoutError`. | +| **Measurement outcome** | The bitstring obtained from one sampling, represented by `Outcome`. | + +--- + +## `cqlib_core::device` API overview + +### Topology + +| Name | Description | +| --- | --- | +| [`Topology`](1_topology.md) | A directed graph formed by physical qubits and a coupling list, with add, remove and connectivity queries. | +| [`Topology::line`](1_topology.md) | A constructor that builds a topology in a line shape. | + +### Device and properties + +| Name | Description | +| --- | --- | +| [`Device`](2_properties_device.md) | The device object: topology, qubit set, native gate set, calibration time and default properties. | +| [`Device::line`](2_properties_device.md) / [`Device::ring`](2_properties_device.md) / [`Device::star`](2_properties_device.md) / [`Device::grid`](2_properties_device.md) | Factory methods that build a device in common shapes. | +| [`QubitProp`](2_properties_device.md) | Qubit properties: readout error, confusion probability, `t1` / `t2`, frequency and native instruction properties. | +| [`EdgeProp`](2_properties_device.md) | Edge properties: the native instruction properties on that directed edge. | +| [`InstructionProp`](2_properties_device.md) | Instruction properties: the instruction object, error rate and optional duration. | +| [`Device::validate_circuit`](2_properties_device.md) / [`Device::validate_operation`](2_properties_device.md) | Validate a circuit or a single operation against the native capability and the topology. | + +### Layout + +| Name | Description | +| --- | --- | +| [`Layout`](3_layout.md) | The mapping from logical qubits to physical qubits, with binding, unbinding and physical qubit swap. | +| [`Layout::from_pairs`](3_layout.md) | Build a layout from `(logical index, physical index)` pairs and the total number of physical qubits. | +| [`LogicalQubit`](3_layout.md) / [`PhysicalQubit`](3_layout.md) | The logical qubit and physical qubit types, whose `Display` forms are `L0` and `P11` respectively. | + +### Noise model + +| Name | Description | +| --- | --- | +| [`NoiseModel`](4_noise.md) | A container that registers noise channels by qubit and operation. | +| [`SingleQubitNoise`](4_noise.md) | The single-qubit noise channel enum, with bit flip, phase flip, Pauli, depolarizing and two kinds of damping. | +| [`TwoQubitNoise`](4_noise.md) | The two-qubit noise channel enum, with depolarizing, independent combination and correlated Pauli. | +| [`ReadoutError`](4_noise.md) | Readout error, recording `P(0\|1)` and `P(1\|0)` separately. | +| [`OperationKey`](4_noise.md) | The key for noise queries, formed by combining a gate and qubit indices. | + +### Execution result + +| Name | Description | +| --- | --- | +| [`ExecutionResult`](5_result.md) | The result of a task execution, with counts, probabilities and timestamps. | +| [`Status`](5_result.md) | Task status: `Queued`, `Running`, `Completed`, `Failed` and `Cancelled`. | +| [`Outcome`](5_result.md) | A compact measurement result bitstring, supporting bit-wise access and restoration. | + +--- + +## Quick examples + +### 1. Build a device and configure properties + +```rust +use std::collections::HashSet; + +use cqlib_core::device::{Device, PhysicalQubit, QubitProp, Topology}; + +let device = Device::line("line", 3).unwrap(); +assert_eq!(device.name(), "line"); +assert_eq!(device.qubits().count(), 3); +assert_eq!(device.num_usable_qubits(), 3); +assert!(device + .topology() + .supports_directed_coupling(PhysicalQubit::new(0), PhysicalQubit::new(1))); +assert!(!device + .topology() + .supports_directed_coupling(PhysicalQubit::new(1), PhysicalQubit::new(0))); + +let q0 = PhysicalQubit::new(0); +let q1 = PhysicalQubit::new(1); +let topology = Topology::new(vec![q0, q1], vec![(q0, q1, "CX".to_string())]).unwrap(); +let mut device = Device::new("demo".to_string(), HashSet::from_iter([q0, q1]), topology) + .unwrap() + .with_default_t1(40.0) + .with_default_t2(20.0); + +device + .add_qubit_properties(q0, QubitProp::new(0.02).with_t1(60.0).with_t2(30.0)) + .unwrap(); + +assert_eq!(device.get_t1(q0), Some(60.0)); +assert_eq!(device.get_readout_error(q0), Some(0.02)); +``` + +### 2. Create a layout + +```rust +use cqlib_core::device::{Layout, LogicalQubit, PhysicalQubit}; + +let logical = vec![LogicalQubit::new(0), LogicalQubit::new(1)]; +let physical = vec![ + PhysicalQubit::new(100), + PhysicalQubit::new(101), + PhysicalQubit::new(102), +]; + +let layout = Layout::new(logical, physical, None).unwrap(); + +assert_eq!(layout.num_logical(), 2); +assert_eq!(layout.num_physical(), 3); +assert_eq!(layout.num_vacant_physical(), 1); +assert_eq!( + layout.vacant_physical_qubits().collect::>(), + vec![PhysicalQubit::new(102)] +); +``` + +A layout can also be constructed directly from qubit pairs: + +```rust +use cqlib_core::device::{Layout, LogicalQubit, PhysicalQubit}; + +let layout = Layout::from_pairs(&[(2, 3), (0, 1)], 5).unwrap(); + +assert_eq!(layout.num_vacant_physical(), 3); +assert_eq!(layout.get_physical(LogicalQubit::new(2)), Some(PhysicalQubit::new(3))); +assert_eq!(layout.get_physical(LogicalQubit::new(1)), None); +``` + +### 3. Construct an execution result from measurement counts + +```rust +use std::collections::HashMap; + +use cqlib_core::circuit::Qubit; +use cqlib_core::device::{ExecutionResult, Outcome, Status}; + +let zero = Outcome::from_indices(1, []); +let one = Outcome::from_indices(1, [0]); + +let mut counts = HashMap::new(); +counts.insert(zero.clone(), 3); +counts.insert(one.clone(), 1); + +let result = ExecutionResult::from_counts( + "task".to_string(), + vec![Qubit::new(0)], + 4, + 1, + Some("backend".to_string()), + counts, +); + +assert_eq!(result.status(), &Status::Completed); +assert_eq!(result.counts().get(&zero), Some(&3)); + +let probabilities = result.probabilities().as_ref().unwrap(); +assert_eq!(probabilities.get(&zero), Some(&0.75)); +``` + +--- + +## Validation and error handling + +Errors of the device module are split into several enums by domain; the module root exports `DeviceError`, `DeviceValidationError`, `TopologyError` and `LayoutError`, while `NoiseError` and `OutcomeError` live in their own submodules. + +| Error | When it occurs | +| --- | --- | +| `TopologyError::{QubitNotFound, CouplingNotFound, QubitAlreadyExists, CouplingAlreadyExists, SelfCoupling, DuplicateQubitRemoval, DuplicateCouplingRemoval}` | Topology construction and modification: operating on unknown or duplicate qubits and couplings, self-coupling, duplicate removal. | +| `DeviceError::{InvalidOnlineQubit, QubitNotInDevice, QubitNotInTopology, EdgeNotInTopology, InvalidTopology}` | During device construction and property writes, a qubit or edge is not in the device or topology, or the underlying topology itself is invalid. | +| `DeviceError::{NonStandardNativeInstruction, InvalidNativeInstructionArity, InvalidNativeInstructionErrorRate, InvalidNativeInstructionDuration}` | An invalid native instruction: a non-standard gate, an arity outside the allowed range (at most two qubits at device level, exactly one qubit at qubit level, exactly two qubits at edge level), or an invalid value for the error rate or the duration. | +| `DeviceValidationError::{UnusablePhysicalQubit, MissingDirectedCoupling, UnsupportedInstruction, UndecomposedInstruction}` | A `validate_operation`, `validate_value_operation` or `validate_circuit` check failed: the qubit is unusable, a directed coupling is missing, the instruction is not in the native gate set, or the instruction has not been decomposed yet. | +| `LayoutError::{TooManyLogicalQubits, DuplicateLogicalQubit, DuplicatePhysicalQubit, InvalidLogicalQubit, InvalidPhysicalQubit, LogicalQubitAlreadyBound, PhysicalQubitAlreadyOccupied, LogicalQubitNotBound}` | Layout construction and binding operations: more logical qubits than physical qubits, duplicate or invalid indices, a target position already occupied, or unbinding a logical qubit that is not bound. | +| `NoiseError::{InvalidProbability, QubitCollision, InconsistentArity, Internal}` | A noise channel parameter outside `[0, 1]`, a repeated qubit within the same gate, or an arity inconsistent with the gate; the path is `cqlib_core::device::noise::NoiseError`. | +| `OutcomeError::InvalidCharacter` | The bitstring passed to `Outcome::from_bitstring` contains a character other than `0` / `1`; the path is `cqlib_core::device::result::OutcomeError`. | + +Native capability writes at device level and at qubit/edge level are both validated before taking effect, and a rejected update leaves the original state of the object unchanged. diff --git a/docs/api/en/rust/2_device/1_topology.md b/docs/api/en/rust/2_device/1_topology.md new file mode 100644 index 00000000..6264d19c --- /dev/null +++ b/docs/api/en/rust/2_device/1_topology.md @@ -0,0 +1,107 @@ +# Topology + +`Topology` is the hardware connectivity graph structure in `cqlib_core::device`, using a directed graph to represent qubit coupling relationships. + +## Import + +```rust +use cqlib_core::device::{PhysicalQubit, Topology, TopologyError}; +``` + +## Construction + +### `Topology::new(qubits, coupling_map) -> Result` + +Parameters: + +- `qubits: Vec` +- `coupling_map: Vec<(PhysicalQubit, PhysicalQubit, String)>` + +Notes: + +- Edges are treated with directed semantics; `(q0, q1)` and `(q1, q0)` are not equivalent. +- Only one coupling is allowed for the same ordered pair of qubits, and a different name still counts as a duplicate; duplicate qubits and self-coupling return an error. + +### `Topology::line(qubits) -> Result` + +Parameters: + +- `qubits: Vec` + +Notes: + +- Constructs a directed line `qubits[0] -> qubits[1] -> ...` in the given order, with `len(qubits) - 1` couplings and an empty string as the coupling name. + +## Read-only interface + +- `graph(&self) -> &StableGraph` +- `num_qubits(&self) -> usize` +- `num_couplings(&self) -> usize` +- `qubits(&self) -> impl Iterator` +- `supports_directed_coupling(&self, control: PhysicalQubit, target: PhysicalQubit) -> bool` +- `supports_coupling_either_direction(&self, a: PhysicalQubit, b: PhysicalQubit) -> bool` +- `successors(&self, qubit: PhysicalQubit) -> impl Iterator` +- `predecessors(&self, qubit: PhysicalQubit) -> impl Iterator` +- `neighbors_undirected(&self, qubit: PhysicalQubit) -> impl Iterator` +- `undirected_edges(&self) -> impl Iterator` +- `get_coupling_name(&self, control: PhysicalQubit, target: PhysicalQubit) -> Option` +- `contains_qubit(&self, qubit: &PhysicalQubit) -> bool` +- `out_degree(&self, qubit: &PhysicalQubit) -> usize` +- `in_degree(&self, qubit: &PhysicalQubit) -> usize` + +Notes: + +- `successors` / `out_degree` count outgoing edges, while `predecessors` / `in_degree` count incoming edges. +- Each pair from `undirected_edges` is sorted by key value, and two couplings in opposite directions are folded into one. +- When a qubit is not in the topology, neighbor queries return an empty iterator and degree queries return `0`. + +## Modification interface + +- `add_qubits(&mut self, qubits: impl IntoIterator) -> Result<(), TopologyError>` +- `add_couplings(&mut self, couplings: impl IntoIterator) -> Result<(), TopologyError>` +- `remove_qubits(&mut self, qubits: impl IntoIterator) -> Result<(), TopologyError>` +- `remove_couplings(&mut self, couplings: impl IntoIterator) -> Result<(), TopologyError>` + +Common errors: + +- `TopologyError::QubitNotFound` +- `TopologyError::QubitAlreadyExists` +- `TopologyError::CouplingNotFound` +- `TopologyError::CouplingAlreadyExists` +- `TopologyError::SelfCoupling` +- `TopologyError::DuplicateQubitRemoval` +- `TopologyError::DuplicateCouplingRemoval` + +## Example + +```rust +use cqlib_core::device::{PhysicalQubit, Topology}; + +let q0 = PhysicalQubit::new(0); +let q1 = PhysicalQubit::new(1); +let q2 = PhysicalQubit::new(2); + +let mut topo = Topology::new( + vec![q0, q1, q2], + vec![(q0, q1, "CX".to_string()), (q1, q2, "CZ".to_string())], +) +.unwrap(); + +assert_eq!(topo.num_qubits(), 3); +assert_eq!(topo.num_couplings(), 2); +assert!(topo.supports_directed_coupling(q1, q2)); +assert!(!topo.supports_directed_coupling(q2, q1)); +assert!(topo.supports_coupling_either_direction(q2, q1)); +assert_eq!(topo.get_coupling_name(q1, q2).as_deref(), Some("CZ")); +assert_eq!(topo.successors(q1).collect::>(), vec![q2]); +assert_eq!(topo.neighbors_undirected(q1).collect::>(), vec![q0, q2]); +assert_eq!(topo.out_degree(&q1), 1); +assert_eq!(topo.in_degree(&q1), 1); + +topo.add_qubits([PhysicalQubit::new(3)]).unwrap(); +topo.add_couplings([(q2, PhysicalQubit::new(3), "CX".to_string())]) + .unwrap(); +topo.remove_couplings([(q2, PhysicalQubit::new(3))]).unwrap(); +topo.remove_qubits([PhysicalQubit::new(3)]).unwrap(); +assert_eq!(topo.num_qubits(), 3); +``` diff --git a/docs/api/en/rust/2_device/2_properties_device.md b/docs/api/en/rust/2_device/2_properties_device.md new file mode 100644 index 00000000..5b861448 --- /dev/null +++ b/docs/api/en/rust/2_device/2_properties_device.md @@ -0,0 +1,246 @@ +# Device / Properties + +This page covers the types in `cqlib_core::device` used for device property modeling: + +- `InstructionProp` +- `QubitProp` +- `EdgeProp` +- `Device` + +## Import + +```rust +use std::collections::HashSet; + +use cqlib_core::circuit::{Instruction, StandardGate}; +use cqlib_core::device::{ + Device, DeviceError, EdgeProp, InstructionProp, PhysicalQubit, QubitProp, Topology, +}; +use time::OffsetDateTime; +``` + +## InstructionProp + +### Construction and methods + +- `InstructionProp::new(instruction: Instruction, error_rate: f64) -> InstructionProp` +- `with_length(self, length: f64) -> InstructionProp` +- `set_length(&mut self, length: f64)` +- `with_instruction(self, instruction: Instruction) -> InstructionProp` +- `set_instruction(&mut self, instruction: Instruction)` +- `with_error_rate(self, error_rate: f64) -> InstructionProp` +- `set_error_rate(&mut self, error_rate: f64)` + +### Read-only interface + +- `instruction(&self) -> &Instruction` +- `error_rate(&self) -> f64` +- `length(&self) -> Option` + +## QubitProp + +### Construction and builder + +- `QubitProp::new(readout_error: f64) -> QubitProp` +- `with_prob_meas0_prep1(self, prob: f64) -> QubitProp` +- `set_prob_meas0_prep1(&mut self, prob: f64)` +- `with_prob_meas1_prep0(self, prob: f64) -> QubitProp` +- `set_prob_meas1_prep0(&mut self, prob: f64)` +- `with_t1(self, t1: f64) -> QubitProp` +- `set_t1(&mut self, t1: f64)` +- `with_t2(self, t2: f64) -> QubitProp` +- `set_t2(&mut self, t2: f64)` +- `with_frequency(self, frequency: f64) -> QubitProp` +- `set_frequency(&mut self, frequency: f64)` +- `with_native_instruction(self, prop: InstructionProp) -> Result` +- `set_native_instruction(&mut self, prop: InstructionProp) -> Result<(), DeviceError>` + +### Read-only interface + +- `readout_error(&self) -> f64` +- `prob_meas0_prep1(&self) -> Option` +- `prob_meas1_prep0(&self) -> Option` +- `t1(&self) -> Option` +- `t2(&self) -> Option` +- `frequency(&self) -> Option` +- `native_instructions(&self) -> &[InstructionProp]` + +Notes: + +- Native instructions of a qubit property accept only single-qubit standard gates; an instruction that is not single-qubit returns `DeviceError::InvalidNativeInstructionArity`. + +## EdgeProp + +### Construction and methods + +- `EdgeProp::new() -> EdgeProp` +- `with_native_instruction(self, prop: InstructionProp) -> Result` +- `set_native_instruction(&mut self, prop: InstructionProp) -> Result<(), DeviceError>` +- `native_instructions(&self) -> &[InstructionProp]` + +Notes: + +- Native instructions of an edge property accept only two-qubit standard gates. + +## Device + +### Construction + +- `Device::new(name: impl Into, qubits: HashSet, topology: Topology) -> Result` + +Notes: + +- Every qubit in the topology must be registered in `qubits`, otherwise `DeviceError::InvalidOnlineQubit` is returned. + +### Static constructor methods + +- `line(name: impl Into, num_qubits: u32) -> Result` +- `line_from_qubits(name: impl Into, physical_qubits: Vec) -> Result` +- `bidirectional_line(name: impl Into, num_qubits: u32) -> Result` +- `ring(name: impl Into, num_qubits: u32) -> Result` +- `star(name: impl Into, num_qubits: u32, center: u32) -> Result` +- `grid(name: impl Into, rows: u32, cols: u32) -> Result` +- `from_edges(name: impl Into, num_qubits: u32, edges: &[(u32, u32)]) -> Result` + +Notes: + +- All qubits generated by the static constructor methods are online; the coupling name of line-like shapes is the empty string. + +### Configuration interface (builder) + +- `with_invalid_qubits(self, invalid_qubits: HashSet) -> Result` +- `set_invalid_qubits(&mut self, invalid_qubits: HashSet) -> Result<(), DeviceError>` +- `with_native_gates(self, gates: Vec) -> Result` +- `set_native_gates(&mut self, gates: Vec) -> Result<(), DeviceError>` +- `with_calibration_time(self, time: OffsetDateTime) -> Device` +- `set_calibration_time(&mut self, time: OffsetDateTime)` +- `with_default_t1(self, t1: f64) -> Device` +- `set_default_t1(&mut self, t1: f64)` +- `with_default_t2(self, t2: f64) -> Device` +- `set_default_t2(&mut self, t2: f64)` +- `with_default_readout_error(self, error: f64) -> Device` +- `set_default_readout_error(&mut self, error: f64)` +- `with_default_single_qubit_error(self, error: f64) -> Device` +- `set_default_single_qubit_error(&mut self, error: f64)` +- `with_default_two_qubit_error(self, error: f64) -> Device` +- `set_default_two_qubit_error(&mut self, error: f64)` + +Notes: + +- `with_native_gates` and `set_native_gates` accept only standard gates with at most two qubits; an invalid item causes the whole write to be rejected and the original gate set stays unchanged. + +### Write interface + +- `add_qubit_properties(&mut self, qubit: PhysicalQubit, props: QubitProp) -> Result<(), DeviceError>` +- `add_edge_properties(&mut self, control: PhysicalQubit, target: PhysicalQubit, props: EdgeProp) -> Result<(), DeviceError>` + +Common errors: + +- `DeviceError::InvalidOnlineQubit` +- `DeviceError::QubitNotInDevice` +- `DeviceError::QubitNotInTopology` +- `DeviceError::EdgeNotInTopology` +- `DeviceError::NonStandardNativeInstruction` +- `DeviceError::InvalidNativeInstructionArity` +- `DeviceError::InvalidNativeInstructionErrorRate` +- `DeviceError::InvalidNativeInstructionDuration` + +### Query interface + +- `name(&self) -> &str` +- `qubits(&self) -> impl Iterator` +- `invalid_qubits(&self) -> impl Iterator` +- `is_usable_qubit(&self, qubit: PhysicalQubit) -> bool` +- `usable_qubits(&self) -> impl Iterator` +- `num_usable_qubits(&self) -> usize` +- `topology(&self) -> &Topology` +- `native_gates(&self) -> &[Instruction]` +- `qubit_properties(&self, qubit: PhysicalQubit) -> Option<&QubitProp>` +- `edge_properties(&self, control: PhysicalQubit, target: PhysicalQubit) -> Option<&EdgeProp>` +- `supports_native_instruction(&self, instruction: &Instruction, qargs: &[PhysicalQubit]) -> bool` +- `validate_operation(&self, operation: &Operation) -> Result<(), DeviceValidationError>` +- `validate_value_operation(&self, operation: &ValueOperation) -> Result<(), DeviceValidationError>` +- `validate_circuit(&self, circuit: &Circuit) -> Result<(), DeviceValidationError>` +- `single_qubit_error(&self, qubit: PhysicalQubit, instruction: &Instruction) -> Option` +- `two_qubit_error(&self, control: PhysicalQubit, target: PhysicalQubit, instruction: &Instruction) -> Option` +- `edge_error(&self, control: PhysicalQubit, target: PhysicalQubit) -> Option` +- `get_t1(&self, qubit: PhysicalQubit) -> Option` +- `get_t2(&self, qubit: PhysicalQubit) -> Option` +- `get_readout_error(&self, qubit: PhysicalQubit) -> Option` +- `default_single_qubit_error(&self) -> Option` +- `default_two_qubit_error(&self) -> Option` +- `default_t1(&self) -> Option` +- `default_t2(&self) -> Option` +- `default_readout_error(&self) -> Option` +- `calibration_time(&self) -> Option` + +Notes: + +- `get_t1` / `get_t2` / `get_readout_error` read the local property first and fall back to the default value when it is missing. +- `single_qubit_error` / `two_qubit_error` take the local native instruction property first and fall back to the default single-qubit / two-qubit error rate when it is missing; `None` is returned when the qubit is unusable or the direction does not exist. +- `edge_error` reads only calibration data, taking the minimum error rate of the native instructions on that directed edge. + +## Example + +```rust +use std::collections::HashSet; + +use cqlib_core::circuit::{Instruction, StandardGate}; +use cqlib_core::device::{Device, EdgeProp, InstructionProp, PhysicalQubit, QubitProp, Topology}; + +let q0 = PhysicalQubit::new(0); +let q1 = PhysicalQubit::new(1); +let q2 = PhysicalQubit::new(2); + +let topology = Topology::new( + vec![q0, q1, q2], + vec![(q0, q1, "CX".to_string()), (q1, q2, "CX".to_string())], +) +.unwrap(); + +let mut device = Device::new("mock_backend", HashSet::from([q0, q1, q2]), topology) + .unwrap() + .with_default_t1(50.0) + .with_default_t2(35.0) + .with_default_readout_error(0.05) + .with_native_gates(vec![ + Instruction::Standard(StandardGate::H), + Instruction::Standard(StandardGate::CX), + ]) + .unwrap(); + +let qp0 = QubitProp::new(0.02) + .with_t1(80.0) + .with_t2(70.0) + .with_native_instruction(InstructionProp::new( + Instruction::Standard(StandardGate::H), + 0.001, + )) + .unwrap(); +device.add_qubit_properties(q0, qp0).unwrap(); + +let ep01 = EdgeProp::new() + .with_native_instruction(InstructionProp::new( + Instruction::Standard(StandardGate::CX), + 0.02, + )) + .unwrap(); +device.add_edge_properties(q0, q1, ep01).unwrap(); + +assert_eq!(device.get_t1(q0), Some(80.0)); +assert_eq!(device.get_t1(q2), Some(50.0)); // 回退默认值 +assert_eq!( + device.single_qubit_error(q0, &Instruction::Standard(StandardGate::H)), + Some(0.001) +); +assert_eq!( + device.two_qubit_error(q0, q1, &Instruction::Standard(StandardGate::CX)), + Some(0.02) +); +assert_eq!(device.edge_error(q0, q1), Some(0.02)); +assert_eq!(device.num_usable_qubits(), 3); + +device.set_invalid_qubits(HashSet::from([q2])).unwrap(); +assert!(!device.is_usable_qubit(q2)); +assert_eq!(device.num_usable_qubits(), 2); +``` diff --git a/docs/api/en/rust/2_device/3_layout.md b/docs/api/en/rust/2_device/3_layout.md new file mode 100644 index 00000000..dc12a160 --- /dev/null +++ b/docs/api/en/rust/2_device/3_layout.md @@ -0,0 +1,118 @@ +# Layout + +`Layout` maintains the mapping between logical qubits and physical qubits during routing. + +## Import + +```rust +use std::collections::BTreeMap; + +use cqlib_core::device::{Layout, LayoutError, LogicalQubit, PhysicalQubit}; +``` + +## Construction + +### `Layout::new(logical, physical, init_map) -> Result` + +Parameters: + +- `logical: Vec` +- `physical: Vec` +- `init_map: Option>` (logical -> physical) + +Notes: + +- Logical qubits that do not appear in `init_map` are mapped to the remaining vacant physical qubits in input order. + +Common errors: + +- `LayoutError::TooManyLogicalQubits` +- `LayoutError::DuplicateLogicalQubit` +- `LayoutError::DuplicatePhysicalQubit` +- `LayoutError::InvalidLogicalQubit` +- `LayoutError::InvalidPhysicalQubit` + +### `Layout::from_pairs(logical_physical, physical_count) -> Result` + +Parameters: + +- `logical_physical: &[(u32, u32)]`: `(logical index, physical index)` pairs. +- `physical_count: u32`: the total number of physical qubits; physical qubits are `0..physical_count`. + +Notes: + +- The logical qubits are the logical indices appearing in `logical_physical`; physical qubits that are not referenced are vacant physical qubits. + +## Read-only interface + +- `num_logical(&self) -> usize` +- `num_physical(&self) -> usize` +- `num_vacant_physical(&self) -> usize` +- `get_physical(&self, logical: LogicalQubit) -> Option` +- `get_logical(&self, physical: PhysicalQubit) -> Option` +- `logical_qubits(&self) -> impl Iterator` +- `physical_qubits(&self) -> impl Iterator` +- `vacant_physical_qubits(&self) -> impl Iterator` +- `is_physical_vacant(&self, physical: PhysicalQubit) -> bool` +- `l2p_map(&self) -> &BTreeMap` +- `p2l_map(&self) -> &BTreeMap` + +## Update interface + +- `bind(&mut self, logical: LogicalQubit, physical: PhysicalQubit) -> Result<(), LayoutError>` +- `unbind(&mut self, logical: LogicalQubit) -> Result` +- `swap_physical(&mut self, phys_a: PhysicalQubit, phys_b: PhysicalQubit) -> Result<(), LayoutError>` + +Swaps the logical qubits carried by two physical qubits, which is the basic operation for moving logical qubits during routing. Either physical qubit may be vacant: swapping with a vacant physical qubit moves a logical qubit to the vacant position. + +Common errors: + +- `LayoutError::LogicalQubitAlreadyBound` +- `LayoutError::PhysicalQubitAlreadyOccupied` +- `LayoutError::LogicalQubitNotBound` + +Note: + +- When a physical qubit is not in the layout set, `LayoutError::InvalidPhysicalQubit` is returned and no panic is triggered. + +## Example + +```rust +use std::collections::BTreeMap; + +use cqlib_core::device::{Layout, LogicalQubit, PhysicalQubit}; + +let logical = vec![LogicalQubit::new(0), LogicalQubit::new(1)]; +let physical = vec![ + PhysicalQubit::new(10), + PhysicalQubit::new(11), + PhysicalQubit::new(12), +]; +let init_map = [(LogicalQubit::new(0), PhysicalQubit::new(11))] + .into_iter() + .collect::>(); + +let mut layout = Layout::new(logical, physical, Some(init_map)).unwrap(); +assert_eq!(layout.num_logical(), 2); +assert_eq!(layout.num_physical(), 3); +assert_eq!(layout.num_vacant_physical(), 1); +assert_eq!( + layout.get_physical(LogicalQubit::new(0)), + Some(PhysicalQubit::new(11)) +); +assert!(layout.is_physical_vacant(PhysicalQubit::new(12))); + +let before_11 = layout.get_logical(PhysicalQubit::new(11)); +let before_12 = layout.get_logical(PhysicalQubit::new(12)); +layout + .swap_physical(PhysicalQubit::new(11), PhysicalQubit::new(12)) + .unwrap(); +assert_eq!(layout.get_logical(PhysicalQubit::new(12)), before_11); +assert_eq!(layout.get_logical(PhysicalQubit::new(11)), before_12); + +assert_eq!( + layout.unbind(LogicalQubit::new(0)).unwrap(), + PhysicalQubit::new(12) +); +assert!(layout.is_physical_vacant(PhysicalQubit::new(12))); +``` diff --git a/docs/api/en/rust/2_device/4_noise.md b/docs/api/en/rust/2_device/4_noise.md new file mode 100644 index 00000000..0e249dfa --- /dev/null +++ b/docs/api/en/rust/2_device/4_noise.md @@ -0,0 +1,171 @@ +# NoiseModel / Noise Channels + +This page covers the noise modeling API in `cqlib_core::device`: + +- `SingleQubitNoise` +- `TwoQubitNoise` +- `ReadoutError` +- `OperationKey` +- `NoiseModel` +- `NoiseError` + +## Import + +```rust +use cqlib_core::circuit::{Qubit, StandardGate}; +use cqlib_core::device::noise::NoiseError; +use cqlib_core::device::{ + NoiseModel, OperationKey, ReadoutError, SingleQubitNoise, TwoQubitNoise, +}; +use cqlib_core::qis::Pauli; +``` + +## NoiseError + +Defined in `cqlib_core::device::noise`; not re-exported from the `cqlib_core::device` module root. + +Common error variants: + +- `InvalidProbability { value, context }` +- `QubitCollision { qubits }` +- `InconsistentArity { expected, actual }` +- `Internal(String)` + +## SingleQubitNoise + +Enum variants: + +- `BitFlip(f64)` +- `PhaseFlip(f64)` +- `Pauli { px, py, pz }` +- `Depolarizing(f64)` +- `AmplitudeDamping(f64)` +- `PhaseDamping(f64)` + +Methods: + +- `is_valid(&self) -> bool` +- `to_kraus(&self) -> Vec>` + +## TwoQubitNoise + +Enum variants: + +- `Depolarizing(f64)` +- `Independent { q0_noise: SingleQubitNoise, q1_noise: SingleQubitNoise }` +- `CorrelatedPauli { op_q0: Pauli, op_q1: Pauli, p: f64 }` + +Methods: + +- `is_valid(&self) -> bool` +- `to_kraus(&self) -> Vec>` + +## ReadoutError + +Struct fields: + +- `p_0_given_1: f64` +- `p_1_given_0: f64` + +Methods: + +- `is_valid(&self) -> bool` + +## OperationKey + +Construction: + +- `OperationKey::new_single(gate: StandardGate, q0: Qubit) -> OperationKey` +- `OperationKey::new_double(gate: StandardGate, q0: Qubit, q1: Qubit) -> Result` +- `OperationKey::new_triple(gate: StandardGate, q0: Qubit, q1: Qubit, q2: Qubit) -> Result` + +Queries: + +- `qubits(&self) -> &[usize]` +- `gate(&self) -> &StandardGate` + +Notes: + +- `OperationKey` implements `Hash`/`Eq` and can be used as a `HashMap` key. + +## NoiseModel + +Construction: + +- `NoiseModel::new() -> NoiseModel` + +Writes: + +- `add_readout_error(&mut self, qubit: Qubit, error: ReadoutError) -> Result<(), NoiseError>` +- `add_single_qubit_error(&mut self, gate: StandardGate, qubit: Qubit, noise: SingleQubitNoise) -> Result<(), NoiseError>` +- `add_two_qubit_error(&mut self, gate: StandardGate, q0: Qubit, q1: Qubit, noise: TwoQubitNoise) -> Result<(), NoiseError>` + +Queries: + +- `get_readout_error(&self, key: &Qubit) -> Option<&ReadoutError>` +- `get_single_qubit_errors(&self, key: &OperationKey) -> Option<&Vec>` +- `get_two_qubit_errors(&self, key: &OperationKey) -> Option<&Vec>` + +Notes: + +- Multiple channels can be registered for the same gate and qubit combination, and a query returns the list; an unregistered combination returns `None`. +- Two identical qubits for a two-qubit gate make `add_two_qubit_error` return `NoiseError::QubitCollision`. + +## Example + +```rust +use cqlib_core::circuit::{Qubit, StandardGate}; +use cqlib_core::device::{NoiseModel, OperationKey, ReadoutError, SingleQubitNoise, TwoQubitNoise}; +use cqlib_core::qis::Pauli; + +let mut nm = NoiseModel::new(); + +nm.add_readout_error( + Qubit::new(0), + ReadoutError { + p_0_given_1: 0.02, + p_1_given_0: 0.01, + }, +) +.unwrap(); + +nm.add_single_qubit_error( + StandardGate::X, + Qubit::new(0), + SingleQubitNoise::BitFlip(0.005), +) +.unwrap(); +nm.add_single_qubit_error( + StandardGate::X, + Qubit::new(0), + SingleQubitNoise::PhaseFlip(0.004), +) +.unwrap(); + +nm.add_two_qubit_error( + StandardGate::CX, + Qubit::new(0), + Qubit::new(1), + TwoQubitNoise::Depolarizing(0.02), +) +.unwrap(); +nm.add_two_qubit_error( + StandardGate::CX, + Qubit::new(0), + Qubit::new(1), + TwoQubitNoise::CorrelatedPauli { + op_q0: Pauli::X, + op_q1: Pauli::Z, + p: 0.05, + }, +) +.unwrap(); + +let skey = OperationKey::new_single(StandardGate::X, Qubit::new(0)); +let tkey = OperationKey::new_double(StandardGate::CX, Qubit::new(0), Qubit::new(1)).unwrap(); + +assert_eq!(nm.get_single_qubit_errors(&skey).unwrap().len(), 2); +assert_eq!(nm.get_two_qubit_errors(&tkey).unwrap().len(), 2); +assert_eq!(nm.get_readout_error(&Qubit::new(0)).unwrap().p_0_given_1, 0.02); +assert!(nm.get_readout_error(&Qubit::new(9)).is_none()); +``` diff --git a/docs/api/en/rust/2_device/5_result.md b/docs/api/en/rust/2_device/5_result.md new file mode 100644 index 00000000..de5e0ae6 --- /dev/null +++ b/docs/api/en/rust/2_device/5_result.md @@ -0,0 +1,171 @@ +# Outcome / Status / ExecutionResult + +This page covers the types related to quantum task results: + +- `Outcome` +- `Status` +- `ExecutionResult` +- `OutcomeError` + +## Import + +```rust +use cqlib_core::circuit::Qubit; +use cqlib_core::device::result::OutcomeError; +use cqlib_core::device::{ExecutionResult, Outcome, Status}; +use std::collections::HashMap; +use time::OffsetDateTime; +``` + +## OutcomeError + +Defined in `cqlib_core::device::result`; not re-exported from the `cqlib_core::device` module root. + +Currently contains: + +- `OutcomeError::InvalidCharacter(index, ch)`: the bitstring contains an invalid character. + +## Outcome + +### Construction and conversion + +- `Outcome::new(chunks: SmallVec<[u64; 4]>) -> Outcome` +- `Outcome::from_indices(width: usize, indices: impl IntoIterator) -> Outcome`: construct a result of width `width` whose bits are `1` only at the positions listed in `indices`; indices beyond `width` are ignored. +- `Outcome::from_bitstring(s: &str) -> Result` +- Implements `FromStr`; `s.parse::()` is equivalent to `Outcome::from_bitstring(s)`, with the same error type `OutcomeError`. +- `to_bitstring(&self, num_qubits: usize) -> String`: output the bitstring with the given width. + +Notes: + +- The bitstring is always written in standard binary form (most significant bit on the left), while internally it is stored in little-endian blocks. +- The bitstring width is not stored by `Outcome`: parsing `"001"` and parsing `"1"` yield the same `Outcome`. The output width must therefore be specified by the caller through `to_bitstring`; when the requested width is smaller than the stored value, the high bits are truncated. +- `Outcome` implements `Clone + Eq + Hash` and can be used directly as the key of a counts `HashMap`. + +### Queries + +- `is_one(&self, index: usize) -> bool` + +## Status + +Enum variants: + +- `Queued` +- `Running` +- `Completed` +- `Failed { error_msg: String, error_code: i32 }` +- `Cancelled` + +Methods: + +- `is_terminal(&self) -> bool` +- `is_success(&self) -> bool` + +## ExecutionResult + +### Construction + +```rust +ExecutionResult::new( + task_id: String, + qubits: Vec, + shots: usize, + num_qubits: usize, + backend: Option, + created_at: Option, +) -> ExecutionResult +``` + +```rust +ExecutionResult::from_counts( + task_id: String, + qubits: Vec, + shots: usize, + num_qubits: usize, + backend: Option, + counts: HashMap, +) -> ExecutionResult +``` + +Notes: + +- `from_counts` applies construction, `start`, `finish` and `calc_probabilities` in order, obtaining an already completed result directly. + +### Lifecycle methods + +- `start(&mut self, t: Option) -> &mut Self` +- `finish(&mut self, counts: HashMap, t: Option) -> &mut Self` +- `fail(&mut self, msg: String, code: i32)` +- `cancel(&mut self)` +- `calc_probabilities(&mut self) -> &mut Self` + +Notes: + +- `finish` clears the computed probabilities when it writes the counts; `calc_probabilities` normalizes by the sum of the recorded counts, and produces no probabilities when the total count is 0. + +### Read methods + +- `task_id(&self) -> &str` +- `shots(&self) -> usize` +- `num_qubits(&self) -> usize` +- `qubits(&self) -> &Vec` +- `status(&self) -> &Status` +- `created_at(&self) -> &OffsetDateTime` +- `started_at(&self) -> &Option` +- `finished_at(&self) -> &Option` +- `backend(&self) -> Option<&String>` +- `counts(&self) -> &HashMap` +- `probabilities(&self) -> &Option>` + +Notes: + +- `qubits[i]` corresponds to bit `i` of the bitstring (weight `2^i`); the bitstring is written with the most significant bit on the left, so the string order is the reverse of the `qubits` order. + +## Example + +```rust +use cqlib_core::circuit::Qubit; +use cqlib_core::device::{ExecutionResult, Outcome, Status}; +use std::collections::HashMap; + +// 比特串按最高位在左书写:位 2 在左,位 0 在右。 +let outcome = Outcome::from_indices(3, [0, 2]); +assert!(outcome.is_one(0)); +assert!(!outcome.is_one(1)); +assert_eq!(outcome.to_bitstring(3), "101"); +assert_eq!(Outcome::from_bitstring("101").unwrap(), outcome); +assert!(Outcome::from_bitstring("10a").is_err()); + +let mut result = ExecutionResult::new( + "task-1".to_string(), + vec![Qubit::new(0), Qubit::new(1)], + 1000, + 2, + Some("sim".to_string()), + None, +); + +assert_eq!(result.status(), &Status::Queued); +result.start(None); +assert_eq!(result.status(), &Status::Running); + +let mut counts = HashMap::new(); +counts.insert(Outcome::from_bitstring("00").unwrap(), 750); +counts.insert(Outcome::from_bitstring("11").unwrap(), 250); +result.finish(counts, None).calc_probabilities(); + +assert_eq!(result.status(), &Status::Completed); +assert_eq!(result.counts().len(), 2); +assert_eq!(result.probabilities().as_ref().unwrap().len(), 2); + +let done = ExecutionResult::from_counts( + "task-2".to_string(), + vec![Qubit::new(0), Qubit::new(1)], + 1000, + 2, + Some("sim".to_string()), + HashMap::from([(Outcome::from_bitstring("01").unwrap(), 400)]), +); +assert_eq!(done.status(), &Status::Completed); +assert!(done.counts().contains_key(&Outcome::from_bitstring("01").unwrap())); +assert!(done.probabilities().is_some()); +``` diff --git a/docs/api/en/rust/2_device/6_qubits.md b/docs/api/en/rust/2_device/6_qubits.md new file mode 100644 index 00000000..b0e27f98 --- /dev/null +++ b/docs/api/en/rust/2_device/6_qubits.md @@ -0,0 +1,156 @@ +# Qubit Identifiers + +`cqlib_core::device` + +`LogicalQubit` and `PhysicalQubit` are the strongly typed qubit identifiers used by the device-side interfaces: the former identifies a logical qubit in a circuit, and the latter identifies a physical qubit on a device. Both types are newtype wrappers around `Qubit` with the same internal representation, but they are distinguished from each other at the type level. + +## Import + +```rust +use cqlib_core::circuit::Qubit; +use cqlib_core::device::{Layout, LogicalQubit, PhysicalQubit}; +``` + +--- + +## LogicalQubit + +The logical qubit identifier, used for logical qubits in a circuit. + +```rust +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)] +#[repr(transparent)] +pub struct LogicalQubit(Qubit); +``` + +Fields: + +- The inner `Qubit` field is private and is not accessed directly through a public interface. + +### Construction + +- `LogicalQubit::new(id: u32) -> LogicalQubit`: construct from a numeric identifier; `const fn`. +- `LogicalQubit::from_qubit(qubit: Qubit) -> LogicalQubit`: wrap an existing circuit qubit; `const fn`. +- Implements `From`, equivalent to `from_qubit`. + +### Methods + +- `fn qubit(self) -> Qubit`: return the underlying circuit qubit; `const fn`. +- `fn id(self) -> u32`: return the numeric identifier; `const fn`. + +### Trait implementations + +- `Debug`, `Clone`, `Copy`, `PartialEq`, `Eq`, `Hash`, `Ord`, `PartialOrd`: derived implementations; ordering and hashing are based on the numeric identifier. +- `Display`: outputs `L{id}`, for example `L0`. +- `From for LogicalQubit`: equivalent to `LogicalQubit::from_qubit`. +- `From for Qubit`: equivalent to `LogicalQubit::qubit`. + +Example: + +```rust +use cqlib_core::circuit::Qubit; +use cqlib_core::device::LogicalQubit; + +let qubit = Qubit::new(3); +let logical = LogicalQubit::from_qubit(qubit); + +assert_eq!(logical.id(), 3); +assert_eq!(logical.qubit(), qubit); +assert_eq!(Qubit::from(logical), qubit); +assert_eq!(logical.to_string(), "L3"); + +let ordered = [LogicalQubit::new(2), LogicalQubit::new(0)]; +assert!(ordered[1] < ordered[0]); +``` + +--- + +## PhysicalQubit + +The physical qubit identifier, used for qubit positions on a device. + +```rust +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)] +#[repr(transparent)] +pub struct PhysicalQubit(Qubit); +``` + +Fields: + +- The inner `Qubit` field is private and is not accessed directly through a public interface. + +### Construction + +- `PhysicalQubit::new(id: u32) -> PhysicalQubit`: construct from a numeric identifier; `const fn`. +- `PhysicalQubit::from_qubit(qubit: Qubit) -> PhysicalQubit`: wrap an identifier in qubit form; `const fn`. +- Implements `From`, equivalent to `from_qubit`. + +### Methods + +- `fn qubit(self) -> Qubit`: return the underlying qubit identifier; `const fn`. +- `fn id(self) -> u32`: return the numeric identifier; `const fn`. + +### Trait implementations + +- `Debug`, `Clone`, `Copy`, `PartialEq`, `Eq`, `Hash`, `Ord`, `PartialOrd`: derived implementations; ordering and hashing are based on the numeric identifier. +- `Display`: outputs `P{id}`, for example `P100`. +- `From for PhysicalQubit`: equivalent to `PhysicalQubit::from_qubit`. +- `From for Qubit`: equivalent to `PhysicalQubit::qubit`. + +Example: + +```rust +use cqlib_core::circuit::Qubit; +use cqlib_core::device::PhysicalQubit; + +let qubit = Qubit::new(11); +let physical = PhysicalQubit::new(qubit.id()); + +assert_eq!(physical.id(), 11); +assert_eq!(physical.qubit(), qubit); +assert_eq!(physical.to_string(), "P11"); + +assert!(PhysicalQubit::new(0) < PhysicalQubit::new(2)); +``` + +--- + +## Role in layout + +A layout maintains the mapping from logical qubits to physical qubits, which is the main scenario in which the two types are used together: logical qubits come from a circuit, physical qubits come from a device, and the mapping result is recorded with strongly typed identifiers. There is no conversion between the two types, nor can they be compared directly; the same numeric identifier does not mean that the two are the same identifier. + +Example: + +```rust +use cqlib_core::device::{Layout, LogicalQubit, PhysicalQubit}; +use std::collections::BTreeMap; + +let logical = vec![LogicalQubit::new(0), LogicalQubit::new(1)]; +let physical = vec![ + PhysicalQubit::new(100), + PhysicalQubit::new(101), + PhysicalQubit::new(102), +]; + +let init_map = [(LogicalQubit::new(1), PhysicalQubit::new(102))] + .into_iter() + .collect::>(); + +let layout = Layout::new(logical, physical, Some(init_map)).unwrap(); + +assert_eq!(layout.num_logical(), 2); +assert_eq!(layout.num_physical(), 3); +assert_eq!(layout.num_vacant_physical(), 1); +assert_eq!( + layout.get_physical(LogicalQubit::new(0)), + Some(PhysicalQubit::new(100)) +); +assert_eq!( + layout.get_physical(LogicalQubit::new(1)), + Some(PhysicalQubit::new(102)) +); +assert_eq!( + layout.vacant_physical_qubits().collect::>(), + vec![PhysicalQubit::new(101)] +); +``` diff --git a/docs/api/en/rust/3_qis/0_overview.md b/docs/api/en/rust/3_qis/0_overview.md new file mode 100644 index 00000000..3bdb55ae --- /dev/null +++ b/docs/api/en/rust/3_qis/0_overview.md @@ -0,0 +1,195 @@ +# QIS Quantum Information + +`cqlib_core::qis` + +`cqlib_core::qis` provides the foundational objects and local simulation capabilities related to quantum information science: several representations of quantum states, Pauli operators and Hamiltonians, construction of Pauli evolution circuits, and fidelity, entropy and entanglement metrics. This page is the overview and navigation for the module. + +## Overview + +The simulation result of a quantum circuit lands on a quantum state. Different tasks require different state representations: a circuit containing only standard gates and rotation gates can be described with a pure state representation; a scenario involving noise or requiring the description of a quantum channel must use a mixed state representation; a circuit containing only Clifford gates can use the stabilizer representation, describing the quantum state with data of polynomial size. + +`cqlib_core::qis` organizes "the representation of a state" together with "the operations acting on a state": every simulator provides a set of common operations acting on states and accepts a whole circuit through an entry point that interfaces with the circuit IR. Observables, evolution and metrics are built on top of the state representations. + +### Four state representations + +| Representation | Use case | Noise support | Describable scale | +| --- | --- | --- | --- | +| `Statevector` | Ideal universal circuits, pure states | None | Small-scale systems | +| `DensityMatrix` | Mixed states and quantum channels | Kraus operators | Small-scale systems | +| `DensityMatrixNoise` | Device-level simulation with noise | Full noise model (gate noise and readout noise) | Small-scale systems | +| `StabilizerState` | Circuits containing only Clifford gates | None | Large-scale systems | + +The four representations share consistent interface naming: gate operations correspond by name (`apply_h`, `apply_cx`, and so on), and the measurement and sampling interfaces are identical (`measure`, `measure_all`, `sample_shots`, `sample`, `probs`). + +### Interfacing with circuits + +All three dense representations provide two entry points for interfacing with circuits: + +- `from_circuit`: execute a circuit and return the evolved state without modifying the input circuit. +- `apply_circuit`: apply a circuit in place to an existing state; the number of qubits of the circuit must match that of the state. + +`StabilizerState` additionally provides `run_circuit`, which returns runtime classical data while executing a Clifford circuit, with the return type `CircuitExecutionResult`. + +### Operators, evolution and metrics + +Pauli operators and Pauli strings are the basic objects for describing observables and stabilizer generators. `Hamiltonian` is composed of Pauli strings with coefficients; once it implements the `Observable` trait it can be passed as an observable to the `expectation` method of a state to obtain an expectation value. `PauliEvolution` converts Pauli strings into executable evolution circuits. The `metrics` and `entropy` modules provide purity, fidelity, trace distance, entropy and entanglement metrics. + +--- + +## Common entry points + +```rust +use cqlib_core::qis::{DensityMatrix, Statevector}; + +// 纯态模拟 +let mut sv = Statevector::new(3); +sv.apply_h(0).unwrap(); +sv.apply_cx(0, 1).unwrap(); +let probs_sv = sv.probabilities(); + +// 混合态模拟 +let mut dm = DensityMatrix::new(3); +dm.apply_h(0).unwrap(); +dm.apply_cx(0, 1).unwrap(); +let probs_dm = dm.probabilities(); + +// 纯态下两种表示给出相同的结果 +assert!((probs_sv[0] - probs_dm[0]).abs() < 1e-10); +``` + +--- + +## Core concepts and terms + +| Term | Description | +| --- | --- | +| **Statevector** | The complex amplitude vector of a pure state in the computational basis; component `i` corresponds to the basis state `\|i>`. | +| **Mixed state** | A quantum state that cannot be described by a single statevector; represented by a density matrix. | +| **Density matrix** | A matrix describing a quantum state; the diagonal entries give the measurement probabilities in the computational basis. | +| **Quantum channel** | A trace-preserving map of states, represented by a set of Kraus operators `K_k`. | +| **Stabilizer state** | The eigenstate jointly determined by a set of mutually commuting Pauli operators (the stabilizer generators). | +| **Clifford gate** | A gate that maps the Pauli group to itself; the stabilizer representation accepts only this kind of gate. | +| **Readout noise** | The classical error introduced by the measurement step; it acts on the probability distribution rather than on the quantum state itself. | +| **Collapse** | Measurement causes the state to be projected onto the subspace consistent with the measurement outcome. | +| **Sampling** | Generating multiple independent measurement outcomes according to the probability distribution of the state. | +| **Observable** | A physical quantity whose expectation value can be obtained; described by the `Observable` trait. | + +--- + +## `cqlib_core::qis` API overview + +### Modules and state representations + +| Name | Description | +| --- | --- | +| [`cqlib_core::qis`](0_overview.md) | Module overview, terminology and error handling. | +| [`Statevector`](1_statevector.md) | Pure state simulator, storing `2^n` complex amplitudes. | +| [`DensityMatrix`](2_density_matrix.md) | Mixed state simulator, supporting Kraus operators and partial trace. | +| [`DensityMatrixNoise`](3_density_matrix_noise.md) | Density matrix simulator with a noise model. | +| [`StabilizerState`](4_stabilizer.md) | Stabilizer state simulator, supporting large-scale Clifford circuits. | + +### Classical data + +| Name | Description | +| --- | --- | +| [`ClassicalState`](5_classical_state.md) / [`RuntimeValue`](5_classical_state.md) | Classical values and variables produced at runtime by a Clifford circuit. | + +### Operators and observables + +| Name | Description | +| --- | --- | +| [`Pauli`](6_pauli.md) / [`Phase`](6_pauli.md) | Single-qubit Pauli operators and phases. | +| [`PauliString`](6_pauli.md) / [`PauliIter`](6_pauli.md) | Pauli strings and their per-qubit iterator. | +| [`Hamiltonian`](7_hamiltonian.md) / [`Observable`](7_hamiltonian.md) | A sum of Pauli strings with coefficients, and the unified interface for observables. | + +### Evolution and metrics + +| Name | Description | +| --- | --- | +| [`PauliEvolution`](8_evolution.md) / [`TrotterMode`](8_evolution.md) | Construction of Pauli string evolution circuits and the Trotter decomposition mode. | +| [`metrics`](9_metrics_entropy.md) / [`entropy`](9_metrics_entropy.md) | Fidelity, purity, trace distance, and entropy and entanglement metrics. | + +--- + +## Quick examples + +### 1. Pure and mixed states give the same probability distribution + +```rust +use cqlib_core::qis::{DensityMatrix, Statevector}; + +let mut sv = Statevector::new(2); +sv.apply_h(0).unwrap(); +sv.apply_cx(0, 1).unwrap(); + +let mut dm = DensityMatrix::new(2); +dm.apply_h(0).unwrap(); +dm.apply_cx(0, 1).unwrap(); + +let probs_sv = sv.probabilities(); +let probs_dm = dm.probabilities(); +for (a, b) in probs_sv.iter().zip(probs_dm.iter()) { + assert!((a - b).abs() < 1e-10); +} +``` + +### 2. Stabilizer state sampling + +```rust +use cqlib_core::qis::StabilizerState; +use std::collections::HashMap; + +let mut state = StabilizerState::new(2); +state.apply_h(0).unwrap(); +state.apply_cx(0, 1).unwrap(); + +let mut counts = HashMap::new(); +for outcome in state.sample_shots(1000) { + *counts.entry(outcome.to_bitstring(2)).or_insert(0usize) += 1; +} + +// Bell 态的采样结果只会出现关联结果 +assert!(counts.keys().all(|bits| bits == "00" || bits == "11")); +``` + +### 3. Compute the expectation value of an observable + +```rust +use cqlib_core::qis::{Hamiltonian, PauliString, Statevector}; + +let mut sv = Statevector::new(2); +sv.apply_h(0).unwrap(); +sv.apply_cx(0, 1).unwrap(); + +let mut h = Hamiltonian::new(2); +h.add_term("ZZ".parse::().unwrap(), 0.5.into()).unwrap(); + +let exp = sv.expectation(&h).unwrap(); +assert!(exp.is_finite()); +``` + +--- + +## Validation and error handling + +`QisError` is the unified error type of `cqlib_core::qis`; failures in state construction, gate application, measurement and expectation value computation are all returned through it. + +| Error | When it occurs | +| --- | --- | +| `QisError::QubitMismatch` | The number of qubits of an operator or circuit does not match that of the state. | +| `QisError::DimensionMismatch` | The dimensions of a matrix or vector do not match. | +| `QisError::InvalidStateDimension` | The length of the statevector is not a power of 2, or does not match the declared number of qubits. | +| `QisError::InvalidProbability` | A probability value is outside `[0, 1]`. | +| `QisError::IndexOutOfBounds` | The accessed qubit or amplitude index is out of range. | +| `QisError::UnsupportedOperation` | The current simulator does not support the operation. | +| `QisError::PauliStringParseError` | Pauli string parsing failed. | +| `QisError::NotNormalized` | An explicitly provided state is not normalized. | +| `QisError::NotHermitian` | An operator required to be Hermitian is not self-adjoint. | +| `QisError::NotPositiveSemidefinite` | The density matrix is not positive semidefinite. | +| `QisError::InvalidParameterValue` | A parameter value is invalid. | +| `QisError::InvalidSubsystem` | The subsystem description in an entanglement metric is invalid. | +| `QisError::UnsupportedDimension` | The dimension required by the operation is not satisfied. | +| `QisError::NonCliffordGate` | A non-Clifford gate was applied to a stabilizer state. | +| `QisError::CircuitError` | Circuit execution failed, for example a gate has no matrix representation or an unresolved symbolic parameter is present. | + +See [Statevector](1_statevector.md), [DensityMatrix](2_density_matrix.md), [DensityMatrixNoise](3_density_matrix_noise.md) and [StabilizerState](4_stabilizer.md) for the specific trigger conditions on each page. diff --git a/docs/api/en/rust/3_qis/1_statevector.md b/docs/api/en/rust/3_qis/1_statevector.md new file mode 100644 index 00000000..bc6a0a7a --- /dev/null +++ b/docs/api/en/rust/3_qis/1_statevector.md @@ -0,0 +1,269 @@ +# Statevector + +`cqlib_core::qis::state` + +This page covers the pure state simulator `Statevector`: construction, amplitude access, entry points for interfacing with circuits, the various gate operations, the measurement and sampling interfaces, and expectation value computation. + +## Import + +```rust +use cqlib_core::qis::Statevector; +``` + +--- + +## Statevector + +Pure state simulator, representing a quantum state as `2^num_qubits` complex amplitudes. + +```rust +pub struct Statevector { + pub num_qubits: usize, +} +``` + +Fields: + +- `num_qubits` (`usize`): the number of qubits. + +The amplitudes are stored in a private aligned buffer of length `2^num_qubits`; component `i` is the coefficient of the computational basis state `|i>`, and qubit 0 corresponds to the least significant bit. The amplitudes are accessed through `data()` and `data_mut()`. Implements `Debug` and `Clone`. + +Methods: + +### Construction and amplitude access + +- `fn new(num_qubits: usize) -> Self`: construct the `|0...0>` state with the first component set to 1 and the rest 0. +- `fn from_state(num_qubits: usize, initial_state: Vec) -> Result`: construct from the given amplitudes; the length must equal `2^num_qubits` and the state must already be normalized. +- `fn data(&self) -> &[Complex64]`: read all amplitudes as a shared slice. +- `fn data_mut(&mut self) -> &mut [Complex64]`: read all amplitudes as a mutable slice; modification may break normalization. +- `fn probabilities(&self) -> Vec`: return the measurement probability distribution over all computational basis states, that is the squared modulus of each amplitude. + +### Interfacing with circuits + +- `fn from_circuit(circuit: &Circuit) -> Result`: execute the circuit and return the evolved state; the input circuit is not modified. +- `fn apply_circuit(&mut self, circuit: &Circuit) -> Result<(), QisError>`: apply the circuit in place to the current state; the number of qubits of the circuit must match that of the state. + +Both entry points decompose the circuit first and then execute its instructions one by one. Standard gates, controlled gates, multi-controlled gates, unitary gates with a matrix representation and `Reset` are supported; barrier and delay instructions are ignored, and measurement declarations produced by `Circuit::measure*` do not participate in the state evolution. + +### General gate entry points + +- `fn apply_standard_gate(&mut self, gate: StandardGate, qubits: &[usize], params: &[f64]) -> Result<(), QisError>`: dispatch to the corresponding dedicated implementation by the standard gate enum; the number of `qubits` and the number of `params` must each equal the value required by `gate`. +- `fn apply_single_qubit_gate(&mut self, qubit: usize, matrix: [[Complex64; 2]; 2]) -> Result<(), QisError>`: apply an arbitrary 2×2 unitary matrix. +- `fn apply_two_qubit_gate(&mut self, q0: usize, q1: usize, matrix: [[Complex64; 4]; 4]) -> Result<(), QisError>`: apply an arbitrary 4×4 unitary matrix with basis order `|00>`, `|01>`, `|10>`, `|11>`, where `q0` is the high-order qubit. +- `fn apply_unitary_gate(&mut self, qubits: &[usize], matrix: &ndarray::Array2) -> Result<(), QisError>`: apply an arbitrary `2^n × 2^n` unitary matrix, with `qubits` giving the qubits the matrix acts on. +- `fn apply_pauli_rotation(&mut self, pauli: &PauliString, theta: f64) -> Result<(), QisError>`: apply `exp(-i·θ/2·P)` in place, where `P` is a Hermitian Pauli string; this method does not decompose the exponential into a sequence of elementary gates. + +### Single-qubit gates + +- `fn apply_x(&mut self, qubit: usize) -> Result<(), QisError>`: Pauli-X. +- `fn apply_y(&mut self, qubit: usize) -> Result<(), QisError>`: Pauli-Y. +- `fn apply_z(&mut self, qubit: usize) -> Result<(), QisError>`: Pauli-Z. +- `fn apply_h(&mut self, qubit: usize) -> Result<(), QisError>`: Hadamard gate. +- `fn apply_s(&mut self, qubit: usize) -> Result<(), QisError>`: S gate. +- `fn apply_sdg(&mut self, qubit: usize) -> Result<(), QisError>`: S† gate. +- `fn apply_t(&mut self, qubit: usize) -> Result<(), QisError>`: T gate. +- `fn apply_tdg(&mut self, qubit: usize) -> Result<(), QisError>`: T† gate. +- `fn apply_u(&mut self, qubit: usize, theta: f64, phi: f64, lambda: f64) -> Result<(), QisError>`: general single-qubit gate `U(θ, φ, λ)`. +- `fn apply_phase(&mut self, qubit: usize, theta: f64) -> Result<(), QisError>`: phase gate `P(θ)`. +- `fn apply_rx(&mut self, qubit: usize, theta: f64) -> Result<(), QisError>`: rotation around the X axis by `θ`. +- `fn apply_ry(&mut self, qubit: usize, theta: f64) -> Result<(), QisError>`: rotation around the Y axis by `θ`. +- `fn apply_rz(&mut self, qubit: usize, theta: f64) -> Result<(), QisError>`: rotation around the Z axis by `θ`. +- `fn apply_x2p(&mut self, qubit: usize) -> Result<(), QisError>`: `Rx(π/2)`. +- `fn apply_x2m(&mut self, qubit: usize) -> Result<(), QisError>`: `Rx(-π/2)`. +- `fn apply_y2p(&mut self, qubit: usize) -> Result<(), QisError>`: `Ry(π/2)`. +- `fn apply_y2m(&mut self, qubit: usize) -> Result<(), QisError>`: `Ry(-π/2)`. +- `fn apply_xy2p(&mut self, qubit: usize, theta: f64) -> Result<(), QisError>`: `XY2P(θ)`. +- `fn apply_xy2m(&mut self, qubit: usize, theta: f64) -> Result<(), QisError>`: `XY2M(θ)`. +- `fn apply_xy(&mut self, qubit: usize, theta: f64) -> Result<(), QisError>`: `XY(θ)`, a different gate from `XY2P` and `XY2M`. +- `fn apply_rxy(&mut self, qubit: usize, theta: f64, phi: f64) -> Result<(), QisError>`: rotate by `theta` around the axis in the XY plane at azimuth `phi`. +- `fn apply_gphase(&mut self, phi: f64) -> Result<(), QisError>`: multiply the whole state by the global phase `e^(iφ)`. + +### Two-qubit gates + +- `fn apply_swap(&mut self, q0: usize, q1: usize) -> Result<(), QisError>`: swap two qubits. +- `fn apply_cx(&mut self, control: usize, target: usize) -> Result<(), QisError>`: controlled X gate. +- `fn apply_cy(&mut self, control: usize, target: usize) -> Result<(), QisError>`: controlled Y gate. +- `fn apply_cz(&mut self, q0: usize, q1: usize) -> Result<(), QisError>`: controlled Z gate; the two parameters are symmetric. +- `fn apply_crx(&mut self, control: usize, target: usize, theta: f64) -> Result<(), QisError>`: controlled `Rx(θ)`. +- `fn apply_cry(&mut self, control: usize, target: usize, theta: f64) -> Result<(), QisError>`: controlled `Ry(θ)`. +- `fn apply_crz(&mut self, control: usize, target: usize, theta: f64) -> Result<(), QisError>`: controlled `Rz(θ)`. +- `fn apply_rxx(&mut self, q0: usize, q1: usize, theta: f64) -> Result<(), QisError>`: `exp(-i·θ/2·X⊗X)`. +- `fn apply_ryy(&mut self, q0: usize, q1: usize, theta: f64) -> Result<(), QisError>`: `exp(-i·θ/2·Y⊗Y)`. +- `fn apply_rzz(&mut self, q0: usize, q1: usize, theta: f64) -> Result<(), QisError>`: `exp(-i·θ/2·Z⊗Z)`. +- `fn apply_rzx(&mut self, q0: usize, q1: usize, theta: f64) -> Result<(), QisError>`: `exp(-i·θ/2·Z⊗X)`. +- `fn apply_fsim(&mut self, q0: usize, q1: usize, theta: f64, phi: f64) -> Result<(), QisError>`: fermionic simulation gate, where `theta` is the iSWAP angle and `phi` is the controlled phase angle. + +### Three-qubit gates + +- `fn apply_ccx(&mut self, c0: usize, c1: usize, target: usize) -> Result<(), QisError>`: Toffoli gate; flips the target qubit when both control qubits are `|1>`. + +### Measurement, reset and sampling + +- `fn measure(&mut self, qubit: usize) -> Result`: measure the given qubit in the Z basis and collapse the state; returns `false` for outcome `|0>` and `true` for outcome `|1>`; after measurement the state is renormalized within the corresponding subspace. This operation is destructive. +- `fn measure_with_rng(&mut self, qubit: usize, rng: &mut impl Rng) -> Result`: measure using the random number generator provided by the caller, for reproducibility. +- `fn measure_all(&mut self) -> Outcome`: measure one by one in the order `0..num_qubits` and return a bit-packed `Outcome`. +- `fn reset(&mut self, qubit: usize) -> Result<(), QisError>`: reset the given qubit to `|0>`, implemented as a Z-basis measurement followed by an X correction when the outcome is `|1>`. +- `fn sample_shots(&self, shots: usize) -> Vec`: sample several independent measurement outcomes in parallel without modifying `self`. +- `fn sample(&self, measurement: &Measurement, shots: usize) -> Result`: sample according to the output convention specified by the circuit `Measurement`; `measurement.qubits()[i]` corresponds to bit `i` of each `Outcome`. +- `fn probs(&self, measurement: &Measurement) -> Result, QisError>`: return the marginal probability distribution selected by `Measurement`; this method does not execute a circuit and uses `Measurement` only as an output convention. + +### Expectation value + +- `fn expectation(&self, h: &dyn Observable) -> Result`: compute `⟨ψ|H|ψ⟩`. + +--- + +## Example + +### 1. Prepare a Bell state + +```rust +use cqlib_core::qis::Statevector; +use num_complex::Complex64; +use std::f64::consts::FRAC_1_SQRT_2; + +let mut sv = Statevector::new(2); +sv.apply_h(0).unwrap(); +sv.apply_cx(0, 1).unwrap(); + +// (|00⟩ + |11⟩)/√2 +assert_eq!(sv.num_qubits, 2); +assert!((sv.data()[0] - Complex64::new(FRAC_1_SQRT_2, 0.0)).norm() < 1e-10); +assert!((sv.data()[3] - Complex64::new(FRAC_1_SQRT_2, 0.0)).norm() < 1e-10); + +let probs = sv.probabilities(); +assert!((probs[0] - 0.5).abs() < 1e-10); +assert!((probs[3] - 0.5).abs() < 1e-10); +``` + +### 2. Construct from a circuit and apply a circuit in place + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::qis::Statevector; + +let mut circuit = Circuit::new(2); +circuit.h(Qubit::new(0)).unwrap(); +circuit.cx(Qubit::new(0), Qubit::new(1)).unwrap(); + +let sv = Statevector::from_circuit(&circuit).unwrap(); +assert_eq!(sv.num_qubits, 2); + +let mut sv2 = Statevector::new(2); +sv2.apply_circuit(&circuit).unwrap(); +assert!(sv2.data().iter().zip(sv.data()).all(|(a, b)| (a - b).norm() < 1e-10)); +``` + +### 3. Apply a custom unitary gate + +```rust +use cqlib_core::qis::Statevector; +use num_complex::Complex64; +use std::f64::consts::FRAC_1_SQRT_2; + +// H = [[1/√2, 1/√2], [1/√2, -1/√2]] +let h_matrix = [ + [Complex64::new(FRAC_1_SQRT_2, 0.0), Complex64::new(FRAC_1_SQRT_2, 0.0)], + [Complex64::new(FRAC_1_SQRT_2, 0.0), Complex64::new(-FRAC_1_SQRT_2, 0.0)], +]; + +let mut sv = Statevector::new(1); +sv.apply_single_qubit_gate(0, h_matrix).unwrap(); + +assert!((sv.data()[0] - Complex64::new(FRAC_1_SQRT_2, 0.0)).norm() < 1e-10); +assert!((sv.data()[1] - Complex64::new(FRAC_1_SQRT_2, 0.0)).norm() < 1e-10); +``` + +### 4. Apply a multi-qubit unitary gate + +```rust +use cqlib_core::qis::Statevector; +use ndarray::Array2; + +let mut sv = Statevector::new(2); +sv.apply_h(0).unwrap(); +let before: Vec<_> = sv.data().to_vec(); + +// 4×4 单位矩阵作用在比特 [0, 1] 上 +let identity = Array2::eye(4); +sv.apply_unitary_gate(&[0, 1], &identity).unwrap(); + +assert!(sv.data().iter().zip(before.iter()).all(|(a, b)| (a - b).norm() < 1e-10)); +``` + +### 5. Pauli string rotation + +```rust +use cqlib_core::qis::{Pauli, PauliString, Statevector}; + +let mut sv = Statevector::new(2); +sv.apply_h(0).unwrap(); + +let mut pauli = PauliString::new(2); +pauli.set_pauli(0, Pauli::X); +pauli.set_pauli(1, Pauli::Z); + +sv.apply_pauli_rotation(&pauli, 0.413).unwrap(); + +let norm: f64 = sv.data().iter().map(|amp| amp.norm_sqr()).sum(); +assert!((norm - 1.0).abs() < 1e-10); +``` + +### 6. Measurement and sampling + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::qis::Statevector; + +let mut sv = Statevector::new(4); +sv.apply_x(0).unwrap(); +sv.apply_x(2).unwrap(); + +let outcome = sv.measure_all(); +assert!(outcome.is_one(0)); +assert!(!outcome.is_one(1)); +assert!(outcome.is_one(2)); +assert!(!outcome.is_one(3)); + +// 按 Measurement 指定的比特顺序采样 +let mut circuit = Circuit::new(2); +circuit.x(Qubit::new(0)).unwrap(); +let out = circuit.measure_bits([Qubit::new(1), Qubit::new(0)]).unwrap(); + +let sv = Statevector::from_circuit(&circuit).unwrap(); +let result = sv.sample(&out, 16).unwrap(); +assert_eq!(result.shots(), 16); +``` + +### 7. Compute an expectation value + +```rust +use cqlib_core::qis::{Hamiltonian, Pauli, PauliString, Statevector}; + +let sv = Statevector::new(1); + +let mut ps = PauliString::new(1); +ps.set_pauli(0, Pauli::Z); +let h = Hamiltonian::from_pauli(ps); + +let exp = sv.expectation(&h).unwrap(); +assert!((exp - 1.0).abs() < 1e-10); +``` + +--- + +## Validation and error handling + +Failures in construction, gate application, measurement and expectation value computation are all returned through `QisError`, and no panics are used. + +| Error | When it occurs | +| --- | --- | +| `QisError::InvalidStateDimension` | The number of amplitudes of `from_state` does not equal `2^num_qubits`; the number of circuit qubits of `apply_circuit` does not match that of the state. | +| `QisError::NotNormalized` | The amplitudes passed to `from_state` are not normalized. | +| `QisError::IndexOutOfBounds` | The qubit index is out of range. | +| `QisError::InvalidParameterValue` | A gate is applied to a zero-qubit system; a two-qubit gate is given the same qubit twice; the qubit list of `apply_unitary_gate` contains duplicates; the number of qubits or parameters of `apply_standard_gate` does not match the gate. | +| `QisError::UnsupportedOperation` | The circuit contains an operation the current entry point does not support, for example classical control flow. | +| `QisError::CircuitError` | A gate in the circuit has no matrix representation, contains an unresolved symbolic parameter, or references a qubit that does not exist. | +| `QisError::QubitMismatch` | The number of qubits the observable of `expectation` acts on does not match that of the state. | + +Unnormalized amplitudes, a gate operation on a zero-qubit system, and a two-qubit gate formed from the same qubit are all input errors on the caller's side and can usually be avoided before running. diff --git a/docs/api/en/rust/3_qis/2_density_matrix.md b/docs/api/en/rust/3_qis/2_density_matrix.md new file mode 100644 index 00000000..8a1a4602 --- /dev/null +++ b/docs/api/en/rust/3_qis/2_density_matrix.md @@ -0,0 +1,274 @@ +# DensityMatrix + +`cqlib_core::qis::state` + +This page covers the mixed state simulator `DensityMatrix`: construction, matrix data access, physicality validation, entry points for interfacing with circuits, the various gate operations, quantum channels and reduction, the measurement and sampling interfaces, and expectation value computation. + +## Import + +```rust +use cqlib_core::qis::DensityMatrix; +``` + +--- + +## DensityMatrix + +Mixed state simulator, representing a quantum state as a `2^N × 2^N` density matrix. Compared with a pure state described by a single set of complex amplitudes, a density matrix can represent pure states, mixed states and the result of applying a quantum channel. + +```rust +pub struct DensityMatrix { + pub num_qubits: usize, +} +``` + +Fields: + +- `num_qubits` (`usize`): the number of qubits. + +The matrix elements are stored in row-major order in a private buffer of length `4^N`, accessed through `data()`. Implements `Debug`, `Clone` and `AddAssign` (`+=`). + +Methods: + +### Construction and data access + +- `fn new(num_qubits: usize) -> Self`: construct the pure state `|0...0><0...0|`. +- `fn maximally_mixed(num_qubits: usize) -> Self`: construct the maximally mixed state `I / 2^N`, where every computational basis state is equally probable and there are no coherence terms. +- `fn zeros(num_qubits: usize) -> Self`: construct an all-zero matrix; it is not a valid physical state (trace 0) and is used as the accumulation starting point for operations such as quantum channels. +- `fn from_state(num_qubits: usize, initial_state: Vec) -> Result`: construct by the outer product of pure state amplitudes `ρ = |ψ><ψ|`. +- `fn from_density_matrix_state(num_qubits: usize, dm_state: Vec) -> Result`: construct directly from a flattened `2^N × 2^N` matrix, validating Hermiticity, positive semidefiniteness and unit trace. +- `fn data(&self) -> &[Complex64]`: read the flattened matrix elements as a shared slice. +- `fn probabilities(&self) -> Vec`: return the diagonal entries of the density matrix, that is the measurement probabilities on each computational basis state. + +### Physicality validation + +- `fn trace(&self) -> Complex64`: return the trace; the trace of a valid physical state equals `1.0`. +- `fn is_hermitian(&self, tol: f64) -> bool`: determine whether `ρ = ρ†` holds. +- `fn is_positive_semidefinite_approx(&self, tol: f64) -> bool`: determine whether all eigenvalues are not less than `-tol`; returns `false` without panicking when NaN or Inf is present. +- `fn validate_physical(&self, tol: f64) -> Result<(), QisError>`: validate Hermiticity, positive semidefiniteness and unit trace in order. + +### Interfacing with circuits + +- `fn from_circuit(circuit: &Circuit) -> Result`: execute the circuit and return the evolved density matrix. +- `fn apply_circuit(&mut self, circuit: &Circuit) -> Result<(), QisError>`: apply the circuit in place to the current density matrix; the number of qubits of the circuit must match that of the state. + +The two entry points support the same instructions as `Statevector`: standard gates, controlled gates, multi-controlled gates, unitary gates with a matrix representation and `Reset`; barriers and delays are ignored, and measurement declarations produced by `Circuit::measure*` do not participate in the state evolution. + +### General gate entry points + +- `fn apply_standard_gate(&mut self, gate: StandardGate, qubits: &[usize], params: &[f64]) -> Result<(), QisError>`: dispatch by the standard gate enum; the number of `qubits` and `params` must each equal the value required by `gate`. +- `fn apply_single_qubit_gate(&mut self, qubit: usize, matrix: [[Complex64; 2]; 2]) -> Result<(), QisError>`: apply an arbitrary 2×2 unitary matrix. +- `fn apply_two_qubit_gate(&mut self, q0: usize, q1: usize, matrix: [[Complex64; 4]; 4]) -> Result<(), QisError>`: apply an arbitrary 4×4 unitary matrix. +- `fn apply_unitary_gate(&mut self, qubits: &[usize], matrix: &ndarray::Array2) -> Result<(), QisError>`: apply an arbitrary `2^n × 2^n` unitary matrix as `ρ → U ρ U†`. + +### Single-qubit gates + +- `fn apply_x(&mut self, qubit: usize) -> Result<(), QisError>`: Pauli-X. +- `fn apply_y(&mut self, qubit: usize) -> Result<(), QisError>`: Pauli-Y. +- `fn apply_z(&mut self, qubit: usize) -> Result<(), QisError>`: Pauli-Z. +- `fn apply_h(&mut self, qubit: usize) -> Result<(), QisError>`: Hadamard gate. +- `fn apply_s(&mut self, qubit: usize) -> Result<(), QisError>`: S gate. +- `fn apply_sdg(&mut self, qubit: usize) -> Result<(), QisError>`: S† gate. +- `fn apply_t(&mut self, qubit: usize) -> Result<(), QisError>`: T gate. +- `fn apply_tdg(&mut self, qubit: usize) -> Result<(), QisError>`: T† gate. +- `fn apply_u(&mut self, qubit: usize, theta: f64, phi: f64, lambda: f64) -> Result<(), QisError>`: general single-qubit gate `U(θ, φ, λ)`. +- `fn apply_phase(&mut self, qubit: usize, theta: f64) -> Result<(), QisError>`: phase gate `P(θ)`. +- `fn apply_rx(&mut self, qubit: usize, theta: f64) -> Result<(), QisError>`: rotation around the X axis by `θ`. +- `fn apply_ry(&mut self, qubit: usize, theta: f64) -> Result<(), QisError>`: rotation around the Y axis by `θ`. +- `fn apply_rz(&mut self, qubit: usize, theta: f64) -> Result<(), QisError>`: rotation around the Z axis by `θ`. +- `fn apply_x2p(&mut self, qubit: usize) -> Result<(), QisError>`: `Rx(π/2)`. +- `fn apply_x2m(&mut self, qubit: usize) -> Result<(), QisError>`: `Rx(-π/2)`. +- `fn apply_y2p(&mut self, qubit: usize) -> Result<(), QisError>`: `Ry(π/2)`. +- `fn apply_y2m(&mut self, qubit: usize) -> Result<(), QisError>`: `Ry(-π/2)`. +- `fn apply_xy2p(&mut self, qubit: usize, theta: f64) -> Result<(), QisError>`: `XY2P(θ)`. +- `fn apply_xy2m(&mut self, qubit: usize, theta: f64) -> Result<(), QisError>`: `XY2M(θ)`. +- `fn apply_xy(&mut self, qubit: usize, theta: f64) -> Result<(), QisError>`: `XY(θ)`. +- `fn apply_rxy(&mut self, qubit: usize, theta: f64, phi: f64) -> Result<(), QisError>`: rotate around the specified axis in the XY plane. +- `fn apply_gphase(&mut self, phi: f64)`: global phase has no observable effect on a density matrix; this method does not change the state and returns no result. + +### Two-qubit and three-qubit gates + +- `fn apply_swap(&mut self, q0: usize, q1: usize) -> Result<(), QisError>`: swap two qubits. +- `fn apply_cx(&mut self, control: usize, target: usize) -> Result<(), QisError>`: controlled X gate. +- `fn apply_cy(&mut self, control: usize, target: usize) -> Result<(), QisError>`: controlled Y gate. +- `fn apply_cz(&mut self, q0: usize, q1: usize) -> Result<(), QisError>`: controlled Z gate. +- `fn apply_crx(&mut self, control: usize, target: usize, theta: f64) -> Result<(), QisError>`: controlled `Rx(θ)`. +- `fn apply_cry(&mut self, control: usize, target: usize, theta: f64) -> Result<(), QisError>`: controlled `Ry(θ)`. +- `fn apply_crz(&mut self, control: usize, target: usize, theta: f64) -> Result<(), QisError>`: controlled `Rz(θ)`. +- `fn apply_rxx(&mut self, q0: usize, q1: usize, theta: f64) -> Result<(), QisError>`: `exp(-i·θ/2·X⊗X)`. +- `fn apply_ryy(&mut self, q0: usize, q1: usize, theta: f64) -> Result<(), QisError>`: `exp(-i·θ/2·Y⊗Y)`. +- `fn apply_rzz(&mut self, q0: usize, q1: usize, theta: f64) -> Result<(), QisError>`: `exp(-i·θ/2·Z⊗Z)`. +- `fn apply_rzx(&mut self, q0: usize, q1: usize, theta: f64) -> Result<(), QisError>`: `exp(-i·θ/2·Z⊗X)`. +- `fn apply_fsim(&mut self, q0: usize, q1: usize, theta: f64, phi: f64) -> Result<(), QisError>`: fermionic simulation gate. +- `fn apply_ccx(&mut self, c0: usize, c1: usize, target: usize) -> Result<(), QisError>`: Toffoli gate. + +### Quantum channels and reduction + +- `fn apply_kraus(&mut self, ops: &[Vec], qs: &[usize]) -> Result<(), QisError>`: apply the quantum channel given by Kraus operators, evolving as `ρ → Σ_k K_k ρ K_k†`; each operator is given as a flattened vector, and the number of qubits is determined by `qs.len()`. +- `fn partial_trace(&self, keep: &[usize]) -> Result`: compute the partial trace over the qubits outside `keep`, returning a new density matrix with `keep.len()` qubits. + +### Measurement, reset and sampling + +- `fn measure(&mut self, qubit: usize) -> Result`: measure in the Z basis and collapse the density matrix; returns `true` for outcome `|1>` and `false` for outcome `|0>`; after collapse it is renormalized as `ρ' = Π_b ρ Π_b / Tr(Π_b ρ)`. +- `fn measure_all(&mut self) -> Outcome`: measure one by one in the order `0..num_qubits` and return a bit-packed `Outcome`. +- `fn reset(&mut self, qubit: usize) -> Result<(), QisError>`: reset the given qubit to `|0>`. +- `fn sample_shots(&self, shots: usize) -> Vec`: sample several independent measurement outcomes in parallel without modifying `self`. +- `fn sample(&self, measurement: &Measurement, shots: usize) -> Result`: sample according to the output convention specified by the circuit `Measurement`. +- `fn probs(&self, measurement: &Measurement) -> Result, QisError>`: return the marginal probability distribution selected by `Measurement`. + +### Expectation value + +- `fn expectation(&self, h: &dyn Observable) -> Result`: compute `Tr(ρ·H)`. + +--- + +## Example + +### 1. Prepare a state and read the probabilities + +```rust +use cqlib_core::qis::DensityMatrix; + +let mut dm = DensityMatrix::new(1); +dm.apply_h(0).unwrap(); + +let probs = dm.probabilities(); +assert_eq!(probs.len(), 2); +assert!((probs[0] - 0.5).abs() < 1e-10); +assert!((probs[1] - 0.5).abs() < 1e-10); +``` + +### 2. Maximally mixed state and trace + +```rust +use cqlib_core::qis::DensityMatrix; + +let dm = DensityMatrix::maximally_mixed(2); +assert_eq!(dm.num_qubits, 2); +assert_eq!(dm.data().len(), 16); +assert!((dm.trace().re - 1.0).abs() < 1e-10); +``` + +### 3. Construction from a pure state and from a matrix + +```rust +use cqlib_core::qis::DensityMatrix; +use num_complex::Complex64; + +// 由纯态振幅构造 +let state = vec![Complex64::new(0.0, 0.0), Complex64::new(1.0, 0.0)]; +let dm = DensityMatrix::from_state(1, state).unwrap(); +assert!((dm.data()[0].re - 0.0).abs() < 1e-10); +assert!((dm.data()[3].re - 1.0).abs() < 1e-10); + +// 由展平的密度矩阵构造 +let mut flat = vec![Complex64::new(0.0, 0.0); 4]; +flat[0] = Complex64::new(0.5, 0.0); +flat[3] = Complex64::new(0.5, 0.0); +let mixed = DensityMatrix::from_density_matrix_state(1, flat).unwrap(); +assert!((mixed.probabilities()[0] - 0.5).abs() < 1e-10); +assert!((mixed.probabilities()[1] - 0.5).abs() < 1e-10); +``` + +### 4. Validate physicality + +```rust +use cqlib_core::qis::DensityMatrix; + +let mut dm = DensityMatrix::new(1); +dm.apply_h(0).unwrap(); + +assert!(dm.is_hermitian(1e-10)); +assert!(dm.is_positive_semidefinite_approx(1e-10)); +assert!(dm.validate_physical(1e-10).is_ok()); +``` + +### 5. Apply a quantum channel + +```rust +use cqlib_core::qis::DensityMatrix; +use num_complex::Complex64; + +let p: f64 = 0.3; +let k0 = vec![ + Complex64::new((1.0 - p).sqrt(), 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new((1.0 - p).sqrt(), 0.0), +]; +let k1 = vec![ + Complex64::new(0.0, 0.0), + Complex64::new(p.sqrt(), 0.0), + Complex64::new(p.sqrt(), 0.0), + Complex64::new(0.0, 0.0), +]; + +let mut dm = DensityMatrix::new(1); +dm.apply_kraus(&[k0, k1], &[0]).unwrap(); + +assert!((dm.trace().re - 1.0).abs() < 1e-10); +``` + +### 6. Compute a partial trace + +```rust +use cqlib_core::qis::DensityMatrix; + +let mut dm = DensityMatrix::new(2); +dm.apply_h(0).unwrap(); +dm.apply_cx(0, 1).unwrap(); + +// 追踪掉比特 1,比特 0 变成最大混合态 +let reduced = dm.partial_trace(&[0]).unwrap(); +assert_eq!(reduced.num_qubits, 1); +assert!((reduced.probabilities()[0] - 0.5).abs() < 1e-10); +assert!((reduced.probabilities()[1] - 0.5).abs() < 1e-10); +``` + +### 7. Accumulate density matrices + +```rust +use cqlib_core::qis::DensityMatrix; + +// 以全零矩阵作为累加起点 +let mut acc = DensityMatrix::zeros(1); +acc += DensityMatrix::new(1); + +assert!((acc.trace().re - 1.0).abs() < 1e-10); +assert!((acc.probabilities()[0] - 1.0).abs() < 1e-10); +``` + +### 8. Expectation value + +```rust +use cqlib_core::qis::{DensityMatrix, Hamiltonian, Pauli, PauliString}; + +let mut dm = DensityMatrix::new(2); +dm.apply_h(0).unwrap(); +dm.apply_cx(0, 1).unwrap(); + +let mut ps = PauliString::new(2); +ps.set_pauli(0, Pauli::Z); +ps.set_pauli(1, Pauli::Z); +let h = Hamiltonian::from_pauli(ps); + +let exp = dm.expectation(&h).unwrap(); +assert!((exp - 1.0).abs() < 1e-10); +``` + +--- + +## Validation and error handling + +| Error | When it occurs | +| --- | --- | +| `QisError::InvalidStateDimension` | The number of amplitudes of `from_state` does not equal `2^num_qubits`; the matrix length of `from_density_matrix_state` does not equal `4^num_qubits`; the number of circuit qubits of `apply_circuit` does not match that of the state. | +| `QisError::NotNormalized` | A trace not equal to 1 detected by `from_state`, `from_density_matrix_state` or `validate_physical`. | +| `QisError::NotHermitian` | A non-self-adjoint matrix detected by `from_density_matrix_state` or `validate_physical`. | +| `QisError::NotPositiveSemidefinite` | A negative eigenvalue detected by `from_density_matrix_state` or `validate_physical`. | +| `QisError::IndexOutOfBounds` | The qubit index is out of range. | +| `QisError::InvalidParameterValue` | A gate operation on a zero-qubit system; a two-qubit gate given the same qubit twice; the operator list of `apply_kraus` is empty, the qubit list is empty, or the operator size does not match the number of qubits; the kept-qubit list of `partial_trace` contains duplicates; the number of qubits or parameters of `apply_standard_gate` does not match the gate. | +| `QisError::UnsupportedOperation` | The circuit contains an operation the current entry point does not support. | +| `QisError::CircuitError` | A gate in the circuit has no matrix representation, contains an unresolved symbolic parameter, or references a qubit that does not exist; the number of qubits of the observable of `expectation` does not match. | + +`+=` requires both density matrices to have the same number of qubits; a mismatch panics directly, so the caller must confirm the qubit counts before the call. diff --git a/docs/api/en/rust/3_qis/3_density_matrix_noise.md b/docs/api/en/rust/3_qis/3_density_matrix_noise.md new file mode 100644 index 00000000..aade08d4 --- /dev/null +++ b/docs/api/en/rust/3_qis/3_density_matrix_noise.md @@ -0,0 +1,242 @@ +# DensityMatrixNoise + +`cqlib_core::qis::state` + +This page covers the density matrix simulator with a noise model, `DensityMatrixNoise`: how the noise model is attached, the entry points that connect it to circuits, gate operations, the readout noise interface, and measurement, sampling and expectation value computation. + +## Import + +```rust +use cqlib_core::qis::DensityMatrixNoise; +use cqlib_core::device::{NoiseModel, ReadoutError, SingleQubitNoise, TwoQubitNoise}; +``` + +--- + +## DensityMatrixNoise + +A simulator that layers a noise model on top of the density matrix simulator. After every gate it queries the noise model for noise entries on the same gate and the same qubits and applies the corresponding channel; when no noise model is configured or no entry matches, only the ideal gate is applied. + +```rust +pub struct DensityMatrixNoise { + pub state: DensityMatrix, + pub noise_model: Option, +} +``` + +Fields: + +- `state` (`DensityMatrix`): the underlying density matrix state. +- `noise_model` (`Option`): the noise model applied to gate operations and readout; `None` means ideal simulation. + +Implements `Debug` and `Clone`. + +Methods: + +### Construction and circuit integration + +- `fn new(num_qubits: usize, noise_model: Option) -> Self`: construct from a qubit count and a noise model; the initial state is `|0...0><0...0|`. +- `fn from_circuit(circuit: &Circuit, noise_model: Option) -> Result`: execute a circuit and return the simulator; the circuit is first decomposed into basis gates, and noise is applied after every gate. +- `fn apply_circuit(&mut self, circuit: &Circuit) -> Result<(), QisError>`: apply a circuit to the current simulator in place; noise is applied according to the configured model. + +### Gate operations + +- `fn apply_standard_gate_noise(&mut self, gate: StandardGate, qs: &[usize], params: &[f64]) -> Result<(), QisError>`: the unified entry point for standard gates; it validates and applies the ideal gate first, then applies matching gate noise according to the noise model. +- `fn apply_x(&mut self, q: usize) -> Result<(), QisError>`: Pauli-X. +- `fn apply_y(&mut self, q: usize) -> Result<(), QisError>`: Pauli-Y. +- `fn apply_z(&mut self, q: usize) -> Result<(), QisError>`: Pauli-Z. +- `fn apply_h(&mut self, q: usize) -> Result<(), QisError>`: Hadamard gate. +- `fn apply_s(&mut self, q: usize) -> Result<(), QisError>`: S gate. +- `fn apply_sdg(&mut self, q: usize) -> Result<(), QisError>`: S† gate. +- `fn apply_t(&mut self, q: usize) -> Result<(), QisError>`: T gate. +- `fn apply_tdg(&mut self, q: usize) -> Result<(), QisError>`: T† gate. +- `fn apply_u(&mut self, q: usize, theta: f64, phi: f64, lam: f64) -> Result<(), QisError>`: general single-qubit gate. +- `fn apply_phase(&mut self, q: usize, theta: f64) -> Result<(), QisError>`: phase gate `P(θ)`. +- `fn apply_rx(&mut self, q: usize, theta: f64) -> Result<(), QisError>`: rotation around the X axis by `θ`. +- `fn apply_ry(&mut self, q: usize, theta: f64) -> Result<(), QisError>`: rotation around the Y axis by `θ`. +- `fn apply_rz(&mut self, q: usize, theta: f64) -> Result<(), QisError>`: rotation around the Z axis by `θ`. +- `fn apply_gphase(&mut self, theta: f64) -> Result<(), QisError>`: global phase. +- `fn apply_x2p(&mut self, q: usize) -> Result<(), QisError>`: `Rx(π/2)`. +- `fn apply_x2m(&mut self, q: usize) -> Result<(), QisError>`: `Rx(-π/2)`. +- `fn apply_y2p(&mut self, q: usize) -> Result<(), QisError>`: `Ry(π/2)`. +- `fn apply_y2m(&mut self, q: usize) -> Result<(), QisError>`: `Ry(-π/2)`. +- `fn apply_rxy(&mut self, q: usize, theta: f64, phi: f64) -> Result<(), QisError>`: rotation around the specified axis in the XY plane. +- `fn apply_xy2p(&mut self, q: usize, theta: f64) -> Result<(), QisError>`: `XY2P(θ)`. +- `fn apply_xy2m(&mut self, q: usize, theta: f64) -> Result<(), QisError>`: `XY2M(θ)`. +- `fn apply_xy(&mut self, q: usize, theta: f64) -> Result<(), QisError>`: `XY(θ)`. +- `fn apply_cx(&mut self, control: usize, target: usize) -> Result<(), QisError>`: controlled X gate. +- `fn apply_cy(&mut self, control: usize, target: usize) -> Result<(), QisError>`: controlled Y gate. +- `fn apply_cz(&mut self, q0: usize, q1: usize) -> Result<(), QisError>`: controlled Z gate. +- `fn apply_ccx(&mut self, c1: usize, c2: usize, t: usize) -> Result<(), QisError>`: Toffoli gate. +- `fn apply_swap(&mut self, q0: usize, q1: usize) -> Result<(), QisError>`: swap two qubits. +- `fn apply_crx(&mut self, control: usize, target: usize, theta: f64) -> Result<(), QisError>`: controlled `Rx(θ)`. +- `fn apply_cry(&mut self, control: usize, target: usize, theta: f64) -> Result<(), QisError>`: controlled `Ry(θ)`. +- `fn apply_crz(&mut self, control: usize, target: usize, theta: f64) -> Result<(), QisError>`: controlled `Rz(θ)`. +- `fn apply_rxx(&mut self, q0: usize, q1: usize, theta: f64) -> Result<(), QisError>`: `exp(-i·θ/2·X⊗X)`. +- `fn apply_ryy(&mut self, q0: usize, q1: usize, theta: f64) -> Result<(), QisError>`: `exp(-i·θ/2·Y⊗Y)`. +- `fn apply_rzz(&mut self, q0: usize, q1: usize, theta: f64) -> Result<(), QisError>`: `exp(-i·θ/2·Z⊗Z)`. +- `fn apply_rzx(&mut self, q0: usize, q1: usize, theta: f64) -> Result<(), QisError>`: `exp(-i·θ/2·Z⊗X)`. +- `fn apply_fsim(&mut self, q0: usize, q1: usize, theta: f64, phi: f64) -> Result<(), QisError>`: fermionic simulation gate. +- `fn apply_unitary_gate(&mut self, qs: &[usize], mat: &ndarray::Array2) -> Result<(), QisError>`: apply an arbitrary unitary matrix. This entry point has no corresponding standard gate type, so no gate noise is applied; when noise modeling is required, use the concrete gate methods. + +### Probabilities and readout noise + +The simulator expresses "noise on the quantum state" and "noise on readout" separately: the former acts on the density matrix itself, while the latter only changes the reported probability distribution. + +- `fn probabilities(&self) -> Vec`: return the diagonal entries of the density matrix, that is the measurement probability of each computational basis state. Includes gate noise already applied and excludes readout noise. +- `fn probabilities_with_readout(&self, qubits: &[usize]) -> Result, QisError>`: based on `probabilities`, apply readout noise to the qubits listed in `qubits`. +- `fn probs(&self, measurement: &Measurement) -> Result, QisError>`: return the marginal probability distribution selected by `Measurement`, excluding readout noise. +- `fn probs_with_readout(&self, measurement: &Measurement) -> Result, QisError>`: based on `probs`, apply readout noise to `measurement.qubits()`. + +### Measurement, sampling and expectation values + +- `fn measure(&mut self, qubit: usize) -> Result`: measure in the Z basis and collapse the underlying density matrix; the measurement operation applies no noise. +- `fn measure_all(&mut self) -> Outcome`: measure one by one in the order `0..num_qubits` and return a bit-packed `Outcome`. +- `fn reset(&mut self, qubit: usize) -> Result<(), QisError>`: reset the given qubit to `|0>`; reset applies no readout noise. +- `fn sample_shots(&self, shots: usize) -> Vec`: sample a number of independent measurement results in parallel; the quantum state itself is sampled, and no readout noise is applied. +- `fn sample(&self, measurement: &Measurement, shots: usize) -> Result`: sample according to the output convention specified by the circuit `Measurement`, likewise with no readout noise applied. +- `fn expectation(&self, h: &dyn Observable) -> Result`: compute `Tr(ρ·O)`; the result includes gate noise and excludes readout noise. + +### Configuring the noise model + +The noise model is provided by the device module; the configuration methods include: + +- `NoiseModel::new()`: construct an empty model. +- `add_single_qubit_error(StandardGate, Qubit, SingleQubitNoise) -> Result<(), NoiseError>`: configure noise for a single-qubit gate. +- `add_two_qubit_error(StandardGate, Qubit, Qubit, TwoQubitNoise) -> Result<(), NoiseError>`: configure noise for a two-qubit gate. +- `add_readout_error(Qubit, ReadoutError) -> Result<(), NoiseError>`: configure readout noise for the given qubit. + +For details of the discrete noise types, see the device module documentation. + +--- + +## Example + +### 1. Single-qubit simulation with bit flip noise + +```rust +use cqlib_core::circuit::{Qubit, StandardGate}; +use cqlib_core::device::{NoiseModel, SingleQubitNoise}; +use cqlib_core::qis::DensityMatrixNoise; + +let mut noise_model = NoiseModel::new(); +noise_model + .add_single_qubit_error(StandardGate::X, Qubit::new(0), SingleQubitNoise::BitFlip(0.1)) + .unwrap(); + +let mut sim = DensityMatrixNoise::new(1, Some(noise_model)); +sim.apply_x(0).unwrap(); + +// 10% 比特翻转使 P(|1>) 为 0.9 +let probs = sim.probabilities_with_readout(&[0]).unwrap(); +assert!((probs[1] - 0.9).abs() < 1e-6); +assert!((probs[0] - 0.1).abs() < 1e-6); +``` + +### 2. Construct a simulator from a circuit + +```rust +use cqlib_core::circuit::{Circuit, Qubit, StandardGate}; +use cqlib_core::device::{NoiseModel, SingleQubitNoise}; +use cqlib_core::qis::DensityMatrixNoise; + +let mut circuit = Circuit::new(1); +circuit.x(Qubit::new(0)).unwrap(); + +let mut noise_model = NoiseModel::new(); +noise_model + .add_single_qubit_error(StandardGate::X, Qubit::new(0), SingleQubitNoise::BitFlip(0.1)) + .unwrap(); + +let sim = DensityMatrixNoise::from_circuit(&circuit, Some(noise_model)).unwrap(); +assert_eq!(sim.state.num_qubits, 1); +assert!((sim.probabilities_with_readout(&[0]).unwrap()[1] - 0.9).abs() < 1e-6); +``` + +### 3. Readout noise + +```rust +use cqlib_core::circuit::Qubit; +use cqlib_core::device::{NoiseModel, ReadoutError}; +use cqlib_core::qis::DensityMatrixNoise; + +let mut noise_model = NoiseModel::new(); +noise_model + .add_readout_error( + Qubit::new(0), + ReadoutError { + p_0_given_1: 0.0, + p_1_given_0: 0.1, + }, + ) + .unwrap(); + +let sim = DensityMatrixNoise::new(1, Some(noise_model)); +let probs = sim.probabilities_with_readout(&[0]).unwrap(); + +// 真实态仍为 |0>,读出误差使 P(|1>) 为 0.1 +assert!((probs[1] - 0.1).abs() < 1e-6); +assert!((probs[0] - 0.9).abs() < 1e-6); +``` + +### 4. Distinguish the ideal distribution from the post-readout distribution + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::device::{NoiseModel, Outcome, ReadoutError}; +use cqlib_core::qis::DensityMatrixNoise; + +let mut circuit = Circuit::new(1); +circuit.x(Qubit::new(0)).unwrap(); +let out = circuit.measure(Qubit::new(0)).unwrap(); + +let mut noise_model = NoiseModel::new(); +noise_model + .add_readout_error( + Qubit::new(0), + ReadoutError { + p_0_given_1: 1.0, + p_1_given_0: 0.0, + }, + ) + .unwrap(); + +let sim = DensityMatrixNoise::from_circuit(&circuit, Some(noise_model)).unwrap(); +let ideal = sim.probs(&out).unwrap(); +let with_readout = sim.probs_with_readout(&out).unwrap(); + +assert!((ideal[&Outcome::from_bitstring("1").unwrap()] - 1.0).abs() < 1e-10); +assert!((with_readout[&Outcome::from_bitstring("0").unwrap()] - 1.0).abs() < 1e-10); +``` + +### 5. Sampling applies no readout noise + +```rust +use cqlib_core::qis::DensityMatrixNoise; + +let mut sim = DensityMatrixNoise::new(2, None); +sim.apply_h(0).unwrap(); +sim.apply_cx(0, 1).unwrap(); + +let shots = sim.sample_shots(200); +assert_eq!(shots.len(), 200); +for outcome in &shots { + assert_eq!(outcome.is_one(0), outcome.is_one(1)); +} +``` + +--- + +## Validation and error handling + +| Error | When it occurs | +| --- | --- | +| `QisError::InvalidParameterValue` | The qubit count or parameter count of `apply_standard_gate_noise` does not match the gate; the noise operation itself failed. | +| `QisError::InvalidStateDimension` | The qubit count of the circuit in `apply_circuit` is inconsistent with the simulator. | +| `QisError::UnsupportedOperation` | The circuit contains an operation not supported by the current entry point, for example classical control flow. | +| `QisError::CircuitError` | A gate in the circuit has no matrix representation, contains an unresolved symbolic parameter, or references a qubit that does not exist. | +| `QisError::IndexOutOfBounds` | The qubit index is out of range. | +| `QisError::QubitMismatch` | The qubit count of the observable passed to `expectation` does not match. | +| `cqlib_core::device::NoiseError` | A noise model configuration method failed, for example an invalid probability value or an arity mismatch between the gate and the qubits. | + +Readout noise takes effect only in `probabilities_with_readout` and `probs_with_readout`; `probabilities`, `probs`, `sample_shots` and `sample` all report the distribution before readout noise is applied. diff --git a/docs/api/en/rust/3_qis/4_stabilizer.md b/docs/api/en/rust/3_qis/4_stabilizer.md new file mode 100644 index 00000000..feaf069d --- /dev/null +++ b/docs/api/en/rust/3_qis/4_stabilizer.md @@ -0,0 +1,242 @@ +# StabilizerState + +`cqlib_core::qis::state` + +This page covers the stabilizer state simulator `StabilizerState` and the circuit execution result `CircuitExecutionResult`: construction, Clifford gate operations, generator access, Pauli expectation values, and the measurement and sampling interfaces. + +## Import + +```rust +use cqlib_core::qis::StabilizerState; +use cqlib_core::qis::state::stabilizer::CircuitExecutionResult; +``` + +--- + +## StabilizerState + +A stabilizer state simulator that describes a quantum state with a set of mutually commuting Pauli operators (the stabilizer generators). It can represent only stabilizer states, and therefore accepts only Clifford gates, which map the Pauli group onto itself; it returns an error for non-Clifford operations such as arbitrary-angle rotations, the `T` gate or `fSim`. The qubit scale it can describe is far larger than a dense representation. + +```rust +pub struct StabilizerState { + pub num_qubits: usize, +} +``` + +Fields: + +- `num_qubits` (`usize`): the number of qubits. + +The generator table is kept in a private field. Implements `Debug` and `Clone`; `PartialEq` is not implemented, so to compare two states, compare the values returned by `get_stabilizers()`. + +Methods: + +### Construction + +- `fn new(n: usize) -> Self`: construct the `|0...0>` state; the initial generators are the destabilizer `X_i` in row `i` and the stabilizer `Z_i` in row `n+i`, both with phase `+1`. +- `fn num_qubits(&self) -> usize`: return the number of qubits, consistent with the value of the field of the same name. + +### Circuit integration + +- `fn from_circuit(circuit: &Circuit) -> Result`: execute a Clifford circuit and return the final stabilizer state. +- `fn apply_circuit(&mut self, input_circuit: &Circuit) -> Result<(), QisError>`: apply a Clifford circuit to the current state in place; the qubit count of the circuit must match that of the state. +- `fn run_circuit(circuit: &Circuit) -> Result`: execute a Clifford circuit and return both the final state and the runtime classical data. + +`from_circuit` and `apply_circuit` are state-level entry points: they treat the measurement and store operations produced by `Circuit::measure*` as output declarations and ignore them, neither collapsing the state nor writing classical data; when execution semantics are required (measurement collapses immediately and writes classical data), use `run_circuit`. + +The supported instructions are the Clifford gates `I`, `H`, `X`, `Y`, `Z`, `S`, `SDG`, `X2P`, `X2M`, `Y2P`, `Y2M`, `CX`, `CY`, `CZ` and `SWAP`, plus the `Reset` instruction; barriers and delays have no effect. `run_circuit` additionally executes `MeasureBit`, `MeasureBits` and `Store`, but does not support control flow gates. + +### Clifford gates + +- `fn apply_h(&mut self, qubit: usize) -> Result<(), QisError>`: Hadamard gate. +- `fn apply_x(&mut self, qubit: usize) -> Result<(), QisError>`: Pauli-X. +- `fn apply_y(&mut self, qubit: usize) -> Result<(), QisError>`: Pauli-Y. +- `fn apply_z(&mut self, qubit: usize) -> Result<(), QisError>`: Pauli-Z. +- `fn apply_s(&mut self, qubit: usize) -> Result<(), QisError>`: S gate. +- `fn apply_sdg(&mut self, qubit: usize) -> Result<(), QisError>`: S† gate. +- `fn apply_x2p(&mut self, qubit: usize) -> Result<(), QisError>`: `√X`, that is `Rx(π/2)`. +- `fn apply_x2m(&mut self, qubit: usize) -> Result<(), QisError>`: `√X†`, that is `Rx(-π/2)`. +- `fn apply_y2p(&mut self, qubit: usize) -> Result<(), QisError>`: `√Y`, that is `Ry(π/2)`. +- `fn apply_y2m(&mut self, qubit: usize) -> Result<(), QisError>`: `√Y†`, that is `Ry(-π/2)`. +- `fn apply_cx(&mut self, control: usize, target: usize) -> Result<(), QisError>`: controlled X gate. +- `fn apply_cy(&mut self, control: usize, target: usize) -> Result<(), QisError>`: controlled Y gate. +- `fn apply_cz(&mut self, q0: usize, q1: usize) -> Result<(), QisError>`: controlled Z gate. +- `fn apply_swap(&mut self, q0: usize, q1: usize) -> Result<(), QisError>`: swap two qubits. +- `fn apply_standard_gate(&mut self, gate: StandardGate, qubits: &[usize], params: &[f64]) -> Result<(), QisError>`: dispatch by the standard gate enum; an error is raised when `gate` is not in the Clifford set. + +### Generators and Pauli expectation values + +- `fn get_stabilizers(&self) -> Vec`: return the `n` stabilizer generators. +- `fn get_destabilizers(&self) -> Vec`: return the `n` destabilizer generators. +- `fn pauli_expectation(&self, pauli: &PauliString) -> Result`: return `⟨ψ|P|ψ⟩`; a value of `1` means `P` belongs to the stabilizer group, `-1` means it belongs to the negated stabilizer group, and `0` means neither. This test covers products of generators, not just individual generators. +- `fn to_stim_format(&self) -> String`: export the generator table in a compatible format. + +### Probabilities, measurement and sampling + +- `fn probability_of(&self, bits: &[bool]) -> Result`: return the probability of the given computational basis state; a bitstring compatible with the stabilizer state returns `Ok(1.0 / 2.0^k)`, where `k` is the number of qubits whose measurement outcome is random; an incompatible one returns `Ok(0.0)`. The call does not modify `self`. +- `fn probabilities(&self) -> Result, QisError>`: return the probability distribution over all computational basis states, of length `2^n`, where index `i` corresponds to the basis state whose binary representation is `i`, with qubit 0 as the least significant bit. This interface is suitable only for small systems and returns an error when `n > 20`. +- `fn measure(&mut self, qubit: usize) -> Result`: measure in the Z basis and collapse the state; `false` means the outcome `0` and `true` means the outcome `1`. +- `fn measure_all(&mut self) -> Outcome`: measure all qubits in order and return a bit-packed `Outcome`. +- `fn reset(&mut self, qubit: usize) -> Result<(), QisError>`: reset the given qubit to `|0>`, implemented as a Z-basis measurement followed by an X correction when the outcome is `|1>`. +- `fn sample_shots(&self, shots: usize) -> Vec`: sample a number of independent measurement results in parallel; the same initial state always produces the same set of samples. +- `fn sample(&self, measurement: &Measurement, shots: usize) -> Result`: sample according to the output convention specified by the circuit `Measurement`, without reading or executing the circuit itself; `measurement.qubits()[i]` corresponds to bit `i` of each `Outcome`. +- `fn probs(&self, measurement: &Measurement) -> Result, QisError>`: return the marginal probability distribution selected by `Measurement`; its qubit order is consistent with `sample`. This interface reuses `probabilities` and is therefore likewise subject to the `n <= 20` limit. + +--- + +## CircuitExecutionResult + +The object returned by `run_circuit`, containing both the final quantum state and the runtime classical data produced during execution. + +```rust +pub struct CircuitExecutionResult { + pub state: StabilizerState, + pub classical: ClassicalState, +} +``` + +Fields: + +- `state`: the stabilizer state after all operations have been executed. +- `classical`: measurement results and mutable variables, indexed by the handles in the circuit. + +Implements `Debug`. For the concrete form of runtime classical values, see [ClassicalState](5_classical_state.md). + +--- + +## Example + +### 1. Stabilizer sampling of a Bell state + +```rust +use cqlib_core::qis::StabilizerState; + +let mut s = StabilizerState::new(2); +s.apply_h(0).unwrap(); +s.apply_cx(0, 1).unwrap(); + +let shots = s.sample_shots(500); +assert_eq!(shots.len(), 500); +for shot in &shots { + assert_eq!(shot.is_one(0), shot.is_one(1)); +} +``` + +### 2. Read the stabilizer generators + +```rust +use cqlib_core::qis::StabilizerState; + +let s = StabilizerState::new(2); +let stabs = s.get_stabilizers(); +assert_eq!(stabs.len(), 2); + +// 初态 |00⟩ 的稳定子为 +ZI 与 +IZ +let stabs: Vec = stabs.iter().map(|p| p.to_string()).collect(); +assert_eq!(stabs[0], "+IZ"); +assert_eq!(stabs[1], "+ZI"); +``` + +### 3. Compute the probability of a given basis state + +```rust +use cqlib_core::qis::StabilizerState; + +let mut s = StabilizerState::new(2); +s.apply_h(0).unwrap(); +s.apply_cx(0, 1).unwrap(); + +// 只有 |00⟩ 与 |11⟩ 可能出现,各占一半 +assert!((s.probability_of(&[false, false]).unwrap() - 0.5).abs() < 1e-12); +assert!((s.probability_of(&[true, true]).unwrap() - 0.5).abs() < 1e-12); +assert_eq!(s.probability_of(&[false, true]).unwrap(), 0.0); +assert_eq!(s.probability_of(&[true, false]).unwrap(), 0.0); +``` + +### 4. Pauli expectation value + +```rust +use cqlib_core::qis::{Pauli, PauliString, Phase, StabilizerState}; + +// |+⟩ 是 +X 的本征态 +let mut s = StabilizerState::new(1); +s.apply_h(0).unwrap(); + +let mut x = PauliString::new(1); +x.set_pauli(0, Pauli::X); +assert_eq!(s.pauli_expectation(&x).unwrap(), 1); + +// Bell 态下 ⟨ZI⟩ 为 0 +let mut bell = StabilizerState::new(2); +bell.apply_h(0).unwrap(); +bell.apply_cx(0, 1).unwrap(); + +let mut zi = PauliString::new(2); +zi.set_pauli(0, Pauli::Z); +zi.set_pauli(1, Pauli::I); +zi.phase = Phase::Plus; +assert_eq!(bell.pauli_expectation(&zi).unwrap(), 0); +``` + +### 5. Construct from a circuit and obtain the marginal distribution + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::qis::StabilizerState; + +let mut c = Circuit::new(2); +c.h(Qubit::new(0)).unwrap(); +c.cx(Qubit::new(0), Qubit::new(1)).unwrap(); +let out = c.measure_bits([Qubit::new(1), Qubit::new(0)]).unwrap(); + +// 该测量声明不使 Bell 态坍缩 +let stab = StabilizerState::from_circuit(&c).unwrap(); +let probs = stab.probs(&out).unwrap(); +assert!((probs.values().sum::() - 1.0).abs() < 1e-10); +``` + +### 6. Execute a circuit and read the runtime classical data + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::qis::{RuntimeValue, StabilizerState}; + +let mut c = Circuit::new(1); +c.x(Qubit::new(0)).unwrap(); +let measured = c.measure(Qubit::new(0)).unwrap(); + +let result = StabilizerState::run_circuit(&c).unwrap(); +assert_eq!( + result.classical.value(measured.value()), + Some(&RuntimeValue::Bit(true)) +); +``` + +### 7. Non-Clifford gates are rejected + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::qis::StabilizerState; + +let mut c = Circuit::new(1); +c.t(Qubit::new(0)).unwrap(); + +let result = StabilizerState::from_circuit(&c); +assert!(result.is_err()); +``` + +--- + +## Validation and error handling + +| Error | When it occurs | +| --- | --- | +| `QisError::NonCliffordGate` | A non-Clifford gate is applied to a stabilizer state, for example `T`, `T†`, `Rx`, `Ry`, `Rz` or `Phase` at an arbitrary angle, or a general unitary gate. | +| `QisError::QubitMismatch` | The bitstring length of `probability_of` does not match the qubit count; the qubit count of the Pauli string in `pauli_expectation` is inconsistent with the state. | +| `QisError::IndexOutOfBounds` | The qubit index is out of range. | +| `QisError::InvalidParameterValue` | A gate is applied to a zero-qubit system; a two-qubit gate is given the same qubit twice; `probabilities` or `probs` is called when the qubit count exceeds the limit. | +| `QisError::InvalidStateDimension` | The qubit count of the circuit in `apply_circuit` is inconsistent with the state. | +| `QisError::UnsupportedOperation` | The circuit contains an operation not supported by the current entry point, such as control flow. | +| `QisError::CircuitError` | A gate in the circuit cannot be executed or contains an unresolved symbolic parameter. | + +Repeated sampling of the same initial state yields the same set of samples, so sampling results can be used for reproducible comparison; when different random results are needed, rebuild the circuit or change the gate sequence. diff --git a/docs/api/en/rust/3_qis/5_classical_state.md b/docs/api/en/rust/3_qis/5_classical_state.md new file mode 100644 index 00000000..f678d9cc --- /dev/null +++ b/docs/api/en/rust/3_qis/5_classical_state.md @@ -0,0 +1,177 @@ +# Classical State + +`cqlib_core::qis::state` + +This page introduces the runtime classical data in `cqlib_core::qis`: `RuntimeValue` is a typed runtime value, and `ClassicalState` stores and indexes these values by circuit handle. + +The circuit IR stores classical handles rather than concrete values. When a circuit containing measurement or classical storage is executed, the runtime classical data fills these handles with actual values: immutable classical values are produced by measurement, and mutable classical variables are written by store statements. The identity of a handle is determined jointly by the circuit `CircuitId` and the index within the table, so handles from different circuits are not interchangeable. + +## Import + +```rust +use cqlib_core::qis::{ClassicalState, RuntimeValue}; +``` + +Both types are also located in `cqlib_core::qis::state`; the two paths are equivalent. + +--- + +## RuntimeValue + +```rust +pub enum RuntimeValue { + Bit(bool), + Bool(bool), + UInt { width: u32, value: u128 }, + BitVec { width: u32, bits: Outcome }, +} +``` + +Variants: + +| Variant | Payload | Description | +| --- | --- | --- | +| `RuntimeValue::Bit(bool)` | `bool` | A single bit, typically representing a single-qubit measurement result. | +| `RuntimeValue::Bool(bool)` | `bool` | A logical boolean, used for control flow conditions. | +| `RuntimeValue::UInt { width, value }` | `width: u32`, `value: u128` | An unsigned integer of the given bit width; the value is held in a `u128`. | +| `RuntimeValue::BitVec { width, bits }` | `width: u32`, `bits: Outcome` | A bit vector of the given bit width; the bits are carried in the device result type `Outcome`. | + +The bit order of `BitVec` is consistent with the device result layer: bit index `0` is the least significant bit, and when formatted as a string the most significant bit appears leftmost. + +### Methods + +- `fn ty(&self) -> ClassicalType`: return the static classical type corresponding to this runtime value. +- `fn to_bitstring(&self) -> Option`: return the bitstring with the most significant bit on the left; `Bit` and `BitVec` return `Some`, while `Bool` and `UInt` return `None`. + +### Other behavior + +- Supports `Debug` and `Clone`. +- Comparison and hashing are by value: two values are equal only when the type, bit width and payload are all the same, so `RuntimeValue::Bit(true)` and `RuntimeValue::Bool(true)` are not equal. + +Example: + +```rust +use cqlib_core::device::Outcome; +use cqlib_core::qis::RuntimeValue; + +let bit = RuntimeValue::Bit(true); +assert_eq!(bit.to_bitstring().as_deref(), Some("1")); + +let uint = RuntimeValue::UInt { width: 8, value: 42 }; +assert_eq!(uint, RuntimeValue::UInt { width: 8, value: 42 }); +assert_eq!(uint.to_bitstring(), None); + +// 类型不同则值不相等,即使载荷相同 +assert_ne!(RuntimeValue::Bit(true), RuntimeValue::Bool(true)); + +let bit_vec = RuntimeValue::BitVec { + width: 3, + bits: Outcome::from_bitstring("101").unwrap(), +}; +assert_eq!(bit_vec.to_bitstring().as_deref(), Some("101")); +``` + +--- + +## ClassicalState + +```rust +pub struct ClassicalState { + // 字段不公开 +} +``` + +Stores the runtime classical data of one circuit: a table of immutable classical values, a table of mutable classical variables, and their respective static type tables. The indices of the two correspond one-to-one with the handle indices in the circuit. + +### Methods + +- `fn value(&self, value: ClassicalValue) -> Option<&RuntimeValue>`: return the runtime result of a given immutable classical value. +- `fn var(&self, var: ClassicalVar) -> Option<&RuntimeValue>`: return the current runtime value of a given mutable classical variable. + +Both methods return `None` when the handle belongs to another circuit, the type does not match, the index is beyond the corresponding table, or the corresponding value has not yet been produced (not measured) or not yet stored. + +### How it is obtained + +`ClassicalState` is not used as an ordinary constructed object; it is returned by the circuit execution entry point of the stabilizer simulator: + +```rust +pub fn run_circuit(circuit: &Circuit) -> Result +``` + +`CircuitExecutionResult` contains the final quantum state and the classical data of this execution: + +```rust +pub struct CircuitExecutionResult { + pub state: StabilizerState, + pub classical: ClassicalState, +} +``` + +### Other behavior + +- Supports `Debug` and `Clone`. + +Example: + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::qis::{RuntimeValue, StabilizerState}; + +let mut c = Circuit::new(2); +c.h(Qubit::new(0)).unwrap(); +c.cx(Qubit::new(0), Qubit::new(1)).unwrap(); +let left = c.measure(Qubit::new(0)).unwrap(); +let right = c.measure(Qubit::new(1)).unwrap(); + +let result = StabilizerState::run_circuit(&c).unwrap(); +let Some(RuntimeValue::Bit(left_bit)) = result.classical.value(left.value()) else { + panic!("expected first Bell measurement to produce a bit"); +}; +let Some(RuntimeValue::Bit(right_bit)) = result.classical.value(right.value()) else { + panic!("expected second Bell measurement to produce a bit"); +}; + +// Bell 态上的两次测量结果必须保持关联 +assert_eq!(left_bit, right_bit); +``` + +The result of a multi-qubit measurement is packed into a `BitVec` in the little-endian input order of `Circuit::measure_bits()`: + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::qis::{RuntimeValue, StabilizerState}; + +let mut c = Circuit::new(3); +c.x(Qubit::new(0)).unwrap(); +c.x(Qubit::new(2)).unwrap(); +let measured = c + .measure_bits([Qubit::new(2), Qubit::new(1), Qubit::new(0)]) + .unwrap(); + +let result = StabilizerState::run_circuit(&c).unwrap(); +let Some(RuntimeValue::BitVec { width, bits }) = result.classical.value(measured.value()) else { + panic!("expected BitVec measurement result"); +}; +assert_eq!(*width, 3); +assert_eq!(bits.to_bitstring(*width as usize), "101"); +``` + +--- + +## Validation and error handling + +`value()` and `var()` express a missing lookup with `Option` and do not return errors themselves. Validation on the write side happens during circuit execution; a type mismatch or a handle that does not belong to the current circuit is returned as a `QisError`: + +| Error | When it occurs | +| --- | --- | +| `QisError::CircuitError` | A classical handle that does not belong to the circuit is used during circuit execution, corresponding to `CircuitError::ForeignClassicalHandle`. | +| `QisError::UnsupportedOperation` | The type of a classical value or classical variable is inconsistent with the type registered in the circuit IR, or the type of a runtime value cannot take part in that expression operation. | + +Because lookups use the `CircuitId` and index carried by the handle, a cross-circuit read only yields `None` and never reads the data of another circuit. + +--- + +## Related pages + +- [QIS Overview](0_overview.md): module overview and glossary. +- [Pauli Operators](6_pauli.md): the bit order convention used when computing Pauli string expectation values from measurement probabilities. diff --git a/docs/api/en/rust/3_qis/6_pauli.md b/docs/api/en/rust/3_qis/6_pauli.md new file mode 100644 index 00000000..028b8ed5 --- /dev/null +++ b/docs/api/en/rust/3_qis/6_pauli.md @@ -0,0 +1,346 @@ +# Pauli Operators and Pauli Strings + +`cqlib_core::qis::pauli` + +This page covers the single-qubit Pauli operator `Pauli`, the Pauli group phase `Phase`, the multi-qubit Pauli string `PauliString`, and the iterator `PauliIter` that traverses a Pauli string qubit by qubit. + +`PauliString` uses a symplectic encoding: each qubit is described by a pair of bits, where `x[i] = 1` means that qubit carries an X component and `z[i] = 1` means it carries a Z component, and `(0,0)`, `(1,0)`, `(1,1)`, `(0,1)` correspond to `I`, `X`, `Y`, `Z` respectively. The phase change of a multiplication is accumulated bit by bit through a lookup table and the bit vectors are XORed bit by bit, so the cost of a multiplication is constant time per qubit. + +## Import + +```rust +use cqlib_core::qis::pauli::{Pauli, PauliIter, PauliString, Phase}; +``` + +`Pauli`, `PauliString`, `PauliIter` and `Phase` are also re-exported at the top level of `cqlib_core::qis`. + +--- + +## Phase + +The phase factor of the Pauli group, isomorphic to the cyclic group of order 4, representing $i^n$. + +```rust +#[repr(u8)] +pub enum Phase { + Plus = 0, // i^0 = 1 + I = 1, // i^1 = i + Minus = 2, // i^2 = -1 + MinusI = 3, // i^3 = -i +} +``` + +Variants: + +| Variant | Complex value | Exponent | +| --- | --- | --- | +| `Phase::Plus` | `1` | 0 | +| `Phase::I` | `i` | 1 | +| `Phase::Minus` | `-1` | 2 | +| `Phase::MinusI` | `-i` | 3 | + +### Methods + +- `fn to_complex(&self) -> Complex64`: return the complex number corresponding to this phase. + +### Other behavior + +- `From`: conversion by remainder modulo 4, so `Phase::from(5)` equals `Phase::I`. +- `Add`, `AddAssign`: adding phases is multiplying phases, $i^a \cdot i^b = i^{(a+b) \bmod 4}$. +- `Add`, `AddAssign`: an unsigned integer is used as the exponent increment. +- `Mul`: equivalent to addition. +- `Display`: outputs `1`, `i`, `-1`, `-i`, without quotes or debug decoration. +- Supports `Debug`, `Clone`, `Copy`, `PartialEq`, `Eq`, `Hash`. + +Example: + +```rust +use cqlib_core::qis::pauli::Phase; +use num_complex::Complex64; + +// i * i = -1 +assert_eq!(Phase::I + Phase::I, Phase::Minus); +assert_eq!(Phase::I * Phase::I, Phase::Minus); + +// 指数回绕 +assert_eq!(Phase::MinusI + Phase::I, Phase::Plus); + +// 按模 4 构造 +assert_eq!(Phase::from(5), Phase::I); +assert_eq!(Phase::from(100), Phase::Plus); + +assert_eq!(Phase::I.to_complex(), Complex64::new(0.0, 1.0)); +assert_eq!(Phase::MinusI.to_string(), "-i"); +``` + +--- + +## Pauli + +The single-qubit Pauli operator. + +```rust +pub enum Pauli { + X, + Y, + Z, + I, +} +``` + +Matrix representation: + +| Variant | Matrix | Description | +| --- | --- | --- | +| `Pauli::I` | $\begin{pmatrix} 1 & 0 \\ 0 & 1 \end{pmatrix}$ | Identity operator | +| `Pauli::X` | $\begin{pmatrix} 0 & 1 \\ 1 & 0 \end{pmatrix}$ | Bit flip | +| `Pauli::Y` | $\begin{pmatrix} 0 & -i \\ i & 0 \end{pmatrix}$ | Y operator | +| `Pauli::Z` | $\begin{pmatrix} 1 & 0 \\ 0 & -1 \end{pmatrix}$ | Phase flip | + +### Methods + +- `fn to_symplectic(&self) -> (u8, u8)`: return the `(x, z)` symplectic components. +- `fn to_matrix(&self) -> Array2`: return the 2×2 complex matrix. +- `fn mul_with_phase(&self, other: Pauli) -> (Pauli, Phase)`: return the product operator and the phase, for example `X * Z = -iY`. + +### Other behavior + +- `TryFrom`: accepts the uppercase characters `I`, `X`, `Y`, `Z`; other characters return `QisError::InvalidParameterValue`. +- `FromStr`: single-character parsing; an empty string or multiple characters return `QisError::InvalidParameterValue`. +- `Display`: outputs the operator character. +- Supports `Debug`, `Clone`, `Copy`, `PartialEq`, `Eq`, `Hash`. + +Example: + +```rust +use cqlib_core::qis::pauli::{Pauli, Phase}; + +// X * Y = iZ +let (product, phase) = Pauli::X.mul_with_phase(Pauli::Y); +assert_eq!(product, Pauli::Z); +assert_eq!(phase, Phase::I); + +// Y * X = -iZ,与上式相差符号 +let (product, phase) = Pauli::Y.mul_with_phase(Pauli::X); +assert_eq!(product, Pauli::Z); +assert_eq!(phase, Phase::MinusI); + +assert_eq!(Pauli::Y.to_symplectic(), (1, 1)); +assert_eq!("X".parse::().unwrap(), Pauli::X); +assert!("XY".parse::().is_err()); +``` + +--- + +## PauliString + +A multi-qubit Pauli string, $P = \bigotimes_{i=0}^{N-1} P_i$. + +```rust +pub struct PauliString { + pub num_qubits: usize, + pub phase: Phase, + pub z: BitVec, + pub x: BitVec, +} +``` + +Fields: + +| Field | Type | Description | +| --- | --- | --- | +| `num_qubits` | `usize` | The number of qubits $N$ contained in the string. | +| `phase` | `Phase` | The global phase factor. | +| `z` | `BitVec` | The Z component bit vector, of length `num_qubits`. | +| `x` | `BitVec` | The X component bit vector, of length `num_qubits`. | + +String and qubit index convention: the leftmost character of the string corresponds to the highest qubit index, that is, the string is written in tensor product order. Thus `"XZI"` means qubit 2 is `X`, qubit 1 is `Z` and qubit 0 is `I`. The fields themselves are stored in ascending qubit index order, and `x[0]` and `z[0]` describe qubit 0. + +### Construction and queries + +- `fn new(num_qubits: usize) -> Self`: create a Pauli string of all identity operators, with phase `Phase::Plus`. +- `fn set_pauli(&mut self, idx: usize, p: Pauli)`: set the operator on the given qubit; panics when the index is out of range. +- `fn get_pauli(&self, idx: usize) -> Pauli`: read the operator on the given qubit; panics when the index is out of range. +- `fn try_set_pauli(&mut self, idx: usize, pauli: Pauli) -> Result<(), QisError>`: returns `QisError::IndexOutOfBounds` when out of range, without modifying the original string. +- `fn try_get_pauli(&self, idx: usize) -> Result`: returns `QisError::IndexOutOfBounds` when out of range. +- `fn iter(&self) -> PauliIter<'_>`: traverse in ascending qubit index order. +- `fn support(&self) -> Vec`: return the qubit indices of the non-identity operators, in ascending order. +- `fn x_mask(&self) -> usize`: pack the X component bit vector into an unsigned integer mask, where bit `i` corresponds to qubit `i`. +- `fn z_mask(&self) -> usize`: pack the Z component bit vector into an unsigned integer mask. + +### Operations + +- `fn commutes_with(&self, other: &Self) -> bool`: determine commutation by the symplectic inner product; an inner product of 0 means the two commute; panics when the qubit counts differ. +- Multiplication between strings is provided by `Mul` and `MulAssign`, and the phase accumulates with the product; see "Other behavior". + +### Matrix, phase and expectation value + +- `fn to_matrix(&self) -> Array2`: return a dense $2^N \times 2^N$ matrix. Qubit 0 is the least significant tensor factor, the string is expanded as $P_{N-1} \otimes \cdots \otimes P_0$, and the global phase is included in the result. +- `fn y_phase(&self) -> Complex64`: return the phase factor contributed by the Y components. In the symplectic encoding $Y = iXZ$, so $n$ Y's contribute $i^n$. +- `fn expectation(&self, probs: &HashMap) -> Result`: compute the expectation value $\langle P \rangle = \sum_s p(s)\langle s|P|s\rangle$ over a computational basis probability distribution. + +Conventions and errors of `expectation`: + +- The keys of the probability map are written in little-endian, with the rightmost character corresponding to qubit 0. For example `"01"` means qubit 0 is 1 and qubit 1 is 0. +- When the string contains X or Y, the expectation value in the computational basis is always 0, and `Ok(0.0)` is returned directly. +- When it contains only Z and I, the expectation value is $\langle P \rangle = \text{phase} \times \sum_s p(s) \times (-1)^{\sum_i z[i] s[i]}$. +- When the key length is inconsistent with `num_qubits`, `QisError::DimensionMismatch` is returned; when a key contains a character other than `0` and `1`, `QisError::PauliStringParseError` is returned. + +### Other behavior + +- `Display`: outputs a form such as `+XYZ` or `-iZIX`, printing qubits with the highest index on the left. +- `FromStr`: accepts `[+|-][i|j]`, for example `"XZI"`, `"+XYZ"`, `"-iZII"`, `"+jX"`; a parse failure returns `PauliStringParseError`. +- `Mul for &PauliString`: returns a new Pauli string; panics when the qubit counts of the two sides differ. +- `MulAssign<&PauliString>`: multiplies in place, accumulating the phase bit by bit and XORing the bit vectors bit by bit. +- `IntoIterator for &PauliString`: equivalent to `iter()`. +- Supports `Debug`, `Clone`, `PartialEq`, `Eq`, `Hash`. + +Example: + +```rust +use cqlib_core::qis::pauli::{Pauli, PauliString}; + +// 逐位设置 +let mut ps = PauliString::new(3); +ps.set_pauli(0, Pauli::X); +assert_eq!(ps.to_string(), "+IIX"); + +ps.set_pauli(2, Pauli::Z); +assert_eq!(ps.to_string(), "+ZIX"); + +// 字符串解析使用最高比特在左的顺序 +let parsed: PauliString = "XZI".parse().unwrap(); +assert_eq!(parsed.get_pauli(0), Pauli::I); +assert_eq!(parsed.get_pauli(1), Pauli::Z); +assert_eq!(parsed.get_pauli(2), Pauli::X); +assert_eq!(parsed.support(), vec![1, 2]); +``` + +```rust +use cqlib_core::qis::pauli::{Pauli, PauliString}; + +// X * Z = -iY,相位随乘积一并保存 +let mut x = PauliString::new(1); +x.set_pauli(0, Pauli::X); +let mut z = PauliString::new(1); +z.set_pauli(0, Pauli::Z); + +let product = &x * &z; +assert_eq!(product.to_string(), "-iY"); + +// 原地乘法:X * Y = iZ +let mut y = PauliString::new(1); +y.set_pauli(0, Pauli::Y); + +x *= &y; +assert_eq!(x.to_string(), "+iZ"); +``` + +Commutation and masks: + +```rust +use cqlib_core::qis::pauli::{Pauli, PauliString}; + +// XX 与 ZZ 在两个比特上都反对易,反对易次数为偶数,因此整体对易 +let mut xx = PauliString::new(2); +xx.set_pauli(0, Pauli::X); +xx.set_pauli(1, Pauli::X); + +let mut zz = PauliString::new(2); +zz.set_pauli(0, Pauli::Z); +zz.set_pauli(1, Pauli::Z); + +assert!(xx.commutes_with(&zz)); + +// 同一比特上的 X 与 Z 反对易 +let mut x = PauliString::new(1); +x.set_pauli(0, Pauli::X); +let mut z = PauliString::new(1); +z.set_pauli(0, Pauli::Z); +assert!(!x.commutes_with(&z)); + +// 掩码:Y 同时置起 X 与 Z 分量 +let mut ps = PauliString::new(3); +ps.set_pauli(0, Pauli::X); +ps.set_pauli(2, Pauli::Y); +assert_eq!(ps.x_mask(), 0b101); +``` + +Computing an expectation value from a probability distribution: + +```rust +use cqlib_core::qis::pauli::{Pauli, PauliString}; +use std::collections::HashMap; + +let mut zz = PauliString::new(2); +zz.set_pauli(0, Pauli::Z); +zz.set_pauli(1, Pauli::Z); + +// Bell 态的概率分布,键按小端书写 +let mut probs = HashMap::new(); +probs.insert("00".to_string(), 0.5); +probs.insert("11".to_string(), 0.5); + +assert!((zz.expectation(&probs).unwrap() - 1.0).abs() < 1e-10); +``` + +--- + +## PauliIter + +An iterator that traverses a Pauli string in ascending qubit index order. + +```rust +pub struct PauliIter<'a> { + // 字段不公开 +} +``` + +- `Iterator`: returns the operator on each qubit; `size_hint` is exact. +- Implements `ExactSizeIterator` and `FusedIterator`. + +Example: + +```rust +use cqlib_core::qis::pauli::{Pauli, PauliString}; + +let pauli: PauliString = "XYZI".parse().unwrap(); +let collected: Vec = pauli.iter().collect(); +let expected: Vec = (0..pauli.num_qubits) + .map(|index| pauli.get_pauli(index)) + .collect(); + +assert_eq!(collected, expected); + +// 借用 PauliString 也可以直接进入 for 循环 +let mut count = 0; +for _ in &pauli { + count += 1; +} +assert_eq!(count, 4); +``` + +--- + +## Validation and error handling + +`QisError` and `PauliStringParseError` are the error sources of the interfaces on this page: + +| Error | When it occurs | +| --- | --- | +| `QisError::InvalidParameterValue` | When `Pauli` is constructed from a character or a string, the input is not `I`, `X`, `Y` or `Z`, or the input is an empty string or holds multiple characters. | +| `QisError::IndexOutOfBounds` | The qubit index of `try_get_pauli()` and `try_set_pauli()` is not smaller than `num_qubits`. | +| `QisError::DimensionMismatch` | The probability key length of `expectation()` is inconsistent with `num_qubits`. | +| `QisError::PauliStringParseError` | A probability key of `expectation()` contains a character other than `0` and `1`. | +| `PauliStringParseError::EmptyString` | The input is an empty string when parsing a `PauliString`. | +| `PauliStringParseError::InvalidCharacter` | A character other than `I`, `X`, `Y` and `Z` appears when parsing a `PauliString`. | +| `PauliStringParseError::NoOperators` | Only a phase prefix and no operator is present when parsing a `PauliString`, for example `"-"`. | + +The following cases are expressed as a panic rather than a returned error: an out-of-range index in `set_pauli()` and `get_pauli()`, and inconsistent qubit counts on the two sides of `commutes_with()`, `Mul` and `MulAssign`. When out-of-range indices must be handled in error form, use `try_set_pauli()` and `try_get_pauli()` instead. + +--- + +## Related pages + +- [QIS Overview](0_overview.md): module overview and terminology. +- [Hamiltonian and Observable](7_hamiltonian.md): construct an observable from Pauli strings and coefficients and compute its expectation value. diff --git a/docs/api/en/rust/3_qis/7_hamiltonian.md b/docs/api/en/rust/3_qis/7_hamiltonian.md new file mode 100644 index 00000000..a5f1ae93 --- /dev/null +++ b/docs/api/en/rust/3_qis/7_hamiltonian.md @@ -0,0 +1,284 @@ +# Hamiltonian and Observable + +`cqlib_core::qis::hamiltonian` +`cqlib_core::qis::observable` + +This page covers the observable `Hamiltonian`, formed by Pauli strings and complex coefficients, and the unified interface `Observable` trait for computing expectation values. + +`Hamiltonian` is a sparse representation of a $2^N \times 2^N$ matrix, of the form $H = \sum_k c_k P_k$, where $c_k$ is a complex coefficient and $P_k$ is an $N$-qubit Pauli string. The same structure serves both as the Hamiltonian of a physical interpretation and as an arbitrary observable whose expectation value is needed. + +## Import + +```rust +use cqlib_core::qis::hamiltonian::Hamiltonian; +use cqlib_core::qis::observable::Observable; +``` + +Both types are also re-exported at the top level of `cqlib_core::qis`. + +--- + +## Hamiltonian + +```rust +pub struct Hamiltonian { + pub num_qubits: usize, + pub terms: Vec<(PauliString, Complex64)>, +} +``` + +Fields: + +| Field | Type | Description | +| --- | --- | --- | +| `num_qubits` | `usize` | The number of qubits the observable acts on. | +| `terms` | `Vec<(PauliString, Complex64)>` | The term list; each term consists of a Pauli string and a complex coefficient. | + +### Construction + +- `fn new(num_qubits: usize) -> Self`: create the zero operator for the given number of qubits, with an empty term list. +- `fn from_pauli(pauli: PauliString) -> Self`: construct from a single Pauli string with coefficient `1.0`. Equivalent to the `Into` conversion of `PauliString`. +- `fn from_list(ops: Vec<(PauliString, Complex64)>) -> Result`: construct from a term list. An empty list returns the zero operator with `num_qubits` equal to 0; when the terms have inconsistent qubit counts, `QisError::QubitMismatch` is returned. Equivalent to `TryFrom>`. +- `fn add_term(&mut self, op: PauliString, coeff: Complex64) -> Result<(), QisError>`: append one term. When the qubit count of `op` is inconsistent with `num_qubits`, `QisError::QubitMismatch` is returned. + +### Simplification and scaling + +- `fn simplify(&mut self)`: simplify the term list. The operation has two steps: absorb the internal phase of each `PauliString` into the coefficient, normalizing all Pauli strings to `Phase::Plus`; then group by the X and Z component vectors and merge the coefficients of identical operators. Coefficients whose absolute value is smaller than $10^{-10}$ are discarded. +- `fn scale(&mut self, factor: Complex64)`: multiply the coefficients of all terms by the same complex factor. + +### Queries and conversion + +- `fn all_terms_commute(&self) -> bool`: determine whether all Pauli terms commute pairwise. When true, $e^{-iHt}$ can be decomposed term by term into an exact evolution. +- `fn to_matrix(&self) -> Result, QisError>`: return a dense $2^N \times 2^N$ matrix. When the qubit count of any term is inconsistent with `num_qubits`, `QisError::QubitMismatch` is returned. An empty operator returns the zero matrix of the corresponding dimension. +- `fn to_trotter_circuit(&self, time: f64, steps: usize, mode: TrotterMode) -> Result`: see [Trotter Evolution](8_evolution.md). +- `fn to_evolution_circuit(&self, time: f64, steps: usize, mode: TrotterMode) -> Result`: see [Trotter Evolution](8_evolution.md). + +### Other behavior + +- `Display`: outputs `(coefficient) * Pauli string` term by term, with the coefficient in the display form of a complex number and terms joined by ` + `. An empty term list outputs an empty string. +- `Add`: concatenates the term lists of two Hamiltonians directly, without merging; panics when the two sides have different qubit counts. When identical terms must be merged, call `simplify()` after the addition. +- `From`, `TryFrom>`: correspond to `from_pauli()` and `from_list()` respectively. +- Implements `Observable`, see below. +- Supports `Debug`, `Clone`, `PartialEq`. + +Example: + +```rust +use cqlib_core::qis::{Hamiltonian, PauliString}; + +let mut h = Hamiltonian::new(2); +h.add_term("ZZ".parse::().unwrap(), 0.5.into()).unwrap(); +h.add_term("XX".parse::().unwrap(), 0.3.into()).unwrap(); + +assert_eq!(h.terms.len(), 2); +// ZZ 与 XX 在每个量子比特上都同时反交换,整体相乘后相互抵消,因此两者对易 +assert!(h.all_terms_commute()); +``` + +Switching to terms whose commutation differs on each qubit makes the result false: + +```rust +use cqlib_core::qis::{Hamiltonian, PauliString}; + +let mut h = Hamiltonian::new(2); +h.add_term("XI".parse::().unwrap(), 0.5.into()).unwrap(); +h.add_term("ZI".parse::().unwrap(), 0.3.into()).unwrap(); + +// 同一量子比特上的 X 与 Z 反交换 +assert!(!h.all_terms_commute()); +``` + +```rust +use cqlib_core::qis::{Hamiltonian, PauliString}; +use num_complex::Complex64; + +// 同一 Pauli 串的两项会被合并,系数相加 +let mut h = Hamiltonian::from_list(vec![ + ("ZZ".parse::().unwrap(), 0.5.into()), + ("ZZ".parse::().unwrap(), 0.3.into()), +]) +.unwrap(); + +h.simplify(); + +assert_eq!(h.terms.len(), 1); +assert!((h.terms[0].1 - Complex64::new(0.8, 0.0)).norm() < 1e-10); +``` + +```rust +use cqlib_core::qis::{Hamiltonian, PauliString}; + +// ZZ 与 IZ 对易,演化可以按项精确分解 +let mut h = Hamiltonian::new(2); +h.add_term("ZZ".parse().unwrap(), 1.0.into()).unwrap(); +h.add_term("IZ".parse().unwrap(), 0.5.into()).unwrap(); + +assert!(h.all_terms_commute()); +``` + +Dense matrix: + +```rust +use cqlib_core::qis::{Hamiltonian, PauliString, Phase}; +use num_complex::Complex64; + +let mut x: PauliString = "X".parse().unwrap(); +x.phase = Phase::Minus; +let z: PauliString = "Z".parse().unwrap(); + +let hamiltonian = Hamiltonian::from_list(vec![ + (x, Complex64::new(2.0, 0.0)), + (z, Complex64::new(0.0, 1.0)), +]) +.unwrap(); + +assert_eq!( + hamiltonian.to_matrix().unwrap(), + ndarray::arr2(&[ + [Complex64::new(0.0, 1.0), Complex64::new(-2.0, 0.0)], + [Complex64::new(-2.0, 0.0), Complex64::new(0.0, -1.0)], + ]) +); +``` + +--- + +## Observable + +The unified interface for computing the expectation value of an observable, used to evaluate the same observable on different quantum state representations. + +```rust +pub trait Observable { + fn expectation_statevector(&self, sv: &Statevector) -> Result; + fn expectation_density_matrix(&self, dm: &DensityMatrix) -> Result; + fn expectation_probs( + &self, + measurements: &[(PauliString, HashMap)], + ) -> Result; + fn num_qubits(&self) -> usize; + fn variance_statevector(&self, _sv: &Statevector) -> Result { + Err(QisError::UnsupportedOperation( + "variance_statevector not implemented for this observable type".into(), + )) + } +} +``` + +### Required methods + +- `fn expectation_statevector(&self, sv: &Statevector) -> Result`: compute the pure-state expectation value $\langle \psi | O | \psi \rangle$. When the qubit count of the state is inconsistent with `num_qubits()`, `QisError::QubitMismatch` is returned. +- `fn expectation_density_matrix(&self, dm: &DensityMatrix) -> Result`: compute the mixed-state expectation value $\text{Tr}(\rho O)$. When the qubit counts are inconsistent, `QisError::QubitMismatch` is returned. +- `fn expectation_probs(&self, measurements: &[(PauliString, HashMap)]) -> Result`: compute the expectation value from measurement probabilities. Each element is a measurement basis together with a map from state string to probability. State strings are written in little-endian, with the rightmost character corresponding to qubit 0. +- `fn num_qubits(&self) -> usize`: return the number of qubits the observable acts on, used for the dimension check before computing an expectation value. + +### Default methods + +- `fn variance_statevector(&self, sv: &Statevector) -> Result`: compute the pure-state variance $\text{Var}(O) = \langle O^2 \rangle - \langle O \rangle^2$. The default implementation in the trait returns `QisError::UnsupportedOperation`; an observable that supports variance computation must override this method. + +### Implementors + +| Type | Description | +| --- | --- | +| `Hamiltonian` | Computes term by term and accumulates by coefficient, for a multi-term Hamiltonian. | +| `PauliString` | A single-term Pauli string, for direct measurement of a single observable. | + +### Measurement basis matching of `expectation_probs` + +For every non-identity term, the interface looks for a compatible measurement basis in the given measurement list: a measurement basis is considered compatible if its X and Z components at the non-identity positions of the term agree with those of the term. Identity terms contribute their coefficient and global phase directly and need no measurement basis. + +When no compatible measurement basis is found, `QisError::UnsupportedOperation` is returned. The implementations of `Hamiltonian` and `PauliString` both match by this rule, with slight differences in the error branches related to state strings: + +| Case | `Hamiltonian` | `PauliString` | +| --- | --- | --- | +| State string length inconsistent with the qubit count | `QisError::DimensionMismatch` | `QisError::QubitMismatch` | +| State string contains a character other than `0` and `1` | `QisError::PauliStringParseError` | `QisError::UnsupportedOperation` | +| No compatible measurement basis found | `QisError::UnsupportedOperation` | `QisError::UnsupportedOperation` | + +The implementation of `Hamiltonian` ignores the phase introduced by Y components in a term and counts only the global phase and the coefficient. + +### Example + +Pure-state expectation value on a Bell state: + +```rust +use cqlib_core::qis::{Hamiltonian, Observable, PauliString, Statevector}; + +let mut sv = Statevector::new(2); +sv.apply_h(0).unwrap(); +sv.apply_cx(0, 1).unwrap(); + +let ps: PauliString = "ZZ".parse().unwrap(); +let h = Hamiltonian::from_pauli(ps); + +assert!((h.expectation_statevector(&sv).unwrap() - 1.0).abs() < 1e-10); +assert_eq!(h.num_qubits(), 2); +``` + +Computing an expectation value from measurement probabilities. All three Z terms are compatible with the same ZZ measurement basis: + +```rust +use cqlib_core::qis::{Hamiltonian, Observable, PauliString}; +use std::collections::HashMap; + +let mut h = Hamiltonian::new(2); +let mut z1 = PauliString::new(2); +z1.set_pauli(1, cqlib_core::qis::pauli::Pauli::Z); +h.add_term(z1, 2.0.into()).unwrap(); + +let mut z0 = PauliString::new(2); +z0.set_pauli(0, cqlib_core::qis::pauli::Pauli::Z); +h.add_term(z0, 3.0.into()).unwrap(); + +let mut zz = PauliString::new(2); +zz.set_pauli(0, cqlib_core::qis::pauli::Pauli::Z); +zz.set_pauli(1, cqlib_core::qis::pauli::Pauli::Z); +h.add_term(zz.clone(), 4.0.into()).unwrap(); + +let mut probs = HashMap::new(); +probs.insert("10".to_string(), 1.0); + +let measurements = vec![(zz, probs)]; +assert!((h.expectation_probs(&measurements).unwrap() - (-3.0)).abs() < 1e-10); +``` + +Variance: + +```rust +use cqlib_core::qis::{Hamiltonian, Observable, PauliString, Statevector}; + +// |+> 态上 Z 的方差为 1,系数 2.0 的平方给出 4.0 +let mut sv = Statevector::new(1); +sv.apply_h(0).unwrap(); + +let mut z = PauliString::new(1); +z.set_pauli(0, cqlib_core::qis::pauli::Pauli::Z); + +let mut h = Hamiltonian::new(1); +h.add_term(z, 2.0.into()).unwrap(); + +assert!((h.variance_statevector(&sv).unwrap() - 4.0).abs() < 1e-10); +``` + +--- + +## Validation and error handling + +| Error | When it occurs | +| --- | --- | +| `QisError::QubitMismatch` | When constructing or appending a term, the qubit count of the Pauli string is inconsistent with `num_qubits`; when computing an expectation value or variance, the qubit count of the state is inconsistent with that of the observable. | +| `QisError::NotHermitian` | The observable is not Hermitian when computing a variance, for example the phase of a `PauliString` is `±i`, or a coefficient whose imaginary part still exceeds the tolerance remains after simplification. | +| `QisError::UnsupportedOperation` | An observable that does not override `variance_statevector()` calls that method; `expectation_probs()` finds no compatible measurement basis. | +| `QisError::DimensionMismatch` | In `Hamiltonian::expectation_probs()`, the state string length is inconsistent with the qubit count. | +| `QisError::PauliStringParseError` | In `Hamiltonian::expectation_probs()`, a state string contains a character other than `0` and `1`. | + +`Add` panics when the qubit counts of the two sides are inconsistent, rather than returning an error. + +Variance computation tolerates a certain amount of numerical noise: a small negative value near zero is truncated to 0, while a negative value beyond the tolerance returns `QisError::InvalidParameterValue` instead. + +--- + +## Related pages + +- [QIS Overview](0_overview.md): module overview and terminology. +- [Pauli Operators and Pauli Strings](6_pauli.md): construction of terms, commutation and symplectic encoding. +- [Trotter Evolution](8_evolution.md): convert a Hamiltonian into an evolution circuit. diff --git a/docs/api/en/rust/3_qis/8_evolution.md b/docs/api/en/rust/3_qis/8_evolution.md new file mode 100644 index 00000000..b453802b --- /dev/null +++ b/docs/api/en/rust/3_qis/8_evolution.md @@ -0,0 +1,321 @@ +# Pauli Evolution and Trotter Decomposition + +`cqlib_core::qis::evolution` + +This page covers the interfaces that convert Pauli strings and Hamiltonians into evolution circuits: the `PauliEvolution` trait provides the rotation gate of a single Pauli string, two entry points on `Hamiltonian` extend it into a full time-evolution circuit, and `TrotterMode` determines the approximation used for multiple non-commuting terms. + +## Import + +```rust +use cqlib_core::qis::evolution::{PauliEvolution, TrotterMode}; +``` + +`PauliEvolution` and `TrotterMode` are also re-exported at the top level of `cqlib_core::qis`. + +--- + +## TrotterMode + +The Trotter-Suzuki decomposition mode for the time evolution $U(t) = e^{-iHt}$ of a Hamiltonian. + +```rust +pub enum TrotterMode { + FirstOrder, + SecondOrder, + Randomized(u64), +} +``` + +Variants: + +| Variant | Payload | Approximation form | Error order | +| --- | --- | --- | --- | +| `TrotterMode::FirstOrder` | None | $U(t) \approx \left[\prod_k e^{-i c_k t/n \cdot P_k}\right]^n$ | $O(t^2/n)$ | +| `TrotterMode::SecondOrder` | None | Each step applies a half-angle evolution in forward order and then in reverse order | $O(t^3/n^2)$ | +| `TrotterMode::Randomized(u64)` | Random seed | Same as first order, but the order of terms within each step is shuffled randomly | Same order as first order | + +The seed carried by `Randomized` determines the permutation of the terms within each step; the same seed gives the same circuit, which makes experiments reproducible. + +### Other behavior + +- Supports `Debug`, `Clone`, `Copy`, `PartialEq`, `Eq`, `Hash`. + +Example: + +```rust +use cqlib_core::qis::evolution::TrotterMode; + +// 同一种子的模式相等,可重复使用 +assert_eq!(TrotterMode::Randomized(42), TrotterMode::Randomized(42)); +assert_ne!(TrotterMode::Randomized(42), TrotterMode::Randomized(43)); +assert_ne!(TrotterMode::FirstOrder, TrotterMode::SecondOrder); +``` + +--- + +## PauliEvolution + +The extension trait that appends Pauli rotation gates to a circuit. + +```rust +pub trait PauliEvolution { + fn pauli_evolution( + &mut self, + pauli: &PauliString, + angle: impl Into, + qubits: &[Qubit], + ) -> Result<(), CircuitError>; +} +``` + +### Methods + +- `fn pauli_evolution(&mut self, pauli: &PauliString, angle: impl Into, qubits: &[Qubit]) -> Result<(), CircuitError>`: append the evolution operator $e^{-i\theta/2 \cdot P}$. + +Parameters: + +- `pauli` (`&PauliString`): the Pauli string $P$ to exponentiate; its phase must be $\pm 1$ (Hermitian). +- `angle` (`impl Into`): the rotation angle $\theta$, which can be a fixed floating-point number or a symbolic parameter. +- `qubits` (`&[Qubit]`): the positions acted on; the length must match `pauli.num_qubits`. + +Note the angle convention here: $\theta$ is the angle corresponding to the half-factor of the rotation, and $\theta$ in $e^{-i\theta/2 \cdot P}$ must be $2ct$ to be equivalent to $e^{-ictP}$. When a Hamiltonian is passed to an evolution entry point, no manual conversion is needed, because the entry point already handles this convention internally. + +### Implementors + +| Type | Description | +| --- | --- | +| `Circuit` | Appends the rotation gate directly to the end of the target circuit. | + +### Implementation steps + +First, phase check. The internal phase of the Pauli string must be $\pm 1$; only Hermitian operators can be generators of unitary evolution, so a phase of $\pm i$ raises an error directly. + +Second, phase absorption. Absorb $\pm 1$ into the rotation angle to obtain the effective angle $\theta_{\text{eff}} = \theta \cdot \text{phase}$. + +Third, basis change. Apply to each non-identity qubit the gate that rotates its component into the Z basis: `X` uses `H`, `Y` uses $S^\dagger$ followed by `H`, and `Z` needs no transformation. + +Fourth, CNOT chain. Apply `CNOT` in increasing order of the non-identity qubit indices, accumulating the parity level by level. + +Fifth, core rotation. Apply `RZ(θ_eff)` on the last non-identity qubit. + +Sixth, the CNOT chain in reverse order and the inverse basis change, restoring the qubits to their original basis. + +When the Pauli string is all identity operators, the evolution degenerates into a global phase $e^{-i\theta/2}$, produces no gates at all, and the phase is recorded in the global phase of the circuit. + +### Errors + +- `CircuitError::QubitCountMismatch`: the length of `qubits` is inconsistent with `pauli.num_qubits`. +- `CircuitError::InvalidOperation`: the phase of the Pauli string is $\pm i$ and it cannot serve as a generator of unitary evolution. + +### Example + +Single-qubit rotation: + +```rust +use cqlib_core::circuit::Circuit; +use cqlib_core::qis::evolution::PauliEvolution; +use cqlib_core::qis::pauli::PauliString; + +let mut circuit = Circuit::new(1); +let qubits = circuit.qubits(); + +// Z 分量不需要基变换,只产生一个 RZ 门 +let pauli: PauliString = "Z".parse().unwrap(); +circuit + .pauli_evolution(&pauli, std::f64::consts::PI, &qubits) + .unwrap(); + +assert_eq!(circuit.operations().len(), 1); +``` + +Multi-qubit rotation: + +```rust +use cqlib_core::circuit::Circuit; +use cqlib_core::qis::evolution::PauliEvolution; +use cqlib_core::qis::pauli::PauliString; + +let mut circuit = Circuit::new(2); +let qubits = circuit.qubits(); + +// ZZ 链:CNOT(0->1) - RZ(1) - CNOT(0->1) +let pauli: PauliString = "ZZ".parse().unwrap(); +circuit + .pauli_evolution(&pauli, std::f64::consts::PI / 2.0, &qubits) + .unwrap(); + +let ops = circuit.operations(); +assert_eq!(ops.len(), 3); +assert_eq!(ops[0].qubits[0], qubits[0]); +assert_eq!(ops[0].qubits[1], qubits[1]); +assert_eq!(ops[1].qubits[0], qubits[1]); +``` + +An all-identity string contributes only a global phase: + +```rust +use cqlib_core::circuit::Circuit; +use cqlib_core::qis::evolution::PauliEvolution; +use cqlib_core::qis::pauli::PauliString; + +let mut circuit = Circuit::new(2); +let qubits = circuit.qubits(); + +let pauli: PauliString = "II".parse().unwrap(); +circuit + .pauli_evolution(&pauli, std::f64::consts::PI, &qubits) + .unwrap(); + +assert_eq!(circuit.operations().len(), 0); +``` + +Inconsistent qubit counts: + +```rust +use cqlib_core::circuit::Circuit; +use cqlib_core::qis::evolution::PauliEvolution; +use cqlib_core::qis::pauli::PauliString; + +let mut circuit = Circuit::new(2); +let qubits = circuit.qubits(); + +let pauli: PauliString = "XXX".parse().unwrap(); +assert!(circuit.pauli_evolution(&pauli, 1.0, &qubits).is_err()); +``` + +--- + +## Evolution entry points on Hamiltonian + +Both entry points accept the same parameters: + +- `time` (`f64`): the total evolution time $t$. +- `steps` (`usize`): the number of Trotter steps $n$, which must be greater than 0. +- `mode` (`TrotterMode`): the decomposition mode. + +Both first copy the Hamiltonian and simplify it, then check the imaginary part of the coefficients: when the absolute value of the imaginary part exceeds $10^{-10}$, `QisError::NotHermitian` is returned, because only Hermitian operators can generate unitary evolution. + +### `fn to_trotter_circuit(&self, time: f64, steps: usize, mode: TrotterMode) -> Result` + +Generate an approximate evolution circuit in the given mode; the qubit count of the output circuit matches `num_qubits`. + +For a term $P_k$ with coefficient $c_k$, the time slice is $\Delta t = t/n$ and the rotation angle within the term is $2 c_k \Delta t$. The first-order mode applies all terms slice by slice in forward order; the second-order mode applies a half-angle evolution of $\Delta t/2$ in forward order and then once in reverse order within each slice; the randomized mode builds on first order and randomly permutes the order of the terms within each slice, the permutation being determined by the seed carried by `TrotterMode::Randomized`. + +Example: + +```rust +use cqlib_core::qis::{Hamiltonian, PauliString}; +use cqlib_core::qis::evolution::TrotterMode; + +// H = 0.5 * ZZ +let mut h = Hamiltonian::new(2); +h.add_term("ZZ".parse::().unwrap(), 0.5.into()).unwrap(); + +// 2 步,每步 3 个门 +let circuit = h.to_trotter_circuit(1.0, 2, TrotterMode::FirstOrder).unwrap(); +assert_eq!(circuit.operations().len(), 6); + +// 二阶模式每片再重复一遍半角演化,1 步共 6 个门 +let circuit = h.to_trotter_circuit(1.0, 1, TrotterMode::SecondOrder).unwrap(); +assert_eq!(circuit.operations().len(), 6); +``` + +### `fn to_evolution_circuit(&self, time: f64, steps: usize, mode: TrotterMode) -> Result` + +Automatically chooses between exact decomposition and Trotter approximation. It first determines whether all terms commute pairwise: + +- When they commute, the exact path is taken, applying each term once according to $U(t) = \prod_k e^{-i c_k t P_k}$; in this case `steps` and `mode` have no effect. +- When they do not commute, it falls back to the same logic as `to_trotter_circuit`, and `steps` and `mode` take effect. + +Example: + +```rust +use cqlib_core::qis::{Hamiltonian, PauliString}; +use cqlib_core::qis::evolution::TrotterMode; + +// ZZ 与 ZI 对易,直接走精确路径 +let mut h = Hamiltonian::new(2); +h.add_term("ZZ".parse::().unwrap(), 0.5.into()).unwrap(); +h.add_term("ZI".parse::().unwrap(), 0.3.into()).unwrap(); + +let circuit = h.to_evolution_circuit(1.0, 1, TrotterMode::FirstOrder).unwrap(); +assert_eq!(circuit.num_qubits(), 2); +``` + +In the commuting case, the exact path and first-order Trotter give the same circuit matrix: + +```rust +use cqlib_core::circuit::circuit_to_matrix; +use cqlib_core::qis::{Hamiltonian, PauliString}; +use cqlib_core::qis::evolution::TrotterMode; + +let mut h = Hamiltonian::new(1); +h.add_term("Z".parse::().unwrap(), 0.7.into()).unwrap(); + +let time = 0.4_f64; +let exact = h.to_evolution_circuit(time, 1, TrotterMode::FirstOrder).unwrap(); +let trotter = h.to_trotter_circuit(time, 1, TrotterMode::FirstOrder).unwrap(); + +let m_exact = circuit_to_matrix(&exact, None).unwrap(); +let m_trotter = circuit_to_matrix(&trotter, None).unwrap(); +for (a, b) in m_exact.iter().zip(m_trotter.iter()) { + assert!((a - b).norm() < 1e-12); +} +``` + +Matrix form of the exact path: + +```rust +use cqlib_core::circuit::circuit_to_matrix; +use cqlib_core::qis::{Hamiltonian, PauliString}; +use cqlib_core::qis::evolution::TrotterMode; + +// H = 0.5*Z,t = π,U = e^{-i*0.5*π*Z} = diag(-i, i) +let mut h = Hamiltonian::new(1); +h.add_term("Z".parse::().unwrap(), 0.5.into()).unwrap(); + +let circuit = h + .to_evolution_circuit(std::f64::consts::PI, 1, TrotterMode::FirstOrder) + .unwrap(); +let matrix = circuit_to_matrix(&circuit, None).unwrap(); + +assert!((matrix[[0, 0]] - num_complex::Complex64::new(0.0, -1.0)).norm() < 1e-12); +assert!((matrix[[1, 1]] - num_complex::Complex64::new(0.0, 1.0)).norm() < 1e-12); +``` + +--- + +## Validation and error handling + +| Error | When it occurs | +| --- | --- | +| `QisError::InvalidParameterValue` | `steps` is 0, or the term list of the Hamiltonian is empty. | +| `QisError::NotHermitian` | A coefficient whose imaginary part still exceeds the tolerance remains after simplification. | +| `QisError::UnsupportedOperation` | An error occurred during circuit construction, for example a Pauli string with phase $\pm i$ is present among the terms. | +| `CircuitError::QubitCountMismatch` | The length of `qubits` in `pauli_evolution()` is inconsistent with the qubit count of the Pauli string. | +| `CircuitError::InvalidOperation` | The phase of the Pauli string in `pauli_evolution()` is $\pm i$. | + +The checks on `steps` and on an empty Hamiltonian happen first, so `to_evolution_circuit()` still requires `steps` to be greater than 0 even when it will take the exact path. + +Example: + +```rust +use cqlib_core::qis::Hamiltonian; +use cqlib_core::qis::evolution::TrotterMode; + +let h = Hamiltonian::new(2); +assert!(h.to_trotter_circuit(1.0, 1, TrotterMode::FirstOrder).is_err()); + +let mut h = Hamiltonian::new(2); +h.add_term("ZZ".parse().unwrap(), 0.5.into()).unwrap(); +assert!(h.to_trotter_circuit(1.0, 0, TrotterMode::FirstOrder).is_err()); +assert!(h.to_evolution_circuit(1.0, 0, TrotterMode::FirstOrder).is_err()); +``` + +--- + +## Related pages + +- [QIS Overview](0_overview.md): module overview and terminology. +- [Hamiltonian and Observable](7_hamiltonian.md): construction of terms, simplification, commutation checks and expectation values. +- [Pauli Operators and Pauli Strings](6_pauli.md): phase values and string parsing conventions. diff --git a/docs/api/en/rust/3_qis/9_metrics_entropy.md b/docs/api/en/rust/3_qis/9_metrics_entropy.md new file mode 100644 index 00000000..cda03779 --- /dev/null +++ b/docs/api/en/rust/3_qis/9_metrics_entropy.md @@ -0,0 +1,299 @@ +# Metrics and Entropy + +`cqlib_core::qis::metrics` +`cqlib_core::qis::entropy` + +This page covers the metrics and information measures between quantum states. `metrics` collects purity, fidelity, trace distance, von Neumann entropy and partial transpose related quantities; `entropy` collects linear entropy, Rényi entropy, and the measures aimed at entanglement. The functions of both modules take a statevector or a density matrix as input and return `Result` (except `density_matrix_to_faer`). + +## Import + +```rust +use cqlib_core::qis::metrics::{ + density_matrix_to_faer, entropy, logarithmic_negativity, partial_transpose, purity_mixed, + purity_pure, state_fidelity_mixed, state_fidelity_pure, state_fidelity_pure_mixed, + trace_distance_mixed, trace_distance_pure, +}; +use cqlib_core::qis::entropy::{ + concurrence, entanglement_entropy_pure, entanglement_of_formation, linear_entropy, negativity, + renyi_entropy, +}; +``` + +Both modules are public under `cqlib_core::qis`; `metrics` and `entropy` themselves are not re-exported at the top level. + +--- + +## Purity + +### `purity_pure(sv: &Statevector) -> Result` + +Return the squared norm $\langle \psi | \psi \rangle$ of the statevector. A normalized statevector gives `1.0`, so this function can also serve as a numerical check of normalization. + +### `purity_mixed(dm: &DensityMatrix) -> Result` + +Return the purity $\text{Tr}(\rho^2)$ of the density matrix. Since $\rho$ is a Hermitian matrix, the implementation sums the squared moduli of all elements. The value falls between $1/2^N$ (maximally mixed state) and `1.0` (pure state). + +Example: + +```rust +use cqlib_core::qis::state::{DensityMatrix, Statevector}; +use cqlib_core::qis::metrics::{purity_mixed, purity_pure}; + +let sv = Statevector::new(2); +assert!((purity_pure(&sv).unwrap() - 1.0).abs() < 1e-10); + +// 单比特完全混合态 Tr((I/2)^2) = 0.5 +let dm = DensityMatrix::maximally_mixed(1); +assert!((purity_mixed(&dm).unwrap() - 0.5).abs() < 1e-10); +``` + +--- + +## Fidelity + +### `state_fidelity_pure(sv1: &Statevector, sv2: &Statevector) -> Result` + +The fidelity between two pure states, $F(\psi, \phi) = |\langle \psi | \phi \rangle|^2$, ranging from `0.0` (orthogonal) to `1.0` (the same state). When the qubit counts are inconsistent, `QisError::QubitMismatch` is returned. + +### `state_fidelity_pure_mixed(sv: &Statevector, dm: &DensityMatrix) -> Result` + +The fidelity between a pure state and a mixed state, $F(\psi, \rho) = \langle \psi | \rho | \psi \rangle$. When the qubit counts are inconsistent, `QisError::QubitMismatch` is returned. + +### `state_fidelity_mixed(dm1: &DensityMatrix, dm2: &DensityMatrix) -> Result` + +The fidelity between two mixed states, $F(\rho, \sigma) = \left(\text{Tr}\sqrt{\sqrt{\rho}\,\sigma\sqrt{\rho}}\right)^2$. When the qubit counts are inconsistent, `QisError::QubitMismatch` is returned; when the eigendecomposition fails, `QisError::UnsupportedOperation` is returned. + +Example: + +```rust +use cqlib_core::qis::state::{DensityMatrix, Statevector}; +use cqlib_core::qis::metrics::{state_fidelity_pure, state_fidelity_pure_mixed}; + +let sv0 = Statevector::new(1); + +// |1> 态 +let mut sv1 = Statevector::new(1); +sv1.data_mut()[0] = num_complex::Complex64::new(0.0, 0.0); +sv1.data_mut()[1] = num_complex::Complex64::new(1.0, 0.0); + +assert!((state_fidelity_pure(&sv0, &sv0).unwrap() - 1.0).abs() < 1e-10); +assert!((state_fidelity_pure(&sv0, &sv1).unwrap() - 0.0).abs() < 1e-10); + +// 纯态 |0> 与完全混合态的保真度为 0.5 +let dm_mixed = DensityMatrix::maximally_mixed(1); +assert!((state_fidelity_pure_mixed(&sv0, &dm_mixed).unwrap() - 0.5).abs() < 1e-10); +``` + +--- + +## Trace distance + +### `trace_distance_pure(sv1: &Statevector, sv2: &Statevector) -> Result` + +The trace distance between two pure states, $D(\psi, \phi) = \sqrt{1 - |\langle \psi | \phi \rangle|^2}$, ranging from `0.0` (the same state) to `1.0` (orthogonal). The fidelity is computed internally first, so an inconsistent qubit count likewise returns `QisError::QubitMismatch`. + +### `trace_distance_mixed(dm1: &DensityMatrix, dm2: &DensityMatrix) -> Result` + +The trace distance between two mixed states, $D(\rho, \sigma) = \frac{1}{2}\text{Tr}|\rho - \sigma|$. When both traces are 1, the value equals the sum of the positive eigenvalues of $\rho - \sigma$. When the qubit counts are inconsistent, `QisError::QubitMismatch` is returned. + +Example: + +```rust +use cqlib_core::qis::state::{DensityMatrix, Statevector}; +use cqlib_core::qis::metrics::{trace_distance_mixed, trace_distance_pure}; + +let sv0 = Statevector::new(1); + +let mut sv1 = Statevector::new(1); +sv1.data_mut()[0] = num_complex::Complex64::new(0.0, 0.0); +sv1.data_mut()[1] = num_complex::Complex64::new(1.0, 0.0); + +assert!((trace_distance_pure(&sv0, &sv0).unwrap() - 0.0).abs() < 1e-10); +assert!((trace_distance_pure(&sv0, &sv1).unwrap() - 1.0).abs() < 1e-10); + +// |0><0| 与 |1><1| 的迹距离为 1 +let dm0 = DensityMatrix::new(1); +let dm1 = DensityMatrix::from_density_matrix_state( + 1, + vec![ + num_complex::Complex64::new(0.0, 0.0), + num_complex::Complex64::new(0.0, 0.0), + num_complex::Complex64::new(0.0, 0.0), + num_complex::Complex64::new(1.0, 0.0), + ], +) +.unwrap(); + +assert!((trace_distance_mixed(&dm0, &dm1).unwrap() - 1.0).abs() < 1e-10); +``` + +--- + +## Entropy + +`entropy` belongs to `metrics`, while `linear_entropy` and `renyi_entropy` belong to `entropy`. + +### `entropy(dm: &DensityMatrix) -> Result` + +The von Neumann entropy $S(\rho) = -\text{Tr}(\rho \log_2 \rho)$, in units of bits. Eigenvalues whose absolute value is smaller than $10^{-12}$ are ignored during the computation, in order to filter out numerical noise. When the eigendecomposition fails, `QisError::UnsupportedOperation` is returned. + +### `linear_entropy(dm: &DensityMatrix) -> Result` + +The linear entropy $S_L(\rho) = 1 - \text{Tr}(\rho^2)$. It is `0.0` for a pure state and $1 - 1/2^N$ for an $N$-qubit maximally mixed state. + +### `renyi_entropy(dm: &DensityMatrix, alpha: f64) -> Result` + +The Rényi entropy $S_\alpha(\rho) = \frac{1}{1-\alpha}\log_2\left(\sum_i \lambda_i^\alpha\right)$, where $\lambda_i$ are the eigenvalues of the density matrix. When $\alpha$ is not greater than 0, `QisError::InvalidParameterValue` is returned; when the difference between $\alpha$ and 1 is smaller than `f64::EPSILON`, the von Neumann entropy is used instead, avoiding division by zero. The result is truncated to a non-negative value. + +Example: + +```rust +use cqlib_core::qis::state::DensityMatrix; +use cqlib_core::qis::metrics::entropy; +use cqlib_core::qis::entropy::{linear_entropy, renyi_entropy}; + +// 纯态熵为 0 +let pure = DensityMatrix::new(1); +assert!(entropy(&pure).unwrap().abs() < 1e-10); +assert!(linear_entropy(&pure).unwrap().abs() < 1e-10); + +// 单比特完全混合态的冯·诺依曼熵为 1 比特,线性熵为 0.5 +let mixed = DensityMatrix::maximally_mixed(1); +assert!((entropy(&mixed).unwrap() - 1.0).abs() < 1e-10); +assert!((linear_entropy(&mixed).unwrap() - 0.5).abs() < 1e-10); + +// Rényi 熵在 α 趋向 1 时退化为冯·诺依曼熵 +assert!((renyi_entropy(&mixed, 1.0).unwrap() - 1.0).abs() < 1e-10); +assert!((renyi_entropy(&mixed, 2.0).unwrap() - 1.0).abs() < 1e-10); +assert!(renyi_entropy(&mixed, -1.0).is_err()); +``` + +--- + +## Entanglement measures + +`entanglement_entropy_pure`, `negativity`, `concurrence` and `entanglement_of_formation` belong to `entropy`, while `partial_transpose` and `logarithmic_negativity` belong to `metrics`. + +### `entanglement_entropy_pure(sv: &Statevector, subsys_a: &[usize]) -> Result` + +The entanglement entropy of a bipartite pure state, that is the von Neumann entropy of the reduced density matrix of subsystem A, $E(|\psi\rangle) = S(\rho_A)$. `subsys_a` gives the qubit indices of subsystem A, and the remaining qubits form subsystem B. + +`QisError::InvalidSubsystem` is returned in the following cases: `subsys_a` is empty, contains all qubits, or has duplicate indices. When an index is out of range, `QisError::IndexOutOfBounds` is returned. + +### `partial_transpose(dm: &DensityMatrix, target_qubits: &[usize]) -> Result` + +Perform a partial transpose on the given subsystem, transposing only the indices corresponding to `target_qubits` and leaving the remaining qubits unchanged. The matrix is flattened in row-major order, with the index convention `idx = (ket << n) | bra`. When a target index is out of range, `QisError::IndexOutOfBounds` is returned. + +### `negativity(dm: &DensityMatrix, subsys_a: &[usize]) -> Result` + +The negativity, taken as the sum of the absolute values of the negative eigenvalues after the partial transpose, $N(\rho) = \frac{1}{2}\left(\|\rho^{T_A}\|_1 - 1\right)$. On $2 \otimes 2$ and $2 \otimes 3$ systems, a value of 0 is a necessary and sufficient condition for separability. Unlike the logarithmic negativity, this quantity is not additive under the tensor product. + +### `logarithmic_negativity(dm: &DensityMatrix, sys_a: &[usize]) -> Result` + +The logarithmic negativity $E_N(\rho) = \log_2 \|\rho^{T_A}\|_1$, an entanglement measure that is 0 for separable states and greater than 0 for entangled states, and also an upper bound on the distillable entanglement. The implementation performs the partial transpose first, then sums the absolute values of the eigenvalues and takes the base-2 logarithm, truncating the result below at 0. + +### `concurrence(dm: &DensityMatrix) -> Result` + +The concurrence of a two-qubit system, $C(\rho) = \max(0, \lambda_1 - \lambda_2 - \lambda_3 - \lambda_4)$, where $\lambda_i$ are the eigenvalues of $\sqrt{\sqrt{\rho}\,\tilde{\rho}\sqrt{\rho}}$ in descending order and $\tilde{\rho}$ is the spin-flipped density matrix. The value ranges from `0.0` (separable) to `1.0` (maximally entangled). When `dm` is not two-qubit, `QisError::UnsupportedDimension` is returned. + +### `entanglement_of_formation(dm: &DensityMatrix) -> Result` + +The entanglement of formation, the binary entropy derived from the concurrence. It is `0.0` for separable states and `1.0` for maximally entangled states. A non-two-qubit input likewise returns `QisError::UnsupportedDimension`. + +Example: + +```rust +use cqlib_core::qis::state::{DensityMatrix, Statevector}; +use cqlib_core::qis::entropy::{ + concurrence, entanglement_entropy_pure, entanglement_of_formation, negativity, +}; +use cqlib_core::qis::metrics::logarithmic_negativity; + +// Bell 态 |Φ+⟩ 的纠缠熵为 1 比特 +let mut sv = Statevector::new(2); +sv.apply_h(0).unwrap(); +sv.apply_cx(0, 1).unwrap(); +assert!((entanglement_entropy_pure(&sv, &[0]).unwrap() - 1.0).abs() < 1e-10); + +// 同一状态的多种纠缠度量 +let mut dm = DensityMatrix::new(2); +dm.apply_h(0).unwrap(); +dm.apply_cx(0, 1).unwrap(); + +assert!((negativity(&dm, &[0]).unwrap() - 0.5).abs() < 1e-10); +assert!((logarithmic_negativity(&dm, &[0]).unwrap() - 1.0).abs() < 1e-10); +assert!((concurrence(&dm).unwrap() - 1.0).abs() < 1e-10); +assert!((entanglement_of_formation(&dm).unwrap() - 1.0).abs() < 1e-10); +``` + +All measures vanish on a separable state: + +```rust +use cqlib_core::qis::state::{DensityMatrix, Statevector}; +use cqlib_core::qis::entropy::{concurrence, entanglement_entropy_pure, negativity}; +use cqlib_core::qis::metrics::logarithmic_negativity; + +// |+0> = |+> ⊗ |0>,两个比特之间没有纠缠 +let mut sv = Statevector::new(2); +sv.apply_h(0).unwrap(); +assert!(entanglement_entropy_pure(&sv, &[0]).unwrap().abs() < 1e-10); + +let dm = DensityMatrix::new(2); +assert!(negativity(&dm, &[0]).unwrap().abs() < 1e-10); +assert!(logarithmic_negativity(&dm, &[0]).unwrap().abs() < 1e-10); +assert!(concurrence(&dm).unwrap().abs() < 1e-10); +``` + +Partial transpose: + +```rust +use cqlib_core::qis::state::DensityMatrix; +use cqlib_core::qis::metrics::partial_transpose; +use num_complex::Complex64; + +// Bell 态密度矩阵:|00><00| + |00><11| + |11><00| + |11><11|,各占 0.5 +let mut data = vec![Complex64::new(0.0, 0.0); 16]; +data[0] = Complex64::new(0.5, 0.0); +data[3] = Complex64::new(0.5, 0.0); +data[12] = Complex64::new(0.5, 0.0); +data[15] = Complex64::new(0.5, 0.0); +let dm = DensityMatrix::from_density_matrix_state(2, data).unwrap(); + +// 偏转置后 |00><11| 被换到 |01><10| 的位置 +let pt = partial_transpose(&dm, &[0]).unwrap(); +assert!((pt.data()[0] - Complex64::new(0.5, 0.0)).norm() < 1e-10); +assert!((pt.data()[15] - Complex64::new(0.5, 0.0)).norm() < 1e-10); + +// 下标越界 +assert!(partial_transpose(&dm, &[2]).is_err()); +``` + +--- + +## Helper conversion + +### `density_matrix_to_faer(dm: &DensityMatrix) -> Mat` + +Convert a density matrix into the matrix type of the eigendecomposition library, for scenarios that need to call the eigendecomposition directly. The input is flattened in row-major order and the dimension is $2^N$. This function does not return a `Result` and is the low-level step shared by the other functions that rely on eigendecomposition. + +--- + +## Validation and error handling + +| Error | When it occurs | +| --- | --- | +| `QisError::QubitMismatch` | The two states involved in the operation have inconsistent qubit counts. | +| `QisError::InvalidParameterValue` | `alpha` of `renyi_entropy()` is not greater than 0. | +| `QisError::InvalidSubsystem` | The subsystem of `entanglement_entropy_pure()` is empty, contains all qubits, or has duplicate indices. | +| `QisError::IndexOutOfBounds` | A target index of `partial_transpose()` is out of range, or a subsystem index of `entanglement_entropy_pure()` is out of range. | +| `QisError::UnsupportedDimension` | The input of `concurrence()` and `entanglement_of_formation()` is not two-qubit. | +| `QisError::UnsupportedOperation` | The eigendecomposition fails. | + +`trace_distance_pure()` returns `QisError::QubitMismatch` indirectly through `state_fidelity_pure()`; `negativity()` and `logarithmic_negativity()` return index-related errors indirectly through `partial_transpose()`. + +--- + +## Related pages + +- [QIS Overview](0_overview.md): module overview and terminology. +- [Hamiltonian and Observable](7_hamiltonian.md): compute observable expectation values from a state, usable together with the measures on this page. diff --git a/docs/api/en/rust/4_compile/0_overview.md b/docs/api/en/rust/4_compile/0_overview.md new file mode 100644 index 00000000..4e26be8d --- /dev/null +++ b/docs/api/en/rust/4_compile/0_overview.md @@ -0,0 +1,218 @@ +# Compilation + +`cqlib_core::compile` + +`cqlib_core::compile` is the compilation pipeline module of Cqlib. Through the end-to-end `compile()` function it covers the complete flow from a logical circuit to a circuit that satisfies the target constraint (a physical circuit under a device target), and it also splits each compilation stage into independently callable transforms for callers that need fine-grained control. + +## Overview + +Compilation is the process of rewriting a given input circuit so that it matches a specific quantum device topology and is optimized for the execution target. Most circuits must go through a series of transforms before they are compatible with the target device and before the impact of noise on the results is reduced. The recommended entry point of Cqlib is the `compile()` function: a single call performs canonicalization, knowledge rule optimization, gate decomposition, optional layout and routing, target gate set translation and final validation. + +### Compilation pipeline + +Every step of `compile()` leaves a record in `CompileResult.steps`, and the `stage` field of that record is a **coarse stage classification** with exactly seven possible values: + +| `stage` | Classification | +| --- | --- | +| `pre_init` | Target constraint resolution and resource preflight, completed before the circuit is changed. | +| `init` | Establish a stable high-level IR: input canonicalization and expansion of custom gate definitions. | +| `optimization` | Simplification: knowledge rule rewriting, commutative cancellation, resynthesis and single-qubit optimization. | +| `translation` | Reduction of operation kinds: unitary synthesis, multi-controlled gate decomposition, routing gate set lowering, target gate set translation and device native lowering. | +| `routing` | Initial layout selection and SWAP insertion. | +| `output` | Output canonicalization of the artifact. | +| `validation` | Device validation. | + +The `stage` field and the step name are two different things: the step name (such as `optimize.pre_decomposition`) describes what the step does, while `stage` only states which class the step belongs to. The narrative division of the compilation flow below can therefore **span several `stage` values**; the two are not in one-to-one correspondence. + +`compile()` orchestrates the following steps in a fixed order: + +1. **Target resolution and resource validation** (`pre_init`) — resolve the target constraint (`resolve.target`) and validate the ancilla resource policy (`validate.resources`). +2. **Input canonicalization** (`init`) — perform production-grade canonicalization on the input circuit (`canonicalize.input`). +3. **Definition expansion and pre-decomposition optimization** (`init` → `optimization`) — first expand user custom gate definitions (`decompose.definitions`) so that knowledge rules can see the operations inside composite gates; then perform pre-decomposition knowledge rule rewriting (`optimize.pre_decomposition`). +4. **Gate decomposition** (`translation` → `optimization`) — matrix unitary gate synthesis (`decompose.unitary`), multi-controlled gate decomposition (`decompose.mc_gates`, requesting ancillas according to the resource policy), followed by post-decomposition canonicalization (`canonicalize.after_decomposition`), commutative cancellation (`optimize.commutative_cancellation`), two-qubit block resynthesis (`resynthesize.two_qubit_blocks`) and single-qubit optimization (`optimize.one_qubit.post_decomposition`). +5. **Post-decomposition optimization** (`optimization`) — knowledge rule rewriting (`optimize.post_decomposition`), then iterate single-qubit optimization and two-qubit block resynthesis to a fixed point (`optimize.one_qubit_fixed_point`). +6. **Routing gate set lowering** (`translation`) — lower gates that the SABRE gate-arity constraint does not support (such as `CCX`) into operations on at most two qubits (`decompose.routing_basis`). +7. **Layout and routing** (`routing` → `optimization`) — under a device target, select the initial layout and insert SWAPs to satisfy the topology adjacency constraint (`route.sabre`); in enhanced mode, perform cleanup and resynthesis after routing (`optimize.post_routing`). +8. **Target gate set translation** (`translation`) — lower the circuit to the target gate set (`translate.target_basis`); a device target lowers it further to the native instruction set (`lower.device_instructions`). +9. **Native optimization and final validation** (`optimization` / `validation`) — optimize native instructions to a fixed point (`optimize.native_fixed_point`); a device target finally performs device validation (`validate.device`). + +Without a device target, the layout, routing and device validation steps are skipped, and the skip reason is recorded in `steps`. Each stage can also be invoked individually, bypassing `compile()`; see [Transform](2_transform.md) and the following pages. + +### Choosing the optimization strength + +`CompileMode::Normal` is conservative logical optimization and uses the production default pass parameters; `CompileMode::Enhanced` uses a stronger staged workflow and a larger pass budget, and performs target-aware cleanup when a target constraint is present. For most use cases `Normal` is sufficient; use `Enhanced` when the compilation time budget is ample and better post-routing cleanup is preferred. + +### Choosing the target constraint + +- `CompileTarget::Logical`: performs only logical optimization, and the output is not bound to any hardware. Suitable for circuit simplification at the algorithmic level. +- `CompileTarget::Basis(vec![...])`: lowers to an explicit standard basis, without involving device topology. +- `CompileTarget::Device(DeviceCompileTarget { .. })`: routing and lowering for a concrete device; the output must match the native capabilities of the device, and final device validation is performed. +- `CompileTarget::TopologyBasis { .. }`: routes on the device topology while lowering to an explicit basis; the device only provides capacity, layout and routing constraints and does not require the output basis to match the native capabilities of the device. + +### Reproducibility + +Layout and routing during compilation involve randomized heuristics. Passing `seed` in `DeviceCompileTarget` yields deterministic results: the same circuit, the same configuration and the same seed produce the same cqlib result. Outputs are not guaranteed to be identical across cqlib versions; the seed only guarantees reproducibility within the same version. + +--- + +## Common entry points + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::compile::{CompileConfig, CompileMode, CompileTarget, compile}; +use cqlib_core::compile::resource::ResourcePolicy; + +let mut circuit = Circuit::new(2); +circuit.h(Qubit::new(0)).unwrap(); +circuit.cx(Qubit::new(0), Qubit::new(1)).unwrap(); + +let result = compile( + &circuit, + CompileConfig { + mode: CompileMode::Normal, + target: CompileTarget::Logical, + resource_policy: ResourcePolicy::default(), + }, +) +.unwrap(); + +for step in &result.steps { + if step.changed && !step.skipped { + println!("{} {}", step.stage, step.name); + } +} +``` + +--- + +## Core concepts and terms + +| Term | Description | +| --- | --- | +| **Compilation pipeline** | The flow that progressively converts an abstract circuit into a circuit satisfying the target constraint, including stages such as logical optimization, decomposition, layout, routing and target gate set translation. | +| **Pass** | An independent transform step in the compilation pipeline. Most passes can either be orchestrated automatically by `compile()` or invoked individually. | +| **Transformer** | The unified trait of a transform. Implements `transform(&self, circuit, analysis) -> Result`; the input circuit is left unchanged. | +| **Target constraint** | The conditions the compilation output must satisfy: logical space (`CompileTarget::Logical`), standard basis (`Basis`), physical device (`Device`) and device topology plus basis (`TopologyBasis`). | +| **Initial layout** | The mapping from logical qubits to physical qubits before compilation starts. It can be provided by the caller or selected automatically by a layout algorithm. | +| **Routing** | The process of inserting movement operations such as SWAPs on a constrained topology so that all two-body interactions satisfy the adjacency constraint. | +| **Gate decomposition** | Expanding a composite gate, custom unitary gate or multi-controlled gate into a sequence of more basic gates. | +| **Knowledge rule** | A rule describing adjacent operation patterns and their replacement form, used for pattern simplification, cancellation and native gate matching. | +| **Ancilla** | An auxiliary qubit requested on demand during decomposition. `AncillaRequirement::CleanZero` requires it to be in `\|0>` on entry to and exit from the transform, while `Dirty` requires its original state to be fully restored. | + +--- + +## `cqlib_core::compile` API overview + +### End-to-end compilation entry points + +| Name | Description | +| --- | --- | +| [`compile`](1_compiler.md) | The recommended entry point; a single call performs configuration, execution and result reporting. | +| [`CompileConfig`](1_compiler.md) | Compilation configuration, containing the mode, target constraint and resource policy. | +| [`CompileMode`](1_compiler.md) | Optimization strength enum, including `Normal` and `Enhanced`. | +| [`CompileTarget`](1_compiler.md) | Target constraint enum, including logical, basis, device and topology plus basis. | +| [`DeviceCompileTarget`](1_compiler.md) | Device target input, containing the device, an optional initial layout and a random seed. | +| [`CompileResult`](1_compiler.md) | Compilation result, containing the optimized circuit, the step reports and the device layout metadata. | +| [`WorkflowStepReport`](1_compiler.md) | The execution record of a single workflow step. | +| [`DeviceCompilationMetadata`](1_compiler.md) | The initial and final layouts produced by device compilation. | +| [`CompilerWorkflow`](1_compiler.md) | A reusable workflow object. | + +### Base transform passes + +| Name | Description | +| --- | --- | +| [`Transformer`](2_transform.md) / [`TransformOutcome`](2_transform.md) | The unified transform trait and its result object. | +| [`CircuitAnalysis`](2_transform.md) | A snapshot of circuit features. | +| [`Canonicalizer`](2_transform.md) / [`canonicalize_circuit`](2_transform.md) | The canonicalization pass. | +| [`KnowledgeRewriter`](2_transform.md) / [`rewrite_circuit`](2_transform.md) | A pattern rewriting pass based on knowledge rules. | +| [`OptimizeOneQubitRuns`](2_transform.md) | Optimization of runs of consecutive single-qubit gates. | +| [`CommutativeCancellation`](2_transform.md) | Self-inverse cancellation based on commutation. | +| [`LowerToRoutingBasis`](2_transform.md) | Routing gate set lowering. | + +### Layout and routing + +| Name | Description | +| --- | --- | +| [`trivial_layout`](3_layout.md) / [`greedy_layout`](3_layout.md) / [`vf2_perfect_layout`](3_layout.md) / [`sabre_layout`](3_layout.md) | Four initial layout algorithms. | +| [`LayoutObjective`](3_layout.md) / [`LayoutScore`](3_layout.md) | Layout objective weights and scoring results. | +| [`CircuitLayoutAnalysis`](3_layout.md) / [`InteractionGraph`](3_layout.md) | Circuit interaction graph analysis. | +| [`PhysicalLayoutGraph`](3_layout.md) / [`DistanceTable`](3_layout.md) | Device physical graph and shortest distance table. | +| [`route_with_layout`](4_routing.md) / [`route_sabre`](4_routing.md) | Transform-level routing entry points. | +| [`RoutedCircuit`](4_routing.md) / [`SabreRouteResult`](4_routing.md) | Routing result objects. | +| [`SabreConfig`](5_sabre.md) / [`SabreHeuristicConfig`](5_sabre.md) / [`SabreVf2PrepassConfig`](5_sabre.md) | SABRE algorithm configuration. | +| [`sabre_route`](5_sabre.md) | SABRE routing given a known initial layout. | + +### Decomposition, resynthesis and target gate set + +| Name | Description | +| --- | --- | +| [`decompose` submodule](6_decompose_resynthesis.md) | Definition expansion, unitary gate synthesis and multi-controlled gate decomposition. | +| [`ResynthesizeTwoQubitBlocks`](6_decompose_resynthesis.md) / [`resynthesize_two_qubit_blocks`](6_decompose_resynthesis.md) | Two-qubit block resynthesis. | +| [`TargetBasisLowerer`](6_decompose_resynthesis.md) / [`TargetBasisCostModel`](6_decompose_resynthesis.md) | Target gate set translation and cost evaluation. | +| [`DeviceLowerer`](6_decompose_resynthesis.md) | Device native instruction lowering. | + +### Knowledge rules and resource management + +| Name | Description | +| --- | --- | +| [`Rule`](7_knowledge.md) / [`RuleItem`](7_knowledge.md) / [`Condition`](7_knowledge.md) | Knowledge rule construction. | +| [`RuleLibrary`](7_knowledge.md) / [`RuleMetadata`](7_knowledge.md) / [`RuleKind`](7_knowledge.md) | Rule library validation, classification and queries. | +| [`MatchBindings`](7_knowledge.md) / [`rule_matches_operations`](7_knowledge.md) | Rule structural matching. | +| [`ResourceManager`](8_resource.md) / [`ResourcePolicy`](8_resource.md) / [`ResourceLimits`](8_resource.md) | Ancilla resource planning and leasing. | +| [`Commutation`](9_commutation.md) / [`CommutationChecker`](9_commutation.md) / [`check_commutation`](9_commutation.md) | Conservative proof of commutation. | + +--- + +## Quick examples + +### 1. End-to-end compilation + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::compile::{CompileConfig, CompileMode, CompileTarget, compile}; +use cqlib_core::compile::resource::ResourcePolicy; + +let mut circuit = Circuit::new(2); +circuit.h(Qubit::new(0)).unwrap(); +circuit.cx(Qubit::new(0), Qubit::new(1)).unwrap(); + +let result = compile( + &circuit, + CompileConfig { + mode: CompileMode::Normal, + target: CompileTarget::Logical, + resource_policy: ResourcePolicy::default(), + }, +) +.unwrap(); + +assert!(!result.steps.is_empty()); +``` + +### 2. Running a single transform + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::compile::transform::{Canonicalizer, CanonicalizeConfig, Transformer}; + +let mut circuit = Circuit::new(1); +circuit.i(Qubit::new(0)).unwrap(); + +let outcome = Canonicalizer::new(CanonicalizeConfig::production()) + .transform(&circuit, None) + .unwrap(); +assert!(outcome.changed()); +``` + +--- + +## Validation and error handling + +The compilation process can fail during configuration resolution, transform execution and device validation, and failures are uniformly returned through `CompilerError`. Common cases: + +| Error | When it occurs | +| --- | --- | +| `CompilerError::InvalidInput` | Invalid configuration, for example `routing_trials` is zero or the target gate set is empty. | +| Other `CompilerError` variants | Transform execution failure, unreachable routing, insufficient device capacity, and so on. | + +For device-related errors, refer to the device module documentation; for knowledge rule parsing errors, refer to [Knowledge](7_knowledge.md); for insufficient resource errors, refer to [Resource](8_resource.md). diff --git a/docs/api/en/rust/4_compile/1_compiler.md b/docs/api/en/rust/4_compile/1_compiler.md new file mode 100644 index 00000000..1b9d3337 --- /dev/null +++ b/docs/api/en/rust/4_compile/1_compiler.md @@ -0,0 +1,224 @@ +# Compiler + +The end-to-end compilation entry points of `cqlib_core::compile`, including the `compile()` function, the compilation configuration object, the compilation result object and the reusable workflow object. + +## Import + +```rust +use cqlib_core::compile::{ + CompileConfig, CompileMode, CompileResult, CompileTarget, CompilerWorkflow, + DeviceCompilationMetadata, DeviceCompileTarget, WorkflowStepReport, compile, +}; +``` + +--- + +## Functions + +### `compile(circuit: &Circuit, config: CompileConfig) -> Result` + +Run the configured compilation workflow and return the compilation result. The input circuit is not modified. The returned result records the optimized circuit and the step reports in execution order; a device target additionally returns the initial and final layouts. An error is raised when the configured target, native implementation or final device validation cannot be satisfied. + +Example: + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::compile::{CompileConfig, CompileMode, CompileTarget, compile}; +use cqlib_core::compile::resource::ResourcePolicy; + +let mut circuit = Circuit::new(2); +circuit.h(Qubit::new(0)).unwrap(); +circuit.cx(Qubit::new(0), Qubit::new(1)).unwrap(); + +let result = compile( + &circuit, + CompileConfig { + mode: CompileMode::Normal, + target: CompileTarget::Logical, + resource_policy: ResourcePolicy::default(), + }, +) +.unwrap(); + +assert_eq!(result.mode, CompileMode::Normal); +assert!(!result.steps.is_empty()); +assert_eq!(result.circuit.qubits().len(), 2); +``` + +--- + +## CompileMode + +Optimization strength enum. + +```rust +pub enum CompileMode { + Normal, + Enhanced, +} +``` + +- `CompileMode::Normal`: conservative logical optimization, using the production default pass parameters (the default value). +- `CompileMode::Enhanced`: a stronger staged workflow with a larger pass budget, performing target-aware cleanup when a target constraint is present. + +Implements `Default` (as `Normal`), `Copy`, `Eq`, `Hash`. + +--- + +## CompileConfig + +Compilation configuration, describing the logical optimization strength, the optional target constraint and the ancilla resource permissions available before layout. + +```rust +pub struct CompileConfig { + pub mode: CompileMode, + pub target: CompileTarget, + pub resource_policy: ResourcePolicy, +} +``` + +Fields: + +- `mode`: the optimization workflow mode. +- `target`: a mutually exclusive logical, basis or physical device target. +- `resource_policy`: the ancilla resource permissions of the pre-layout decomposition passes. Controls whether allocating logical clean ancillas or borrowing dirty input qubits is allowed. A device target derives hard capacity from the available physical qubits and is not subject to this policy. + +--- + +## CompileTarget + +Target constraint enum. + +```rust +pub enum CompileTarget { + Logical, + Basis(Vec), + Device(DeviceCompileTarget), + TopologyBasis { + device_target: DeviceCompileTarget, + basis: Vec, + }, +} +``` + +- `Logical`: compiles in the logical qubit space, with no target-related lowering. +- `Basis(Vec)`: lowers to an explicit standard basis. +- `Device(DeviceCompileTarget)`: routing and lowering for a concrete device. +- `TopologyBasis { device_target, basis }`: routes on the device topology while lowering to an explicit basis. The device is used only for capacity, layout and routing, and the output basis is not required to match the native capabilities of the device, so no exact device native lowering or final device validation is performed. + +--- + +## DeviceCompileTarget + +Device compilation input. + +```rust +pub struct DeviceCompileTarget { + pub device: Device, + pub initial_layout: Option, + pub seed: Option, +} +``` + +Fields: + +- `device`: an ordered native-capability device that constrains the output. +- `initial_layout`: the logical-to-physical initial layout provided by the caller. +- `seed`: the deterministic random seed of the device layout and routing heuristics. + +--- + +## CompileResult + +The object returned by `compile()`. + +```rust +pub struct CompileResult { + pub circuit: Circuit, + pub changed: bool, + pub mode: CompileMode, + pub steps: Vec, + pub device_metadata: Option, +} +``` + +Methods: + +- `step(&self, name: &str) -> Option<&WorkflowStepReport>`: return the first step report whose name matches. +- `step_changed(&self, name: &str) -> bool`: whether the report with that name contains a record that was not skipped and did change the circuit. + +--- + +## WorkflowStepReport + +The execution record of a single workflow step. + +```rust +pub struct WorkflowStepReport { + pub stage: &'static str, + pub name: &'static str, + pub changed: bool, + pub skipped: bool, + pub reason: Option, +} +``` + +Fields: + +- `stage`: the coarse-grained stage name, for example `"pre_init"`, `"init"`, `"optimization"`, `"translation"`, `"routing"`, `"output"`, `"validation"`. +- `name`: the step name within the workflow. +- `changed`: whether the step changed the circuit representation. +- `skipped`: whether the step was intentionally skipped. +- `reason`: the skip or configuration description. + +--- + +## DeviceCompilationMetadata + +The physical layout information produced by device compilation. + +```rust +pub struct DeviceCompilationMetadata { + pub initial_layout: Layout, + pub final_layout: Layout, +} +``` + +- `initial_layout`: the logical-to-physical layout before routing starts. +- `final_layout`: the logical-to-physical layout after all routing SWAPs. + +--- + +## CompilerWorkflow + +A reusable compilation workflow object. + +- `CompilerWorkflow::new(config: CompileConfig) -> Self` +- `config(&self) -> &CompileConfig` +- `run(&self, circuit: &Circuit) -> Result` + +Example: + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::compile::{CompileConfig, CompileMode, CompileTarget, CompilerWorkflow}; +use cqlib_core::compile::resource::ResourcePolicy; + +let workflow = CompilerWorkflow::new(CompileConfig { + mode: CompileMode::Enhanced, + target: CompileTarget::Logical, + resource_policy: ResourcePolicy::default(), +}); + +let mut circuit = Circuit::new(2); +circuit.h(Qubit::new(0)).unwrap(); +circuit.cx(Qubit::new(0), Qubit::new(1)).unwrap(); + +let result = workflow.run(&circuit).unwrap(); +``` + +--- + +## Errors + +`CompilerError` is the unified error type of the compilation module; invalid configuration (such as `routing_trials` being zero or an empty target gate set), transform execution failures and final device validation failures are all returned through it. diff --git a/docs/api/en/rust/4_compile/2_transform.md b/docs/api/en/rust/4_compile/2_transform.md new file mode 100644 index 00000000..0c0a29df --- /dev/null +++ b/docs/api/en/rust/4_compile/2_transform.md @@ -0,0 +1,214 @@ +# Transform + +`cqlib_core::compile::transform` provides reusable compilation passes. All passes implement the `Transformer` trait and return a `TransformOutcome`, leaving the input circuit unmodified. The end-to-end `compile()` orchestrates these passes automatically; they can be invoked directly when a single stage needs to be observed or debugged. + +## Import + +```rust +use cqlib_core::compile::transform::{ + CircuitAnalysis, CanonicalizeConfig, CanonicalizeResult, Canonicalizer, + canonicalize_circuit, CommutativeCancellation, KnowledgeRewriteResult, KnowledgeRewriteStats, + KnowledgeRewriter, LowerToRoutingBasis, OptimizeOneQubitRuns, RewriteConfig, RewriteMode, + TransformOutcome, Transformer, rewrite_circuit, +}; +``` + +--- + +## Transformer / TransformOutcome + +The unified transform trait and its result object. + +```rust +pub trait Transformer { + fn name(&self) -> &'static str; + fn transform( + &self, + circuit: &Circuit, + analysis: Option<&CircuitAnalysis>, + ) -> Result; +} +``` + +The `analysis` parameter of `transform` is an optional precomputed circuit analysis: passing it avoids repeated structural scans across workflow stages, and with `None` the transform derives the facts it needs itself. `TransformOutcome` methods: + +- `changed(&self) -> bool`: whether the pass changed the circuit. +- `into_circuit(self, original: &Circuit) -> Circuit`: take out the output circuit; with `Unchanged` it is equivalent to the input (the core workflow should match the enum directly to preserve zero-copy). + +--- + +## CircuitAnalysis + +A snapshot of circuit features, used by the workflow to decide which passes to run. The fields are public: + +```rust +pub struct CircuitAnalysis { + pub has_classical_data: bool, + pub has_classical_control: bool, + pub has_measurement: bool, + pub has_classical_values: bool, + pub has_classical_vars: bool, + pub has_runtime_classical: bool, + pub needs_classical_handle_preservation: bool, + pub has_circuit_gate_definitions: bool, + pub has_unitary_circuit_definitions: bool, + pub has_unitary_gates: bool, + pub has_mc_gates: bool, +} +``` + +- `CircuitAnalysis::analyze(circuit: &Circuit) -> Self`: analyze the circuit and return the snapshot. + +--- + +## Canonicalizer / canonicalize_circuit + +The canonicalization pass, which unifies instruction forms, folds the global phase and removes no-ops. + +### CanonicalizeConfig + +The fields are private and configured through builder methods: + +- `CanonicalizeConfig::new() -> Self`: the production default configuration. +- `CanonicalizeConfig::production() -> Self`: the production default configuration (the same as `new`). +- `with_round_limit(self, round_limit: u8) -> Self`: the maximum number of canonicalization rounds. +- `recurse_control_flow(self, enabled: bool) -> Self`: whether to recurse into control flow blocks. +- The remaining options `fold_gphase`, `canonicalize_instruction_form`, `drop_noops` and `canonicalize_barriers` each have a builder method of the same name. + +### Canonicalizer + +- `Canonicalizer::new(config: CanonicalizeConfig) -> Self` +- `Canonicalizer::production() -> Self`: the production default configuration. +- `config(&self) -> &CanonicalizeConfig` +- `run(&self, circuit: &Circuit) -> Result` +- Implements `Transformer` and `Default`. + +### CanonicalizeResult + +```rust +pub struct CanonicalizeResult { + pub circuit: Circuit, + pub changed: bool, + pub rounds: u8, +} +``` + +### canonicalize_circuit(circuit: &Circuit) -> Result + +The functional entry point, using the production default configuration. + +--- + +## KnowledgeRewriter / rewrite_circuit + +The pattern rewriting pass based on knowledge rules, and the core of logical optimization in the compilation pipeline. + +### RewriteMode + +```rust +pub enum RewriteMode { + Optimize, + Lowering, +} +``` + +- `Optimize`: conservative optimization; an accepted replacement must strictly improve the local cost. +- `Lowering`: explicit lowering; decomposition and hardware native rules may expand locally. + +### RewriteConfig + +The fields are private and configured through builder methods: + +- `RewriteConfig::new() -> Self`: the production default configuration. +- `RewriteConfig::production() -> Self`: the production optimization configuration. +- `RewriteConfig::lowering() -> Self`: the lowering configuration. +- `with_max_rounds(self, max_rounds: u8) -> Self` +- `with_max_window_ops(self, max_window_ops: usize) -> Self` +- `with_max_pattern_len(self, max_pattern_len: usize) -> Self` +- `recurse_control_flow(self, enabled: bool) -> Self` +- `skip_labeled_ops(self, enabled: bool) -> Self` +- `with_enabled_kinds(self, kinds: Vec) -> Self` +- `with_mode(self, mode: RewriteMode) -> Self` +- `with_target_instructions(self, instructions: Vec) -> Self` +- `try_with_target_instructions(self, instructions: Vec) -> Result` +- `enabled_kinds(&self) -> &[RuleKind]` +- `target_instruction_basis(&self) -> Option>` + +### KnowledgeRewriter + +- `KnowledgeRewriter::new(config: RewriteConfig) -> Self` +- `KnowledgeRewriter::production() -> Self` +- `KnowledgeRewriter::lowering() -> Self` +- `config(&self) -> &RewriteConfig` +- `run(&self, circuit: &Circuit) -> Result` +- Implements `Transformer`. + +### KnowledgeRewriteResult + +```rust +pub struct KnowledgeRewriteResult { + pub circuit: Circuit, + pub changed: bool, + pub stats: KnowledgeRewriteStats, +} +``` + +### KnowledgeRewriteStats + +```rust +pub struct KnowledgeRewriteStats { + pub rounds_executed: u8, + pub rules_applied: usize, + pub changed_sequences: usize, + pub reached_fixpoint: bool, +} +``` + +### rewrite_circuit(circuit: &Circuit, config: &RewriteConfig) -> Result + +The functional entry point. + +--- + +## OptimizeOneQubitRuns + +Optimization of runs of consecutive single-qubit gates. + +- `OptimizeOneQubitRuns::logical() -> Self`: a target-neutral optimizer using the strict logical cost. +- `OptimizeOneQubitRuns::basis(target_basis: Vec) -> Result`: optimize by the cost after exact lowering to the target gate set. +- Implements `Transformer`. + +--- + +## CommutativeCancellation + +A self-inverse cancellation pass based on commutation. + +- `CommutativeCancellation::new() -> Self` +- Implements `Transformer`. + +--- + +## LowerToRoutingBasis + +The routing gate set lowering pass, which converts the circuit into a form suitable for the routing stage. + +- `LowerToRoutingBasis::new(preferred_basis: Option>) -> Self` +- Implements `Transformer`. + +--- + +## Example + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::compile::transform::{CanonicalizeConfig, Canonicalizer, Transformer}; + +let mut circuit = Circuit::new(1); +circuit.i(Qubit::new(0)).unwrap(); + +let outcome = Canonicalizer::new(CanonicalizeConfig::production()) + .transform(&circuit, None) + .unwrap(); +assert!(outcome.changed()); +``` diff --git a/docs/api/en/rust/4_compile/3_layout.md b/docs/api/en/rust/4_compile/3_layout.md new file mode 100644 index 00000000..77a2e432 --- /dev/null +++ b/docs/api/en/rust/4_compile/3_layout.md @@ -0,0 +1,215 @@ +# Layout + +`cqlib_core::compile::transform::layout` provides initial layout algorithms and layout analysis tools. A layout algorithm maps logical qubits to physical qubits and provides the starting point for the subsequent routing stage. + +## Import + +```rust +use cqlib_core::compile::transform::layout::{ + CircuitLayoutAnalysis, Interaction, InteractionGraph, LayoutDiagnostics, LayoutObjective, + LayoutResult, LayoutScore, PhysicalLayoutGraph, Vf2EdgeRequirement, Vf2LayoutConfig, + analyze_circuit_for_layout, greedy_layout, prepare_sabre_circuit, prepare_sabre_device_target, + sabre_layout, trivial_layout, vf2_perfect_layout, +}; +``` + +--- + +## Layout functions + +- `trivial_layout(circuit: &Circuit, device: &Device, objective: &LayoutObjective) -> Result`: trivial layout, mapping in index order. +- `greedy_layout(circuit: &Circuit, device: &Device, objective: &LayoutObjective) -> Result`: greedy layout, placing in order of interaction strength. +- `vf2_perfect_layout(circuit: &Circuit, device: &Device, objective: &LayoutObjective, config: &Vf2LayoutConfig) -> Result`: VF2 exact layout, which produces a perfect layout when the circuit interaction graph can be embedded exactly into the device topology. +- `sabre_layout(circuit: &Circuit, device: &Device, objective: &LayoutObjective, config: &SabreConfig) -> Result`: SABRE layout, refining candidate layouts with the SABRE heuristic. + +Precomputed entry points: + +- `analyze_circuit_for_layout(circuit: &Circuit) -> CircuitLayoutAnalysis` +- `prepare_sabre_circuit(circuit: &Circuit) -> PreparedSabreCircuit` +- `prepare_sabre_device_target(device: &Device) -> Result` +- `trivial_layout_prepared(analysis: &CircuitLayoutAnalysis, physical: &PhysicalLayoutGraph, objective: &LayoutObjective) -> Result` +- `greedy_layout_prepared(...)`: the same as above. +- `vf2_perfect_layout_prepared(analysis, physical, objective, config) -> Result` +- `sabre_layout_prepared(prepared, prepared_target, objective, config) -> Result` + +--- + +## LayoutObjective + +Layout objective weights, used to score candidate layouts. + +```rust +pub struct LayoutObjective { + pub distance_weight: f64, + pub direction_weight: f64, + pub two_qubit_error_weight: f64, + pub readout_error_weight: f64, +} +``` + +- `LayoutObjective::topology_only() -> Self`: a purely topological objective. +- `LayoutObjective::fidelity_aware() -> Self`: the default fidelity-aware objective. +- `LayoutObjective::auto_from_physical(physical: &PhysicalLayoutGraph) -> Self`: select the fidelity-aware objective when calibration data is available, otherwise fall back to the topological objective. +- `LayoutObjective::fidelity_required(physical: &PhysicalLayoutGraph) -> Result`: the fidelity-aware objective; raises an error when no calibration data is available. +- `uses_fidelity(&self) -> bool` +- `score_layout(&self, analysis: &CircuitLayoutAnalysis, physical: &PhysicalLayoutGraph, layout: &Layout) -> Result` + +--- + +## LayoutScore + +The layout scoring result. + +```rust +pub struct LayoutScore { + pub total: f64, + pub distance: f64, + pub direction: f64, + pub two_qubit_error: f64, + pub readout_error: f64, + pub used_fidelity: bool, +} +``` + +--- + +## LayoutDiagnostics + +Diagnostic information of the layout process. + +```rust +pub struct LayoutDiagnostics { + pub is_perfect: bool, + pub candidates_evaluated: usize, + pub used_fidelity: bool, + pub notes: Vec, +} +``` + +- `LayoutDiagnostics::new() -> Self` +- `with_note(self, note: impl Into) -> Self`: append a diagnostic note. + +--- + +## LayoutResult + +The object returned by the layout functions. + +```rust +pub struct LayoutResult { + pub layout: Layout, + pub score: Option, + pub diagnostics: LayoutDiagnostics, +} +``` + +--- + +## Vf2EdgeRequirement / Vf2LayoutConfig + +```rust +pub enum Vf2EdgeRequirement { ... } +``` + +- `Vf2EdgeRequirement::positive_interactions()`: require only positive interaction edges to match. +- `Vf2EdgeRequirement::all_interactions()`: require all interaction edges to match. + +```rust +pub struct Vf2LayoutConfig { + pub candidate_limit: usize, + pub call_limit: Option, + pub edge_requirement: Vf2EdgeRequirement, +} +``` + +Implements `Default`. + +--- + +## Layout analysis objects + +### Interaction + +A single logical qubit pair interaction. + +```rust +pub struct Interaction { + pub left: LogicalQubit, + pub right: LogicalQubit, + pub weight: f64, + pub directed_weight_left_to_right: f64, + pub directed_weight_right_to_left: f64, + pub first_seen_order: usize, +} +``` + +### InteractionGraph + +The circuit interaction graph: + +- `interactions(&self) -> impl Iterator` +- `is_empty(&self) -> bool` +- `logical_activity(&self) -> impl Iterator` + +### CircuitLayoutAnalysis + +The circuit layout analysis result. + +```rust +pub struct CircuitLayoutAnalysis { + pub logical_qubits: Vec, + pub interactions: InteractionGraph, +} +``` + +### DistanceTable + +The shortest distance table of the physical graph: + +- `qubits(&self) -> &[PhysicalQubit]` +- `distance(&self, a: PhysicalQubit, b: PhysicalQubit) -> Option` + +### PhysicalLayoutGraph + +The device physical layout graph: + +- `PhysicalLayoutGraph::from_device(device: &Device) -> Result` +- `physical_qubits(&self) -> &[PhysicalQubit]` +- `distances(&self) -> &DistanceTable` +- `distance(&self, a, b) -> Option` +- `is_adjacent_undirected(&self, a, b) -> bool` +- `readout_error(&self, qubit) -> Option` +- `supports_two_qubit_gate_directed(&self, a, b) -> bool` +- `two_qubit_gate_error_directed(&self, a, b) -> Option` +- `supports_directed_coupling(&self, a, b) -> bool` +- `has_fidelity_data(&self) -> bool`, `has_readout_error_data(&self) -> bool`, `has_two_qubit_error_data(&self) -> bool` + +### PreparedSabreCircuit / PreparedSabreTarget + +- `PreparedSabreCircuit`: returned by `prepare_sabre_circuit()`, carrying the circuit analysis (`analysis()`, `logical_qubits()`). +- `PreparedSabreTarget`: returned by `prepare_sabre_device_target()`, carrying the physical graph through `physical()`. + +--- + +## Example + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::compile::sabre::SabreConfig; +use cqlib_core::compile::transform::layout::{LayoutObjective, sabre_layout}; +use cqlib_core::device::Device; + +let mut circuit = Circuit::new(3); +circuit.cx(Qubit::new(0), Qubit::new(2)).unwrap(); + +let device = Device::line("line-3", 3).unwrap(); +let result = sabre_layout( + &circuit, + &device, + &LayoutObjective::topology_only(), + &SabreConfig::default(), +) +.unwrap(); + +println!("layout: {:?}", result.layout); +``` diff --git a/docs/api/en/rust/4_compile/4_routing.md b/docs/api/en/rust/4_compile/4_routing.md new file mode 100644 index 00000000..c60f5f06 --- /dev/null +++ b/docs/api/en/rust/4_compile/4_routing.md @@ -0,0 +1,82 @@ +# Routing + +`cqlib_core::compile::transform::routing` provides transform-level routing entry points. Routing inserts SWAPs on a constrained topology so that all two-body interactions in the circuit satisfy the device adjacency constraint. + +## Import + +```rust +use cqlib_core::compile::transform::routing::{RoutedCircuit, SabreRouteResult, route_sabre, route_with_layout}; +use cqlib_core::compile::transform::layout::LayoutObjective; +use cqlib_core::compile::sabre::SabreConfig; +``` + +--- + +## Functions + +### `route_with_layout(circuit: &Circuit, device: &Device, initial_layout: &Layout, config: &SabreConfig) -> Result` + +Deterministic routing given a known initial layout. + +### `route_sabre(circuit: &Circuit, device: &Device, objective: &LayoutObjective, config: &SabreConfig) -> Result` + +SABRE routing with layout selection: first select the initial layout according to `objective`, then perform routing. + +--- + +## RoutedCircuit + +The routing result given a known initial layout; the fields are private: + +- `circuit(&self) -> &Circuit`: the routed circuit. +- `into_circuit(self) -> Circuit`: consume and take out the circuit. +- `initial_layout(&self) -> &Layout`: the layout before routing starts. +- `final_layout(&self) -> &Layout`: the layout after routing ends. +- `swap_count(&self) -> usize`: the number of inserted SWAPs. +- `diagnostics(&self) -> &SabreRoutingDiagnostics`: routing diagnostic information; see [SABRE](5_sabre.md). + +--- + +## SabreRouteResult + +The routing result with layout selection; the fields are private: + +- `routed(&self) -> &RoutedCircuit`: the routing result. +- `into_routed(self) -> RoutedCircuit`: consume and take out the routing result. +- `layout_score(&self) -> Option<&LayoutScore>`: the score of the selected layout. SABRE selects the winner by predicted native routing quality; this score is for diagnostics only and is not the routing selection key. +- `layout_diagnostics(&self) -> &LayoutDiagnostics`: diagnostic information of the layout process. + +--- + +## Example + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::compile::sabre::SabreConfig; +use cqlib_core::compile::transform::layout::LayoutObjective; +use cqlib_core::compile::transform::routing::route_sabre; +use cqlib_core::device::Device; + +let mut circuit = Circuit::new(3); +circuit.cx(Qubit::new(0), Qubit::new(2)).unwrap(); + +let device = Device::line("line-3", 3).unwrap(); +let result = route_sabre( + &circuit, + &device, + &LayoutObjective::topology_only(), + &SabreConfig::default(), +) +.unwrap(); + +println!("swap_count: {}", result.routed().swap_count()); +``` + +--- + +## Errors + +- `CompilerError::InvalidInput`: invalid SABRE configuration, for example `routing_trials` is zero. +- Other `CompilerError` variants: routing failure, for example a missing or unreachable initial layout, or insufficient device capacity. + +For details of the lower-level `sabre_route` and `SabreConfig`, see [SABRE](5_sabre.md). diff --git a/docs/api/en/rust/4_compile/5_sabre.md b/docs/api/en/rust/4_compile/5_sabre.md new file mode 100644 index 00000000..005b468b --- /dev/null +++ b/docs/api/en/rust/4_compile/5_sabre.md @@ -0,0 +1,158 @@ +# SABRE + +`cqlib_core::compile::sabre` provides the low-level entry points and configuration for SWAP routing. Use `sabre_route` when the initial layout is known and the circuit must satisfy the device topology constraint. Fixing the random seed makes the tie-breaking and selection across multiple trials reproducible. + +## Import + +```rust +use cqlib_core::compile::sabre::{ + SabreConfig, SabreHeuristicConfig, SabreRoutingDiagnostics, SabreRoutingResult, + SabreVf2PrepassConfig, normalize_initial_layout, sabre_route, validate_reachable_interactions, +}; +``` + +--- + +## Functions + +### `sabre_route(circuit: &Circuit, device: &Device, initial_layout: &Layout, config: &SabreConfig) -> Result` + +SABRE routing given a known initial layout. + +### `normalize_initial_layout(logical_qubits: &[LogicalQubit], device: &Device, initial_layout: &Layout) -> Result` + +Normalize a complete logical-to-physical layout against the qubits available on the device. + +### `validate_reachable_interactions(circuit: &Circuit, device: &Device, initial_layout: &Layout) -> Result<(), CompilerError>` + +Validate native movement reachability without performing routing. + +--- + +## SabreConfig + +Configuration shared by SABRE layout refinement and routing. + +```rust +pub struct SabreConfig { + pub layout_trials: usize, + pub layout_assignment_budget: usize, + pub vf2_prepass: Option, + pub refinement_iterations: usize, + pub routing_trials: usize, + pub seed: Option, + pub heuristic: SabreHeuristicConfig, +} +``` + +Fields: + +- `layout_trials`: the number of randomized initial layouts added on top of the deterministic and interaction-aware candidates. +- `layout_assignment_budget`: the maximum amount of exploration of layout component assignment states; exhaustion reports budget exhaustion, which is distinguished from proven infeasibility. +- `vf2_prepass`: the bounded VF2 prepass, used to add topologically perfect candidates; `None` disables the prepass. +- `refinement_iterations`: the number of forward/reverse refinement rounds per layout trial. +- `routing_trials`: the number of complete routing trials performed for each fully refined layout. The search returns the best route directly and does not route intermediate refinement states. +- `seed`: the deterministic random seed. The same seed produces the same cqlib result. +- `heuristic`: the swap selection heuristic configuration. + +Methods: + +- `SabreConfig::deterministic_seeded(seed: u64) -> Self`: a compact deterministic configuration, suitable for tests and examples. +- `validate(&self) -> Result<(), CompilerError>`: validate the routing fields (layout refinement fields are intentionally ignored, since routing starts from a concrete initial layout and does not depend on layout-specific knobs). + +Implements `Default`: `layout_trials=10`, `layout_assignment_budget=1_000_000`, `vf2_prepass=Some(SabreVf2PrepassConfig { candidate_limit: 10, call_limit: 1_000_000 })`, `refinement_iterations=1`, `routing_trials=1`, `seed=None`, `heuristic=SabreHeuristicConfig::default()`. + +--- + +## SabreHeuristicConfig + +SABRE swap selection heuristic configuration. The main score combines the current frontier layer sum, the lookahead scaled by active layers and multiplicative congestion; when a native plan is available, candidates are decided within a narrow structural window using the exact native two-qubit cost. + +```rust +pub struct SabreHeuristicConfig { + pub basic_weight: f64, + pub lookahead_weights: Vec, + pub decay_increment: Option, + pub decay_reset: usize, + pub attempt_limit: usize, + pub best_epsilon: f64, +} +``` + +Fields: + +- `basic_weight`: the weight of the total distance of the current frontier layer. +- `lookahead_weights`: the weight scaling the lookahead layer sum per active layer; the default is `[0.5, 0.25, 0.125, 0.0625, 0.03125]`. +- `decay_increment`: the increment of the congestion multiplier of a physical qubit after a heuristic SWAP uses it; `None` disables congestion control. +- `decay_reset`: the number of heuristic SWAP attempts before the decay value is reset. +- `attempt_limit`: the number of consecutive heuristic SWAPs allowed while no frontier node has been routed; beyond it the search falls back to shortest-path escape. +- `best_epsilon`: the floating-point tolerance at which candidate SWAP scores are treated as tied. + +Implements `Default`: `basic_weight=1.0`, `lookahead_weights=[0.5, 0.25, 0.125, 0.0625, 0.03125]`, `decay_increment=Some(0.002)`, `decay_reset=10`, `attempt_limit=1000`, `best_epsilon=1e-10`. + +--- + +## SabreVf2PrepassConfig + +The bounded VF2 prepass used to seed SABRE layout candidates. + +```rust +pub struct SabreVf2PrepassConfig { + pub candidate_limit: usize, + pub call_limit: usize, +} +``` + +- `candidate_limit`: the upper limit on the number of complete perfect mappings scored by VF2. +- `call_limit`: the upper limit on the number of partial mapping extensions attempted by VF2. + +--- + +## SabreRoutingResult + +The object returned by `sabre_route`. + +```rust +pub struct SabreRoutingResult { + pub circuit: Circuit, + pub initial_layout: Layout, + pub final_layout: Layout, + pub swap_count: usize, + pub diagnostics: SabreRoutingDiagnostics, +} +``` + +- `circuit`: the physical circuit after SWAP insertion. +- `initial_layout`: the initial logical-to-physical layout used by the selected trial. +- `final_layout`: the final layout after all routing operations. +- `swap_count`: the number of inserted SWAPs (including the control flow epilogue). +- `diagnostics`: diagnostics of the routing search behavior. + +--- + +## SabreRoutingDiagnostics + +Diagnostic information of the routing process; the fields are public: + +```rust +pub struct SabreRoutingDiagnostics { + pub trials_evaluated: usize, + pub selected_trial_index: usize, + pub fallback_count: usize, + pub control_flow_blocks_routed: usize, + pub two_qubit_depth: usize, + pub operation_count: usize, + pub native_two_qubit_count: usize, + pub native_two_qubit_depth: usize, + pub native_total_depth: usize, + pub native_operation_count: usize, + pub unknown_loop_count: usize, + pub requirement_signature_count: usize, + pub eager_pair_state_count: usize, + pub lazy_pair_l1_lookup_count: usize, + pub lazy_pair_l1_hit_count: usize, + pub lazy_pair_l1_cached_count: usize, +} +``` + +Implements `Default` and `PartialEq`. diff --git a/docs/api/en/rust/4_compile/6_decompose_resynthesis.md b/docs/api/en/rust/4_compile/6_decompose_resynthesis.md new file mode 100644 index 00000000..18ba15dc --- /dev/null +++ b/docs/api/en/rust/4_compile/6_decompose_resynthesis.md @@ -0,0 +1,478 @@ +# Decompose / Resynthesis + +`cqlib_core::compile::transform::decompose` + +Gate decomposition, two-qubit block resynthesis, target gate set translation and device lowering, located in the `decompose`, `resynthesis`, `target_basis` and `device_lowering` submodules of `cqlib_core::compile::transform`. + +Gate decomposition is layered by the representation available for an operation: definition expansion handles gates that carry their own implementation circuit, numeric synthesis handles unitary gates with a fixed matrix, and multi-controlled gate decomposition handles controlled operations. Definition expansion runs before numeric synthesis, so that unitary gates carrying an implementation circuit are already expanded before entering matrix synthesis. + +## Import + +```rust +use cqlib_core::compile::transform::decompose::{ + DecompositionRuleStats, DecomposeDefinitions, DecomposeMcGates, DecomposeUnitaries, + McGateDecomposeConfig, UnitaryDecomposeConfig, decompose_mc_gates, + decompose_mc_gates_with_rule_stats, decompose_unitaries, + decompose_unitaries_with_rule_stats, expand_definitions, mc_gate, unitary, +}; +use cqlib_core::compile::transform::decompose::unitary::{ + KakDecomposition, OneQubitUnitaryDecomposition, TwoQubitSynthesisTarget, + TwoQubitUnitaryDecomposeBasis, TwoQubitUnitarySynthesisResult, kak_decompose, + synthesize_numeric_1q_unitary, synthesize_numeric_2q_unitary, +}; +use cqlib_core::compile::transform::resynthesis::{ + ResynthesizeTwoQubitBlocks, TwoQubitBlockResynthesisConfig, + resynthesize_two_qubit_blocks, +}; +use cqlib_core::compile::transform::target_basis::{ + TargetBasisCost, TargetBasisCostModel, TargetBasisLowerer, TargetBasisSignature, +}; +use cqlib_core::compile::transform::DeviceLowerer; +``` + +--- + +## Definition expansion + +- `expand_definitions(circuit: &Circuit) -> Result`: expand circuit gate definitions and return a new circuit. +- `DecomposeDefinitions`: a `Transformer` adapter implementing the `decompose_definitions` step. + +--- + +## Unitary gate synthesis + +The circuit-level entry points are in `cqlib_core::compile::transform::decompose`, and the corresponding numeric synthesis primitives are in `cqlib_core::compile::transform::decompose::unitary`. + +- `decompose_unitaries(circuit: &Circuit, config: UnitaryDecomposeConfig) -> Result` +- `decompose_unitaries_with_rule_stats(circuit: &Circuit, config: UnitaryDecomposeConfig) -> Result<(TransformOutcome, DecompositionRuleStats), CompilerError>` +- `DecomposeUnitaries`: a `Transformer` adapter with the configuration bound at construction time, constructed with `DecomposeUnitaries::new(config)`; implements `Default`. + +### UnitaryDecomposeConfig + +```rust +pub struct UnitaryDecomposeConfig { + pub two_qubit_target: TwoQubitSynthesisTarget, + pub recurse_control_flow: bool, +} +``` + +Fields: + +- `two_qubit_target`: the target capabilities used by exact two-qubit numeric synthesis, constructed with `TwoQubitSynthesisTarget::from_instructions(target_basis: Option<&[Instruction]>)`; `None` means no target constraint, enabling the neutral Pauli rotation fallback. +- `recurse_control_flow`: whether to recurse into control flow blocks. + +Implements `Default`, equivalent to no target constraint with recursion into control flow blocks. + +### DecompositionRuleStats + +```rust +pub struct DecompositionRuleStats { + pub hits: usize, + pub misses: usize, + pub inserts: usize, +} +``` + +In-pass rule cache statistics: hits, misses and insert counts. + +--- + +## Numeric unitary gate synthesis + +`cqlib_core::compile::transform::decompose::unitary` provides direct synthesis from a matrix to a gate sequence, without circuit traversal. + +- `synthesize_numeric_1q_unitary(matrix: &Array2) -> Result` +- `synthesize_numeric_2q_unitary(matrix: &Array2, qubits: [Qubit; 2], basis: TwoQubitUnitaryDecomposeBasis) -> Result` +- `kak_decompose(matrix: &Array2) -> Result` + +`Array2` and `Complex64` come from `ndarray` and `num_complex` respectively. + +Synthesis planning and cost evaluation for a target gate set are handled internally by this submodule and are not used as regular interfaces. + +### OneQubitUnitaryDecomposition + +```rust +pub struct OneQubitUnitaryDecomposition { + pub theta: f64, + pub phi: f64, + pub lambda: f64, + pub global_phase: f64, +} +``` + +The matrix represented is `exp(i * global_phase) * U(theta, phi, lambda)`. + +### TwoQubitUnitarySynthesisResult + +```rust +pub struct TwoQubitUnitarySynthesisResult { + pub operations: Vec, + pub global_phase: f64, +} +``` + +- `operations`: the sequence of standard gate operations implementing the unitary, exact up to `global_phase`. +- `global_phase`: the scalar phase multiplied onto the operation sequence. + +### KakDecomposition + +```rust +pub struct KakDecomposition { + pub global_phase: f64, + pub k1l: Array2, + pub k1r: Array2, + pub k2l: Array2, + pub k2r: Array2, + pub a: f64, + pub b: f64, + pub c: f64, +} +``` + +Fields: + +- `global_phase`: the scalar phase multiplied onto the complete decomposition. +- `k1l`, `k1r`: the left and right local factors applied after the Cartan interaction. +- `k2l`, `k2r`: the left and right local factors applied before the Cartan interaction. +- `a`, `b`, `c`: the canonical Pauli-XX, Pauli-YY and Pauli-ZZ interaction coordinates. + +### TwoQubitUnitaryDecomposeBasis + +```rust +pub enum TwoQubitUnitaryDecomposeBasis { + PauliRotations, + Cx, + Cy, + Cz, + Rzz, +} +``` + +Variants: + +- `PauliRotations`: local `U` gates plus `RXX`, `RYY` and `RZZ`. +- `Cx`, `Cy`, `Cz`: local `U` gates plus the corresponding controlled gate template. +- `Rzz`: local `U` gates plus `RZZ`. + +### TwoQubitSynthesisTarget + +A target gate set capability description that tells numeric unitary gate synthesis and two-qubit block resynthesis which native gate set the backend supports. + +```rust +pub struct TwoQubitSynthesisTarget { + // 字段私有 +} +``` + +Methods: + +- `from_instructions(target_basis: Option<&[Instruction]>) -> Result`: construct a capability description from a target gate set; `None` means no target constraint. +- `from_standard_gates(native_1q: Vec, native_2q: Vec, fallback_pauli: bool) -> Result`: give the native single-qubit and two-qubit gates directly. +- `unconstrained() -> Self`: no target constraint, enabling the neutral Pauli rotation fallback. +- `native_1q(&self) -> &[StandardGate]` / `native_2q(&self) -> &[StandardGate]`: read the registered native gates. +- `fallback_pauli(&self) -> bool`: whether the Pauli rotation fallback is enabled. + +Implements `Default`, equivalent to `unconstrained()`. + +--- + +## Multi-controlled gate decomposition + +- `DecomposeMcGates`: a `Transformer` adapter with the configuration bound at construction time, constructed with `DecomposeMcGates::new(config)`; implements `Default`. +- `decompose_mc_gates(circuit: &Circuit, config: McGateDecomposeConfig) -> Result`: rewrite multi-controlled gates. +- `decompose_mc_gates_with_rule_stats(circuit: &Circuit, config: McGateDecomposeConfig) -> Result<(TransformOutcome, DecompositionRuleStats), CompilerError>`: the diagnostic form, additionally returning the decomposition rule statistics of this run. +- `decompose_mc_gates_for_device(circuit: &Circuit, device: &Device, resource_policy: ResourcePolicy) -> Result`: rewrite multi-controlled gates within the limit of the qubits available on the device; this is a pre-layout logical transform and does not check the coupling topology. + +### McGateDecomposeConfig + +```rust +pub struct McGateDecomposeConfig { + pub resource_policy: ResourcePolicy, + pub resource_limits: ResourceLimits, +} +``` + +- `resource_policy`: the ancilla resource permissions of this decomposition pass. +- `resource_limits`: the hard logical qubit limit. + +--- + +## Multi-controlled gate primitives + +`cqlib_core::compile::transform::decompose::mc_gate` provides exact synthesis primitives grouped by gate family; they return the operation sequence directly and do not traverse circuits, select algorithms or request ancillas. **The normal compilation flow should use `DecomposeMcGates` through the transformer interface**; call these primitives directly only when ancilla allocation must be controlled manually or a custom lowering strategy is being implemented. + +The primitives are grouped by gate family, and each group usually provides a no-ancilla form, a several-clean-ancillas form, a several-dirty-ancillas form and a fixed-depth variant for a single ancilla: + +| Gate family | Coverage | +| --- | --- | +| MCX | Multi-controlled X gates. | +| Multi-controlled SU(2) | Multi-controlled special unitary rotations, with the rotation axis given by `Su2RotationAxis`. | +| Pauli | The multi-controlled Pauli gate family. | +| RZZ | Multi-controlled RZZ. | +| Pauli rotations | Multi-controlled Pauli rotations. | +| Rotations | Multi-controlled `RX` / `RY` / `RZ` and their intrinsic controlled forms. | +| Phase | Multi-controlled `S` / `SDG` / `T` / `TDG` / `Phase`. | +| QCIS | Synthesis primitives for the QCIS gate set. | +| Hadamard | Multi-controlled Hadamard. | +| SWAP | Multi-controlled SWAP. | +| FSIM | Multi-controlled FSIM. | +| Unitary gates | Multi-controlled general unitary gates. | + +Common parameters: + +- `controls` (`&[Qubit]`): the control qubits, which must be the full set of controls after expansion, including the intrinsic controls of the gate itself. +- `target`, or `first` and `second` for two-qubit primitives: the target qubits. +- `clean_ancillas` / `clean_ancilla`: clean ancillas, which must enter as `|0>` and be restored to `|0>`. +- `dirty_ancillas` / `dirty_ancilla`: dirty ancillas, which may enter in any unknown state but must be restored exactly. + +The primitives return an operation sequence that can be assembled into a circuit. Extra ancillas beyond the consumed prefix are ignored; ancillas must be distinct from all control qubits and target qubits. The complete list is in the public surface of the `mc_gate` module. + +--- + +## Two-qubit block resynthesis + +- `resynthesize_two_qubit_blocks(circuit: &Circuit, config: TwoQubitBlockResynthesisConfig) -> Result` +- `ResynthesizeTwoQubitBlocks`: a `Transformer` adapter with the configuration bound at construction time, constructed with `ResynthesizeTwoQubitBlocks::new(config)`. + +### TwoQubitBlockResynthesisConfig + +```rust +pub struct TwoQubitBlockResynthesisConfig { + pub two_qubit_target: TwoQubitSynthesisTarget, + pub max_block_ops: usize, + pub max_crossed_ops: usize, + pub max_scan_span: usize, + pub skip_labeled_ops: bool, + pub recurse_control_flow: bool, + pub commutation: CommutationConfig, +} +``` + +Fields: + +- `two_qubit_target`: the target capabilities used by exact two-qubit numeric synthesis. +- `max_block_ops`: the maximum number of source operations allowed in a single synthesis block. +- `max_crossed_ops`: the maximum number of non-block operations that may be crossed when collecting a block. Crossed operations stay in place and must commute with the synthesis replacement. +- `max_scan_span`: the collection budget on each side of a two-qubit anchor. +- `skip_labeled_ops`: treat labeled operations as hard boundaries. +- `recurse_control_flow`: whether to recurse into structured classical control bodies. +- `commutation`: the semantic commutation engine configuration used by the local collector. + +Methods: + +- `TwoQubitBlockResynthesisConfig::normal(two_qubit_target: TwoQubitSynthesisTarget) -> Self`: the default budget configuration. +- `TwoQubitBlockResynthesisConfig::enhanced(two_qubit_target: TwoQubitSynthesisTarget) -> Self`: a relaxed-budget variant that trades compilation time for better post-routing cleanup. + +--- + +## Target gate set translation + +### TargetBasisLowerer + +- `TargetBasisLowerer::new(target_basis: Vec) -> Result` +- `target_basis(&self) -> &[Instruction]` +- `requires_lowering(&self, circuit: &Circuit) -> bool` +- Implements `Transformer`. + +### TargetBasisSignature + +- `TargetBasisSignature::from_standard_gates(gates: &[StandardGate]) -> Self` + +### TargetBasisCost + +```rust +pub struct TargetBasisCost { + pub two_qubit_ops: usize, + pub depth: usize, + pub total_ops: usize, + pub parameterized_ops: usize, +} +``` + +### TargetBasisCostModel + +- `TargetBasisCostModel::new(target_basis: Vec) -> Result` +- `TargetBasisCostModel::from_lowerer(lowerer: Arc) -> Result` +- `target_basis(&self) -> &[Instruction]` +- `cost_of_fixed_operations(&self, qubits: Vec, operations: Vec) -> Result` + +--- + +## Device lowering + +### DeviceLowerer + +```rust +pub struct DeviceLowerer<'a> { + device: &'a Device, +} +``` + +- `DeviceLowerer::new(device: &'a Device) -> Self` +- `device(&self) -> &'a Device` +- Implements `Transformer`; lowers the circuit to the device native instruction set. + +--- + +## Example + +### 1. Definition expansion and unitary gate synthesis + +```rust +use cqlib_core::circuit::{Circuit, Qubit, UnitaryGate}; +use cqlib_core::compile::transform::decompose::{ + UnitaryDecomposeConfig, decompose_unitaries, expand_definitions, +}; +use ndarray::array; +use num_complex::Complex64; + +// 定义展开:把 CircuitGate 展开为其定义线路 +let mut definition = Circuit::new(1); +definition.h(Qubit::new(0)).unwrap(); +let gate = definition.to_gate("custom_h").unwrap(); + +let mut circuit = Circuit::new(1); +circuit.append(gate, [Qubit::new(0)], [], None).unwrap(); + +let expanded = expand_definitions(&circuit).unwrap(); +assert_eq!(expanded.num_qubits(), 1); + +// 自定义酉门合成 +let matrix = array![ + [Complex64::new(0.0, 0.0), Complex64::new(1.0, 0.0)], + [Complex64::new(1.0, 0.0), Complex64::new(0.0, 0.0)], +]; +let gate = UnitaryGate::new("x_matrix", 1, 0).with_matrix(matrix).unwrap(); +let mut circuit = Circuit::new(1); +circuit.unitary(gate, vec![Qubit::new(0)]).unwrap(); + +let synthesized = + decompose_unitaries(&circuit, UnitaryDecomposeConfig::default()).unwrap(); +assert_eq!(synthesized.num_qubits(), 1); +``` + +### 2. Numeric unitary gate synthesis + +```rust +use cqlib_core::circuit::Qubit; +use cqlib_core::compile::transform::decompose::unitary::{ + TwoQubitUnitaryDecomposeBasis, kak_decompose, synthesize_numeric_1q_unitary, + synthesize_numeric_2q_unitary, +}; +use ndarray::array; +use num_complex::Complex64; + +let source = array![ + [Complex64::new(0.0, 0.0), Complex64::new(1.0, 0.0)], + [Complex64::new(1.0, 0.0), Complex64::new(0.0, 0.0)], +]; +let decomposition = synthesize_numeric_1q_unitary(&source).unwrap(); +assert!(decomposition.theta.is_finite()); +assert!(decomposition.global_phase.is_finite()); + +let swap = array![ + [ + Complex64::new(1.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0) + ], + [ + Complex64::new(0.0, 0.0), + Complex64::new(1.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0) + ], + [ + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(1.0, 0.0) + ], + [ + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(1.0, 0.0), + Complex64::new(0.0, 0.0) + ], +]; +let synthesis = synthesize_numeric_2q_unitary( + &swap, + [Qubit::new(0), Qubit::new(1)], + TwoQubitUnitaryDecomposeBasis::Cx, +) +.unwrap(); +assert!(!synthesis.operations.is_empty()); + +let kak = kak_decompose(&swap).unwrap(); +assert_eq!(kak.k1l.dim(), (2, 2)); +assert!(kak.a.is_finite()); +``` + +### 3. Multi-controlled gate decomposition + +```rust +use cqlib_core::circuit::{Circuit, Instruction, MCGate, Qubit, StandardGate}; +use cqlib_core::compile::resource::ResourcePolicy; +use cqlib_core::compile::transform::TransformOutcome; +use cqlib_core::compile::transform::decompose::{McGateDecomposeConfig, decompose_mc_gates}; + +let mut circuit = Circuit::new(3); +circuit + .append( + Instruction::McGate(Box::new(MCGate::new(2, StandardGate::X))), + [Qubit::new(0), Qubit::new(1), Qubit::new(2)], + [], + None, + ) + .unwrap(); + +let result = decompose_mc_gates( + &circuit, + McGateDecomposeConfig { + resource_policy: ResourcePolicy::default(), + ..McGateDecomposeConfig::default() + }, +) +.unwrap(); + +let TransformOutcome::Changed(decomposed) = result else { + panic!("the MC gate should be decomposed"); +}; +assert!(matches!( + decomposed.operations()[0].instruction, + Instruction::Standard(StandardGate::CCX), +)); +``` + +### 4. Two-qubit block resynthesis + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::compile::transform::decompose::unitary::TwoQubitSynthesisTarget; +use cqlib_core::compile::transform::resynthesis::{ + TwoQubitBlockResynthesisConfig, resynthesize_two_qubit_blocks, +}; + +let mut circuit = Circuit::new(2); +circuit.cx(Qubit::new(0), Qubit::new(1)).unwrap(); +circuit.cx(Qubit::new(0), Qubit::new(1)).unwrap(); + +let config = + TwoQubitBlockResynthesisConfig::normal(TwoQubitSynthesisTarget::unconstrained()); +assert_eq!(config.max_block_ops, 16); + +let result = resynthesize_two_qubit_blocks(&circuit, config).unwrap(); +assert!(matches!( + result, + cqlib_core::compile::transform::TransformOutcome::Changed(_) +)); +``` + +--- + +## Errors + +- `CompilerError`: decomposition or synthesis failure, for example the matrix is not unitary, the target gate set cannot cover the input operations, or resources are insufficient. +- `CompilerError::InvalidInput`: the input matrix has the wrong number of qubits, contains non-finite elements, or is not unitary. +- `CompilerError::TransformFailed`: duplicated qubits, insufficient ancillas, an unsupported gate family, or an underlying synthesis failure. diff --git a/docs/api/en/rust/4_compile/7_knowledge.md b/docs/api/en/rust/4_compile/7_knowledge.md new file mode 100644 index 00000000..91b90f6f --- /dev/null +++ b/docs/api/en/rust/4_compile/7_knowledge.md @@ -0,0 +1,142 @@ +# Knowledge + +`cqlib_core::compile::knowledge` provides compilation knowledge rules, a validated rule library and structural matching. A knowledge rule describes an adjacent operation pattern, optional symbolic parameter constraints and the operation sequence that replaces that pattern, and it is the source of rules for knowledge rule rewriting and gate decomposition. + +## Import + +```rust +use cqlib_core::compile::knowledge::{ + ConcreteOperationView, MatchBindings, MatchError, MatchedReplacement, RuleId, RuleKind, + RuleLibrary, RuleLibraryError, RuleMetadata, conditions_hold, instantiate_target, + match_rule_item, rule_matches_operations, +}; +use cqlib_core::compile::knowledge::rule::{Condition, Rule, RuleItem, RuleValidationError}; +``` + +--- + +## Rule construction + +Qubit labels inside a rule are non-negative integers and must be contiguous starting from zero. They are placeholders: the label `0` can bind to any concrete `Qubit` during matching. + +### RuleItem + +A single operation in a rule pattern or in a replacement target. + +- `RuleItem::standard(gate: StandardGate, qubits: &[u32], params: Vec) -> Self` +- `RuleItem::mc_gate(gate: MCGate, qubits: &[u32], params: Vec) -> Self` +- `symbols(&self) -> HashSet`: the symbols appearing in the item. +- `validate(&self) -> Result<(), RuleValidationError>`: structural validation. +- `equivalent_to(&self, other: &Self) -> bool`: item equivalence test. + +### Condition + +Symbolic parameter constraints for rule matching: + +```rust +pub enum Condition { + Eq(Parameter, Parameter), + EqMod(Parameter, Parameter, Parameter), +} +``` + +- `symbols(&self) -> HashSet`: the symbols appearing in the constraint. + +### Rule + +- `Rule::new(name: &str, ops: Vec, target: Vec) -> Rule`: construct a rule; conditions are attached by subsequent setters or equivalent methods. +- `validate(&self) -> Result<(), RuleValidationError>` +- `num_qubits(&self) -> usize`: the number of qubit labels involved in the rule. +- `collect_free_symbols(&self) -> HashSet` +- `operation_qubits(&self) -> BTreeSet`, `target_qubits(&self) -> BTreeSet` +- `operation_symbols`, `target_symbols`, `condition_symbols`: the symbol set of each part. + +--- + +## RuleKind + +Rule categories: + +```rust +pub enum RuleKind { + Simplify, // 化简 + Cancel, // 抵消 + Merge, // 合并 + Commute, // 显式对易(A; B -> B; A) + Decompose, // 分解或降级 + Canonicalize, // 规范化 + HardwareNative,// 硬件原生 + Other, +} +``` + +--- + +## RuleLibrary + +The rule library, responsible for loading, validating, classifying and querying rules. + +- `RuleLibrary::new() -> Self` +- `RuleLibrary::builtin_rules() -> Result<&'static RuleLibrary, RuleLibraryError>` +- `RuleLibrary::from_rules(rules: Vec, kind: RuleKind) -> Result` +- `RuleLibrary::from_dsl_str(source: &str, kind: RuleKind) -> Result` +- `RuleLibrary::from_dsl_file(path: impl AsRef, kind: RuleKind) -> Result` +- `add_rule(&mut self, rule: Rule, kind: RuleKind) -> Result` +- `extend_rules(&mut self, rules: Vec, kind: RuleKind) -> Result, RuleLibraryError>` +- `len(&self) -> usize`, `is_empty(&self) -> bool` +- `rules(&self) -> &[Rule]` +- `get(&self, id: RuleId) -> Option<&Rule>`, `metadata(&self, id: RuleId) -> Option<&RuleMetadata>` +- `id_by_name(&self, name: &str) -> Option`, `get_by_name(&self, name: &str) -> Option<&Rule>`, `contains(&self, name: &str) -> bool` +- `candidates_for_first_instruction(&self, instruction: &Instruction) -> Result<&[RuleId], RuleLibraryError>` +- `rules_by_kind(&self, kind: RuleKind) -> &[RuleId]` +- `filter_rule_ids_by_instruction_keys(&self, op_instructions: &[Instruction], target_instructions: &[Instruction]) -> Result, RuleLibraryError>` + +### RuleId + +An index of a rule within the library; the tuple struct `RuleId(index)` supports comparison and hashing. + +### RuleMetadata + +```rust +pub struct RuleMetadata { + pub id: RuleId, + pub kind: RuleKind, + pub pattern_len: usize, + pub rewrite_len: usize, + pub qubit_count: usize, + pub first_instruction: Instruction, + pub cost_delta: isize, + pub has_conditions: bool, +} +``` + +--- + +## Structural matching + +### MatchBindings + +The match binding result: + +- `MatchBindings::new() -> Self` +- `qubits(&self) -> &HashMap`: the binding from rule qubit labels to concrete qubits. +- `qubit(&self, rule_qubit: u32) -> Option` +- `params(&self) -> &HashMap`: symbolic parameter bindings. +- `param(&self, symbol: &str) -> Option<&Parameter>` + +### Matching functions + +- `match_rule_item(item: &RuleItem, concrete: ConcreteOperationView<'_>, bindings: &mut MatchBindings) -> Result`: match a single rule item against a concrete operation view, updating the bindings in place. +- `conditions_hold(conditions: Option<&[Condition]>, bindings: &MatchBindings) -> bool` +- `instantiate_target(target: &[RuleItem], bindings: &MatchBindings) -> Result, MatchError>` +- `rule_matches_operations(rule: &Rule, operations: &[ConcreteOperationView<'_>]) -> Result, MatchError>`: a non-match returns `Ok(None)`. + +The matching functions return `MatchError`; common variants include `MatchError::UnsupportedRuleInstruction` and others. + +--- + +## Raises + +- `RuleLibraryError`: rule library construction or validation failure. +- `RuleValidationError`: rule structural validation failure. +- `MatchError`: matching failure. diff --git a/docs/api/en/rust/4_compile/8_resource.md b/docs/api/en/rust/4_compile/8_resource.md new file mode 100644 index 00000000..db816f5e --- /dev/null +++ b/docs/api/en/rust/4_compile/8_resource.md @@ -0,0 +1,141 @@ +# Resource + +`cqlib_core::compile::resource` provides request, planning, leasing and recycling management for ancilla qubits. + +Clean resources and dirty resources are restoration contracts, not state simulation results. A clean resource must be in `|0>` on entry to and exit from a transform; a dirty resource may be in an unknown or entangled state, and the transform must fully restore its input state before releasing the lease. + +## Import + +```rust +use cqlib_core::compile::resource::{ + AncillaRequirement, ResourceError, ResourceLease, ResourceLimits, ResourceManager, ResourcePlan, + ResourcePolicy, ResourceRequest, +}; +``` + +--- + +## Resource contract objects + +### AncillaRequirement + +```rust +pub enum AncillaRequirement { + CleanZero, // 进出变换时处于 |0> + Dirty, // 可进入未知态,必须精确还原 +} +``` + +### ResourceRequest + +```rust +pub struct ResourceRequest { + pub requirement: AncillaRequirement, + pub count: usize, + pub excluded: BTreeSet, +} +``` + +- `requirement`: the state restoration contract required by the consuming algorithm. +- `count`: the number of ancilla qubits required by the algorithm. +- `excluded`: qubits that the algorithm must not consume as ancilla resources, typically including data, control and target qubits. Every excluded qubit must already be registered in the manager. + +A request does not occupy resources; it is used only for planning by `ResourceManager::preview`. + +### ResourcePlan + +A side-effect-free planning snapshot produced by `preview`. It can be inspected and compared, but it does not reserve qubits and does not modify the circuit; the selected plan must be passed to `commit` before the qubits in it can be used. A plan is a manager-specific snapshot, and any successful resource pool change invalidates old plans. + +The fields are private and read through methods: + +- `qubits(&self) -> &[Qubit]`: the planned qubits. +- `num_new_qubits(&self) -> usize`: the number of new qubits introduced by the plan. + +### ResourceLease + +An effective lease produced by `commit`. + +- `qubits(&self) -> &[Qubit]`: the leased qubits. + +--- + +## ResourcePolicy / ResourceLimits + +```rust +pub struct ResourcePolicy { + pub max_pre_layout_clean_ancillas: usize, + pub allow_dirty_borrowing: bool, +} +``` + +- `max_pre_layout_clean_ancillas`: the total number of logical clean ancillas the compiler may create before layout. Released qubits still count toward the total and can be reused. +- `allow_dirty_borrowing`: whether input qubits may be borrowed under the dirty resource contract. It is only allowed when the consuming algorithm accepts an unknown input state and restores it exactly; it never lets an input qubit satisfy a clean-zero request. + +Implements `Default` (creates no clean ancillas and borrows no input qubits). + +```rust +pub struct ResourceLimits { + pub max_total_qubits: Option, +} +``` + +- `max_total_qubits`: the upper limit on the total number of qubits of the complete logical circuit (including input qubits and compiler-allocated clean ancillas); `None` means no total qubit limit is enforced. + +Implements `Default` and `Copy`. + +--- + +## ResourceManager + +The resource manager, which separates side-effect-free planning from actual changes. + +- `ResourceManager::from_circuit(circuit: &Circuit, policy: ResourcePolicy, limits: ResourceLimits) -> Result`: construct a manager from a circuit. The manager must evolve in sync with the circuit. +- `preview(&self, request: &ResourceRequest) -> Result`: preview a request without occupying resources or changing the circuit. +- `commit(&mut self, circuit: &mut Circuit, plan: ResourcePlan) -> Result`: actually occupy resources according to the plan and modify the circuit. `plan` is passed by value. +- `release(&mut self, lease: &ResourceLease) -> Result<(), ResourceError>`: release a lease. +- `enter_post_layout(&mut self, circuit: &Circuit) -> Result<(), ResourceError>`: enter the post-layout phase. +- `verify_consistency(&self, circuit: &Circuit) -> Result<(), ResourceError>`: validate that the manager and the circuit are consistent. +- `verify_idle(&self, circuit: &Circuit) -> Result<(), ResourceError>`: validate that all leases have been released. + +### Usage conventions + +A resource-consuming transform uses `lease.qubits()` while the lease is in effect and must complete restoration before release (a clean ancilla returns to `|0>`, a dirty ancilla restores its original state). Ordinary compilation only needs the `ResourcePolicy` given in `CompileConfig`; the manager lifecycle is maintained internally by the compiler. + +--- + +## Example + +```rust +use cqlib_core::circuit::Circuit; +use cqlib_core::compile::resource::{ + AncillaRequirement, ResourceLimits, ResourceManager, ResourcePolicy, ResourceRequest, +}; + +let mut circuit = Circuit::new(2); +let mut manager = ResourceManager::from_circuit( + &circuit, + ResourcePolicy { max_pre_layout_clean_ancillas: 1, allow_dirty_borrowing: false }, + ResourceLimits::default(), +) +.unwrap(); + +let request = ResourceRequest { + requirement: AncillaRequirement::CleanZero, + count: 1, + excluded: Default::default(), +}; + +// 预览不占用资源、不改变线路 +let plan = manager.preview(&request).unwrap(); +let lease = manager.commit(&mut circuit, plan).unwrap(); + +// 消耗资源的变换在此使用 lease.qubits(),之后须恢复 |0> +manager.release(&lease).unwrap(); +manager.verify_idle(&circuit).unwrap(); +``` + +--- + +## Errors + +`ResourceError` is the unified error type of resource management; insufficient resources, invalid lease state and manager-circuit inconsistency are all returned through it. diff --git a/docs/api/en/rust/4_compile/9_commutation.md b/docs/api/en/rust/4_compile/9_commutation.md new file mode 100644 index 00000000..7b62c42e --- /dev/null +++ b/docs/api/en/rust/4_compile/9_commutation.md @@ -0,0 +1,87 @@ +# Commutation + +`cqlib_core::compile::commutation` provides conservative proofs of quantum operation commutation. A returned `None` means the configured proof sources cannot establish commutation; it does not prove that the two operations do not commute. + +## Import + +```rust +use cqlib_core::compile::commutation::{ + Commutation, CommutationChecker, CommutationConfig, CommutationResult, + algebraic_commutation, check_commutation, +}; +``` + +--- + +## Functions + +### `check_commutation(lhs_inst, lhs_qubits, lhs_params, rhs_inst, rhs_qubits, rhs_params) -> CommutationResult` + +Determine whether two operations commute using the shared built-in checker. The parameters are the `&Instruction`, `&[Qubit]` and `&[Parameter]` of the left and right sides respectively. + +### `algebraic_commutation(lhs_inst, lhs_qubits, lhs_params, rhs_inst, rhs_qubits, rhs_params) -> CommutationResult` + +Determine commutation using only symbolic algebraic proof sources (including facts such as arity checks, identity, identical operations and disjoint supports). Returning `None` keeps the result conservative and allows the rule or matrix fallback to proceed. + +### CommutationResult + +`pub type CommutationResult = Option;` + +--- + +## Commutation + +The commutation proof result: + +```rust +pub enum Commutation { + Exact, // 精确对易 + UpToGlobalPhase(Parameter), // 相差全局相位(lhs * rhs = exp(i*phase) * rhs * lhs) +} +``` + +Methods: + +- `is_exact(&self) -> bool`: whether it is exact commutation without introducing a global phase. +- `phase(&self) -> Parameter`: the phase carried by the proof; `0.0` for exact commutation. + +--- + +## CommutationConfig + +Commutation checker configuration. + +```rust +pub struct CommutationConfig { + pub enable_rule_oracle: bool, + pub enable_matrix_fallback: bool, + pub max_matrix_qubits: usize, +} +``` + +- `enable_rule_oracle`: enable explicit `A; B -> B; A` rule matching from the compiler knowledge library. +- `enable_matrix_fallback`: enable small-scale local matrix comparison as a fallback for concrete gates. +- `max_matrix_qubits`: the maximum number of support qubits allowed for the matrix fallback; it applies to the sorted union of the supports of the two operations rather than being computed separately for each operation. + +Implements `Default`: `enable_rule_oracle=true`, `enable_matrix_fallback=true`, `max_matrix_qubits=4`. + +--- + +## CommutationChecker + +A reusable commutation checker. The proof order is: structural facts → symbolic algebra over supported gate families → explicit knowledge-library commutation rules (when enabled) → concrete local matrix comparison (when enabled and the size is small enough). + +- `CommutationChecker::builtin() -> Self`: built-in rules with the default configuration. +- `CommutationChecker::with_config(config: CommutationConfig) -> Self`: built-in rules with an explicit configuration. +- `CommutationChecker::from_library(library: &RuleLibrary, config: CommutationConfig) -> Self`: use the commutation rules in the rule library as part of the proof. +- `config(&self) -> &CommutationConfig` +- `check(&self, lhs_inst: &Instruction, lhs_qubits: &[Qubit], lhs_params: &[Parameter], rhs_inst: &Instruction, rhs_qubits: &[Qubit], rhs_params: &[Parameter]) -> CommutationResult` + +--- + +## Notes + +- Proof results are conservative: `None` only means that a proof cannot be established, not that the operations do not commute. +- Parameter comparison is symbolic and tolerant; expressions that are provably equal are treated as the same application even when their syntactic representations differ. +- Operations related to classical control flow are outside the proof scope of this checker. +- Operations with symbolic parameters keep their symbolic parameters unchanged when participating in a proof. diff --git a/docs/api/en/rust/5_visualization/0_overview.md b/docs/api/en/rust/5_visualization/0_overview.md new file mode 100644 index 00000000..6a56ad06 --- /dev/null +++ b/docs/api/en/rust/5_visualization/0_overview.md @@ -0,0 +1,202 @@ +# Visualization + +`cqlib_core::visualization` + +`cqlib_core::visualization` is the visualization module of Cqlib; it renders circuits, quantum states and measurement results into graphical artifacts that can be viewed and saved. The module uses SVG as its unified output form: each drawing entry point first returns an SVG markup string, and an output entry point then writes it to a `.svg` file or rasterizes it into a `.png` file. + +## Overview + +Visualization answers three kinds of questions: what a circuit looks like, how a quantum state is distributed on the Bloch sphere and in the density matrix, and how the counts of one measurement are distributed. The module is divided by these three kinds of objects into the `circuit`, `state` and `result` submodules, plus a shared error type `VisualizationError`. + +### Rendering pipeline + +Circuit drawing has two steps: first convert the circuit into a backend-independent visual IR, then draw it with a concrete backend. + +1. **IR construction** — `build_visual_circuit` places operations into non-overlapping columns in circuit order, resolves the style, label and parameter text of each operation, and recursively collects sub-circuits for control flow blocks. +2. **Backend drawing** — the text backend draws the IR into a Unicode box-drawing character diagram, and the figure backend draws the IR into SVG markup. Both backends consume the same IR, so the gate order, column partitioning and labels of a given circuit are identical under the two backends. +3. **Output** — the SVG string can be embedded directly or written to a file; a path with the `.png` extension is rasterized first. + +Quantum state and measurement result drawing is single-step: the drawing entry points of `state` and `result` perform data validation, normalization and layout internally and return the SVG string directly. + +### Two kinds of circuit backend + +- **Text backend** (`circuit_to_text`): produces a monospaced character diagram that can be folded by width, suitable for viewing in terminals and logs. +- **Figure backend** (`circuit_to_figure`): produces SVG markup, and supports per-gate style overrides, per-column folding and PNG rasterization. + +### Display bit order, folding and column folding + +`reverse_bits` controls the display bit order: when set to `true`, qubits are displayed in reverse order, and the basis labels of quantum states and the rows and columns of density matrices are likewise arranged in reverse order, which makes it easier to align with an externally agreed qubit order. + +The text backend uses `line_width` to control folding and the figure backend uses `fold` to control folding: when the width or the number of columns exceeds the limit, the diagram is split into segments, and segments are joined by direction markers. + +### Inputs for quantum states and measurement results + +Quantum state drawing accepts implementors of `StateVisualizationSource`, which both `Statevector` and `DensityMatrix` implement; a pure state is expanded into a density matrix before drawing. Measurement result drawing accepts `ExecutionResult` directly and formats the measurement results into bitstring labels according to the number of qubits recorded in the result. + +--- + +## Common entry points + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::visualization::{FigureDrawerOptions, circuit_to_figure}; + +let mut circuit = Circuit::new(2); +circuit.h(Qubit::new(0)).unwrap(); +circuit.cx(Qubit::new(0), Qubit::new(1)).unwrap(); + +let svg = circuit_to_figure(&circuit, &FigureDrawerOptions::default()).unwrap(); +assert!(svg.contains(">(), + 7, + 2, + Some("simulator".to_string()), + None, +); +let counts = [("00", 2usize), ("11", 5)] + .into_iter() + .map(|(bits, count)| (Outcome::from_bitstring(bits).unwrap(), count)) + .collect::>(); +result.finish(counts, None); + +let svg = plot_histogram(&result, &ResultPlotOptions::default()).unwrap(); +assert!(svg.contains(" Result` + +Draw a circuit as a Unicode box-drawing character diagram. A visual IR is built internally first and then handed to the text backend for drawing; control flow blocks are expanded into block markers arranged in columns. + +Parameters: + +- `circuit` (`&Circuit`): the circuit to draw. +- `options` (`&TextDrawerOptions`): the text drawing options. + +Returns: + +- `Result`: on success, a UTF-8 text diagram string; returns `"empty circuit"` when the number of qubits in the IR is zero. + +Raises: + +- `VisualizationError::CircuitBuild`: expanding circuit gate definitions fails when `decompose_circuit_gates` is `true`. +- `VisualizationError::UnknownQubit`: an operation references a qubit that is not in the qubit list of the circuit. +- `VisualizationError::ParameterIndexOutOfBounds`: the index of a symbolic parameter exceeds the length of the parameter table of the circuit. + +Example: + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::visualization::{TextDrawerOptions, circuit_to_text}; + +let mut circuit = Circuit::new(3); +circuit.h(Qubit::new(0)).unwrap(); +circuit.cx(Qubit::new(0), Qubit::new(2)).unwrap(); +circuit.measure(Qubit::new(0)).unwrap(); +circuit.measure(Qubit::new(1)).unwrap(); +circuit.measure(Qubit::new(2)).unwrap(); + +let text = circuit_to_text(&circuit, &TextDrawerOptions::default()).unwrap(); +assert!(text.contains("Q0:")); +``` + +--- + +## TextDrawerOptions + +Text drawing options. + +```rust +pub struct TextDrawerOptions { + pub cell_width: usize, + pub show_params: bool, + pub decompose_circuit_gates: bool, + pub line_width: isize, + pub initial_state: bool, + pub reverse_bits: bool, +} +``` + +Fields: + +- `cell_width` (`usize`): a reserved field; the rendering width is currently computed dynamically from the content. The default is `9`. +- `show_params` (`bool`): whether to append parameter text after the gate label. The default is `true`. +- `decompose_circuit_gates` (`bool`): whether to expand circuit gate definitions before drawing. The default is `false`. +- `line_width` (`isize`): the maximum fold width. A value greater than `10` folds at that value; a negative value disables folding; a value between `0` and `10` falls back to the default width. The default is `80`. +- `initial_state` (`bool`): whether to display `|0>` at the start of each qubit wire. The default is `false`. +- `reverse_bits` (`bool`): whether to reverse the display order of the qubits. The default is `false`. + +The defaults are provided by `TextDrawerOptions::default()`, and the struct update syntax can be used to override only the fields that need to change: + +```rust +use cqlib_core::visualization::TextDrawerOptions; + +let options = TextDrawerOptions { + line_width: 12, + ..TextDrawerOptions::default() +}; +``` + +--- + +## `draw_text_from_visual(visual, options) -> Result` + +Draw a text diagram from an already built visual IR. Use this entry point when a cached IR is available, to avoid rebuilding it. + +Parameters: + +- `visual` (`&VisualCircuit`): the visual IR built by `build_visual_circuit`. +- `options` (`&TextDrawerOptions`): the text drawing options. + +Returns: + +- `Result`: on success, a UTF-8 text diagram string; returns `"empty circuit"` when the number of qubits in the IR is zero. + +Raises: + +- The current implementation only returns `Ok`; the `Result` return type is kept for symmetry with `circuit_to_text`. + +Example: + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::visualization::{ + TextDrawerOptions, VisualBuildOptions, build_visual_circuit, draw_text_from_visual, +}; + +let mut circuit = Circuit::new(2); +circuit.h(Qubit::new(0)).unwrap(); +circuit.cx(Qubit::new(0), Qubit::new(1)).unwrap(); + +let options = TextDrawerOptions::default(); +let visual = build_visual_circuit(&circuit, &VisualBuildOptions::default()).unwrap(); +let text = draw_text_from_visual(&visual, &options).unwrap(); +assert!(text.contains("H")); +``` + +--- + +## Output forms + +### Basic circuit + +```rust +let mut circuit = Circuit::new(3); +circuit.h(Qubit::new(0)).unwrap(); +circuit.cx(Qubit::new(0), Qubit::new(2)).unwrap(); +circuit.measure(Qubit::new(0)).unwrap(); +circuit.measure(Qubit::new(1)).unwrap(); +circuit.measure(Qubit::new(2)).unwrap(); +``` + +Output under the default options: + +```text + + Q0: ───H──■──M─ + │ + Q1: ──────┼──M─ + │ + Q2: ──────X──M─ + +``` + +Each wire corresponds to one qubit, and between two qubit wires there is a connection column: a controlled gate marks the control position with `■` and connects the target position with `│` and `X`. Instructions such as measurement, reset and delay appear in the corresponding column as an operation name or symbol. + +### Fold + +When `line_width` is smaller than the natural width of the diagram, the diagram is split into multiple segments: + +```rust +let mut circuit = Circuit::new(1); +for _ in 0..10 { + circuit.h(Qubit::new(0)).unwrap(); +} +let options = TextDrawerOptions { + line_width: 12, + ..TextDrawerOptions::default() +}; +let text = circuit_to_text(&circuit, &options).unwrap(); +``` + +In the output, segments are separated by blank lines, and the end and the start of a segment are marked with `»` and `«` respectively to indicate the continuation direction: + +```text + » + Q0: ───H──H──H──H─» + » + +« » +« Q0: ───H──H──H──H─» +« » + +« +« Q0: ───H──H─ +« +``` + +When `line_width` is negative, no folding occurs and neither `«` nor `»` appears in the output. + +### Displaying the initial state and reversing the bit order + +When `initial_state` is `true`, `|0>` is displayed at the start of each qubit wire: + +```text + + Q0: |0>───H─ + +``` + +When `reverse_bits` is `true`, qubits are displayed in reverse order: + +```rust +let mut circuit = Circuit::new(2); +circuit.x(Qubit::new(0)).unwrap(); +let options = TextDrawerOptions { + reverse_bits: true, + ..TextDrawerOptions::default() +}; +let text = circuit_to_text(&circuit, &options).unwrap(); +``` + +```text + + Q1: ───── + + Q0: ───X─ + +``` + +### Parameters and circuit gates + +When `show_params` is `false`, the gate label shows only the gate name, without parameter text: + +```text + + Q0: ───RX─ + +``` + +When `decompose_circuit_gates` is `false`, circuit gates are displayed as a whole as a bordered block; when set to `true`, the gate definitions are expanded first and drawing follows the expanded operations. + +--- + +## Related pages + +- For the drawing entry points and options of the figure backend, see [SVG circuit diagrams](2_draw_figure.md). +- For the formatting rules of parameter text, see the parameter formatting section in [SVG circuit diagrams](2_draw_figure.md). diff --git a/docs/api/en/rust/5_visualization/2_draw_figure.md b/docs/api/en/rust/5_visualization/2_draw_figure.md new file mode 100644 index 00000000..b9b44db6 --- /dev/null +++ b/docs/api/en/rust/5_visualization/2_draw_figure.md @@ -0,0 +1,322 @@ +# SVG Circuit Diagrams + +`cqlib_core::visualization::circuit::figure` + +This page covers the figure backend for circuits: the drawing entry point `circuit_to_figure`, the entry point based on the visual IR `draw_figure_from_visual`, the drawing options `FigureDrawerOptions`, and the style and parameter formatting types referenced by those options. The figure backend renders a circuit into an SVG markup string, with a white canvas background. + +## Import + +```rust +use cqlib_core::visualization::{FigureDrawerOptions, circuit_to_figure}; +``` + +--- + +## `circuit_to_figure(circuit, options) -> Result` + +Generate the SVG markup of a circuit. A visual IR is built internally first and then handed to the figure backend for drawing. + +Parameters: + +- `circuit` (`&Circuit`): the circuit to draw. +- `options` (`&FigureDrawerOptions`): the figure drawing options. + +Returns: + +- `Result`: on success, an SVG markup string. + +Raises: + +- `VisualizationError::CircuitBuild`: expanding circuit gate definitions fails when `decompose_circuit_gates` is `true`. +- `VisualizationError::UnknownQubit`: an operation references a qubit that is not in the qubit list of the circuit. +- `VisualizationError::ParameterIndexOutOfBounds`: the index of a symbolic parameter exceeds the length of the parameter table of the circuit. + +Example: + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::visualization::{FigureDrawerOptions, circuit_to_figure}; + +let mut circuit = Circuit::new(1); +circuit.h(Qubit::new(0)).unwrap(); + +let svg = circuit_to_figure(&circuit, &FigureDrawerOptions::default()).unwrap(); +assert!(svg.contains(", + pub initial_state: bool, + pub reverse_bits: bool, +} +``` + +Fields: + +- `show_params` (`bool`): whether to append parameter text after the gate label. The default is `true`. +- `decompose_circuit_gates` (`bool`): whether to expand circuit gate definitions before drawing. The default is `false`. +- `parameter_format` (`ParameterFormatOptions`): the formatting options of gate parameter text. The default is `ParameterFormatOptions::default()`. +- `width_per_column` (`f64`): the figure width scaling factor per logical column. The default is `1.2`. +- `height_per_qubit` (`f64`): the figure height scaling factor per qubit. The default is `0.9`. +- `dpi` (`u32`): the rasterization DPI when exporting PNG. The default is `160`, at which the PNG scale is `1.0` and the size matches the native SVG pixels; larger values scale the PNG size proportionally, and the SVG output is unaffected. Drawing fails when the value is `0`. +- `gate_width` (`f64`): the base gate width (in data units), which also serves as the lower bound of the column width. The default is `1.1`. +- `gate_height` (`f64`): the base gate height (in data units). The default is `1.5`. +- `moment_spacing` (`f64`): the horizontal spacing between adjacent columns. The default is `0.3`. +- `connect_height` (`f64`): the vertical spacing between rows after column folding. The default is `2.0`. +- `fold` (`i32`): the maximum number of columns per row. The default is `18`; a negative value or `0` disables column folding. +- `style` (`FigureDrawStyle`): the style preset. The default is `FigureDrawStyle::Cqlib`. +- `gate_styles` (`HashMap`): style overrides by gate name, merged on top of the base style table. The default is an empty map. +- `initial_state` (`bool`): whether to display `|0>` in the qubit labels. The default is `false`. +- `reverse_bits` (`bool`): whether to reverse the display order of the qubits. The default is `false`. + +The defaults are provided by `FigureDrawerOptions::default()`: + +```rust +use cqlib_core::visualization::FigureDrawerOptions; + +let options = FigureDrawerOptions { + fold: 8, + ..FigureDrawerOptions::default() +}; +``` + +--- + +## FigureDrawStyle + +Figure style preset. + +```rust +pub enum FigureDrawStyle { + /// Cqlib 默认样式。 + Cqlib, +} +``` + +`FigureDrawStyle::Cqlib` determines the default colors and line widths of circuit wires, connection lines, gate borders, gate fills and labels. + +--- + +## GateStyle + +The style override item of a single gate. All fields are optional; when a field is `None`, the base style applies. + +```rust +pub struct GateStyle { + pub border_color: Option, + pub background_color: Option, + pub font_size: Option, + pub text_color: Option, + pub line_color: Option, + pub line_width: Option, +} +``` + +Fields: + +- `border_color` (`Option`): the gate border color. The default is `None`. +- `background_color` (`Option`): the gate fill color. The default is `None`. +- `font_size` (`Option`): the gate label font size, which also participates in gate width measurement. The default is `None`. +- `text_color` (`Option`): the gate label color. The default is `None`. +- `line_color` (`Option`): the connection line color of the gate. The default is `None`. +- `line_width` (`Option`): the line width of the gate. The default is `None`. + +Writing an entry keyed by gate name into `FigureDrawerOptions::gate_styles` overrides the style of that gate: + +```rust +use cqlib_core::circuit::{Circuit, Parameter, Qubit}; +use cqlib_core::visualization::{FigureDrawerOptions, GateStyle, circuit_to_figure}; +use std::collections::HashMap; + +let mut circuit = Circuit::new(1); +let long_param = Parameter::symbol("styledlongparametername"); +circuit.rz(Qubit::new(0), long_param).unwrap(); + +let mut gate_styles = HashMap::new(); +gate_styles.insert( + "RZ".to_string(), + GateStyle { + font_size: Some(16.0), + ..GateStyle::default() + }, +); + +let svg = circuit_to_figure( + &circuit, + &FigureDrawerOptions { + gate_styles, + ..FigureDrawerOptions::default() + }, +) +.unwrap(); +assert!(svg.contains(" Self`: construct a formatter with the given options. +- `fn format_circuit_param(&self, circuit: &Circuit, param: &CircuitParam) -> Result`: format one parameter entry. `CircuitParam::Fixed` uses its numeric value directly; `CircuitParam::Index` is resolved into a symbolic expression through the parameter table of the circuit. + +Raises: + +- `VisualizationError::ParameterIndexOutOfBounds`: the index of `CircuitParam::Index` exceeds the length of the parameter table of the circuit. + +```rust +use cqlib_core::circuit::{Circuit, CircuitParam}; +use cqlib_core::visualization::{ParameterFormatOptions, ParameterFormatter}; + +let formatter = ParameterFormatter::new(ParameterFormatOptions::default()); +let circuit = Circuit::new(0); + +assert_eq!( + formatter + .format_circuit_param(&circuit, &CircuitParam::Fixed(1.2345)) + .unwrap(), + "1.23" +); +``` + +--- + +## `draw_figure_from_visual(visual, options, output_path) -> String` + +Draw SVG from an already built visual IR. Use this entry point when a cached IR is available, to avoid rebuilding it. + +Parameters: + +- `visual` (`&VisualCircuit`): the visual IR built by `build_visual_circuit`. +- `options` (`&FigureDrawerOptions`): the figure drawing options. +- `output_path` (`Option<&str>`): a reserved parameter, currently ignored. + +Returns: + +- `String`: an SVG markup string. + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::visualization::{ + FigureDrawerOptions, VisualBuildOptions, build_visual_circuit, draw_figure_from_visual, +}; + +let mut circuit = Circuit::new(1); +circuit.h(Qubit::new(0)).unwrap(); +let visual = build_visual_circuit(&circuit, &VisualBuildOptions::default()).unwrap(); +let svg = draw_figure_from_visual(&visual, &FigureDrawerOptions::default(), None); +assert!(svg.contains(" Result` + +Draw a single Bloch vector. + +Parameters: + +- `vector` (`[f64; 3]`): the Bloch coordinates `(x, y, z)`; the components usually fall within `[-1, 1]`. A vector with a norm greater than `1` is projected back onto the unit sphere, and a vector inside the sphere is left unchanged. +- `options` (`&StatePlotOptions`): the plot options. + +Returns: + +- `Result`: an SVG markup string. The vector label is fixed to `q0`. + +Example: + +```rust +use cqlib_core::visualization::{StatePlotOptions, plot_bloch_vector}; + +let svg = plot_bloch_vector([0.0, 0.0, 1.0], &StatePlotOptions::default()).unwrap(); +assert!(svg.contains(" Result` + +Draw one reduced Bloch vector for each qubit. Each vector is computed from the reduced density matrix of that qubit, and labels are written as `q0`, `q1` and so on according to the qubit index. + +Parameters: + +- `state` (`&S`, `S: StateVisualizationSource + ?Sized`): the input state; both `Statevector` and `DensityMatrix` can be passed directly. +- `options` (`&StatePlotOptions`): the plot options. + +Returns: + +- `Result`: an SVG markup string. Multiple spheres are arranged in a grid with at most 4 per row. + +Raises: + +- `VisualizationError::InvalidInput`: the state buffer length does not match the number of qubits, or the number of qubits is too large and the matrix dimension overflows. + +Example: + +```rust +use cqlib_core::qis::Statevector; +use cqlib_core::visualization::{StatePlotOptions, plot_bloch_multivector}; +use num_complex::Complex64; + +let state = Statevector::from_state( + 1, + vec![ + Complex64::new(1.0 / 2.0_f64.sqrt(), 0.0), + Complex64::new(1.0 / 2.0_f64.sqrt(), 0.0), + ], +) +.unwrap(); + +let svg = plot_bloch_multivector(&state, &StatePlotOptions::default()).unwrap(); +assert!(svg.contains(" Result` + +Draw the real and imaginary parts of the density matrix, one panel for each part. Only inputs with a non-zero imaginary part are drawn: when the imaginary parts of all elements are zero, no imaginary part panel is generated in the SVG. The area of each cell scales with `sqrt(|value| / max_abs)`, so that small-magnitude elements remain visible; `max_abs` is the maximum magnitude of the matrix elements, with a lower bound of `1e-12`. + +Parameters: + +- `state` (`&S`, `S: StateVisualizationSource + ?Sized`): the input state. A pure state is expanded into a density matrix first. +- `options` (`&StatePlotOptions`): the plot options. The fill opacity is controlled by `alpha`. + +Returns: + +- `Result`: an SVG markup string, with panel titles `Re[rho]` and `Im[rho]`. + +Raises: + +- `VisualizationError::InvalidInput`: the state buffer length does not match the number of qubits, or the number of qubits is too large and the matrix dimension overflows. + +Example: + +```rust +use cqlib_core::qis::Statevector; +use cqlib_core::visualization::{StatePlotOptions, plot_state_city}; +use num_complex::Complex64; + +let state = Statevector::from_state( + 1, + vec![Complex64::new(1.0, 0.0), Complex64::new(0.0, 0.0)], +) +.unwrap(); + +let svg = plot_state_city(&state, &StatePlotOptions::default()).unwrap(); +assert!(svg.contains("Re[rho]")); +``` + +--- + +## `plot_state_paulivec(state, options) -> Result` + +Draw Pauli basis expectation values as a bar chart. The labels are tensor products of `I`, `X`, `Y` and `Z`, `4^n` of them in total, where `n` is the number of qubits. Non-negative bars and negative bars use different colors. + +Parameters: + +- `state` (`&S`, `S: StateVisualizationSource + ?Sized`): the input state. +- `options` (`&StatePlotOptions`): the plot options. The first element of `color` is the color of non-negative bars, and the second element is the color of negative bars. + +Returns: + +- `Result`: an SVG markup string. Rotated horizontal axis labels are drawn only when the number of bars does not exceed `64`. + +Raises: + +- `VisualizationError::InvalidInput`: the state buffer length does not match the number of qubits, or the number of qubits is too large and the matrix dimension overflows. + +Example: + +```rust +use cqlib_core::qis::Statevector; +use cqlib_core::visualization::{StatePlotOptions, plot_state_paulivec}; +use num_complex::Complex64; + +let state = Statevector::from_state( + 1, + vec![ + Complex64::new(1.0 / 2.0_f64.sqrt(), 0.0), + Complex64::new(1.0 / 2.0_f64.sqrt(), 0.0), + ], +) +.unwrap(); + +let svg = plot_state_paulivec(&state, &StatePlotOptions::default()).unwrap(); +assert!(svg.contains(", + pub color: Vec, + pub alpha: f64, + pub reverse_bits: bool, + pub figsize: Option<(f64, f64)>, +} +``` + +Fields: + +- `title` (`Option`): the plot title. The default is `None`, and no title is drawn. +- `color` (`Vec`): the color scheme used by some plot families. `plot_state_paulivec` takes the first element as the color of non-negative bars, defaulting to `#4569d4`, and the second element as the color of negative bars, defaulting to `#d64b5f`. The remaining plot families use fixed color schemes. The default is an empty vector. +- `alpha` (`f64`): the fill opacity of the `plot_state_city` bars. The default is `1.0`. +- `reverse_bits` (`bool`): whether to reverse the display bit order of the computational basis. The default is `false`. When set to `true`, the row and column labels of the density matrix plot are arranged in reverse order and the matrix elements are mapped back to storage positions by bit order; the label order of the Bloch plots and the Pauli plot is reversed accordingly. +- `figsize` (`Option<(f64, f64)>`): the figure size, in inch-like units, converted into SVG pixels at `100` pixels per unit. The default is `None`, and each plot family computes the canvas size from its own layout. When a size is given, the lower bounds for the width and height of the density matrix plot and the Pauli plot are `4` and `3` respectively, and the lower bounds for the width and height of the Bloch plots are both `3`. + +```rust +use cqlib_core::visualization::StatePlotOptions; + +let options = StatePlotOptions { + title: Some("State city".to_string()), + reverse_bits: true, + ..StatePlotOptions::default() +}; +``` + +--- + +## Auxiliary API + +### StateVisualizationSource + +The input trait accepted by the quantum state drawing entry points. It adapts core state objects into density matrix data rather than introducing another data container. + +```rust +pub trait StateVisualizationSource { + fn num_qubits(&self) -> usize; + fn density_matrix_data(&self) -> Result, VisualizationError>; +} +``` + +Implementors: + +- `Statevector`: expanded into a density matrix as `ρ = |ψ⟩⟨ψ|`. +- `DensityMatrix`: returns its data directly. + +Implementors must return row-major density matrix data of length `4^num_qubits`; an invalid dimension raises `VisualizationError::InvalidInput`. + +### `state_to_density_matrix(state) -> Result<(usize, Vec), VisualizationError>` + +Convert a core state object into a row-major density matrix. Returns the number of qubits and data of length `4^num_qubits`. + +Raises: + +- `VisualizationError::InvalidInput`: the number of qubits is inconsistent with the buffer length, or the number of qubits is too large and the matrix dimension overflows. + +```rust +use cqlib_core::qis::Statevector; +use cqlib_core::visualization::state_to_density_matrix; +use num_complex::Complex64; + +let state = Statevector::from_state(1, vec![Complex64::new(1.0, 0.0), Complex64::new(0.0, 0.0)]) + .unwrap(); +let (num_qubits, rho) = state_to_density_matrix(&state).unwrap(); +assert_eq!(num_qubits, 1); +assert_eq!(rho.len(), 4); +``` + +### `local_bloch_vectors(state) -> Result, VisualizationError>` + +Compute the reduced Bloch vector of each qubit, returning a list of `(qubit index, [x, y, z])` sorted by qubit index in ascending order. + +Raises: + +- `VisualizationError::InvalidInput`: the state buffer length does not match the number of qubits. + +```rust +use cqlib_core::qis::Statevector; +use cqlib_core::visualization::local_bloch_vectors; +use num_complex::Complex64; + +let state = Statevector::from_state(1, vec![Complex64::new(1.0, 0.0), Complex64::new(0.0, 0.0)]) + .unwrap(); +let vectors = local_bloch_vectors(&state).unwrap(); +assert_eq!(vectors.len(), 1); +assert!((vectors[0].1[2] - 1.0).abs() < 1e-10); +``` + +--- + +## Example + +### Pure states and mixed states use the same entry points + +Both `Statevector` and `DensityMatrix` implement `StateVisualizationSource`, so the same drawing entry points work for both: + +```rust +use cqlib_core::qis::{DensityMatrix, Statevector}; +use cqlib_core::visualization::{StatePlotOptions, plot_state_city, plot_state_paulivec}; +use num_complex::Complex64; + +let statevector = Statevector::from_state( + 1, + vec![ + Complex64::new(1.0 / 2.0_f64.sqrt(), 0.0), + Complex64::new(1.0 / 2.0_f64.sqrt(), 0.0), + ], +) +.unwrap(); +let density_matrix = + DensityMatrix::from_state(statevector.num_qubits, statevector.data().to_vec()).unwrap(); + +let sv_svg = plot_state_paulivec(&statevector, &StatePlotOptions::default()).unwrap(); +let dm_svg = plot_state_paulivec(&density_matrix, &StatePlotOptions::default()).unwrap(); +assert_eq!(sv_svg, dm_svg); +``` + +### Bloch plot of a multi-qubit state + +For a multi-qubit state, one reduced Bloch vector is drawn per qubit, which can be used to observe the local information of each qubit in an entangled state: + +```rust +use cqlib_core::qis::Statevector; +use cqlib_core::visualization::{StatePlotOptions, plot_bloch_multivector}; +use num_complex::Complex64; + +let bell = Statevector::from_state( + 2, + vec![ + Complex64::new(1.0 / 2.0_f64.sqrt(), 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(1.0 / 2.0_f64.sqrt(), 0.0), + ], +) +.unwrap(); + +let svg = plot_bloch_multivector(&bell, &StatePlotOptions::default()).unwrap(); +assert!(svg.contains("q0")); +assert!(svg.contains("q1")); +``` + +--- + +## Related pages + +- To write drawing results out to a file, see [Writing to file and output](5_render_to_file.md). diff --git a/docs/api/en/rust/5_visualization/4_result_plots.md b/docs/api/en/rust/5_visualization/4_result_plots.md new file mode 100644 index 00000000..dc1ae203 --- /dev/null +++ b/docs/api/en/rust/5_visualization/4_result_plots.md @@ -0,0 +1,186 @@ +# Measurement Result Visualization + +`cqlib_core::visualization::result` + +This page covers measurement result visualization: the histogram `plot_histogram`, the probability distribution plot `plot_distribution`, and the plot options `ResultPlotOptions`. Both entry points accept execution result objects and return an SVG markup string. + +## Import + +```rust +use cqlib_core::visualization::{ResultPlotOptions, plot_distribution, plot_histogram}; +``` + +--- + +## `plot_histogram(result, options) -> Result` + +Draw the measurement counts of an execution result as a histogram. The counts are formatted into bitstring labels according to the number of qubits of the result and used as the horizontal axis ticks; the vertical axis starts from zero and no negative region is drawn. + +Parameters: + +- `result` (`&ExecutionResult`): the execution result containing the measurement counts. +- `options` (`&ResultPlotOptions`): the ordering, color scheme and layout options. + +Returns: + +- `Result`: an SVG markup string. + +Raises: + +- `VisualizationError::InvalidInput`: the execution result contains no counts, or the plot options are inconsistent. + +Example: + +```rust +use cqlib_core::circuit::Qubit; +use cqlib_core::device::{ExecutionResult, Outcome}; +use cqlib_core::visualization::{ResultPlotOptions, plot_histogram}; +use std::collections::HashMap; + +let mut result = ExecutionResult::new( + "task-visualization".to_string(), + (0..2).map(|idx| Qubit::new(idx as u32)).collect::>(), + 7, + 2, + Some("simulator".to_string()), + None, +); +let counts = [("00", 2usize), ("11", 5)] + .into_iter() + .map(|(bits, count)| (Outcome::from_bitstring(bits).unwrap(), count)) + .collect::>(); +result.finish(counts, None); + +let svg = plot_histogram(&result, &ResultPlotOptions::default()).unwrap(); +assert!(svg.contains(" Result` + +Normalize the measurement counts of an execution result into probabilities and draw them. Each count is divided by the total sum of counts to obtain a probability, and the bar heights and vertical axis ticks are labeled by probability. + +Parameters: + +- `result` (`&ExecutionResult`): the execution result containing the measurement counts. +- `options` (`&ResultPlotOptions`): the ordering, color scheme and layout options. + +Returns: + +- `Result`: an SVG markup string. + +Raises: + +- `VisualizationError::InvalidInput`: the execution result contains no counts, the sum of counts is zero, or the plot options are inconsistent. + +Example: + +```rust +let svg = plot_distribution(&result, &ResultPlotOptions::default()).unwrap(); +assert!(svg.contains(", + pub color: Vec, + pub number_to_keep: Option, + pub sort: String, + pub target_string: Option, + pub legend: Option>, + pub bar_labels: bool, + pub title: Option, +} +``` + +Fields: + +- `figsize` (`Option<(f64, f64)>`): the figure size, in inch-like units, converted into SVG pixels at `100` pixels per unit, with lower bounds of `2` for both the width and the height. The default is `None`, and the canvas is `760 × 480` pixels. +- `color` (`Vec`): the color of each dataset. The default is an empty vector, in which case a built-in qualitative palette is used. +- `number_to_keep` (`Option`): keep the `k` entries with the largest values and aggregate the rest into an entry labeled `rest`. The default is `None`, and no aggregation is performed. No aggregation is performed when `k` is `0` or not less than the number of entries; ties are ordered by the label of the key, which keeps the output consistent across platforms. +- `sort` (`String`): the ordering strategy. The default is `"asc"`. See the table below for the values. +- `target_string` (`Option`): the target bitstring used by `sort = "hamming"`. The default is `None`. +- `legend` (`Option>`): the legend entries, one per dataset. The default is `None`, and no legend is drawn. The result drawing entry points produce only one dataset, so when a legend is given its length must be `1`. +- `bar_labels` (`bool`): whether to draw numeric labels on top of the bars. The default is `true`. +- `title` (`Option`): the plot title. The default is `None`, and no title is drawn. + +### Ordering strategies + +| Value | Description | +| --- | --- | +| `asc` | Ascending lexicographic order of the bitstring labels. | +| `desc` | Descending lexicographic order of the bitstring labels. | +| `value` | Ascending order of the peak value of each label across the datasets; ties in the peak are ordered by ascending label. | +| `value_desc` | Descending order of the peak value of each label across the datasets; ties in the peak are ordered by ascending label. | +| `hamming` | Ascending order of the Hamming distance to `target_string`; ties in distance are ordered by ascending label. The label `rest` is always placed last. | + +```rust +use cqlib_core::visualization::ResultPlotOptions; + +let options = ResultPlotOptions { + sort: "value_desc".to_string(), + number_to_keep: Some(8), + title: Some("Measurement counts".to_string()), + ..ResultPlotOptions::default() +}; +``` + +--- + +## Example + +### Ordering by Hamming distance and aggregating the remaining entries + +Reusing the execution result from the previous example, order by the Hamming distance to the target bitstring, keep only the `1` entry with the largest count, and aggregate the rest into `rest`: + +```rust +use cqlib_core::visualization::{ResultPlotOptions, plot_histogram}; + +let options = ResultPlotOptions { + color: vec!["#123456".to_string()], + number_to_keep: Some(1), + sort: "hamming".to_string(), + target_string: Some("00".to_string()), + legend: Some(vec!["sim".to_string()]), + bar_labels: false, + title: Some("Hamming order".to_string()), + ..ResultPlotOptions::default() +}; + +let svg = plot_histogram(&result, &options).unwrap(); +assert!(svg.contains("rest")); +``` + +### Custom canvas size and color scheme + +Reusing the execution result from the previous example, specify the canvas size, color scheme, legend and title: + +```rust +use cqlib_core::visualization::{ResultPlotOptions, plot_distribution}; + +let options = ResultPlotOptions { + figsize: Some((3.2, 2.4)), + color: vec!["#0f766e".to_string()], + legend: Some(vec!["probability".to_string()]), + bar_labels: false, + title: Some("Distribution options".to_string()), + ..ResultPlotOptions::default() +}; + +let svg = plot_distribution(&result, &options).unwrap(); +assert!(svg.contains("width=\"320\"")); +assert!(svg.contains("height=\"240\"")); +``` + +--- + +## Related pages + +- To write drawing results out to a file, see [Writing to file and output](5_render_to_file.md). diff --git a/docs/api/en/rust/5_visualization/5_render_to_file.md b/docs/api/en/rust/5_visualization/5_render_to_file.md new file mode 100644 index 00000000..edb03249 --- /dev/null +++ b/docs/api/en/rust/5_visualization/5_render_to_file.md @@ -0,0 +1,155 @@ +# Writing to file and output + +`cqlib_core::visualization` + +This page covers the file output entry points of visualization: `render_figure_to_file` for circuit diagrams, and `render_state_plot_to_file` and `render_result_plot_to_file` for quantum state plots and result plots. The three entry points share the same output contract: the extension determines the written format, and the content is SVG markup. + +## Import + +```rust +use cqlib_core::visualization::{ + render_figure_to_file, render_result_plot_to_file, render_state_plot_to_file, +}; +``` + +--- + +## `render_figure_to_file(circuit, output_path, options) -> Result<(), VisualizationError>` + +Render a circuit directly to an output file. A visual IR is built and SVG markup generated internally, and then the file is written; calling `circuit_to_figure` first is not necessary. + +Parameters: + +- `circuit` (`&Circuit`): the circuit to render. +- `output_path` (`&str`): the target file path. A `.png` extension writes a raster image, and any other extension writes an SVG vector image. +- `options` (`&FigureDrawerOptions`): the figure drawing options. `dpi` determines the rasterization scale of the PNG: the default `160` corresponds to a scale of `1.0`, and larger values scale the PNG size proportionally. + +Returns: + +- `Result<(), VisualizationError>`: no data is returned on success. + +Raises: + +- `VisualizationError::CircuitBuild`: expanding circuit gate definitions fails when `decompose_circuit_gates` is `true`. +- `VisualizationError::UnknownQubit`: an operation references a qubit that is not in the qubit list of the circuit. +- `VisualizationError::ParameterIndexOutOfBounds`: the index of a symbolic parameter exceeds the length of the parameter table of the circuit. +- `VisualizationError::InvalidInput`: `dpi` is `0`, or the scaled canvas size exceeds the representable range. +- `VisualizationError::SvgRenderFailed`: SVG parsing or rasterization failure. +- `VisualizationError::Io`: writing the file fails. + +Example: + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::visualization::{FigureDrawerOptions, render_figure_to_file}; + +let mut circuit = Circuit::new(1); +circuit.h(Qubit::new(0)).unwrap(); + +render_figure_to_file(&circuit, "circuit.svg", &FigureDrawerOptions::default()).unwrap(); +render_figure_to_file(&circuit, "circuit.png", &FigureDrawerOptions::default()).unwrap(); +``` + +--- + +## `render_state_plot_to_file(svg, output_path) -> Result<(), VisualizationError>` + +Write the SVG markup produced by the quantum state drawing entry points out to a file. + +Parameters: + +- `svg` (`&str`): the SVG markup returned by a quantum state drawing entry point. +- `output_path` (`&str`): the target file path. A `.png` extension writes a raster image, and any other extension writes an SVG vector image. + +Returns: + +- `Result<(), VisualizationError>`: no data is returned on success. + +Raises: + +- `VisualizationError::InvalidInput`: the canvas size after PNG scaling exceeds the representable range. +- `VisualizationError::SvgRenderFailed`: SVG parsing or rasterization failure. +- `VisualizationError::Io`: writing the file fails. + +Example: + +```rust +use cqlib_core::qis::Statevector; +use cqlib_core::visualization::{ + StatePlotOptions, plot_bloch_multivector, render_state_plot_to_file, +}; +use num_complex::Complex64; + +let state = Statevector::from_state(1, vec![Complex64::new(1.0, 0.0), Complex64::new(0.0, 0.0)]) + .unwrap(); +let svg = plot_bloch_multivector(&state, &StatePlotOptions::default()).unwrap(); +render_state_plot_to_file(&svg, "bloch.svg").unwrap(); +``` + +--- + +## `render_result_plot_to_file(svg, output_path) -> Result<(), VisualizationError>` + +Write the SVG markup produced by the result drawing entry points out to a file. + +Parameters: + +- `svg` (`&str`): the SVG markup returned by `plot_histogram` or `plot_distribution`. +- `output_path` (`&str`): the target file path. A `.png` extension writes a raster image, and any other extension writes an SVG vector image. + +Returns: + +- `Result<(), VisualizationError>`: no data is returned on success. + +Raises: + +- `VisualizationError::InvalidInput`: the canvas size after PNG scaling exceeds the representable range. +- `VisualizationError::SvgRenderFailed`: SVG parsing or rasterization failure. +- `VisualizationError::Io`: writing the file fails. + +Example: + +```rust +use cqlib_core::circuit::Qubit; +use cqlib_core::device::{ExecutionResult, Outcome}; +use cqlib_core::visualization::{ + ResultPlotOptions, plot_histogram, render_result_plot_to_file, +}; +use std::collections::HashMap; + +let mut result = ExecutionResult::new( + "task-visualization".to_string(), + (0..2).map(|idx| Qubit::new(idx as u32)).collect::>(), + 7, + 2, + Some("simulator".to_string()), + None, +); +let counts = [("00", 2usize), ("11", 5)] + .into_iter() + .map(|(bits, count)| (Outcome::from_bitstring(bits).unwrap(), count)) + .collect::>(); +result.finish(counts, None); + +let svg = plot_histogram(&result, &ResultPlotOptions::default()).unwrap(); +render_result_plot_to_file(&svg, "histogram.svg").unwrap(); +``` + +--- + +## Output formats + +| Extension | Written content | +| --- | --- | +| `.png` (case-insensitive) | A raster image. The SVG is first sized into a bitmap by the scale factor and then encoded as PNG. | +| Other extensions | The SVG markup text is written as-is. | + +Rasterization scale: circuit diagrams are scaled by `dpi / 160`, giving a scale of `1.0` at the default `dpi`; quantum state plots and result plots are always written at a scale of `1.0`. Scaling changes only the bitmap size, and the SVG coordinate system stays unchanged. + +--- + +## Related pages + +- For circuit drawing options, see [SVG circuit diagrams](2_draw_figure.md). +- For quantum state drawing entry points, see [Quantum state visualization](3_state_plots.md). +- For result drawing entry points, see [Measurement result visualization](4_result_plots.md). diff --git a/docs/api/en/rust/5_visualization/6_visual_ir.md b/docs/api/en/rust/5_visualization/6_visual_ir.md new file mode 100644 index 00000000..b3e7793e --- /dev/null +++ b/docs/api/en/rust/5_visualization/6_visual_ir.md @@ -0,0 +1,622 @@ +# Visual IR + +`cqlib_core::visualization` + +This page covers the intermediate representation layer of circuit drawing: the build entry point `build_visual_circuit`, the build options `VisualBuildOptions`, and the IR data types `VisualCircuit`, `VisualOperation`, `VisualOpStyle`, `VisualChildren`, `VisualControlFlowKind` and `VisualCondition`. + +## Import + +```rust +use cqlib_core::visualization::{ + VisualBuildOptions, VisualChildren, VisualCircuit, VisualCondition, VisualControlFlowKind, + VisualOpStyle, VisualOperation, build_visual_circuit, +}; +``` + +--- + +## Position in the pipeline + +Circuit drawing has two steps: first build the circuit into a visual IR independent of the drawing backend, then draw it with a concrete backend. + +```text +Circuit ──build_visual_circuit──▶ VisualCircuit ──┬── circuit_to_text / draw_text_from_visual ──▶ 文本图 + └── circuit_to_figure / draw_figure_from_visual ──▶ SVG 图 +``` + +The build stage carries the work related to circuit semantics: mapping qubits to indices, formatting parameters into label text, classifying instructions into drawing styles, and placing operations into non-overlapping columns. The drawing stage only consumes these results and does not read the circuit back. Control flow blocks are recursively collected as sub-circuits during the build stage and expanded into individual markers by the drawing backend. + +The intermediate layer decouples building from drawing, with three consequences: + +- The text backend and the figure backend consume the same IR, so the gate order, column partitioning and labels of a given circuit are identical under the two backends. +- A custom drawing only needs to consume `VisualCircuit` and does not have to reimplement qubit mapping, parameter formatting and column scheduling. +- Build options can be adjusted independently of drawing options. Entering the text backend through `circuit_to_text` uses the default parameter formatting options; to have the text diagram adopt another parameter display mode, build the IR first and hand it to `draw_text_from_visual`. + +--- + +## `build_visual_circuit(circuit, options) -> Result` + +Build a circuit into the visual IR. + +Parameters: + +- `circuit` (`&Circuit`): the circuit to build. +- `options` (`&VisualBuildOptions`): the build options. + +Returns: + +- `Result`: on success, the visual IR, whose number of qubits equals the number of qubits of the circuit. + +Raises: + +- `VisualizationError::CircuitBuild`: expanding circuit gate definitions fails when `decompose_circuit_gates` is `true`. +- `VisualizationError::UnknownQubit`: an operation references a qubit that is not in the qubit list of the circuit. +- `VisualizationError::ParameterIndexOutOfBounds`: the index of a symbolic parameter exceeds the length of the parameter table of the circuit. + +Example: + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::visualization::{VisualBuildOptions, build_visual_circuit}; + +let mut circuit = Circuit::new(2); +circuit.h(Qubit::new(0)).unwrap(); +circuit.cx(Qubit::new(0), Qubit::new(1)).unwrap(); + +let visual = build_visual_circuit(&circuit, &VisualBuildOptions::default()).unwrap(); +assert_eq!(visual.num_qubits(), 2); +assert_eq!(visual.operations.len(), 2); +assert_eq!(visual.operations[0].label, "H"); +assert_eq!(visual.operations[1].lanes, vec![0, 1]); +assert_eq!(visual.num_columns, 2); +``` + +--- + +## VisualBuildOptions + +Build options of the visual IR. These options affect only the build stage and are shared by the text backend and the figure backend. + +```rust +pub struct VisualBuildOptions { + pub decompose_circuit_gates: bool, + pub reserve_full_span_for_multi_qubit: bool, + pub parameter_format: ParameterFormatOptions, +} +``` + +Fields: + +- `decompose_circuit_gates` (`bool`): whether to expand circuit gate definitions before building. The default is `false`. +- `reserve_full_span_for_multi_qubit` (`bool`): reserve the whole index span (`min..=max`) for operations involving multiple qubits instead of reserving only the indices of the operation itself. The default is `true`. +- `parameter_format` (`ParameterFormatOptions`): the formatting options of parameter label text. The default is `ParameterFormatOptions::default()`, that is, `ParameterDisplayMode::Numeric`. + +The defaults are provided by `VisualBuildOptions::default()`, and the struct update syntax can be used to override only the fields that need to change: + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::visualization::{VisualBuildOptions, build_visual_circuit}; + +let mut sub = Circuit::new(2); +sub.h(Qubit::new(0)).unwrap(); +sub.cx(Qubit::new(0), Qubit::new(1)).unwrap(); +let sub_gate = sub.to_gate("SUB_BELL").unwrap(); + +let mut circuit = Circuit::new(2); +circuit + .append(sub_gate, vec![Qubit::new(0), Qubit::new(1)], Vec::new(), None) + .unwrap(); + +let visual = build_visual_circuit( + &circuit, + &VisualBuildOptions { + decompose_circuit_gates: true, + ..VisualBuildOptions::default() + }, +) +.unwrap(); +assert_eq!(visual.operations[0].label, "H"); +``` + +`parameter_format` is a nested struct, updated field by field: + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::visualization::{ + ParameterDisplayMode, ParameterFormatOptions, VisualBuildOptions, build_visual_circuit, +}; +use std::f64::consts::PI; + +let mut circuit = Circuit::new(1); +circuit.rx(Qubit::new(0), PI / 2.0).unwrap(); + +let options = VisualBuildOptions { + parameter_format: ParameterFormatOptions { + mode: ParameterDisplayMode::PiFractionPreferred, + ..ParameterFormatOptions::default() + }, + ..VisualBuildOptions::default() +}; +let visual = build_visual_circuit(&circuit, &options).unwrap(); +assert_eq!(visual.operations[0].params, vec!["π/2".to_string()]); +``` + +--- + +## VisualCircuit + +The visual IR after layout is complete, independent of the drawing backend. + +```rust +pub struct VisualCircuit { + pub qubits: Vec, + pub operations: Vec, + pub num_columns: usize, +} +``` + +Fields: + +- `qubits` (`Vec`): the qubits in display order; the `lanes` of an operation are indices into this list. +- `operations` (`Vec`): the operations in circuit order, already placed into columns. +- `num_columns` (`usize`): the number of occupied columns. + +Methods: + +- `fn num_qubits(&self) -> usize`: return the number of qubits. + +```rust +use cqlib_core::circuit::Qubit; +use cqlib_core::visualization::{VisualCircuit, VisualOpStyle, VisualOperation}; + +let visual = VisualCircuit { + qubits: vec![Qubit::new(0)], + operations: vec![VisualOperation { + column: 0, + lanes: vec![0], + covered_lanes: vec![0], + label: "H".to_string(), + params: vec![], + style: VisualOpStyle::Gate, + span_box: false, + children: None, + span_cols: 1, + }], + num_columns: 1, +}; +assert_eq!(visual.num_qubits(), 1); +``` + +--- + +## VisualOperation + +The atomic IR node consumed by the drawing backend. One operation corresponds to one piece of drawing content in a column. + +```rust +pub struct VisualOperation { + pub column: usize, + pub lanes: Vec, + pub covered_lanes: Vec, + pub label: String, + pub params: Vec, + pub style: VisualOpStyle, + pub span_box: bool, + pub children: Option, + pub span_cols: usize, +} +``` + +Fields: + +- `column` (`usize`): the column index it is placed into. +- `lanes` (`Vec)`: the indices of the qubits involved in the operation within `VisualCircuit::qubits`, ordered by the operands of the operation. +- `covered_lanes` (`Vec`): the qubit indices reserved in this column to avoid overlap. A control flow block reserves all indices; a barrier reserves all indices when `lanes` is empty; any other operation reserves `[0]` when `lanes` is empty. When more than one index is involved, if `reserve_full_span_for_multi_qubit` is `true` the whole `min..=max` span of `lanes` is reserved, otherwise `lanes` itself is reserved. It is empty when the number of qubits is zero. +- `label` (`String`): the main display label. +- `params` (`Vec`): the formatted parameter labels. +- `style` (`VisualOpStyle`): the drawing style. +- `span_box` (`bool`): when `true`, the operation is drawn as a whole block spanning multiple qubit wires. Custom unitary gates, circuit gates and store instructions take `true`. +- `children` (`Option`): the sub-circuit of a control flow operation; `None` for non-control-flow operations. +- `span_cols` (`usize`): the number of logical columns reserved by the operation. It is `1` for ordinary operations; for a control flow block it is computed from the number of columns of the sub-circuit plus separators and margins. + +```rust +use cqlib_core::visualization::{VisualOpStyle, VisualOperation}; + +let op = VisualOperation { + column: 1, + lanes: vec![0, 1], + covered_lanes: vec![0, 1], + label: "X".to_string(), + params: vec!["π/2".to_string()], + style: VisualOpStyle::Controlled { num_controls: 1 }, + span_box: false, + children: None, + span_cols: 1, +}; +assert_eq!(op.label, "X"); +``` + +--- + +## VisualOpStyle + +The drawing style of a single operation. Each variant determines how the backend draws the operation. + +```rust +pub enum VisualOpStyle { + /// 普通门块。 + Gate, + /// 受控操作,操作数开头的 `num_controls` 个量子比特为控制位。 + Controlled { + num_controls: usize, + }, + /// 受控 Z 标记,按两个以竖线相连的圆点绘制。 + Cz, + /// 两个量子比特间的交换标记。 + Swap, + /// 屏障标记。 + Barrier, + /// 测量标记。 + Measure, + /// 复位标记。 + Reset, + /// 延时标记。 + Delay, + /// 控制流标记。 + ControlFlow { + kind: VisualControlFlowKind, + }, +} +``` + +### Classification rules of styles and labels + +The build stage classifies instructions into styles and labels according to the following table: + +| Instruction | `style` | `label` | +| --- | --- | --- | +| Standard gate `SWAP` | `Swap` | `SWAP` | +| Standard gate `CZ` | `Cz` | `CZ` | +| Standard gate with more than 0 controls | `Controlled { num_controls }` | The target gate name | +| Other standard gates | `Gate` | The gate name | +| Multi-controlled gate with `1` control and base gate `Z` | `Cz` | `CZ` | +| Other multi-controlled gates | `Controlled { num_controls }` | The base gate name | +| Custom unitary gate | `Gate`, `span_box` is `true` | `UnitaryGate::label()`, or `Unitary` when empty | +| Circuit gate | `Gate`, `span_box` is `true` | `CircuitGate::name()`, or `Gate` when empty | +| Barrier | `Barrier` | `B` | +| Measurement | `Measure` | `M` | +| Reset | `Reset` | `R` | +| Delay | `Delay` | `D` | +| Classical control flow | `ControlFlow { kind }` | `IF`, `WH`, `FOR`, `SW`, `Break` or `Continue` | +| Measure-to-bit instruction | `Measure` | `M` | +| Store instruction | `Gate`, `span_box` is `true` | `STORE` | + +Gate names are abbreviated according to the standard gate convention: `SDG` is displayed as `SD`, `TDG` as `TD`, and `Phase` and `GPhase` as `P`. The label of a controlled gate takes its target gate name: `CX` and `CCX` are `X`, `CY` is `Y`, `CRX`, `CRY` and `CRZ` are `RX`, `RY` and `RZ` respectively, and in all other cases the control prefix at the start of the gate name is removed. + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::visualization::{VisualBuildOptions, VisualOpStyle, build_visual_circuit}; + +let mut circuit = Circuit::new(3); +circuit.ccx(Qubit::new(0), Qubit::new(1), Qubit::new(2)).unwrap(); +circuit.swap(Qubit::new(0), Qubit::new(1)).unwrap(); + +let visual = build_visual_circuit(&circuit, &VisualBuildOptions::default()).unwrap(); +assert_eq!(visual.operations[0].label, "X"); +assert!(matches!( + visual.operations[0].style, + VisualOpStyle::Controlled { num_controls: 2 } +)); +assert!(matches!(visual.operations[1].style, VisualOpStyle::Swap)); +``` + +--- + +## VisualChildren + +The sub-circuits attached to a control flow operation. A sub-circuit keeps the qubit list of its parent, so when a branch body or loop body touches only some of the qubits, the drawing still aligns with the surrounding circuit. + +```rust +pub enum VisualChildren { + IfElse { + then_circuit: Box, + else_circuit: Option>, + }, + While { + body_circuit: Box, + }, + For { + body_circuit: Box, + }, + Switch { + case_circuits: Vec<(String, Box)>, + default_circuit: Option>, + }, +} +``` + +Variants: + +- `IfElse`: conditional branching. `then_circuit` is the sub-circuit executed when the condition holds, and `else_circuit` is the sub-circuit executed when the condition does not hold, being `None` when there is no false branch. +- `While`: the body of a `while` loop. `body_circuit` is the sub-circuit executed inside the loop. +- `For`: the body of a `for` loop. `body_circuit` is the sub-circuit executed inside the loop. +- `Switch`: multi-way selection. `case_circuits` gives the label and sub-circuit of each branch in source order, and `default_circuit` is the sub-circuit of the default branch, being `None` when there is no default branch. + +```rust +use cqlib_core::circuit::{Circuit, ClassicalExpr, Qubit}; +use cqlib_core::visualization::{VisualBuildOptions, VisualChildren, build_visual_circuit}; + +let mut circuit = Circuit::new(2); +let condition = ClassicalExpr::bool_literal(true); +circuit + .if_else(condition, |body| body.x(Qubit::new(1)), |body| body.z(Qubit::new(1))) + .unwrap(); + +let visual = build_visual_circuit(&circuit, &VisualBuildOptions::default()).unwrap(); +let op = &visual.operations[0]; +assert_eq!(op.label, "IF true"); + +match op.children.as_ref() { + Some(VisualChildren::IfElse { + then_circuit, + else_circuit, + }) => { + assert_eq!(then_circuit.operations[0].label, "X"); + assert_eq!(else_circuit.as_ref().unwrap().operations[0].label, "Z"); + } + _ => panic!("expected IfElse children"), +} +``` + +--- + +## VisualControlFlowKind + +The control flow marker family used by the drawing backend. + +```rust +pub enum VisualControlFlowKind { + IfElseBlock { + has_false_branch: bool, + condition: VisualCondition, + }, + WhileBlock { + condition: VisualCondition, + }, + ForBlock { + range: VisualCondition, + }, + SwitchBlock { + target: VisualCondition, + }, + Break, + Continue, + IfStart, + ElseStart, + WhileStart, + ForStart, + SwitchStart, + CaseStart, + DefaultStart, + End, +} +``` + +The variants fall into two groups. + +Source block variants, produced by the build stage: + +- `IfElseBlock`: the conditional branch block. `has_false_branch` indicates whether the source block has a false branch, and `condition` is the display metadata of the branch condition. +- `WhileBlock`: the `while` loop block. `condition` is the display metadata of the loop condition. +- `ForBlock`: the `for` loop block. `range` is the display metadata of the loop range expression. +- `SwitchBlock`: the multi-way selection block. `target` is the display metadata of the branch target expression. +- `Break` and `Continue`: structured `break` and `continue` markers. + +Expanded marker variants, produced by the drawing backend when it expands a source block before drawing: + +- `IfStart`, `ElseStart`: the marker for the start of a conditional branch and for the start of its false branch. +- `WhileStart`, `ForStart`: loop start markers. +- `SwitchStart`, `CaseStart`, `DefaultStart`: the start, branch and default branch markers of a multi-way selection. +- `End`: the block end marker. + +Before drawing, both the text backend and the figure backend first expand source block variants into individual markers and then draw them one by one. Marker labels take forms such as `If-0 true`, `Else-0` and `End-0`, with the index incrementing within the same diagram. The expanded marker variants therefore do not appear in the return value of `build_visual_circuit`; they describe an intermediate form of the backend drawing stage. + +```rust +use cqlib_core::circuit::{Circuit, ClassicalExpr, Qubit}; +use cqlib_core::visualization::{ + VisualBuildOptions, VisualChildren, VisualControlFlowKind, VisualOpStyle, + build_visual_circuit, +}; + +let mut circuit = Circuit::new(2); +let condition = ClassicalExpr::bool_literal(false); +circuit + .while_(condition, |body| { + body.h(Qubit::new(0))?; + body.cx(Qubit::new(0), Qubit::new(1)) + }) + .unwrap(); + +let visual = build_visual_circuit(&circuit, &VisualBuildOptions::default()).unwrap(); +let op = &visual.operations[0]; +assert!(op.label.starts_with("WH false")); +assert!(matches!( + op.style, + VisualOpStyle::ControlFlow { + kind: VisualControlFlowKind::WhileBlock { .. } + } +)); + +match op.children.as_ref() { + Some(VisualChildren::While { body_circuit }) => { + assert_eq!(body_circuit.operations.len(), 2); + } + _ => panic!("expected While children"), +} +``` + +A `for` loop and a multi-way selection are likewise recorded as sub-circuits, and the operation sequence of a sub-circuit follows the source order: + +```rust +use cqlib_core::circuit::{Circuit, ClassicalExpr, ClassicalType, Qubit}; +use cqlib_core::visualization::{VisualBuildOptions, VisualChildren, build_visual_circuit}; + +let mut circuit = Circuit::new(2); +let loop_var = circuit.var(ClassicalType::uint(3).unwrap()); +circuit + .for_uint( + loop_var, + ClassicalExpr::uint_literal(3, 0).unwrap(), + ClassicalExpr::uint_literal(3, 4).unwrap(), + ClassicalExpr::uint_literal(3, 1).unwrap(), + |body, _| body.cx(Qubit::new(0), Qubit::new(1)), + ) + .unwrap(); + +let visual = build_visual_circuit(&circuit, &VisualBuildOptions::default()).unwrap(); +assert!(visual.operations[0].label.starts_with("FOR")); +match visual.operations[0].children.as_ref() { + Some(VisualChildren::For { body_circuit }) => { + assert_eq!(body_circuit.operations[0].label, "X"); + } + _ => panic!("expected For children"), +} +``` + +--- + +## VisualCondition + +The condition display metadata used by control flow visualization. + +```rust +pub struct VisualCondition { + pub label: String, +} +``` + +Fields: + +- `label` (`String`): the display label of the branch condition. + +The label text is generated from the classical expression of the condition: a boolean literal is displayed as `true` or `false`, a bit literal as `bit(v)`, an unsigned integer literal shows its numeric value, a bit vector literal as `bits(v)`, and any other expression form falls back to its `Debug` text. + +```rust +use cqlib_core::visualization::VisualCondition; + +let condition = VisualCondition { + label: "true".to_string(), +}; +assert_eq!(condition.label, "true"); +``` + +--- + +## Typical usage + +### Inspecting circuit structure and column partitioning + +Operations sharing the same qubit must be placed in adjacent columns, while operations on different qubits can share one column: + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::visualization::{VisualBuildOptions, build_visual_circuit}; + +let mut circuit = Circuit::new(2); +circuit.h(Qubit::new(0)).unwrap(); +circuit.x(Qubit::new(1)).unwrap(); + +let visual = build_visual_circuit(&circuit, &VisualBuildOptions::default()).unwrap(); +let cols: Vec = visual.operations.iter().map(|op| op.column).collect(); +assert_eq!(cols, vec![0, 0]); +``` + +### Inspecting operation styles and lane reservation + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::visualization::{VisualBuildOptions, VisualOpStyle, build_visual_circuit}; + +let mut circuit = Circuit::new(3); +circuit.ccx(Qubit::new(0), Qubit::new(1), Qubit::new(2)).unwrap(); + +let visual = build_visual_circuit(&circuit, &VisualBuildOptions::default()).unwrap(); +let op = &visual.operations[0]; +assert_eq!(op.label, "X"); +assert!(matches!(op.style, VisualOpStyle::Controlled { num_controls: 2 })); +assert_eq!(op.covered_lanes, vec![0, 1, 2]); +``` + +### Constructing an IR by hand and rendering it + +A hand-constructed `VisualCircuit` can be handed directly to a drawing entry point. The following IR describes a circuit containing only a single `H` gate: + +```rust +use cqlib_core::circuit::Qubit; +use cqlib_core::visualization::{ + TextDrawerOptions, VisualCircuit, VisualOpStyle, VisualOperation, draw_text_from_visual, +}; + +let visual = VisualCircuit { + qubits: vec![Qubit::new(0)], + operations: vec![VisualOperation { + column: 0, + lanes: vec![0], + covered_lanes: vec![0], + label: "H".to_string(), + params: vec![], + style: VisualOpStyle::Gate, + span_box: false, + children: None, + span_cols: 1, + }], + num_columns: 1, +}; + +let text = draw_text_from_visual(&visual, &TextDrawerOptions::default()).unwrap(); +assert!(text.contains("H")); +``` + +The drawing result is: + +```text + + Q0: ───H─ + +``` + +### Rendering with custom build options + +Build options act only in the build stage, so the drawing text of the same circuit can change with the build options: + +```rust +use cqlib_core::circuit::{Circuit, Parameter, Qubit}; +use cqlib_core::visualization::{ + FigureDrawerOptions, ParameterDisplayMode, TextDrawerOptions, VisualBuildOptions, + build_visual_circuit, draw_figure_from_visual, draw_text_from_visual, +}; + +let mut circuit = Circuit::new(1); +let theta = Parameter::symbol("theta"); +circuit.rx(Qubit::new(0), theta + 1.0).unwrap(); + +let mut build_options = VisualBuildOptions::default(); +build_options.parameter_format.mode = ParameterDisplayMode::SymbolicWithValue; +let visual = build_visual_circuit(&circuit, &build_options).unwrap(); + +let text = draw_text_from_visual(&visual, &TextDrawerOptions::default()).unwrap(); +assert!(text.contains("1 + theta")); + +let svg = draw_figure_from_visual(&visual, &FigureDrawerOptions::default(), None); +assert!(svg.contains(" = dyn Fn(&Circuit, Option<&Hamiltonian>, Option) -> (f64, f64) + 'a; +``` + +The parameters are, in order, the circuit to execute, the observable to estimate and the number of shots, and the return value is `(expectation value, variance)`. A denominator circuit carries no observable, so the second parameter is `None`; `ZNEMitigation::run_em_sequence` does not specify a number of shots, so the third parameter is `None`. Public methods uniformly accept it as `&Estimator<'_>`, that is, the trait object reference `&dyn Fn(&Circuit, Option<&Hamiltonian>, Option) -> (f64, f64)`, and both closures and function items can be passed by reference directly. Only virtual distillation uses the variance in the return value; zero-noise extrapolation takes only the expectation value. + +### Mitigation pipeline state + +Each instance of `ErrorMitigation` follows a fixed lifecycle: create → `run()` → `get_mitigated()`. `run` and `get_mitigated` can each succeed only once; calling them out of order or repeatedly returns an error. When a call fails, the instance state stays unchanged and the call can be retried after correcting the parameters. Create a new instance when mitigation is needed more than once. + +--- + +## Common entry points + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::error_mitigation::{ + ErrorMitigation, ExtrapolateMethod, MitigationMethod, ProcessArgs, RunArgs, ZneConfig, +}; +use cqlib_core::qis::{Hamiltonian, Pauli, PauliString}; +use num_complex::Complex64; + +let q0 = Qubit::new(0); +let mut circuit = Circuit::new(1); +circuit.x(q0).unwrap(); + +let mut pauli = PauliString::new(1); +pauli.set_pauli(0, Pauli::Z); +let hamiltonian = Hamiltonian::from_list(vec![(pauli, Complex64::new(1.0, 0.0))]).unwrap(); + +let mut mitigation = ErrorMitigation::new( + circuit, + MitigationMethod::Zne(ZneConfig { + fold_levels: vec![0, 1, 2], + }), +) +.unwrap(); + +// 对接模拟器或真机后端:返回 (期望值, 方差) +let estimator = |circuit: &Circuit, hamiltonian: Option<&Hamiltonian>, shots: Option| { + assert!(hamiltonian.is_some()); + assert_eq!(shots, Some(256)); + (circuit.operations().len() as f64 + 0.5, 0.0) +}; + +mitigation + .run( + &hamiltonian, + RunArgs::Zne { + gate_set: None, + shots: Some(256), + }, + &estimator, + ) + .unwrap(); + +let mitigated = mitigation + .get_mitigated(ProcessArgs::Zne { + method: ExtrapolateMethod::Polynomial, + degree: None, + }) + .unwrap(); + +assert_eq!(mitigated.expectation, 0.5); +``` + +--- + +## Core concepts and terms + +| Term | Description | +| --- | --- | +| **Error mitigation** | A collection of methods that correct the systematic bias caused by noise at the expectation value level; it is not quantum error correction. | +| **Estimator** | A callback provided by the caller, in the form of `Estimator`; it receives the circuit to execute, an optional observable and an optional number of shots, and returns `(expectation value, variance)`. | +| **Zero-noise extrapolation (ZNE)** | A method that amplifies circuit noise through gate folding to obtain a set of expectation values at different noise strengths, and then extrapolates to the zero-noise limit. | +| **Fold level** | The noise amplification level of zero-noise extrapolation. Level 0 corresponds to the original circuit, and level `l` corresponds to the noise factor `2 * l + 1`. | +| **Noise factor** | The factor by which the noise of a folded circuit is amplified relative to the original circuit; it is always equal to `2 * fold level + 1`. | +| **Gate folding** | A construction that rewrites a circuit as `U -> U (U† U)^level`; folding only the specified gates is called selective folding. | +| **Virtual distillation** | A method that estimates `Tr(O ρ^M) / Tr(ρ^M)` through copies and a copy-swap circuit, where `M` is the number of copies. | +| **Copy** | The number of copies of the base circuit prepared in parallel in virtual distillation; the minimum is 2. | +| **copy-swap circuit** | A circuit that places multiple copies of the base circuit side by side on non-overlapping qubit ranges and inserts a bitwise SWAP between the first copy and each of the others; its width equals the number of copies multiplied by the width of the base circuit. | +| **Numerator circuit / denominator circuit** | The two statistics circuits of virtual distillation. The numerator circuit carries the expanded observable; the denominator circuit carries no observable, and the estimator receives `None`. | +| **Mitigation pipeline** | The sequential flow represented by `ErrorMitigation`: `new` creates it, `run` collects the raw data, and `get_mitigated` performs the method-specific post-processing. | + +--- + +## `cqlib_core::error_mitigation` API overview + +### Low-level entry points + +| Name | Description | +| --- | --- | +| [`ZNEMitigation`](1_zne.md) | The low-level entry point of zero-noise extrapolation: gate folding, batch evaluation and extrapolation. | +| [`ExtrapolateMethod`](1_zne.md) | The extrapolation method enum, including polynomial and exponential. | +| [`ZneConfig`](1_zne.md) | The fold level configuration of zero-noise extrapolation, used by the unified pipeline and the method enum. | +| [`VirtualDistillation`](2_virtual_distillation.md) | The low-level entry point of virtual distillation: copy-swap circuit construction and numerator and denominator statistics. | +| [`VirtualDistillationConfig`](2_virtual_distillation.md) | The copy count configuration of virtual distillation, used by the unified pipeline and the method enum. | + +### Unified pipeline + +| Name | Description | +| --- | --- | +| [`ErrorMitigation`](3_unified.md) | The sequential unified entry point, orchestrating construction, execution and post-processing. | +| [`MitigationMethod`](3_unified.md) | The mitigation method enum, carrying the configuration of the corresponding method. | +| [`RunArgs`](3_unified.md) | The argument enum of the execution stage. | +| [`ProcessArgs`](3_unified.md) | The argument enum of the post-processing stage. | +| [`MitigatedResult`](3_unified.md) | The final mitigation result, containing the expectation value and an optional variance. | + +### Callbacks and errors + +| Name | Description | +| --- | --- | +| [`Estimator`](0_overview.md) | The estimator callback type alias, shared by all mitigation methods. | +| [`ErrorMitigationError`](3_unified.md) | The unified error type of the module. | + +--- + +## Quick examples + +### 1. Running virtual distillation through the unified pipeline + +```rust +use cqlib_core::circuit::Circuit; +use cqlib_core::error_mitigation::{ + ErrorMitigation, MitigationMethod, ProcessArgs, RunArgs, VirtualDistillationConfig, +}; +use cqlib_core::qis::{Hamiltonian, Pauli, PauliString}; +use num_complex::Complex64; + +let mut pauli = PauliString::new(1); +pauli.set_pauli(0, Pauli::Z); +let hamiltonian = Hamiltonian::from_list(vec![(pauli, Complex64::new(1.0, 0.0))]).unwrap(); + +let mut mitigation = ErrorMitigation::new( + Circuit::new(1), + MitigationMethod::VirtualDistillation(VirtualDistillationConfig { copies: 2 }), +) +.unwrap(); + +// 分子线路带可观测量,分母线路不带 +mitigation + .run( + &hamiltonian, + RunArgs::VirtualDistillation { + shots_numerator: 3, + shots_denominator: 2, + }, + &|_circuit, hamiltonian_arg, shots| { + if hamiltonian_arg.is_some() { + assert_eq!(shots, Some(3)); + (1.5, 0.25) + } else { + assert_eq!(shots, Some(2)); + (2.0, 1.0) + } + }, + ) + .unwrap(); + +let mitigated = mitigation + .get_mitigated(ProcessArgs::VirtualDistillation) + .unwrap(); + +assert!((mitigated.expectation - 0.75).abs() < 1e-12); +assert!((mitigated.variance.unwrap() - 0.203125).abs() < 1e-12); +``` + +### 2. Global folding and selective folding + +```rust +use cqlib_core::circuit::gate::{Instruction, StandardGate}; +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::error_mitigation::{ExtrapolateMethod, ZNEMitigation}; + +let q0 = Qubit::new(0); +let mut circuit = Circuit::new(1); +circuit.h(q0).unwrap(); +circuit.s(q0).unwrap(); + +let zne = ZNEMitigation::new(circuit, vec![0, 1, 2]); +assert_eq!(zne.noise_factors(), &[1, 3, 5]); + +// 全局折叠:三个等级各得到一条折叠线路 +let global = zne.fold_circuits(None).unwrap(); +assert_eq!(global.len(), 3); + +// 选择性折叠:只放大 S 门,H 门保持原样 +let gate_set = vec![Instruction::Standard(StandardGate::S)]; +let selective = zne.fold_circuits(Some(&gate_set)).unwrap(); +assert_eq!(selective[1].operations().len(), 4); + +// 以噪声因子为自变量外推到零噪声 +let extrapolated = zne + .extrapolate(&[1.5, 3.5, 5.5], ExtrapolateMethod::Polynomial, 1) + .unwrap(); +assert!((extrapolated - 0.5).abs() < 1e-10); +``` + +### 3. Numerator and denominator of low-level virtual distillation + +```rust +use cqlib_core::circuit::Circuit; +use cqlib_core::error_mitigation::VirtualDistillation; +use cqlib_core::qis::{Hamiltonian, Pauli, PauliString}; +use num_complex::Complex64; + +let mut pauli = PauliString::new(1); +pauli.set_pauli(0, Pauli::Z); +let hamiltonian = Hamiltonian::from_list(vec![(pauli, Complex64::new(1.0, 0.0))]).unwrap(); + +let vd = VirtualDistillation::new(Circuit::new(1), 2).unwrap(); + +// 两份基底线路加一对逐比特 SWAP +let copy_swap = vd.build_copy_swap_circuit().unwrap(); +assert_eq!(copy_swap.width(), 2); + +let (mu_vd, var_vd) = vd + .run_vd(&hamiltonian, 3, 2, &|_circuit, hamiltonian_arg, shots| { + if hamiltonian_arg.is_some() { + assert_eq!(shots, Some(3)); + (1.5, 0.25) + } else { + assert_eq!(shots, Some(2)); + (2.0, 1.0) + } + }) + .unwrap(); + +assert!((mu_vd - 0.75).abs() < 1e-12); +assert!((var_vd - 0.203125).abs() < 1e-12); +``` + +--- + +## Validation and error handling + +Validation during mitigation happens in two places: configuration-level validation occurs at the creation stage, and input-level validation occurs at the folding, execution and extrapolation stages, and both are returned through `ErrorMitigationError`. When a fold level is negative, the low-level folding entry point returns the circuit error `CircuitError::InvalidControlOperation`, whereas the creation stage of the unified pipeline rejects that configuration outright. + +| Error | When it occurs | +| --- | --- | +| `ErrorMitigationError::InvalidCopies` | The number of copies of virtual distillation is less than 2. | +| `ErrorMitigationError::InvalidFoldLevel` | A fold level of zero-noise extrapolation is negative; rejected in `ErrorMitigation::new`. | +| `ErrorMitigationError::HamiltonianQubitCountMismatch` | The number of qubits of the Hamiltonian is inconsistent with the width of the base circuit; see [ZNE](1_zne.md) and [Virtual distillation](2_virtual_distillation.md). | +| `ErrorMitigationError::ZeroDenominatorMean` | The mean of the denominator circuit of virtual distillation is zero, so the ratio cannot be computed. | +| `ErrorMitigationError::RunRequiredBeforeMitigation` / `AlreadyRun` / `AlreadyMitigated` | The call order of the mitigation pipeline is wrong, or a call was repeated. | +| `ErrorMitigationError::RunArgsMethodMismatch` / `ProcessArgsMethodMismatch` | The execution arguments or post-processing arguments are inconsistent with `MitigationMethod`. | +| `ErrorMitigationError::EmptyNoisyResults` / `NoisyResultsLengthMismatch` / `InvalidPolynomialDegree` / `NonPositiveNoisyResults` / `SingularExponentialFit` / `SingularPolynomialFit` | Invalid extrapolation input or a failed fit. | +| `ErrorMitigationError::Circuit` / `Qis` | Errors passed through from circuit operations or quantum information computation. | + +For the complete definition of all variants, see [Unified pipeline](3_unified.md). + +--- + +## Next steps + +- [Zero-noise extrapolation](1_zne.md): folding semantics, noise factors and extrapolation methods. +- [Virtual distillation](2_virtual_distillation.md): copy-swap circuits, numerator and denominator statistics. +- [Unified pipeline](3_unified.md): the `ErrorMitigation` state machine, argument enums and error types. diff --git a/docs/api/en/rust/6_error_mitigation/1_zne.md b/docs/api/en/rust/6_error_mitigation/1_zne.md new file mode 100644 index 00000000..7d32ba48 --- /dev/null +++ b/docs/api/en/rust/6_error_mitigation/1_zne.md @@ -0,0 +1,230 @@ +# Zero-Noise Extrapolation + +`cqlib_core::error_mitigation::zne_mitigation` + +Zero-noise extrapolation rewrites the base circuit through gate folding into a set of circuits with increasing noise strength, takes an expectation value on each folded circuit, and then extrapolates to the zero-noise limit by fitting. This page covers the public API of folding, batch evaluation and extrapolation. + +## Import + +```rust +use cqlib_core::circuit::gate::Instruction; +use cqlib_core::error_mitigation::{ExtrapolateMethod, ZNEMitigation, ZneConfig}; +``` + +--- + +## ZNEMitigation + +The low-level entry point of zero-noise extrapolation. It holds the base circuit and the fold levels, and splits folding, evaluation and extrapolation into three independently callable stages. + +```rust +pub struct ZNEMitigation { + circuit: Circuit, + fold_levels: Vec, + noise_factors: Vec, +} +``` + +Fields are private, and are read through methods: + +- `fn circuit(&self) -> &Circuit`: the original (unfolded) base circuit. +- `fn fold_levels(&self) -> &[i32]`: the configured fold levels, in the same order as at construction. +- `fn noise_factors(&self) -> &[i32]`: the noise factors corresponding one-to-one to the fold levels; always `2 * level + 1`. + +Other methods: + +- `fn new(circuit: Circuit, fold_levels: Vec) -> Self`: the creation entry point; it takes ownership of the circuit and converts the levels into noise factors. The construction stage does not validate whether the levels are non-negative. +- `fn fold_circuits(&self, gate_set: Option<&[Instruction]>) -> Result, CircuitError>`: constructs one folded circuit for each fold level. +- `fn run_em_sequence(&self, gate_set: Option<&[Instruction]>, hamiltonian: &Hamiltonian, estimator: &Estimator<'_>) -> Result, ErrorMitigationError>`: folds level by level and calls the estimator for evaluation; the estimator receives `None` as the number of shots. +- `fn run_em_sequence_with_shots(&self, gate_set: Option<&[Instruction]>, hamiltonian: &Hamiltonian, shots: Option, estimator: &Estimator<'_>) -> Result, ErrorMitigationError>`: the same as above, and passes the number of shots through to the estimator. +- `fn extrapolate(&self, noisy_results: &[f64], method: ExtrapolateMethod, degree: usize) -> Result`: dispatches to the two extrapolation methods below according to the method. +- `fn poly_extrapolate(&self, noisy_results: &[f64], degree: usize) -> Result`: polynomial extrapolation. +- `fn exp_extrapolate(&self, noisy_results: &[f64]) -> Result`: exponential extrapolation. + +`ZNEMitigation` has no builder methods and no `Default` implementation: an instance can be created only by `new`, the fold levels cannot be modified through the read interface, and changing the levels requires creating a new instance. It implements `Debug` and `Clone`. + +### `fold_circuits(&self, gate_set: Option<&[Instruction]>) -> Result, CircuitError>` + +Constructs a folded circuit per level; the length of the returned vector equals the number of fold levels, in the same order as `fold_levels`. + +Parameters: + +- `gate_set`: the optional gate set that determines the folding scope. Passing `None` means global folding, rewriting the whole circuit as `U -> U (U† U)^level`, where the inverse circuit is obtained by inverting the base circuit as a whole; passing `Some` means selective folding, which checks the name of each operation, and only operations matching any name in the set are amplified while the remaining operations are kept as they are. + +Returns: + +- `Vec`: the folded circuit corresponding to each fold level. + +Raises: + +- `CircuitError::InvalidControlOperation`: a fold level is negative. +- `CircuitError`: circuit-level errors such as a failure to invert the base circuit. + +At level 0 the copy of the base circuit is returned directly, with nothing appended. Selective folding matches by operation name; the name is given by `Instruction::name()` and is independent of the parameters carried by the operation or of its own circuit definition. + +### `run_em_sequence_with_shots(&self, gate_set: Option<&[Instruction]>, hamiltonian: &Hamiltonian, shots: Option, estimator: &Estimator<'_>) -> Result, ErrorMitigationError>` + +Folds level by level and calls the estimator, returning the expectation value of each folded circuit. `run_em_sequence` is its shot-free version, always passing `None` to the estimator. + +Parameters: + +- `gate_set`: the same as in `fold_circuits`; controls the folding scope. +- `hamiltonian`: the observable to estimate; its number of qubits must equal the width of the base circuit. +- `shots`: the number of shots of each execution, passed through to the estimator as is. +- `estimator`: the estimator callback, called once per folded circuit, with the second parameter always `Some(hamiltonian)`; for the callback form see [Overview](0_overview.md). + +Returns: + +- `Vec`: the expectation value of each folded circuit, in the same order as `fold_levels`. The variance returned by the estimator is discarded at this stage. + +Raises: + +- `ErrorMitigationError::HamiltonianQubitCountMismatch`: the number of qubits of the Hamiltonian is inconsistent with the width of the base circuit; the error carries both the expected and the actual value. +- `ErrorMitigationError::Circuit`: the circuit error passed through when folding fails. + +### Extrapolation + +All three extrapolation methods take the noise factor as the independent variable and `noisy_results` as the dependent variable, return the estimate at zero noise (independent variable 0), and require `noisy_results` to be non-empty and of the same length as the number of noise factors. + +- `fn poly_extrapolate(&self, noisy_results: &[f64], degree: usize) -> Result`: least-squares polynomial fitting; returns the intercept at `x = 0`. `degree` is the polynomial degree and must be less than the number of data points. +- `fn exp_extrapolate(&self, noisy_results: &[f64]) -> Result`: models as `y(x) = A * exp(-x / tau)`, performs a linear regression on `ln(y) = ln(A) + m * x` in log space, and returns `A`. +- `fn extrapolate(&self, noisy_results: &[f64], method: ExtrapolateMethod, degree: usize) -> Result`: `Polynomial` forwards to `poly_extrapolate` using `degree`; `Exponential` ignores `degree` and forwards to `exp_extrapolate`. + +### Example + +```rust +use cqlib_core::circuit::gate::{Instruction, StandardGate}; +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::error_mitigation::ZNEMitigation; + +let q0 = Qubit::new(0); +let mut circuit = Circuit::new(1); +circuit.h(q0).unwrap(); +circuit.s(q0).unwrap(); + +// 折叠等级 [0, 1, 2] 对应噪声因子 [1, 3, 5] +let zne = ZNEMitigation::new(circuit, vec![0, 1, 2]); +assert_eq!(zne.fold_levels(), &[0, 1, 2]); +assert_eq!(zne.noise_factors(), &[1, 3, 5]); + +// 全局折叠:等级 0 原样返回,等级 1 把整条线路折叠为 U -> U U† U +let global = zne.fold_circuits(None).unwrap(); +assert_eq!(global[0].operations().len(), 2); +assert_eq!(global[1].operations().len(), 6); + +// 选择性折叠:只放大 S 门,H 门保持原样 +let gate_set = vec![Instruction::Standard(StandardGate::S)]; +let selective = zne.fold_circuits(Some(&gate_set)).unwrap(); +assert_eq!(selective[1].operations().len(), 4); +``` + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::error_mitigation::{ExtrapolateMethod, ZNEMitigation}; +use cqlib_core::qis::{Hamiltonian, Pauli, PauliString}; +use num_complex::Complex64; + +let q0 = Qubit::new(0); +let mut circuit = Circuit::new(1); +circuit.x(q0).unwrap(); + +let zne = ZNEMitigation::new(circuit, vec![0, 1, 2]); + +let mut pauli = PauliString::new(1); +pauli.set_pauli(0, Pauli::Z); +let hamiltonian = Hamiltonian::from_list(vec![(pauli, Complex64::new(1.0, 0.0))]).unwrap(); + +// 估计器返回 (期望值, 方差),零噪声外推只使用期望值 +let noisy = zne + .run_em_sequence_with_shots( + None, + &hamiltonian, + Some(256), + &|circuit, hamiltonian_arg, shots| { + assert!(hamiltonian_arg.is_some()); + assert_eq!(shots, Some(256)); + (circuit.operations().len() as f64 + 0.5, 0.0) + }, + ) + .unwrap(); + +// 噪声因子为 [1, 3, 5],实测值满足 y = 0.5 + x +assert_eq!(noisy, vec![1.5, 3.5, 5.5]); + +// 多项式外推取 x = 0 处的截距 +let mitigated = zne + .extrapolate(&noisy, ExtrapolateMethod::Polynomial, 1) + .unwrap(); +assert!((mitigated - 0.5).abs() < 1e-10); + +// 指数外推:模型为 y = A * exp(-x / tau),返回 A,同时忽略 degree +let a = 2.5_f64; +let tau = 4.0_f64; +let noisy: Vec = zne + .noise_factors() + .iter() + .map(|&x| a * (-(x as f64) / tau).exp()) + .collect(); +let mitigated = zne + .extrapolate(&noisy, ExtrapolateMethod::Exponential, 99) + .unwrap(); +assert!((mitigated - a).abs() < 1e-10); +``` + +--- + +## ExtrapolateMethod + +The extrapolation method enum; determines which fitting path `extrapolate` takes. + +```rust +pub enum ExtrapolateMethod { + Polynomial, + Exponential, +} +``` + +- `Polynomial`: polynomial extrapolation. The `degree` parameter of `extrapolate` takes effect in this branch. +- `Exponential`: exponential extrapolation. The `degree` parameter of `extrapolate` is ignored in this branch. + +Implements `Debug`, `Clone`, `Copy`, `PartialEq`, `Eq`, `Hash`. + +--- + +## ZneConfig + +The method configuration of zero-noise extrapolation; defined in the submodule `cqlib_core::error_mitigation::unified` where the unified pipeline lives, re-exported from the module root, and carried by `MitigationMethod::Zne`. + +```rust +pub struct ZneConfig { + pub fold_levels: Vec, +} +``` + +Fields: + +- `fold_levels`: the list of fold levels, which determines the number of folded circuits and the noise factors; level `l` corresponds to the noise factor `2 * l + 1`. + +There is no `Default` implementation, so `fold_levels` must be given explicitly. Implements `Debug`, `Clone`, `PartialEq`, `Eq`. + +`ErrorMitigation::new` checks the fold levels at the creation stage and returns `ErrorMitigationError::InvalidFoldLevel` if any level is negative; an empty vector of levels passes the creation-stage check, but later extrapolation raises an error because there are no data points. + +--- + +## Errors + +Folding, evaluation and extrapolation fall into two kinds of error: folding returns circuit errors, and evaluation and extrapolation return the unified error of the module. + +| Error | When it occurs | +| --- | --- | +| `CircuitError::InvalidControlOperation` | `fold_circuits` encounters a negative fold level. | +| `ErrorMitigationError::HamiltonianQubitCountMismatch` | The number of qubits of the Hamiltonian received by `run_em_sequence` or `run_em_sequence_with_shots` is inconsistent with the width of the base circuit. | +| `ErrorMitigationError::EmptyNoisyResults` | The result sequence received by extrapolation is empty, for example when the fold levels are an empty vector. | +| `ErrorMitigationError::NoisyResultsLengthMismatch` | The length of the extrapolation result sequence does not equal the number of noise factors. | +| `ErrorMitigationError::InvalidPolynomialDegree` | The polynomial degree is not less than the number of data points. | +| `ErrorMitigationError::NonPositiveNoisyResults` | The results received by exponential extrapolation contain a non-positive value. | +| `ErrorMitigationError::SingularExponentialFit` | The linear regression of exponential extrapolation degenerates. | +| `ErrorMitigationError::SingularPolynomialFit` | The normal equation matrix of polynomial extrapolation is singular. | +| `ErrorMitigationError::Circuit` / `Qis` | Errors passed through from folding or circuit operations. | + +For the complete definition of the error type, see [Unified pipeline](3_unified.md). diff --git a/docs/api/en/rust/6_error_mitigation/2_virtual_distillation.md b/docs/api/en/rust/6_error_mitigation/2_virtual_distillation.md new file mode 100644 index 00000000..182d5c90 --- /dev/null +++ b/docs/api/en/rust/6_error_mitigation/2_virtual_distillation.md @@ -0,0 +1,180 @@ +# Virtual Distillation + +`cqlib_core::error_mitigation::virtual_distillation` + +Virtual distillation estimates the ratio `Tr(O ρ^M) / Tr(ρ^M)` through multiple copies of the density matrix and a copy-swap circuit, where `M` is the number of copies and `O` is the observable represented by the Hamiltonian. This page covers the public API of copy-swap circuit construction and numerator and denominator statistics. + +## Import + +```rust +use cqlib_core::error_mitigation::VirtualDistillation; +``` + +--- + +## VirtualDistillation + +The low-level entry point of virtual distillation. It holds the base circuit and the number of copies, and can construct the copy-swap circuit on its own, run the denominator or the numerator circuit on its own, or obtain the ratio and variance in one step. + +```rust +pub struct VirtualDistillation { + circuit: Circuit, + copies: usize, +} +``` + +Fields are private, and are read through methods: + +- `fn copies(&self) -> usize`: the currently configured number of copies. + +Other methods: + +- `fn new(circuit: Circuit, copies: usize) -> Result`: the creation entry point; it takes ownership of the circuit. The template circuit can only be passed through `new`, and there is no corresponding read interface. +- `fn set_copies(&mut self, copies: usize) -> Result<(), ErrorMitigationError>`: updates the number of copies. On failure the original value stays unchanged. +- `fn build_copy_swap_circuit(&self) -> Result`: constructs the copy-swap circuit from the base circuit. +- `fn run_denominator_circuit(&self, shots: usize, estimator: &Estimator<'_>) -> Result<(f64, f64), CircuitError>`: runs the denominator circuit and returns `(mean, variance)`. +- `fn run_numerator_circuit(&self, hamiltonian: &Hamiltonian, shots: usize, estimator: &Estimator<'_>) -> Result<(f64, f64), ErrorMitigationError>`: runs the numerator circuit and returns `(mean, variance)`. +- `fn run_vd(&self, hamiltonian: &Hamiltonian, shots_numerator: usize, shots_denominator: usize, estimator: &Estimator<'_>) -> Result<(f64, f64), ErrorMitigationError>`: runs the complete flow and returns the mitigated `(expectation value, variance)`. + +`VirtualDistillation` has no builder methods and no `Default` implementation: an instance can be created only by `new`, and the number of copies is modified through `set_copies`. It implements `Debug` and `Clone`. + +### `new(circuit: Circuit, copies: usize) -> Result` + +Parameters: + +- `circuit`: the base circuit; all derived circuits are constructed from it. +- `copies`: the number of copies; must not be less than 2. + +Returns: + +- `VirtualDistillation`: usable for constructing circuits and taking values. + +Raises: + +- `ErrorMitigationError::InvalidCopies`: the number of copies is less than 2; the error carries the number of copies passed in. + +### `build_copy_swap_circuit(&self) -> Result` + +Constructs the copy-swap circuit, which consists of three parts: first the base circuit is decomposed into gates, then `copies` copies of the base circuit are prepared side by side — the `i`-th copy is shifted as a whole onto the qubit range starting at `i × base width`, and the copies do not overlap — and finally a `SWAP` is inserted bit by bit between the first copy and each of the others. + +- The circuit width is `copies × base width`, where the base width is the width of the decomposed circuit. +- The number of operations is `copies` sets of base operations plus `(copies - 1) × base width` `SWAP`s. + +Returns: + +- `Circuit`: the copy-swap circuit. + +Raises: + +- `CircuitError`: decomposition of the base circuit fails, or appending an operation to the new circuit fails. + +### Numerator and denominator + +- `fn run_denominator_circuit(&self, shots: usize, estimator: &Estimator<'_>) -> Result<(f64, f64), CircuitError>`: estimates the denominator `Tr(ρ^M)` on the copy-swap circuit. The observable parameter the estimator receives is `None` and the number of shots is `Some(shots)`; for the callback form see [Overview](0_overview.md). +- `fn run_numerator_circuit(&self, hamiltonian: &Hamiltonian, shots: usize, estimator: &Estimator<'_>) -> Result<(f64, f64), ErrorMitigationError>`: estimates the numerator `Tr(O ρ^M)` on the copy-swap circuit. The observable is first expanded inside the module to the width of the copy-swap circuit: the original Pauli terms keep their original qubit indices, phase and coefficients unchanged, and `Z` is filled in on the newly added higher-index qubits; the expanded result is passed in through the observable parameter of the estimator, and the number of shots is `Some(shots)`. +- `fn run_vd(&self, hamiltonian: &Hamiltonian, shots_numerator: usize, shots_denominator: usize, estimator: &Estimator<'_>) -> Result<(f64, f64), ErrorMitigationError>`: first validates that the number of qubits of the Hamiltonian equals the width of the base circuit, then executes the numerator circuit and the denominator circuit in turn, and finally combines them into the mitigated result. The numerator and the denominator use their own number of shots. + +Raises for `run_vd`: + +- `ErrorMitigationError::HamiltonianQubitCountMismatch`: the number of qubits of the Hamiltonian is inconsistent with the width of the base circuit; the error carries both the expected and the actual value. +- `ErrorMitigationError::ZeroDenominatorMean`: the mean of the denominator circuit is zero, so the ratio cannot be computed. +- `ErrorMitigationError::Circuit`: the circuit error passed through when the width of the expanded observable is inconsistent with the width of the copy-swap circuit. + +### Ratio and variance + +The return value of `run_vd` combines the expectation value as `mu_vd = mu_num / mu_den`; the variance is combined, under a first-order Taylor approximation and the assumption that the numerator and the denominator are independent, as `var_num / mu_den^2 + mu_num^2 * var_den / mu_den^4`. These formulas are used only inside the module; externally the result is obtained only through `run_vd`, and the unified pipeline reuses the same formulas in the post-processing stage. + +### Example + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::error_mitigation::VirtualDistillation; + +let q0 = Qubit::new(0); +let mut circuit = Circuit::new(1); +circuit.x(q0).unwrap(); + +// 两份单比特副本:X 作用于比特 0、X 作用于比特 1,再加一对逐比特 SWAP +let vd = VirtualDistillation::new(circuit, 2).unwrap(); +let copy_swap = vd.build_copy_swap_circuit().unwrap(); +assert_eq!(copy_swap.width(), 2); +assert_eq!(copy_swap.operations().len(), 3); +``` + +```rust +use cqlib_core::circuit::Circuit; +use cqlib_core::error_mitigation::{ErrorMitigationError, VirtualDistillation}; + +// 副本数通过 set_copies 调整,非法值被拒绝且不改变原值 +let mut vd = VirtualDistillation::new(Circuit::new(1), 2).unwrap(); +assert_eq!(vd.copies(), 2); + +vd.set_copies(3).unwrap(); +assert_eq!(vd.copies(), 3); + +let err = vd.set_copies(1).unwrap_err(); +assert!(matches!(err, ErrorMitigationError::InvalidCopies(1))); +assert_eq!(vd.copies(), 3); +``` + +```rust +use cqlib_core::circuit::Circuit; +use cqlib_core::error_mitigation::VirtualDistillation; +use cqlib_core::qis::{Hamiltonian, Pauli, PauliString}; +use num_complex::Complex64; + +let mut pauli = PauliString::new(1); +pauli.set_pauli(0, Pauli::Z); +let hamiltonian = Hamiltonian::from_list(vec![(pauli, Complex64::new(1.0, 0.0))]).unwrap(); + +// 分子线路带可观测量,分母线路不带;两者使用各自的 shot 数 +let vd = VirtualDistillation::new(Circuit::new(1), 2).unwrap(); +let (mu_vd, var_vd) = vd + .run_vd(&hamiltonian, 3, 2, &|_circuit, hamiltonian_arg, shots| { + if hamiltonian_arg.is_some() { + assert_eq!(shots, Some(3)); + (1.5, 0.25) + } else { + assert_eq!(shots, Some(2)); + (2.0, 1.0) + } + }) + .unwrap(); + +assert!((mu_vd - 0.75).abs() < 1e-12); +assert!((var_vd - 0.203125).abs() < 1e-12); +``` + +--- + +## VirtualDistillationConfig + +The method configuration of virtual distillation; defined in the submodule `cqlib_core::error_mitigation::unified` where the unified pipeline lives, re-exported from the module root, and carried by `MitigationMethod::VirtualDistillation`. + +```rust +pub struct VirtualDistillationConfig { + pub copies: usize, +} +``` + +Fields: + +- `copies`: the number of copies; the minimum is 2. + +There is no `Default` implementation, so `copies` must be given explicitly. Implements `Debug`, `Clone`, `PartialEq`, `Eq`. + +`ErrorMitigation::new` checks this field at the creation stage and returns `ErrorMitigationError::InvalidCopies` when it is less than 2. + +--- + +## Errors + +| Error | When it occurs | +| --- | --- | +| `ErrorMitigationError::InvalidCopies` | The number of copies is less than 2; see `new`, `set_copies` and `ErrorMitigation::new`. | +| `ErrorMitigationError::HamiltonianQubitCountMismatch` | The number of qubits of the Hamiltonian received by `run_vd` is inconsistent with the width of the base circuit. | +| `ErrorMitigationError::ZeroDenominatorMean` | The mean of the denominator circuit is zero. | +| `ErrorMitigationError::Circuit` | Carries `CircuitError::QubitCountMismatch`, indicating that the width of the expanded observable is inconsistent with the width of the copy-swap circuit. | +| `CircuitError` | The circuit error returned directly by `build_copy_swap_circuit` and `run_denominator_circuit`. | + +For the complete definition of the error type, see [Unified pipeline](3_unified.md). diff --git a/docs/api/en/rust/6_error_mitigation/3_unified.md b/docs/api/en/rust/6_error_mitigation/3_unified.md new file mode 100644 index 00000000..67c4d340 --- /dev/null +++ b/docs/api/en/rust/6_error_mitigation/3_unified.md @@ -0,0 +1,384 @@ +# Unified Pipeline + +`cqlib_core::error_mitigation::unified` + +The unified pipeline converges zero-noise extrapolation and virtual distillation onto the same sequential flow: create an instance, execute and take values, and produce the result by post-processing. This page covers the unified pipeline itself, the three argument enums, the result object and the unified error type of the module. + +## Import + +```rust +use cqlib_core::error_mitigation::{ + ErrorMitigation, ErrorMitigationError, ExtrapolateMethod, MitigatedResult, MitigationMethod, + ProcessArgs, RunArgs, VirtualDistillationConfig, ZneConfig, +}; +``` + +--- + +## ErrorMitigation + +The recommended default entry point. It stores the base circuit, the selected mitigation method and the pipeline state, and splits "construct the mitigation circuits and take values" and "compute the mitigated result from the raw data" into the two steps `run` and `get_mitigated`. + +```rust +pub struct ErrorMitigation { + // 字段私有:基底线路、缓解方法与流水线状态 +} +``` + +Methods: + +- `fn new(circuit: Circuit, method: MitigationMethod) -> Result`: creates the pipeline and validates the method configuration. +- `fn run(&mut self, hamiltonian: &Hamiltonian, run_args: RunArgs, estimator: &Estimator<'_>) -> Result<(), ErrorMitigationError>`: constructs and executes the circuits required by the method, and keeps the raw data; it does not return the mitigated value directly. +- `fn get_mitigated(&mut self, process_args: ProcessArgs) -> Result`: computes the final result from the kept raw data. + +The unified pipeline provides no interface for reading the raw data and no interface for modifying the selected method; both are kept only internally. It implements `Debug` and `Clone`. + +### State machine + +The lifecycle of each instance is fixed as create → `run()` → `get_mitigated()`, and each of the two steps can succeed only once: + +| Current state | Call | Result | +| --- | --- | --- | +| Created | `run` | Executes and moves to running. | +| Created | `get_mitigated` | `ErrorMitigationError::RunRequiredBeforeMitigation`. | +| Running | `run` | `ErrorMitigationError::AlreadyRun`. | +| Running | `get_mitigated` with arguments matching the method | Returns `MitigatedResult` and moves to completed. | +| Running | `get_mitigated` with arguments not matching the method | `ErrorMitigationError::ProcessArgsMethodMismatch`. | +| Completed | `run` or `get_mitigated` | `ErrorMitigationError::AlreadyMitigated`. | + +When a call fails the state keeps its original value (except the completed state, which accepts no further calls), so after an argument mismatch or a failed input validation the arguments can be corrected and the call retried. Create a new instance to run a second mitigation flow. + +### `new(circuit: Circuit, method: MitigationMethod) -> Result` + +Parameters: + +- `circuit`: the base circuit; all derived circuits in the `run` stage are constructed from it. +- `method`: the mitigation method and its configuration. + +Returns: + +- `ErrorMitigation`: with the state created. + +Raises: + +- `ErrorMitigationError::InvalidFoldLevel`: a fold level of `MitigationMethod::Zne` is negative. +- `ErrorMitigationError::InvalidCopies`: the number of copies of `MitigationMethod::VirtualDistillation` is less than 2. + +### `run(&mut self, hamiltonian: &Hamiltonian, run_args: RunArgs, estimator: &Estimator<'_>) -> Result<(), ErrorMitigationError>` + +Constructs the mitigation circuits according to the selected method and takes values one by one; the results stay inside the instance. + +Parameters: + +- `hamiltonian`: the observable to estimate; its number of qubits must equal the base circuit width. +- `run_args`: the execution arguments; the variant must match `MitigationMethod`. +- `estimator`: the estimator callback; for the form see [Overview](0_overview.md). + +Execution details differ by method: + +- Zero-noise extrapolation: constructs the folded circuits according to the configured fold levels, calls the estimator one by one with the observable parameter `Some(hamiltonian)` and the number of shots `RunArgs::Zne::shots`, and takes only the expectation value from the return value. The folded circuits, the expectation values, the noise factors and the execution arguments are kept together. +- Virtual distillation: constructs the copy-swap circuit and expands the observable to the width of that circuit; the estimator of the numerator circuit receives `Some(expanded observable)` and `Some(shots_numerator)`, and the denominator circuit receives `None` and `Some(shots_denominator)`. The means and variances of the numerator and the denominator are kept together. + +Returns: + +- `Result<(), ErrorMitigationError>`: success only means the raw data is in place; the mitigated value is given by `get_mitigated`. + +Raises: + +- `ErrorMitigationError::RunArgsMethodMismatch`: the variant of `run_args` is inconsistent with the configured method. +- `ErrorMitigationError::HamiltonianQubitCountMismatch`: the number of qubits of the Hamiltonian is inconsistent with the base circuit width. +- `ErrorMitigationError::AlreadyRun` / `ErrorMitigationError::AlreadyMitigated`: the current state does not allow another execution. +- `ErrorMitigationError::Circuit`: the circuit error passed through when folding, decomposition or circuit construction fails. + +### `get_mitigated(&mut self, process_args: ProcessArgs) -> Result` + +Performs the method-specific post-processing on the kept raw data. + +Parameters: + +- `process_args`: the post-processing arguments; the variant must match `MitigationMethod`. + +Post-processing details differ by method: + +- Zero-noise extrapolation: the extrapolation mode is selected by `ProcessArgs::Zne::method`. In the `Polynomial` branch, when `degree` is `None` the value `min(number of data points - 1, 1)` is taken, where the number of data points equals the number of fold levels, that is, a first-degree polynomial extrapolation by default; the `Exponential` branch does not use `degree`. The `variance` in the result is `None`. +- Virtual distillation: combines the ratio and the variance from the means and variances of the numerator and the denominator; the `variance` in the result is `Some`. + +Returns: + +- `MitigatedResult`: the mitigated expectation value and an optional variance. + +Raises: + +- `ErrorMitigationError::RunRequiredBeforeMitigation`: `run` has not been called. +- `ErrorMitigationError::ProcessArgsMethodMismatch`: the variant of `process_args` is inconsistent with the configured method. +- `ErrorMitigationError::AlreadyMitigated`: a mitigated result has already been taken once. +- `ErrorMitigationError::ZeroDenominatorMean`, `EmptyNoisyResults`, `NoisyResultsLengthMismatch`, `InvalidPolynomialDegree`, `NonPositiveNoisyResults`, `SingularExponentialFit`, `SingularPolynomialFit`: the extrapolation or ratio errors passed through in the post-processing stage; see [Zero-noise extrapolation](1_zne.md) and [Virtual distillation](2_virtual_distillation.md). + +### Example + +```rust +use cqlib_core::circuit::{Circuit, Qubit}; +use cqlib_core::error_mitigation::{ + ErrorMitigation, ErrorMitigationError, ExtrapolateMethod, MitigatedResult, MitigationMethod, + ProcessArgs, RunArgs, ZneConfig, +}; +use cqlib_core::qis::{Hamiltonian, Pauli, PauliString}; +use num_complex::Complex64; + +let q0 = Qubit::new(0); +let mut circuit = Circuit::new(1); +circuit.x(q0).unwrap(); + +let mut pauli = PauliString::new(1); +pauli.set_pauli(0, Pauli::Z); +let hamiltonian = Hamiltonian::from_list(vec![(pauli, Complex64::new(1.0, 0.0))]).unwrap(); + +let mut mitigation = ErrorMitigation::new( + circuit, + MitigationMethod::Zne(ZneConfig { + fold_levels: vec![0, 1, 2], + }), +) +.unwrap(); + +mitigation + .run( + &hamiltonian, + RunArgs::Zne { + gate_set: None, + shots: Some(256), + }, + &|circuit, hamiltonian_arg, shots| { + assert!(hamiltonian_arg.is_some()); + assert_eq!(shots, Some(256)); + (circuit.operations().len() as f64 + 0.5, 0.0) + }, + ) + .unwrap(); + +// run 只能成功调用一次 +let rerun_err = mitigation + .run( + &hamiltonian, + RunArgs::Zne { + gate_set: None, + shots: Some(256), + }, + &|_circuit, _hamiltonian, _shots| (0.0, 0.0), + ) + .unwrap_err(); +assert!(matches!(rerun_err, ErrorMitigationError::AlreadyRun)); + +// degree 为 None 时按 min(数据点数 - 1, 1) 取值,此处数据点数为 3 +let mitigated = mitigation + .get_mitigated(ProcessArgs::Zne { + method: ExtrapolateMethod::Polynomial, + degree: None, + }) + .unwrap(); + +assert_eq!( + mitigated, + MitigatedResult { + expectation: 0.5, + variance: None, + } +); + +// get_mitigated 也只能成功调用一次 +let second_err = mitigation + .get_mitigated(ProcessArgs::Zne { + method: ExtrapolateMethod::Polynomial, + degree: Some(1), + }) + .unwrap_err(); +assert!(matches!(second_err, ErrorMitigationError::AlreadyMitigated)); +``` + +```rust +use cqlib_core::circuit::Circuit; +use cqlib_core::error_mitigation::{ + ErrorMitigation, MitigationMethod, ProcessArgs, RunArgs, VirtualDistillationConfig, +}; +use cqlib_core::qis::{Hamiltonian, Pauli, PauliString}; +use num_complex::Complex64; + +let mut pauli = PauliString::new(1); +pauli.set_pauli(0, Pauli::Z); +let hamiltonian = Hamiltonian::from_list(vec![(pauli, Complex64::new(1.0, 0.0))]).unwrap(); + +let mut mitigation = ErrorMitigation::new( + Circuit::new(1), + MitigationMethod::VirtualDistillation(VirtualDistillationConfig { copies: 2 }), +) +.unwrap(); + +mitigation + .run( + &hamiltonian, + RunArgs::VirtualDistillation { + shots_numerator: 3, + shots_denominator: 2, + }, + &|_circuit, hamiltonian_arg, shots| { + if hamiltonian_arg.is_some() { + assert_eq!(shots, Some(3)); + (1.5, 0.25) + } else { + assert_eq!(shots, Some(2)); + (2.0, 1.0) + } + }, + ) + .unwrap(); + +let mitigated = mitigation + .get_mitigated(ProcessArgs::VirtualDistillation) + .unwrap(); + +assert!((mitigated.expectation - 0.75).abs() < 1e-12); +assert!((mitigated.variance.unwrap() - 0.203125).abs() < 1e-12); +``` + +--- + +## MitigationMethod + +The mitigation method enum, carrying the configuration of the corresponding method. + +```rust +pub enum MitigationMethod { + Zne(ZneConfig), + VirtualDistillation(VirtualDistillationConfig), +} +``` + +- `Zne(ZneConfig)`: selects zero-noise extrapolation and configures the fold levels. +- `VirtualDistillation(VirtualDistillationConfig)`: selects virtual distillation and configures the number of copies. + +Implements `Debug`, `Clone`, `PartialEq`, `Eq`. The method and the two kinds of argument must match: + +| Method | Execution arguments | Post-processing arguments | +| --- | --- | --- | +| `MitigationMethod::Zne` | `RunArgs::Zne` | `ProcessArgs::Zne` | +| `MitigationMethod::VirtualDistillation` | `RunArgs::VirtualDistillation` | `ProcessArgs::VirtualDistillation` | + +For the configuration structs see [Zero-noise extrapolation](1_zne.md) and [Virtual distillation](2_virtual_distillation.md). + +--- + +## RunArgs + +The argument enum of the execution stage. + +```rust +pub enum RunArgs { + Zne { + gate_set: Option>, + shots: Option, + }, + VirtualDistillation { + shots_numerator: usize, + shots_denominator: usize, + }, +} +``` + +- `Zne::gate_set`: the gate set of selective folding; `None` means global folding, and matching is performed by operation name. +- `Zne::shots`: the number of shots passed through to the estimator; `None` means it is not specified. +- `VirtualDistillation::shots_numerator`: the number of shots of the numerator circuit. +- `VirtualDistillation::shots_denominator`: the number of shots of the denominator circuit. + +Implements `Debug`, `Clone`, `PartialEq`, `Eq`, where `PartialEq` is a custom implementation: the `Zne` branch compares `shots` and the name of each operation in `gate_set`, requiring the same number of elements and the same name one by one, and does not compare the parameters carried by the operations or their circuit definitions, so different operation instances with the same name are judged equal; the `VirtualDistillation` branch compares the two shot numbers. Variants that differ are judged unequal. + +--- + +## ProcessArgs + +The argument enum of the post-processing stage. + +```rust +pub enum ProcessArgs { + Zne { + method: ExtrapolateMethod, + degree: Option, + }, + VirtualDistillation, +} +``` + +- `Zne::method`: the extrapolation method. +- `Zne::degree`: the polynomial degree. In the `ExtrapolateMethod::Polynomial` branch, `None` takes `min(number of data points - 1, 1)`, where the number of data points equals the number of fold levels; the `ExtrapolateMethod::Exponential` branch ignores this field. +- `VirtualDistillation`: no additional fields. + +Implements `Debug`, `Clone`, `Copy`, `PartialEq`, `Eq`. + +--- + +## MitigatedResult + +The final mitigation result. + +```rust +pub struct MitigatedResult { + pub expectation: f64, + pub variance: Option, +} +``` + +Fields: + +- `expectation`: the mitigated expectation value. +- `variance`: the variance of the mitigation result, given only by virtual distillation; `None` for zero-noise extrapolation. + +Implements `Debug`, `Clone`, `PartialEq`. + +--- + +## ErrorMitigationError + +The unified error type of the module; configuration validation, input validation, pipeline state and extrapolation fitting errors are all returned through it. + +```rust +pub enum ErrorMitigationError { + Circuit(CircuitError), + Qis(QisError), + InvalidCopies(usize), + HamiltonianQubitCountMismatch { expected: usize, actual: usize }, + ZeroDenominatorMean, + InvalidFoldLevel(i32), + RunRequiredBeforeMitigation, + AlreadyRun, + AlreadyMitigated, + RunArgsMethodMismatch, + ProcessArgsMethodMismatch, + EmptyNoisyResults, + NoisyResultsLengthMismatch { expected: usize, actual: usize }, + InvalidPolynomialDegree { degree: usize, num_points: usize }, + NonPositiveNoisyResults, + SingularExponentialFit, + SingularPolynomialFit, +} +``` + +The first two variants pass through errors from the circuit module and the quantum information module, and the remaining variants give concrete values. Implements `Debug` and `Display`. + +| Error | When it occurs | +| --- | --- | +| `Circuit(CircuitError)` | Folding, circuit decomposition or circuit construction fails. | +| `Qis(QisError)` | A computation at the quantum information level fails. | +| `InvalidCopies(usize)` | The number of copies of virtual distillation is less than 2: `VirtualDistillation::new`, `VirtualDistillation::set_copies` and `ErrorMitigation::new` all check this. | +| `HamiltonianQubitCountMismatch { expected, actual }` | The number of qubits of the Hamiltonian is inconsistent with the base circuit width; `expected` is the circuit width and `actual` is the number of qubits of the Hamiltonian. | +| `ZeroDenominatorMean` | The mean of the denominator circuit of virtual distillation is zero. | +| `InvalidFoldLevel(i32)` | `ErrorMitigation::new` receives a negative fold level. | +| `RunRequiredBeforeMitigation` | `get_mitigated` is called without `run` having been called. | +| `AlreadyRun` | `run` is called a second time on the same instance. | +| `AlreadyMitigated` | An instance that has completed mitigation calls `run` or `get_mitigated` again. | +| `RunArgsMethodMismatch` | The argument variant of `run` is inconsistent with the configured method. | +| `ProcessArgsMethodMismatch` | The argument variant of `get_mitigated` is inconsistent with the configured method. | +| `EmptyNoisyResults` | The result sequence received by extrapolation is empty, for example when the fold levels are an empty vector. | +| `NoisyResultsLengthMismatch { expected, actual }` | The length of the extrapolation result sequence does not equal the number of noise factors. | +| `InvalidPolynomialDegree { degree, num_points }` | The polynomial degree is not less than the number of data points. | +| `NonPositiveNoisyResults` | Exponential extrapolation receives a non-positive result value. | +| `SingularExponentialFit` | The linear regression of exponential extrapolation degenerates. | +| `SingularPolynomialFit` | The normal equation matrix of polynomial extrapolation is singular. | diff --git a/docs/api/python/0_overview.md b/docs/api/python/0_overview.md deleted file mode 100644 index 2a5e8d85..00000000 --- a/docs/api/python/0_overview.md +++ /dev/null @@ -1,47 +0,0 @@ -# Python API 概览 - -## 欢迎查阅 Cqlib Python API 参考手册! - ---- - -## 文档导航 - -### 量子电路(`cqlib.circuit`) - -- [Overview](0_circuit/0_overview.md) -- [Circuit](0_circuit/1_circuit.md) -- [Qubit](0_circuit/2_qubit.md) -- [Parameter](0_circuit/3_parameter.md) -- [Operation / Instruction](0_circuit/4_operation_instruction.md) -- [StandardGate](0_circuit/5_gates_standard.md) -- [UnitaryGate](0_circuit/6_gates_unitary.md) -- [MCGate](0_circuit/7_gates_mc_gate.md) -- [CircuitGate / FrozenCircuit](0_circuit/8_gates_circuit_gate.md) -- [Classical / Control Flow](0_circuit/9_classical_control_flow.md) -- [SymbolicMatrix](0_circuit/10_symbolic_matrix.md) -- [Ansatz](0_circuit/11_ansatz.md) -- [Circuit To Matrix](0_circuit/12_circuit_to_matrix.md) - -### 中间表达(xxx) - -- xxxxxxx - -### 设备模块(xxx) - -- xxxxxxx - -### 量子信息(xxx) - -- xxxxxxx - -### 编译优化(xxx) - -- xxxxxxx - -### 可视化(xxx) - -- xxxxxxx - -### 错误缓解(xxx) - -- xxxxxxx diff --git a/docs/api/python/2_device/1_topology.md b/docs/api/python/2_device/1_topology.md deleted file mode 100644 index 30b5cbff..00000000 --- a/docs/api/python/2_device/1_topology.md +++ /dev/null @@ -1,103 +0,0 @@ -# Topology - -`cqlib.device.Topology` 用于描述硬件物理比特与耦合边关系。 -在 Python 绑定中,它是 `cqlib.compiler.Topology` 的同一类型别名。 - -## 导入 - -```python -from cqlib.device import Topology -``` - ---- - -## 构造 - -### `Topology(qubits, couplings)` - -参数: - -- `qubits` (`list[int]`):物理比特 ID 列表。 -- `couplings` (`list[tuple[int, int] | tuple[int, int, str]]`):耦合边列表。 - - `(u, v)`:门名默认 `CX` - - `(u, v, name)`:显式指定耦合门名 - -说明: - -- 拓扑按有向边语义存储,`(u, v)` 与 `(v, u)` 是两条不同耦合。 - -### `Topology.line(qubits)` - -静态方法,快速构造线性拓扑(相邻比特以 `CX` 连接)。 - -## 属性 - -- `num_qubits -> int` -- `num_couplings -> int` -- `qubits -> list[int]` - -## 修改方法 - -### `add_qubits(qubits)` - -添加物理比特。 - -异常情况: - -- `ValueError`:存在重复比特。 - -### `add_couplings(couplings)` - -添加耦合边。 - -异常情况: - -- `ValueError`:边端点比特不在拓扑中。 - -### `remove_qubits(qubits)` - -删除物理比特(关联耦合边会被一并删除)。 - -异常情况: - -- `ValueError`:待删除比特不存在。 - -### `remove_couplings(couplings)` - -删除耦合边。参数格式为 `list[tuple[int, int]]`。 - -异常情况: - -- `ValueError`:待删除耦合不存在,或端点比特不存在。 - -## 查询方法 - -- `is_connected(u, v) -> bool` -- `neighbors(qubit) -> list[int]` -- `get_coupling_name(u, v) -> str | None` -- `contains_qubit(qubit) -> bool` -- `degree(qubit) -> int` - -说明: - -- `neighbors/degree` 基于出边统计。 - -## 示例 - -```python -from cqlib.device import Topology - -topo = Topology([0, 1, 2], [(0, 1), (1, 2, "CZ")]) - -assert topo.num_qubits == 3 -assert topo.num_couplings == 2 -assert topo.is_connected(1, 2) is True -assert topo.get_coupling_name(1, 2) == "CZ" -assert topo.neighbors(1) == [2] - -topo.add_qubits([3]) -topo.add_couplings([(2, 3, "CX")]) -topo.remove_couplings([(2, 3)]) -topo.remove_qubits([3]) -``` - diff --git a/docs/api/python/2_device/2_properties_device.md b/docs/api/python/2_device/2_properties_device.md deleted file mode 100644 index cdc59187..00000000 --- a/docs/api/python/2_device/2_properties_device.md +++ /dev/null @@ -1,160 +0,0 @@ -# Device / Properties - -本页覆盖 `cqlib.device` 中用于设备标定建模的核心类型: - -- `InstructionProp` -- `QubitProp` -- `EdgeProp` -- `Device` - -## 导入 - -```python -from cqlib.circuit import Instruction, StandardGate -from cqlib.device import Device, EdgeProp, InstructionProp, QubitProp, Topology -``` - ---- - -## InstructionProp - -### `InstructionProp(instruction, error_rate)` - -参数: - -- `instruction` (`Instruction`) -- `error_rate` (`float`) - -### 方法 - -- `with_length(length) -> InstructionProp` - -### 属性 - -- `instruction -> Instruction` -- `error_rate -> float` -- `length -> float | None` - -## QubitProp - -### `QubitProp(readout_error)` - -参数: - -- `readout_error` (`float`) - -### Builder 方法 - -- `with_prob_meas0_prep1(prob) -> QubitProp` -- `with_prob_meas1_prep0(prob) -> QubitProp` -- `with_t1(t1) -> QubitProp` -- `with_t2(t2) -> QubitProp` -- `with_frequency(frequency) -> QubitProp` -- `with_native_instruction(prop) -> QubitProp` - -### 属性 - -- `readout_error -> float` -- `t1 -> float | None` -- `t2 -> float | None` -- `frequency -> float | None` -- `native_instructions -> list[InstructionProp]` - -## EdgeProp - -### `EdgeProp()` - -### 方法 - -- `with_native_instruction(prop) -> EdgeProp` - -### 属性 - -- `native_instructions -> list[InstructionProp]` - -## Device - -### `Device(name, topology)` - -参数: - -- `name` (`str`) -- `topology` (`Topology`) - -### 配置方法(返回新对象) - -- `with_native_gates(gates) -> Device` -- `with_default_t1(t1) -> Device` -- `with_default_t2(t2) -> Device` -- `with_default_readout_error(error) -> Device` -- `with_default_single_qubit_error(error) -> Device` -- `with_default_two_qubit_error(error) -> Device` - -### 写入方法(原位修改) - -- `add_qubit_properties(qubit, props) -> None` -- `add_edge_properties(control, target, props) -> None` - -异常情况: - -- `ValueError`:比特或耦合边不在拓扑中。 - -### 查询属性 - -- `name -> str` -- `qubits -> list[int]` -- `invalid_qubits -> list[int]` -- `topology -> Topology` -- `native_gates -> list[Instruction]` -- `default_single_qubit_error -> float | None` -- `default_two_qubit_error -> float | None` - -### 查询方法 - -- `qubit_properties(qubit) -> QubitProp | None` -- `edge_properties(control, target) -> EdgeProp | None` -- `get_t1(qubit) -> float | None` -- `get_t2(qubit) -> float | None` -- `get_readout_error(qubit) -> float | None` - -说明: - -- `get_t1/get_t2/get_readout_error` 会优先返回局部配置,缺失时回退到默认值。 - -## 示例 - -```python -from cqlib.circuit import Instruction, StandardGate -from cqlib.device import Device, EdgeProp, InstructionProp, QubitProp, Topology - -topo = Topology([0, 1, 2], [(0, 1, "CX"), (1, 2, "CX")]) - -device = ( - Device("mock_backend", topo) - .with_default_t1(50.0) - .with_default_t2(35.0) - .with_default_readout_error(0.05) - .with_native_gates( - [ - Instruction.from_standard_gate(StandardGate.X), - Instruction.from_standard_gate(StandardGate.CX), - ] - ) -) - -device.add_qubit_properties(0, QubitProp(0.02).with_t1(80.0).with_t2(70.0)) -device.add_edge_properties( - 0, - 1, - EdgeProp().with_native_instruction( - InstructionProp( - Instruction.from_standard_gate(StandardGate.CX), - 0.01, - ).with_length(220.0) - ), -) - -print(device.get_t1(0)) # 80.0 -print(device.get_t1(2)) # 50.0(默认值) -``` - diff --git a/docs/api/python/2_device/3_layout.md b/docs/api/python/2_device/3_layout.md deleted file mode 100644 index 5b6d19c0..00000000 --- a/docs/api/python/2_device/3_layout.md +++ /dev/null @@ -1,76 +0,0 @@ -# Layout - -`Layout` 用于维护逻辑比特(virtual qubit)到物理比特(physical qubit)的双向映射。 - -## 导入 - -```python -from cqlib.device import Layout -``` - ---- - -## 构造函数 - -### `Layout(logical, physical, init_map=None)` - -参数: - -- `logical` (`list[int]`):逻辑比特 ID 列表 -- `physical` (`list[int]`):物理比特 ID 列表 -- `init_map` (`dict[int, int] | None`):可选初始映射(逻辑 -> 物理) - -异常情况: - -- `ValueError`:输入不合法(例如逻辑比特多于物理比特、`init_map` 非法映射等)。 - -## 属性 - -- `num_logical -> int` -- `num_ancilla -> int` -- `num_physical -> int` -- `logical_qubits -> list[int]` -- `ancilla_qubits -> list[int]` -- `physical_qubits -> list[int]` -- `v2p_map -> dict[int, int]` -- `p2v_map -> dict[int, int]` - -## 方法 - -### `get_physical(virtual_id) -> int | None` - -根据虚拟比特查物理比特。 - -### `get_virtual(physical_id) -> int | None` - -根据物理比特查虚拟比特。 - -### `swap_physical(phys_a, phys_b) -> None` - -交换两个物理比特上的虚拟比特映射。 - -异常情况: - -- `ValueError`:`phys_a` 或 `phys_b` 不在布局的物理比特集合中。 - -## 示例 - -```python -from cqlib.device import Layout - -layout = Layout(logical=[0, 1], physical=[10, 11, 12], init_map={0: 11}) - -print(layout.num_logical) # 2 -print(layout.num_ancilla) # 1 -print(layout.v2p_map) - -before_11 = layout.get_virtual(11) -before_12 = layout.get_virtual(12) -layout.swap_physical(11, 12) -after_11 = layout.get_virtual(11) -after_12 = layout.get_virtual(12) - -print(before_11, before_12) -print(after_11, after_12) -``` - diff --git a/docs/api/python/2_device/4_noise.md b/docs/api/python/2_device/4_noise.md deleted file mode 100644 index 73291af3..00000000 --- a/docs/api/python/2_device/4_noise.md +++ /dev/null @@ -1,141 +0,0 @@ -# NoiseModel / Noise Channels - -本页覆盖 `cqlib.device` 中噪声建模相关 API: - -- `SingleQubitNoise` -- `TwoQubitNoise` -- `ReadoutError` -- `OperationKey` -- `NoiseModel` - -## 导入 - -```python -from cqlib.circuit import StandardGate -from cqlib.device import ( - NoiseModel, - OperationKey, - ReadoutError, - SingleQubitNoise, - TwoQubitNoise, -) -``` - ---- - -## SingleQubitNoise - -### 静态构造方法 - -- `bit_flip(p) -> SingleQubitNoise` -- `phase_flip(p) -> SingleQubitNoise` -- `pauli(px, py, pz) -> SingleQubitNoise` -- `depolarizing(p) -> SingleQubitNoise` -- `amplitude_damping(gamma) -> SingleQubitNoise` -- `phase_damping(lambda_) -> SingleQubitNoise` - -### 方法与属性 - -- `is_valid() -> bool` -- `to_kraus() -> list[numpy.ndarray]` -- `kind -> str` - -## TwoQubitNoise - -### 静态构造方法 - -- `depolarizing(p) -> TwoQubitNoise` -- `independent(q0_noise, q1_noise) -> TwoQubitNoise` -- `correlated_pauli(op_q0, op_q1, p) -> TwoQubitNoise` - -`correlated_pauli` 的 `op_q0/op_q1` 仅接受 `I/X/Y/Z`。 - -异常情况: - -- `ValueError`:泡利字符非法。 - -### 方法与属性 - -- `is_valid() -> bool` -- `to_kraus() -> list[numpy.ndarray]` -- `kind -> str` - -## ReadoutError - -### `ReadoutError(p_0_given_1, p_1_given_0)` - -属性: - -- `p_0_given_1 -> float` -- `p_1_given_0 -> float` - -方法: - -- `is_valid() -> bool` - -## OperationKey - -### 静态构造方法 - -- `new_single(gate, q0) -> OperationKey` -- `new_double(gate, q0, q1) -> OperationKey` -- `new_triple(gate, q0, q1, q2) -> OperationKey` - -异常情况: - -- `ValueError`:多比特门的比特有重复(qubit collision)或参数非法。 - -### 属性 - -- `gate -> StandardGate` -- `qubits -> list[int]` - -说明: - -- 实现了 `__eq__` 与 `__hash__`,可作为字典键使用。 - -## NoiseModel - -### `NoiseModel()` - -### 写入方法 - -- `add_readout_error(qubit, error) -> None` -- `add_single_qubit_error(gate, qubit, noise) -> None` -- `add_two_qubit_error(gate, q0, q1, noise) -> None` - -异常情况: - -- `ValueError`:噪声参数非法、比特冲突等。 - -### 查询方法 - -- `get_readout_error(qubit) -> ReadoutError | None` -- `get_single_qubit_errors(key) -> list[SingleQubitNoise] | None` -- `get_two_qubit_errors(key) -> list[TwoQubitNoise] | None` - -## 示例 - -```python -from cqlib.circuit import StandardGate -from cqlib.device import ( - NoiseModel, - OperationKey, - ReadoutError, - SingleQubitNoise, - TwoQubitNoise, -) - -nm = NoiseModel() -nm.add_readout_error(0, ReadoutError(0.1, 0.2)) -nm.add_single_qubit_error(StandardGate.X, 0, SingleQubitNoise.bit_flip(0.01)) -nm.add_two_qubit_error(StandardGate.CX, 0, 1, TwoQubitNoise.depolarizing(0.02)) - -skey = OperationKey.new_single(StandardGate.X, 0) -tkey = OperationKey.new_double(StandardGate.CX, 0, 1) - -print(nm.get_readout_error(0)) -print(nm.get_single_qubit_errors(skey)) -print(nm.get_two_qubit_errors(tkey)) -``` - diff --git a/docs/api/python/2_device/5_result.md b/docs/api/python/2_device/5_result.md deleted file mode 100644 index c1ce94ad..00000000 --- a/docs/api/python/2_device/5_result.md +++ /dev/null @@ -1,115 +0,0 @@ -# Outcome / Status / ExecutionResult - -本页覆盖量子任务结果相关 API: - -- `Outcome` -- `Status` -- `ExecutionResult` - -## 导入 - -```python -from cqlib.device import ExecutionResult, Outcome, Status -``` - ---- - -## Outcome - -### `Outcome(bitstring)` - -从二进制字符串构造测量结果对象。 - -异常情况: - -- `ValueError`:字符串含非法字符(非 `0/1`)。 - -### 静态方法 - -- `Outcome.from_bitstring(bitstring) -> Outcome` - -### 方法与属性 - -- `is_one(index) -> bool` -- `to_bitstring(num_qubits) -> str` -- `chunks -> list[int]` - -说明: - -- `Outcome` 支持 `==` 与 `hash(...)`。 - -## Status - -### 静态构造方法 - -- `Status.queued()` -- `Status.running()` -- `Status.completed()` -- `Status.failed(error_msg, error_code)` -- `Status.cancelled()` - -### 属性与方法 - -- `kind -> str`(`queued/running/completed/failed/cancelled`) -- `error_msg -> str | None` -- `error_code -> int | None` -- `is_terminal() -> bool` -- `is_success() -> bool` - -## ExecutionResult - -### `ExecutionResult(task_id, qubits, shots, num_qubits, backend=None)` - -参数: - -- `task_id` (`str`) -- `qubits` (`list[int]`) -- `shots` (`int`) -- `num_qubits` (`int`) -- `backend` (`str | None`) - -### 生命周期方法 - -- `start() -> None` -- `finish(counts) -> None` -- `fail(msg, code) -> None` -- `cancel() -> None` -- `calc_probabilities() -> None` - -`finish(counts)` 中 `counts` 类型为 `dict[str, int]`,key 必须是合法二进制比特串。 - -异常情况: - -- `ValueError`:`counts` 的比特串非法。 - -### 属性 - -- `task_id -> str` -- `shots -> int` -- `num_qubits -> int` -- `qubits -> list[int]` -- `status -> Status` -- `created_at -> str` -- `started_at -> str | None` -- `finished_at -> str | None` -- `backend -> str | None` -- `counts -> dict[str, int]` -- `probabilities -> dict[str, float] | None` - -## 示例 - -```python -from cqlib.device import ExecutionResult - -result = ExecutionResult("task-1", [0, 1], 100, 2, "sim") -print(result.status.kind) # queued - -result.start() -result.finish({"00": 60, "11": 40}) -result.calc_probabilities() - -print(result.status.kind) # completed -print(result.counts) # {'00': 60, '11': 40} -print(result.probabilities) # {'00': 0.6, '11': 0.4} -``` - diff --git a/docs/api/rust/0_circuit/12_cfg.md b/docs/api/rust/0_circuit/12_cfg.md deleted file mode 100644 index ca273b5b..00000000 --- a/docs/api/rust/0_circuit/12_cfg.md +++ /dev/null @@ -1,220 +0,0 @@ -# CircuitCFG - -`cqlib_core::circuit::CircuitCFG` - -```rust -use cqlib_core::circuit::CircuitCFG; -``` - -`CircuitCFG` 是 Rust core 中用于表示量子线路控制流图(Control Flow Graph, CFG)的分析视图。与 `Circuit` 面向用户和结构化线路构造不同,`CircuitCFG` 更偏向编译器内部使用,用于分析、重写和验证包含结构化控制流的线路。 - ---- - -## 核心概念 - -| 类型 | 说明 | -| --- | --- | -| `CircuitCFG` | 线路的控制流图视图,包含基本块、控制流边、入口块、量子比特和经典数据表等信息。 | -| `BasicBlock` | 基本块,保存一段顺序执行的 `Operation`,并可带有一个终结符。 | -| `Terminator` | 基本块末尾的控制转移描述,例如顺序跳转、分支、循环退出等。 | -| `FlowEdge` | 控制流图中的边类型,用于描述块与块之间的执行流关系。 | -| `ControlFlowRegion` | 结构化控制流区域元数据,用于记录某个分支或循环对应的结构化区域。 | -| `OperationMetadata` | 操作级元数据,可用于记录 pass 分析或转换过程中需要保留的信息。 | -| `SwitchRegionCase` | `switch` 控制流区域中的 case 元数据。 | - ---- - -## `BasicBlock` - -`BasicBlock` 表示一段顺序执行的操作序列。 - -常用方法如下: - -| 方法 | 说明 | -| --- | --- | -| `new()` | 创建一个空基本块。 | -| `with_label(label)` | 创建或设置带标签的基本块。 | -| `push_operation(op)` | 向基本块末尾添加一条操作。 | -| `extend_operations(ops)` | 批量追加操作。 | -| `set_terminator(terminator)` | 设置基本块的终结符。 | -| `is_empty()` | 判断基本块是否既无操作也无终结符。 | -| `has_terminator()` | 判断基本块是否已经设置终结符。 | -| `len()` | 返回基本块中的操作数量。 | -| `label()` | 读取基本块标签。 | -| `terminator()` | 读取基本块终结符。 | -| `operations()` | 读取基本块中的操作切片。 | - ---- - -## 创建 `CircuitCFG` - -`CircuitCFG` 提供以下创建接口: - -```rust -pub fn new(num_qubits: usize) -> Self -pub fn from_qubits(qubits: Vec) -> Self -pub fn from_circuit(circuit: &Circuit) -> Result -``` - -| 接口 | 说明 | -| --- | --- | -| `CircuitCFG::new(num_qubits)` | 创建包含连续逻辑量子比特的空 CFG。 | -| `CircuitCFG::from_qubits(qubits)` | 根据指定逻辑量子比特集合创建空 CFG,适合稀疏逻辑编号。 | -| `CircuitCFG::from_circuit(circuit)` | 从已有结构化 `Circuit` 构造 CFG,是最常用的入口。 | - -```rust -use cqlib_core::circuit::{Circuit, CircuitCFG, Qubit}; - -let mut circuit = Circuit::new(2); -circuit.h(Qubit::new(0))?; - -let cfg = CircuitCFG::from_circuit(&circuit)?; - -assert_eq!(cfg.num_qubits(), 2); - -# Ok::<(), cqlib_core::circuit::CircuitError>(()) -``` - ---- - -## 图编辑接口 - -`CircuitCFG` 提供基本的图编辑能力,用于添加基本块、添加边、设置入口块和维护结构化控制流区域元数据。 - -| 方法 | 说明 | -| --- | --- | -| `add_block(block)` | 添加一个 `BasicBlock`,并返回对应的 `NodeIndex`。 | -| `add_edge(from, to, edge)` | 在两个基本块之间添加控制流边。 | -| `entry_block()` | 读取当前 CFG 的入口基本块。 | -| `set_entry_block(index)` | 设置 CFG 的入口基本块。 | -| `set_control_flow_region(branch_block, region)` | 为某个分支或控制流入口块设置结构化区域元数据。 | -| `control_flow_region(branch_block)` | 读取某个基本块关联的结构化控制流区域元数据。 | -| `is_loop_header(block)` | 判断某个基本块是否为循环头。 | -| `block_mut(index)` | 可变读取指定基本块。 | -| `outgoing_edges(index)` | 读取指定基本块的出边。 | - ---- - -## 查询接口 - -`CircuitCFG` 提供以下查询接口,用于遍历图结构和读取线路基础信息。 - -| 方法 | 说明 | -| --- | --- | -| `blocks()` | 遍历 CFG 中的基本块。 | -| `num_blocks()` | 返回基本块数量。 | -| `num_qubits()` | 返回量子比特数量。 | -| `qubits()` | 返回 CFG 中的逻辑量子比特列表。 | -| `classical_vars()` | 返回经典变量类型表。 | -| `classical_values()` | 返回经典值类型表。 | - ---- - -## 验证与重建 - -`CircuitCFG` 提供两个关键接口用于检查和还原图结构: - -```rust -pub fn validate(&self) -> Result<(), CircuitError> -pub fn to_circuit(&self) -> Result -``` - -### `validate()` - -`validate()` 用于检查 CFG 结构是否自洽。典型检查内容包括: - -- 是否存在有效入口块; -- 边引用的基本块是否存在; -- 基本块终结符与出边数量、出边类型是否匹配; -- 控制流区域元数据是否与图结构一致; -- 循环头、循环回边和退出边是否满足约束; -- 经典变量和值的作用域和依赖关系是否可被正确解释; -- `break` / `continue` 等跳转是否处于合法区域内。 - -### `to_circuit()` - -`to_circuit()` 用于将 CFG 重新还原为结构化 `Circuit`。它要求 CFG 不仅是一个合法图,还必须能够对应回 Cqlib 支持的结构化控制流形式。 - -如果 CFG 的图结构已经被破坏,或某些控制流区域无法映射回结构化 `if`、`while`、`for`、`switch` 等结构,`to_circuit()` 会返回错误。 - -建议工作流如下: - -```rust -let mut cfg = CircuitCFG::from_circuit(&circuit)?; - -// 在这里执行 CFG 分析或转换 pass - -cfg.validate()?; -let new_circuit = cfg.to_circuit()?; -``` - ---- - -## `ControlFlowRegion` 元数据 - -`ControlFlowRegion` 用于记录结构化控制流区域的边界和语义。对于 `if`、`while`、`for`、`switch` 等结构,仅有 CFG 边并不足以完整恢复原始结构化语义,还需要区域元数据描述哪些基本块属于同一个控制流结构、入口和出口在哪里、switch case 如何对应等。 - -因此,编写 CFG pass 时需要特别注意: - -- 如果修改了分支结构,应同步更新对应的 `ControlFlowRegion`; -- 如果删除或合并了基本块,应检查区域元数据中是否仍引用旧 block; -- 如果调整循环边,应检查循环头和区域边界是否仍然正确; -- 如果修改 switch case,应同步维护 `SwitchRegionCase` 等元数据; -- 如果只是对基本块内部操作做局部优化,通常不需要修改区域元数据。 - -保持区域元数据与图边一致,是 `to_circuit()` 能否成功重建结构化线路的关键。 - ---- - -## 典型使用流程 - -### 1. 从结构化线路进入 CFG - -```rust -use cqlib_core::circuit::{Circuit, CircuitCFG, Qubit}; - -let mut circuit = Circuit::new(2); -circuit.h(Qubit::new(0))?; -circuit.cx(Qubit::new(0), Qubit::new(1))?; - -let cfg = CircuitCFG::from_circuit(&circuit)?; - -# Ok::<(), cqlib_core::circuit::CircuitError>(()) -``` - -### 2. 遍历基本块 - -```rust -for block in cfg.blocks() { - for op in block.operations() { - // 分析每条 Operation - } - - if block.has_terminator() { - // 分析控制转移 - } -} -``` - -### 3. 修改后验证并重建 - -```rust -cfg.validate()?; -let circuit = cfg.to_circuit()?; -``` - ---- - -## 适用的编译 pass 类型 - -`CircuitCFG` 适合用于需要显式控制流结构的分析和转换任务,例如: - -| pass 类型 | 说明 | -| --- | --- | -| 控制流可达性分析 | 检查不可达基本块、分支路径和循环结构。 | -| 分支内局部门优化 | 在每个基本块内部做门合并、门抵消或局部替换。 | -| 循环体资源估计 | 统计循环体内的门数量、深度或测量使用情况。 | -| 经典数据作用域检查 | 分析 `ClassicalValue` 是否越作用域使用。 | -| 动态线路合法性检查 | 检查控制流、测量和经典数据是否符合后端约束。 | -| 后端控制流降级 | 将结构化控制流转换为目标后端支持的形式。 | -| 分支展开或静态化 | 在条件可静态确定时,将控制流简化为线性操作。 | \ No newline at end of file diff --git a/docs/api/rust/0_overview.md b/docs/api/rust/0_overview.md deleted file mode 100644 index 03efc59f..00000000 --- a/docs/api/rust/0_overview.md +++ /dev/null @@ -1,48 +0,0 @@ -# Rust API 概览 - -## 欢迎查阅 Cqlib Rust API 参考手册! - ---- - -## 文档导航 - -### 量子电路(`cqlib_core::circuit`) - -- [Overview](0_circuit/0_overview.md) -- [Circuit](0_circuit/1_circuit.md) -- [Qubit](0_circuit/2_qubit.md) -- [Parameters](0_circuit/3_parameter.md) -- [Operations / Instructions](0_circuit/4_operation_instruction.md) -- [Standard Gates](0_circuit/5_gate_standard.md) -- [Unitary Gates](0_circuit/6_gate_unitary.md) -- [Multi-Controlled Gates](0_circuit/7_gate_mc_gate.md) -- [Circuit Gates](0_circuit/8_gate_circuit_gate.md) -- [Classical / Control Flow](0_circuit/9_classical_control_flow.md) -- [Symbolic Matrix](0_circuit/10_symbolic_matrix.md) -- [Ansatz](0_circuit/11_ansatz.md) -- [CFG](0_circuit/12_cfg.md) -- [Circuit to Matrix](0_circuit/13_circuit_to_matrix.md) - -### 中间表达(xxx) - -- xxxxxxx - -### 设备模块(xxx) - -- xxxxxxx - -### 量子信息(xxx) - -- xxxxxxx - -### 编译优化(xxx) - -- xxxxxxx - -### 可视化(xxx) - -- xxxxxxx - -### 错误缓解(xxx) - -- xxxxxxx diff --git a/docs/api/rust/1_ir/1_qcis.md b/docs/api/rust/1_ir/1_qcis.md deleted file mode 100644 index d3bf4906..00000000 --- a/docs/api/rust/1_ir/1_qcis.md +++ /dev/null @@ -1,77 +0,0 @@ -# QCIS API - -`cqlib_core::ir` 提供 QCIS 的导入与导出接口,用于在 `Circuit` 与 QCIS 文本/文件之间转换。 - -## 导入 - -```rust -use cqlib_core::ir::{qcis_load, qcis_loads, qcis_dump, qcis_dumps}; -``` - -## 接口一览 - -- `qcis_load(path: PathBuf) -> Circuit` -- `qcis_loads(qcis: &str) -> Result` -- `qcis_dump(circuit: &Circuit, path: &PathBuf) -> Result<(), QcisDumpError>` -- `qcis_dumps(circuit: &Circuit) -> Result` - -## 解析(load / loads) - -### `qcis_loads` - -从 QCIS 字符串解析 `Circuit`,失败时返回 `QcisParseError`。 - -常见错误类型: - -- `InvalidQubitFormat` -- `InvalidQubitId` -- `QubitCountMismatch` -- `ParameterCountMismatch` -- `MissingParameter` -- `InvalidParameter` -- `UnknownGate` - -### `qcis_load` - -从文件读取并解析。当前实现内部使用 `expect`,文件读取/解析失败会 panic;稳健调用建议优先使用 `qcis_loads` 自行处理错误。 - -## 导出(dump / dumps) - -### `qcis_dumps` - -将电路导出为 QCIS 字符串,失败返回 `QcisDumpError`。 - -常见错误类型: - -- `UnsupportedGate`:包含非 QCIS 原生门 -- `SymbolicParameter`:参数不能解析为数值 -- `IoError` - -### `qcis_dump` - -导出并写入文件,返回 `Result<(), QcisDumpError>`。 - -## QCIS 原生门限制 - -QCIS 导出仅支持原生门集。错误信息中当前列出的门包括: - -- `X2P`, `X2M`, `Y2P`, `Y2M`, `XY2P`, `XY2M` -- `CZ`, `RZ`, `I`, `X`, `Y`, `Z`, `H`, `S`, `SD`, `T`, `TD` -- `RX`, `RY`, `RXY` - -遇到非原生门时,应先分解/编译到 QCIS 门集后再导出。 - -## 最小示例 - -```rust -use cqlib_core::circuit::{Circuit, Qubit}; -use cqlib_core::ir::{qcis_dumps, qcis_loads}; - -let mut c = Circuit::new(2); -c.h(Qubit::new(0)).unwrap(); -c.cz(Qubit::new(0), Qubit::new(1)).unwrap(); - -let text = qcis_dumps(&c).unwrap(); -let c2 = qcis_loads(&text).unwrap(); -assert_eq!(c2.num_qubits(), 2); -``` diff --git a/docs/api/rust/1_ir/2_qasm2.md b/docs/api/rust/1_ir/2_qasm2.md deleted file mode 100644 index 6a5a3ec4..00000000 --- a/docs/api/rust/1_ir/2_qasm2.md +++ /dev/null @@ -1,77 +0,0 @@ -# OpenQASM 2.0 API - -`cqlib_core::ir` 提供 OpenQASM 2.0 的导入与导出接口。 - -## 导入 - -```rust -use cqlib_core::ir::{qasm2_load, qasm2_loads, qasm2_dump, qasm2_dumps}; -``` - -## 接口一览 - -- `qasm2_load(path) -> Result` -- `qasm2_loads(source: &str) -> Result` -- `qasm2_dump(circuit: &Circuit, path) -> io::Result<()>` -- `qasm2_dumps(circuit: &Circuit) -> Result` - -## 解析(load / loads) - -### `qasm2_loads` - -从 QASM 字符串解析电路。 - -### `qasm2_load` - -从文件读取并解析电路。 - -两者失败时返回 `QasmParseError`,常见变体: - -- `IoError` -- `ParseError` -- `ConversionError` -- `UndefinedQubit` -- `UndefinedRegister` -- `UndefinedGate` -- `InvalidArgument` -- `MismatchedQubitCount` -- `MismatchedParameterCount` -- `RecursionLimitExceeded` -- `EvaluationError` - -## 导出(dump / dumps) - -### `qasm2_dumps` - -将电路导出为 QASM 字符串。输出包含标准头部与 `qreg/creg` 声明。 - -### `qasm2_dump` - -将导出结果写入文件,返回 `io::Result<()>`。 - -## 支持能力(概览) - -根据当前实现,QASM2 导入导出支持: - -- 标准门与参数门 -- 测量、屏障、重置等指令 -- `CircuitGate` 导出定义 -- 部分扩展门定义输出(如 `crx/cry/rzz/rxx/ryy/rzx`) - -## 最小示例 - -```rust -use cqlib_core::ir::{qasm2_dumps, qasm2_loads}; - -let qasm = r#" -OPENQASM 2.0; -include "qelib1.inc"; -qreg q[2]; -h q[0]; -cx q[0],q[1]; -"#; - -let c = qasm2_loads(qasm).unwrap(); -let out = qasm2_dumps(&c).unwrap(); -assert!(out.contains("OPENQASM 2.0;")); -``` diff --git a/docs/api/rust/2_device/1_topology.md b/docs/api/rust/2_device/1_topology.md deleted file mode 100644 index 9d177042..00000000 --- a/docs/api/rust/2_device/1_topology.md +++ /dev/null @@ -1,73 +0,0 @@ -# Topology - -`Topology` 是 `cqlib_core::device` 中的硬件连接图结构,使用有向图表示比特耦合关系。 - -## 导入 - -```rust -use cqlib_core::circuit::Qubit; -use cqlib_core::device::{Topology, TopologyError}; -``` - -## 构造 - -### `Topology::new(qubits, coupling_map) -> Topology` - -参数: - -- `qubits: Vec` -- `coupling_map: Vec<(Qubit, Qubit, String)>` - -说明: - -- 边按有向语义处理,`(q0, q1)` 与 `(q1, q0)` 不等价。 - -## 只读接口 - -- `graph(&self) -> &StableGraph` -- `num_qubits(&self) -> usize` -- `num_couplings(&self) -> usize` -- `qubits(&self) -> impl Iterator` -- `is_connected(&self, control: Qubit, target: Qubit) -> bool` -- `neighbors(&self, qubit: Qubit) -> impl Iterator` -- `get_coupling_name(&self, control: Qubit, target: Qubit) -> Option` -- `contains_qubit(&self, qubit: &Qubit) -> bool` -- `degree(&self, qubit: &Qubit) -> usize` - -## 修改接口 - -- `add_qubits(&mut self, qubits) -> Result<(), TopologyError>` -- `add_couplings(&mut self, couplings) -> Result<(), TopologyError>` -- `remove_qubits(&mut self, qubits) -> Result<(), TopologyError>` -- `remove_couplings(&mut self, couplings) -> Result<(), TopologyError>` - -常见错误: - -- `TopologyError::QubitNotFound` -- `TopologyError::QubitAlreadyExists` -- `TopologyError::CouplingNotFound` -- `TopologyError::CouplingAlreadyExists` - -## 示例 - -```rust -use cqlib_core::circuit::Qubit; -use cqlib_core::device::Topology; - -let q0 = Qubit::new(0); -let q1 = Qubit::new(1); -let q2 = Qubit::new(2); - -let mut topo = Topology::new( - vec![q0, q1, q2], - vec![(q0, q1, "CX".to_string()), (q1, q2, "CZ".to_string())], -); - -assert_eq!(topo.num_qubits(), 3); -assert!(topo.is_connected(q1, q2)); -assert_eq!(topo.get_coupling_name(q1, q2).as_deref(), Some("CZ")); -assert_eq!(topo.neighbors(q1).collect::>(), vec![q2]); - -topo.add_qubits([Qubit::new(3)]).unwrap(); -topo.add_couplings([(q2, Qubit::new(3), "CX".to_string())]).unwrap(); -``` diff --git a/docs/api/rust/2_device/2_properties_device.md b/docs/api/rust/2_device/2_properties_device.md deleted file mode 100644 index ee43a8d2..00000000 --- a/docs/api/rust/2_device/2_properties_device.md +++ /dev/null @@ -1,144 +0,0 @@ -# Device / Properties - -本页覆盖 `cqlib_core::device` 中用于设备属性建模的类型: - -- `InstructionProp` -- `QubitProp` -- `EdgeProp` -- `Device` - -## 导入 - -```rust -use cqlib_core::circuit::{Instruction, Qubit, StandardGate}; -use cqlib_core::device::{Device, DeviceError, EdgeProp, InstructionProp, QubitProp, Topology}; -use time::OffsetDateTime; -``` - -## InstructionProp - -### 构造与方法 - -- `InstructionProp::new(instruction: Instruction, error_rate: f64) -> InstructionProp` -- `with_length(self, length: f64) -> InstructionProp` - -### 只读接口 - -- `instruction(&self) -> &Instruction` -- `error_rate(&self) -> f64` -- `length(&self) -> Option` - -## QubitProp - -### 构造与 Builder - -- `QubitProp::new(readout_error: f64) -> QubitProp` -- `with_prob_meas0_prep1(self, prob: f64) -> QubitProp` -- `with_prob_meas1_prep0(self, prob: f64) -> QubitProp` -- `with_t1(self, t1: f64) -> QubitProp` -- `with_t2(self, t2: f64) -> QubitProp` -- `with_frequency(self, frequency: f64) -> QubitProp` -- `with_native_instruction(self, prop: InstructionProp) -> QubitProp` - -### 只读接口 - -- `readout_error(&self) -> f64` -- `t1(&self) -> Option` -- `t2(&self) -> Option` -- `frequency(&self) -> Option` -- `native_instructions(&self) -> &[InstructionProp]` - -## EdgeProp - -### 构造与方法 - -- `EdgeProp::new() -> EdgeProp` -- `with_native_instruction(self, prop: InstructionProp) -> EdgeProp` -- `native_instructions(&self) -> &[InstructionProp]` - -## Device - -### 构造 - -- `Device::new(name: String, topology: Topology) -> Device` - -### 配置接口(builder) - -- `with_native_gates(self, gates: Vec) -> Device` -- `with_calibration_time(self, time: OffsetDateTime) -> Device` -- `with_default_t1(self, t1: f64) -> Device` -- `with_default_t2(self, t2: f64) -> Device` -- `with_default_readout_error(self, error: f64) -> Device` -- `with_default_single_qubit_error(self, error: f64) -> Device` -- `with_default_two_qubit_error(self, error: f64) -> Device` - -### 写入接口 - -- `add_qubit_properties(&mut self, qubit: Qubit, props: QubitProp) -> Result<(), DeviceError>` -- `add_edge_properties(&mut self, control: Qubit, target: Qubit, props: EdgeProp) -> Result<(), DeviceError>` - -常见错误: - -- `DeviceError::QubitNotInTopology` -- `DeviceError::EdgeNotInTopology` - -### 查询接口 - -- `name(&self) -> &str` -- `qubits(&self) -> impl Iterator` -- `invalid_qubits(&self) -> impl Iterator` -- `topology(&self) -> &Topology` -- `native_gates(&self) -> &[Instruction]` -- `qubit_properties(&self, qubit: Qubit) -> Option<&QubitProp>` -- `edge_properties(&self, control: Qubit, target: Qubit) -> Option<&EdgeProp>` -- `get_t1(&self, qubit: Qubit) -> Option` -- `get_t2(&self, qubit: Qubit) -> Option` -- `get_readout_error(&self, qubit: Qubit) -> Option` -- `default_single_qubit_error(&self) -> Option` -- `default_two_qubit_error(&self) -> Option` - -说明: - -- `get_t1/get_t2/get_readout_error` 会优先读取局部属性,缺失时回退默认值。 - -## 示例 - -```rust -use cqlib_core::circuit::{Instruction, Qubit, StandardGate}; -use cqlib_core::device::{Device, EdgeProp, InstructionProp, QubitProp, Topology}; - -let q0 = Qubit::new(0); -let q1 = Qubit::new(1); -let q2 = Qubit::new(2); - -let topo = Topology::new( - vec![q0, q1, q2], - vec![(q0, q1, "CX".to_string()), (q1, q2, "CX".to_string())], -); - -let mut device = Device::new("mock_backend".to_string(), topo) - .with_default_t1(50.0) - .with_default_t2(35.0) - .with_default_readout_error(0.05) - .with_native_gates(vec![ - Instruction::from(StandardGate::X), - Instruction::from(StandardGate::CX), - ]); - -device - .add_qubit_properties(q0, QubitProp::new(0.02).with_t1(80.0).with_t2(70.0)) - .unwrap(); - -device - .add_edge_properties( - q0, - q1, - EdgeProp::new().with_native_instruction( - InstructionProp::new(Instruction::from(StandardGate::CX), 0.01).with_length(220.0), - ), - ) - .unwrap(); - -assert_eq!(device.get_t1(q0), Some(80.0)); -assert_eq!(device.get_t1(q2), Some(50.0)); // 回退默认值 -``` diff --git a/docs/api/rust/2_device/3_layout.md b/docs/api/rust/2_device/3_layout.md deleted file mode 100644 index e8fa5a0a..00000000 --- a/docs/api/rust/2_device/3_layout.md +++ /dev/null @@ -1,79 +0,0 @@ -# Layout - -`Layout` 用于维护路由过程中的逻辑比特与物理比特映射关系。 - -## 导入 - -```rust -use cqlib_core::circuit::Qubit; -use cqlib_core::device::{Layout, LayoutError}; -use std::collections::HashMap; -``` - -## 构造 - -### `Layout::new(logical, physical, init_map) -> Result` - -参数: - -- `logical: Vec` -- `physical: Vec` -- `init_map: Option>`(逻辑 -> 物理) - -常见错误: - -- `LayoutError::TooManyLogicalQubits` -- `LayoutError::InvalidVirtualQubit` -- `LayoutError::InvalidPhysicalQubit` -- `LayoutError::DuplicatePhysicalMapping` - -## 只读接口 - -- `num_logical(&self) -> usize` -- `num_ancilla(&self) -> usize` -- `num_physical(&self) -> usize` -- `get_physical(&self, virtual_id: Qubit) -> Option` -- `get_virtual(&self, physical_id: Qubit) -> Option` -- `logical_qubits(&self) -> impl Iterator` -- `ancilla_qubits(&self) -> impl Iterator` -- `physical_qubits(&self) -> impl Iterator` -- `v2p_map(&self) -> &HashMap` -- `p2v_map(&self) -> &HashMap` - -## 更新接口 - -### `swap_physical(&mut self, phys_a: Qubit, phys_b: Qubit)` - -交换两个物理比特上的虚拟比特映射。 - -注意: - -- 若 `phys_a/phys_b` 不在布局物理集合内,当前实现会触发 `panic!`(断言失败)。 - -## 示例 - -```rust -use cqlib_core::circuit::Qubit; -use cqlib_core::device::Layout; -use std::collections::HashMap; - -let logical = vec![Qubit::new(0), Qubit::new(1)]; -let physical = vec![Qubit::new(10), Qubit::new(11), Qubit::new(12)]; - -let mut init = HashMap::new(); -init.insert(Qubit::new(0), Qubit::new(11)); - -let mut layout = Layout::new(logical, physical, Some(init)).unwrap(); -assert_eq!(layout.num_logical(), 2); -assert_eq!(layout.num_physical(), 3); - -let before_11 = layout.get_virtual(Qubit::new(11)); -let before_12 = layout.get_virtual(Qubit::new(12)); -layout.swap_physical(Qubit::new(11), Qubit::new(12)); -let after_11 = layout.get_virtual(Qubit::new(11)); -let after_12 = layout.get_virtual(Qubit::new(12)); - -assert_eq!(before_11, after_12); -assert_eq!(before_12, after_11); -``` - diff --git a/docs/api/rust/2_device/5_result.md b/docs/api/rust/2_device/5_result.md deleted file mode 100644 index 8befe567..00000000 --- a/docs/api/rust/2_device/5_result.md +++ /dev/null @@ -1,120 +0,0 @@ -# Outcome / Status / ExecutionResult - -本页覆盖量子任务结果相关类型: - -- `Outcome` -- `Status` -- `ExecutionResult` -- `OutcomeError` - -## 导入 - -```rust -use cqlib_core::circuit::Qubit; -use cqlib_core::device::{ExecutionResult, Outcome, OutcomeError, Status}; -use std::collections::HashMap; -use time::OffsetDateTime; -``` - -## OutcomeError - -当前包含: - -- `OutcomeError::InvalidCharacter(index, ch)`:比特串含非法字符。 - -## Outcome - -### 构造与转换 - -- `Outcome::new(chunks: SmallVec<[u64; 4]>) -> Outcome` -- `Outcome::from_bitstring(s: &str) -> Result` -- `to_string(&self, num_qubits: usize) -> String` - -### 查询 - -- `is_one(&self, index: usize) -> bool` - -说明: - -- `Outcome` 实现了 `Clone + Eq + Hash`,可直接作为计数字典 `HashMap` 的 key。 - -## Status - -枚举变体: - -- `Queued` -- `Running` -- `Completed` -- `Failed { error_msg: String, error_code: i32 }` -- `Cancelled` - -方法: - -- `is_terminal(&self) -> bool` -- `is_success(&self) -> bool` - -## ExecutionResult - -### 构造 - -```rust -ExecutionResult::new( - task_id: String, - qubits: Vec, - shots: usize, - num_qubits: usize, - backend: Option, - created_at: Option, -) -> ExecutionResult -``` - -### 生命周期方法 - -- `start(&mut self, t: Option) -> &mut Self` -- `finish(&mut self, counts: HashMap, t: Option) -> &mut Self` -- `fail(&mut self, msg: String, code: i32)` -- `cancel(&mut self)` -- `calc_probabilities(&mut self) -> &mut Self` - -### 读取方法 - -- `task_id(&self) -> &str` -- `shots(&self) -> usize` -- `num_qubits(&self) -> usize` -- `qubits(&self) -> &Vec` -- `status(&self) -> &Status` -- `created_at(&self) -> &OffsetDateTime` -- `started_at(&self) -> &Option` -- `finished_at(&self) -> &Option` -- `backend(&self) -> Option<&String>` -- `counts(&self) -> &HashMap` -- `probabilities(&self) -> &Option>` - -## 示例 - -```rust -use cqlib_core::circuit::Qubit; -use cqlib_core::device::{ExecutionResult, Outcome, Status}; -use std::collections::HashMap; - -let mut result = ExecutionResult::new( - "task-1".to_string(), - vec![Qubit::new(0), Qubit::new(1)], - 100, - 2, - Some("sim".to_string()), - None, -); - -assert!(matches!(result.status(), Status::Queued)); -result.start(None); -assert!(matches!(result.status(), Status::Running)); - -let mut counts = HashMap::new(); -counts.insert(Outcome::from_bitstring("00").unwrap(), 60); -counts.insert(Outcome::from_bitstring("11").unwrap(), 40); -result.finish(counts, None).calc_probabilities(); - -assert!(matches!(result.status(), Status::Completed)); -assert!(result.probabilities().is_some()); -``` diff --git a/docs/cn/index.rst b/docs/cn/index.rst new file mode 100644 index 00000000..bda7d0e3 --- /dev/null +++ b/docs/cn/index.rst @@ -0,0 +1,297 @@ +Cqlib 文档 +========== + +.. toctree:: + :maxdepth: 2 + :caption: 快速开始 + + ../documentation/cn/0_get_started/0_introduction + ../documentation/cn/0_get_started/1_installation + ../documentation/cn/0_get_started/2_quickstart + + +.. toctree:: + :maxdepth: 2 + :caption: 量子线路 + + ../documentation/cn/1_cqlib/0_circuit/0_overview + ../documentation/cn/1_cqlib/0_circuit/1_gates + ../documentation/cn/1_cqlib/0_circuit/2_structures + ../documentation/cn/1_cqlib/0_circuit/3_parameters + ../documentation/cn/1_cqlib/0_circuit/4_circuit_analysis + ../documentation/cn/1_cqlib/0_circuit/5_control_flow + + +.. toctree:: + :maxdepth: 2 + :caption: 中间表示 + + ../documentation/cn/1_cqlib/1_ir/0_overview + ../documentation/cn/1_cqlib/1_ir/1_qcis + ../documentation/cn/1_cqlib/1_ir/2_qasm2 + ../documentation/cn/1_cqlib/1_ir/3_qasm3 + ../documentation/cn/1_cqlib/1_ir/4_conversion_workflow + + +.. toctree:: + :maxdepth: 2 + :caption: 设备与后端 + + ../documentation/cn/1_cqlib/2_device/0_overview + ../documentation/cn/1_cqlib/2_device/1_topology + ../documentation/cn/1_cqlib/2_device/2_device + ../documentation/cn/1_cqlib/2_device/3_layout + ../documentation/cn/1_cqlib/2_device/4_noise + ../documentation/cn/1_cqlib/2_device/5_result + + +.. toctree:: + :maxdepth: 2 + :caption: 量子信息 + + ../documentation/cn/1_cqlib/3_qis/0_overview + ../documentation/cn/1_cqlib/3_qis/1_statevector + ../documentation/cn/1_cqlib/3_qis/2_density_matrix + ../documentation/cn/1_cqlib/3_qis/3_density_matrix_noise + ../documentation/cn/1_cqlib/3_qis/4_stabilizer + ../documentation/cn/1_cqlib/3_qis/5_pauli_and_hamiltonian + ../documentation/cn/1_cqlib/3_qis/6_metrics_entropy + + +.. toctree:: + :maxdepth: 2 + :caption: 编译优化 + + ../documentation/cn/1_cqlib/4_compiler/0_overview + ../documentation/cn/1_cqlib/4_compiler/1_layout + ../documentation/cn/1_cqlib/4_compiler/2_sabre_mapping + ../documentation/cn/1_cqlib/4_compiler/3_template_optimization + ../documentation/cn/1_cqlib/4_compiler/4_commutative_and_clifford + + +.. toctree:: + :maxdepth: 2 + :caption: 可视化 + + ../documentation/cn/1_cqlib/5_visualization/0_overview + ../documentation/cn/1_cqlib/5_visualization/1_draw_text + ../documentation/cn/1_cqlib/5_visualization/2_draw_figure + ../documentation/cn/1_cqlib/5_visualization/3_notebook_and_docs + ../documentation/cn/1_cqlib/5_visualization/4_visualization_practices + ../documentation/cn/1_cqlib/5_visualization/5_control_flow_and_special + ../documentation/cn/1_cqlib/5_visualization/6_result_visualization + ../documentation/cn/1_cqlib/5_visualization/7_state_visualization + + +.. toctree:: + :maxdepth: 2 + :caption: 错误缓解 + + ../documentation/cn/1_cqlib/6_error_mitigation/0_overview + ../documentation/cn/1_cqlib/6_error_mitigation/1_zne + ../documentation/cn/1_cqlib/6_error_mitigation/2_virtual_distillation + ../documentation/cn/1_cqlib/6_error_mitigation/3_unified_api + + +.. toctree:: + :maxdepth: 2 + :caption: 天衍平台 + + ../documentation/cn/1_cqlib/7_tianyan/0_overview + ../documentation/cn/1_cqlib/7_tianyan/1_auth_config + ../documentation/cn/1_cqlib/7_tianyan/2_backend_device + ../documentation/cn/1_cqlib/7_tianyan/3_task_result + ../documentation/cn/1_cqlib/7_tianyan/4_qcis_ir_workflow + ../documentation/cn/1_cqlib/7_tianyan/5_readout_mitigation + + +.. toctree:: + :maxdepth: 1 + :caption: Python API 参考 + + ../api/cn/python/0_overview + ../api/cn/python/0_circuit/0_overview + ../api/cn/python/0_circuit/1_circuit + ../api/cn/python/0_circuit/2_qubit + ../api/cn/python/0_circuit/3_parameter + ../api/cn/python/0_circuit/4_operation_instruction + ../api/cn/python/0_circuit/5_gates_standard + ../api/cn/python/0_circuit/6_gates_unitary + ../api/cn/python/0_circuit/7_gates_mc_gate + ../api/cn/python/0_circuit/8_gates_circuit_gate + ../api/cn/python/0_circuit/9_classical_control_flow + ../api/cn/python/0_circuit/10_symbolic_matrix + ../api/cn/python/0_circuit/11_ansatz + ../api/cn/python/0_circuit/12_circuit_to_matrix + ../api/cn/python/0_circuit/13_circuit_dag + ../api/cn/python/1_ir/0_overview + ../api/cn/python/1_ir/1_qcis + ../api/cn/python/1_ir/2_qasm2 + ../api/cn/python/1_ir/3_qasm3 + ../api/cn/python/2_device/0_overview + ../api/cn/python/2_device/1_topology + ../api/cn/python/2_device/2_properties_device + ../api/cn/python/2_device/3_layout + ../api/cn/python/2_device/4_noise + ../api/cn/python/2_device/5_result + ../api/cn/python/2_device/6_qubits + ../api/cn/python/3_qis/0_overview + ../api/cn/python/3_qis/1_statevector + ../api/cn/python/3_qis/2_density_matrix + ../api/cn/python/3_qis/3_density_matrix_noise + ../api/cn/python/3_qis/4_stabilizer + ../api/cn/python/3_qis/5_classical_state + ../api/cn/python/3_qis/6_pauli + ../api/cn/python/3_qis/7_hamiltonian + ../api/cn/python/3_qis/8_evolution + ../api/cn/python/3_qis/9_metrics_entropy + ../api/cn/python/4_compile/0_overview + ../api/cn/python/4_compile/1_compiler + ../api/cn/python/4_compile/2_transform + ../api/cn/python/4_compile/3_layout + ../api/cn/python/4_compile/4_routing + ../api/cn/python/4_compile/5_sabre + ../api/cn/python/4_compile/6_decompose_resynthesis + ../api/cn/python/4_compile/7_knowledge + ../api/cn/python/4_compile/8_resource + ../api/cn/python/4_compile/9_commutation + ../api/cn/python/5_visualization/0_overview + ../api/cn/python/5_visualization/1_draw_text + ../api/cn/python/5_visualization/2_draw_figure + ../api/cn/python/5_visualization/3_state_plots + ../api/cn/python/5_visualization/4_result_plots + ../api/cn/python/5_visualization/5_render_to_file + ../api/cn/python/6_error_mitigation/0_overview + ../api/cn/python/6_error_mitigation/1_zne + ../api/cn/python/6_error_mitigation/2_virtual_distillation + ../api/cn/python/6_error_mitigation/3_unified + + +.. toctree:: + :maxdepth: 1 + :caption: Rust API 参考 + + ../api/cn/rust/0_overview + ../api/cn/rust/0_circuit/0_overview + ../api/cn/rust/0_circuit/1_circuit + ../api/cn/rust/0_circuit/2_qubit + ../api/cn/rust/0_circuit/3_parameter + ../api/cn/rust/0_circuit/4_operation_instruction + ../api/cn/rust/0_circuit/5_gate_standard + ../api/cn/rust/0_circuit/6_gate_unitary + ../api/cn/rust/0_circuit/7_gate_mc_gate + ../api/cn/rust/0_circuit/8_gate_circuit_gate + ../api/cn/rust/0_circuit/9_classical_control_flow + ../api/cn/rust/0_circuit/10_symbolic_matrix + ../api/cn/rust/0_circuit/11_ansatz + ../api/cn/rust/0_circuit/12_cfg + ../api/cn/rust/0_circuit/13_circuit_to_matrix + ../api/cn/rust/0_circuit/14_circuit_dag + ../api/cn/rust/1_ir/0_overview + ../api/cn/rust/1_ir/1_qcis + ../api/cn/rust/1_ir/2_qasm2 + ../api/cn/rust/1_ir/3_qasm3 + ../api/cn/rust/2_device/0_overview + ../api/cn/rust/2_device/1_topology + ../api/cn/rust/2_device/2_properties_device + ../api/cn/rust/2_device/3_layout + ../api/cn/rust/2_device/4_noise + ../api/cn/rust/2_device/5_result + ../api/cn/rust/2_device/6_qubits + ../api/cn/rust/3_qis/0_overview + ../api/cn/rust/3_qis/1_statevector + ../api/cn/rust/3_qis/2_density_matrix + ../api/cn/rust/3_qis/3_density_matrix_noise + ../api/cn/rust/3_qis/4_stabilizer + ../api/cn/rust/3_qis/5_classical_state + ../api/cn/rust/3_qis/6_pauli + ../api/cn/rust/3_qis/7_hamiltonian + ../api/cn/rust/3_qis/8_evolution + ../api/cn/rust/3_qis/9_metrics_entropy + ../api/cn/rust/4_compile/0_overview + ../api/cn/rust/4_compile/1_compiler + ../api/cn/rust/4_compile/2_transform + ../api/cn/rust/4_compile/3_layout + ../api/cn/rust/4_compile/4_routing + ../api/cn/rust/4_compile/5_sabre + ../api/cn/rust/4_compile/6_decompose_resynthesis + ../api/cn/rust/4_compile/7_knowledge + ../api/cn/rust/4_compile/8_resource + ../api/cn/rust/4_compile/9_commutation + ../api/cn/rust/5_visualization/0_overview + ../api/cn/rust/5_visualization/1_draw_text + ../api/cn/rust/5_visualization/2_draw_figure + ../api/cn/rust/5_visualization/3_state_plots + ../api/cn/rust/5_visualization/4_result_plots + ../api/cn/rust/5_visualization/5_render_to_file + ../api/cn/rust/5_visualization/6_visual_ir + ../api/cn/rust/6_error_mitigation/0_overview + ../api/cn/rust/6_error_mitigation/1_zne + ../api/cn/rust/6_error_mitigation/2_virtual_distillation + ../api/cn/rust/6_error_mitigation/3_unified + + +.. toctree:: + :maxdepth: 1 + :caption: C API 参考 + + ../api/cn/c/0_overview + ../api/cn/c/0_circuit/0_overview + ../api/cn/c/0_circuit/1_circuit + ../api/cn/c/0_circuit/2_qubit + ../api/cn/c/0_circuit/3_parameter + ../api/cn/c/0_circuit/4_operation_instruction + ../api/cn/c/0_circuit/5_gate_standard + ../api/cn/c/0_circuit/6_gate_unitary + ../api/cn/c/0_circuit/7_gate_mc_gate + ../api/cn/c/0_circuit/8_gate_circuit_gate + ../api/cn/c/0_circuit/9_classical_control_flow + ../api/cn/c/0_circuit/10_symbolic_matrix + ../api/cn/c/0_circuit/11_ansatz + ../api/cn/c/0_circuit/12_cfg + ../api/cn/c/0_circuit/13_circuit_to_matrix + ../api/cn/c/0_circuit/14_circuit_dag + ../api/cn/c/1_ir/0_overview + ../api/cn/c/1_ir/1_qcis + ../api/cn/c/1_ir/2_qasm2 + ../api/cn/c/1_ir/3_qasm3 + ../api/cn/c/2_device/0_overview + ../api/cn/c/2_device/1_topology + ../api/cn/c/2_device/2_properties_device + ../api/cn/c/2_device/3_layout + ../api/cn/c/2_device/4_noise + ../api/cn/c/2_device/5_result + ../api/cn/c/2_device/6_qubits + ../api/cn/c/3_qis/0_overview + ../api/cn/c/3_qis/1_statevector + ../api/cn/c/3_qis/2_density_matrix + ../api/cn/c/3_qis/3_density_matrix_noise + ../api/cn/c/3_qis/4_stabilizer + ../api/cn/c/3_qis/5_classical_state + ../api/cn/c/3_qis/6_pauli + ../api/cn/c/3_qis/7_hamiltonian + ../api/cn/c/3_qis/8_evolution + ../api/cn/c/3_qis/9_metrics_entropy + ../api/cn/c/4_compile/0_overview + ../api/cn/c/4_compile/1_compiler + ../api/cn/c/4_compile/2_transform + ../api/cn/c/4_compile/3_layout + ../api/cn/c/4_compile/4_routing + ../api/cn/c/4_compile/5_sabre + ../api/cn/c/4_compile/6_decompose_resynthesis + ../api/cn/c/4_compile/7_knowledge + ../api/cn/c/4_compile/8_resource + ../api/cn/c/4_compile/9_commutation + ../api/cn/c/5_visualization/0_overview + ../api/cn/c/5_visualization/1_draw_text + ../api/cn/c/5_visualization/2_draw_figure + ../api/cn/c/5_visualization/3_state_plots + ../api/cn/c/5_visualization/4_result_plots + ../api/cn/c/5_visualization/5_render_to_file + ../api/cn/c/5_visualization/6_visual_ir + ../api/cn/c/6_error_mitigation/0_overview + ../api/cn/c/6_error_mitigation/1_zne + ../api/cn/c/6_error_mitigation/2_virtual_distillation + ../api/cn/c/6_error_mitigation/3_unified + + diff --git a/docs/documentation/0_get_started/0_introduction.md b/docs/documentation/cn/0_get_started/0_introduction.md similarity index 77% rename from docs/documentation/0_get_started/0_introduction.md rename to docs/documentation/cn/0_get_started/0_introduction.md index 965bf526..12e1f5ab 100644 --- a/docs/documentation/0_get_started/0_introduction.md +++ b/docs/documentation/cn/0_get_started/0_introduction.md @@ -2,9 +2,9 @@ ## 欢迎使用 Cqlib! -Cqlib 是由中电信量子集团自主研发、专为工程实践打造的量子计算软件开发工具包(SDK)。依托[“天衍”量子计算云平台](https://qc.zdxlz.com),Cqlib 提供了统一、稳定、高性能的量子线路编程接口与表示能力,实现真机算力接入与高效模拟计算服务的无缝转换。 +Cqlib 是由中电信量子集团自主研发、专为工程实践打造的量子计算软件开发工具包(SDK)。它提供了统一、稳定、高性能的量子线路编程接口与表示能力。 -本套说明文档涵盖[安装教程](1_installation.md)、[快速入门](2_quickstart.md)、[场景化指南](../1_cqlib/0_circuit/0_overview.md)及多种编程语言的 [API 参考](../../api/python/0_overview.md),致力于帮助您更顺畅地完成量子计算任务的构建、执行与可视化,全面提升开发效率与使用体验。 +本套说明文档涵盖[安装教程](1_installation.md)、[快速入门](2_quickstart.md)、[场景化指南](../1_cqlib/0_circuit/0_overview.md)及多种编程语言的 [API 参考](../../../api/cn/python/0_overview.md),致力于帮助您更顺畅地完成量子计算任务的构建、执行与可视化,全面提升开发效率与使用体验。 --- @@ -14,11 +14,11 @@ Cqlib 是由中电信量子集团自主研发、专为工程实践打造的量 - **极致性能,Rust 驱动** 核心层采用 Rust 语言构建,确保[线路编译](../1_cqlib/4_compiler/0_overview.md)与[中间表示](../1_cqlib/1_ir/0_overview.md)转换的高并发处理能力与内存安全性,为大规模量子线路模拟与复杂逻辑处理提供坚实底座。 -- **原生生态,天衍直连** - 深度集成国产自主量子指令集 [QCIS](../1_cqlib/1_ir/1_qcis.md),并与 [“天衍”量子计算云平台](https://qc.zdxlz.com) 无缝衔接,支持从本地开发到云端执行的灵活迁移。 +- **原生生态,QCIS 集成** + 深度集成国产自主量子指令集 [QCIS](../1_cqlib/1_ir/1_qcis.md),支持线路与 QCIS 文本之间的双向导入导出。 - **多语协同,开发友好** - 通过高度抽象的 [Python 接口](../../api/python/0_overview.md)与底层 [C 接口](../../api/c/0_overview.md)协同设计,兼顾算法研发效率与系统集成稳定性,支持跨平台、跨语言的混合编程场景。 + 通过高度抽象的 [Python 接口](../../../api/cn/python/0_overview.md)与底层 [C 接口](../../../api/cn/c/0_overview.md)协同设计,兼顾算法研发效率与系统集成稳定性,支持跨平台、跨语言的混合编程场景。 - **统一架构,灵活扩展** 提供统一的[中间表示](../1_cqlib/1_ir/0_overview.md),支持构建可维护、可扩展的量子软件系统,适配从原型验证到企业级应用开发的完整流程。 @@ -29,13 +29,13 @@ Cqlib 是由中电信量子集团自主研发、专为工程实践打造的量 Cqlib 采用 Rust、Python 和 C 三种语言,整体遵循“核心统一、接口分层”的设计思路,在保证能力一致性的同时兼顾不同开发场景的使用习惯: -- **[Rust](../../api/rust/0_overview.md)** +- **[Rust](../../../api/cn/rust/0_overview.md)** 作为核心实现层,承载量子线路、门与指令、参数系统、中间表示、设备建模、量子信息、错误缓解和编译优化等关键能力。 -- **[Python](../../api/python/0_overview.md)** +- **[Python](../../../api/cn/python/0_overview.md)** 面向算法开发与快速原型验证,提供更高效、直观的 Python 接口,便于交互式使用和快速迭代。 -- **[C](../../api/c/0_overview.md)** +- **[C](../../../api/cn/c/0_overview.md)** 面向系统集成与跨语言调用,提供稳定的 C 接口,便于在异构工程中落地部署。 --- @@ -51,7 +51,7 @@ Cqlib 的核心能力主要由以下八个模块组成: - [设备模块](../1_cqlib/2_device/0_overview.md) 用于描述量子设备相关信息,包括设备拓扑、逻辑比特到物理比特的映射、校准属性、噪声模型和执行结果等,为后端适配和噪声感知编译提供数据基础。 - [量子信息](../1_cqlib/3_qis/0_overview.md) - 提供量子态、算符和量子信息处理相关工具,包括状态向量、密度矩阵、稳定子模拟、Pauli 算符、哈密顿量、量子演化和信息度量等能力。 + 提供量子态、算符和量子信息处理相关工具,包括态矢量、密度矩阵、稳定子模拟、Pauli 算符、哈密顿量、量子演化和信息度量等能力。 - [编译优化](../1_cqlib/4_compiler/0_overview.md) 用于将高层量子线路转换为更适合目标设备执行的线路形式,支持门分解、拓扑布局、路由映射、模板匹配、交换优化和 Clifford 相关优化等功能。 - [可视化](../1_cqlib/5_visualization/0_overview.md) diff --git a/docs/documentation/0_get_started/1_installation.md b/docs/documentation/cn/0_get_started/1_installation.md similarity index 97% rename from docs/documentation/0_get_started/1_installation.md rename to docs/documentation/cn/0_get_started/1_installation.md index e9d4a90e..22fa4837 100644 --- a/docs/documentation/0_get_started/1_installation.md +++ b/docs/documentation/cn/0_get_started/1_installation.md @@ -43,7 +43,7 @@ pip install cqlib 安装完成后,可以通过以下方式验证: -```python +```bash python -m pip show cqlib ``` 如果能够正确输出版本信息,则说明安装成功。 @@ -89,7 +89,7 @@ cd cqlib ### 第三步:编译并安装 Python 绑定 -我们使用 `maturin` 跨语言工具将 Rust 核心编译为 Python 模块: +使用 `maturin` 跨语言工具将 Rust 核心编译为 Python 模块: ```bash # 1. 安装构建工具 diff --git a/docs/documentation/0_get_started/2_quickstart.md b/docs/documentation/cn/0_get_started/2_quickstart.md similarity index 93% rename from docs/documentation/0_get_started/2_quickstart.md rename to docs/documentation/cn/0_get_started/2_quickstart.md index 3396a006..8c7889ba 100644 --- a/docs/documentation/0_get_started/2_quickstart.md +++ b/docs/documentation/cn/0_get_started/2_quickstart.md @@ -26,7 +26,7 @@ Bell 态是量子计算中最常见的双比特纠缠态之一,通常用于演 ## 1. 创建线路 -首先创建一条两比特量子线路,并向其中依次添加 `H` 门和 `CX` 门: +首先创建一条双比特量子线路,并向其中依次添加 `H` 门和 `CX` 门: ```python from cqlib import Circuit @@ -70,9 +70,9 @@ print(qasm2.dumps(circuit)) print(qasm3.dumps(circuit)) ``` -## 5. 状态向量模拟 +## 5. 态矢量模拟 -使用状态向量模拟来查看线路作用后的量子态分布: +使用态矢量模拟来查看线路作用后的量子态分布: ```python from cqlib.qis import Statevector @@ -92,7 +92,7 @@ print(sv.probabilities()) ## 6. 采样测量 -得到状态向量后,可以进一步进行多次采样,模拟实际测量过程中的统计结果: +得到态矢量后,可以进一步进行多次采样,模拟实际测量过程中的统计结果: ```python shots = sv.sample_shots(1000) diff --git a/docs/documentation/1_cqlib/0_circuit/0_overview.md b/docs/documentation/cn/1_cqlib/0_circuit/0_overview.md similarity index 90% rename from docs/documentation/1_cqlib/0_circuit/0_overview.md rename to docs/documentation/cn/1_cqlib/0_circuit/0_overview.md index ba48e151..19cdab44 100644 --- a/docs/documentation/1_cqlib/0_circuit/0_overview.md +++ b/docs/documentation/cn/1_cqlib/0_circuit/0_overview.md @@ -1,6 +1,6 @@ # 量子线路 -`cqlib.circuit` 是 Cqlib 中描述量子程序的基础模块。它负责表达量子比特、门操作、参数表达式、复合线路、非幺正指令、测量结果以及由经典表达式驱动的动态控制流。后续的 [IR 转换](../1_ir/0_overview.md)、[QIS 模拟](../3_qis/0_overview.md)、[编译优化](../4_compiler/0_overview.md)、[设备映射](../2_device/0_overview.md)和[可视化](../5_visualization/0_overview.md)模块,通常都以 `Circuit` 作为输入或中间表示。 +`cqlib.circuit` 是 Cqlib 中描述量子程序的基础模块。它负责表达量子比特、门操作、参数表达式、复合线路、非酉指令、测量结果以及由经典表达式驱动的动态控制流。后续的 [IR 转换](../1_ir/0_overview.md)、[QIS 模拟](../3_qis/0_overview.md)、[编译优化](../4_compiler/0_overview.md)、[设备映射](../2_device/0_overview.md)和[可视化](../5_visualization/0_overview.md)模块,通常都以 `Circuit` 作为输入或中间表示。 --- @@ -14,9 +14,9 @@ | `Circuit` | 量子线路容器,保存量子比特、参数、经典值和操作序列 | `Circuit(...)` | | `StandardGate` | Cqlib 内置标准门集合,包括 Pauli、Clifford、旋转门、双比特门等 | `StandardGate.H`, `StandardGate.RX(theta)` | | `MCGate` | 多控制门,把一个标准门提升为带多个控制比特的门 | `MCGate(2, StandardGate.X())` | -| `UnitaryGate` | 用户自定义幺正门,可通过数值矩阵、符号矩阵或不可变子线路定义 | `UnitaryGate("Oracle", 2)` | +| `UnitaryGate` | 用户自定义酉门,可通过数值矩阵、符号矩阵或不可变子线路定义 | `UnitaryGate("Oracle", 2)` | | `CircuitGate` | 由子线路转换得到的复合门 | `sub.to_gate("Block")` | -| `Directive` | 非幺正指令,如 barrier、measure、reset | `circuit.measure(0)` | +| `Directive` | 非酉指令,如 barrier、measure、reset | `circuit.measure(0)` | | `Parameter` | 符号参数表达式 | `Parameter("theta")` | | `ClassicalType` / `ClassicalExpr` | 动态线路中的经典类型与经典表达式 | `ClassicalType.bit()`, `m.expr()` | | `ValueOperation` | 完整操作表示:指令 + 比特 + 参数 + 可选标签 | `circuit.operations[0]` | diff --git a/docs/documentation/1_cqlib/0_circuit/1_gates.md b/docs/documentation/cn/1_cqlib/0_circuit/1_gates.md similarity index 93% rename from docs/documentation/1_cqlib/0_circuit/1_gates.md rename to docs/documentation/cn/1_cqlib/0_circuit/1_gates.md index 984c9a4d..48821c77 100644 --- a/docs/documentation/1_cqlib/0_circuit/1_gates.md +++ b/docs/documentation/cn/1_cqlib/0_circuit/1_gates.md @@ -1,6 +1,6 @@ # 量子门与指令 -`cqlib.circuit` 提供了丰富的量子门与线路指令,用于构造从基础量子线路到参数化算法模块的各类量子程序。本篇将详细介绍 Cqlib 中量子门与指令的基本用法,包括标准门、参数门、多比特门、多控制门、自定义酉门、子线路门以及测量、重置、屏障等非幺正指令。 +`cqlib.circuit` 提供了丰富的量子门与线路指令,用于构造从基础量子线路到参数化算法模块的各类量子程序。本篇将详细介绍 Cqlib 中量子门与指令的基本用法,包括标准门、参数门、多比特门、多控制门、自定义酉门、子线路门以及测量、重置、屏障等非酉指令。 通过本篇内容,您可以根据算法需求选择合适的门类型,并将其正确添加到量子线路中。您可以通过 `Circuit` 提供的便捷方法快速添加常用门,也可以使用 `StandardGate`、`MCGate`、`UnitaryGate` 和 `CircuitGate` 等对象显式描述更复杂的门操作。 @@ -177,7 +177,7 @@ c.fsim(0, 1, theta, phi) ## 门矩阵与门求逆 -在量子线路分析、算法验证和编译转换过程中,门的矩阵表示是理解其数学作用的重要依据。Cqlib 支持对标准门和多控制门直接计算矩阵,以便您检查门的维度、验证门的幺正性,以及分析门对量子态的具体作用。 +在量子线路分析、算法验证和编译转换过程中,门的矩阵表示是理解其数学作用的重要依据。Cqlib 支持对标准门和多控制门直接计算矩阵,以便您检查门的维度、验证门的酉性,以及分析门对量子态的具体作用。 在计算矩阵时,需要注意: @@ -229,7 +229,7 @@ print(t_inverse) # TDG - `S` 与 `SDG` 互为逆门,`T` 与 `TDG` 互为逆门。 - 旋转门的逆门通常通过角度取负得到,例如 `RX(theta)^† = RX(-theta)`。 - `U(theta, phi, lambda)` 的逆门会根据矩阵定义转换参数。 -- `Barrier` 不改变量子态,可视为自身的逆;`Measure` 和 `Reset` 不具备普通幺正逆操作。 +- `Barrier` 不改变量子态,可视为自身的逆;`Measure` 和 `Reset` 不具备普通酉逆操作。 --- @@ -272,9 +272,9 @@ controlled_cx = StandardGate.CX().control(1) --- -## 自定义幺正门 +## 自定义酉门 -当算法需要使用内置标准门以外的幺正操作时,可以通过 `UnitaryGate` 定义自定义幺正门。自定义幺正门适用于表示算法中的特殊 oracle、问题相关变换、硬件专用门、已知矩阵形式的量子操作。 +当算法需要使用内置标准门以外的酉操作时,可以通过 `UnitaryGate` 定义自定义酉门。自定义酉门适用于表示算法中的特殊 oracle、问题相关变换、硬件专用门、已知矩阵形式的量子操作。 定义 `UnitaryGate` 时,需要明确指定门名称和作用量子比特数量。若使用矩阵定义该门,则矩阵维度必须为 `2^n × 2^n`,其中 `n` 表示该门作用的量子比特数量,即 `num_qubits`。Cqlib 会根据门定义检查矩阵维度与作用比特数量是否匹配。 @@ -290,7 +290,7 @@ c = Circuit(2) c.append_unitary_gate(custom_h, [0]) ``` -除数值矩阵外,自定义幺正门也可以通过符号矩阵定义。符号矩阵适合描述带参数的门族,可以先定义门的符号形式,再从不同线路或不同位置传入具体参数值。这种方式常用于参数化 oracle、可调相位门、算法模板和符号验证场景。 +除数值矩阵外,自定义酉门也可以通过符号矩阵定义。符号矩阵适合描述带参数的门族,可以先定义门的符号形式,再从不同线路或不同位置传入具体参数值。这种方式常用于参数化 oracle、可调相位门、算法模板和符号验证场景。 ```python from cqlib import Circuit, Parameter @@ -368,13 +368,13 @@ gate = CircuitGate("HadamardBlock", frozen) --- -## Directive:非幺正指令 +## Directive:非酉指令 -除普通量子门外,量子线路中还可能包含测量、重置、屏障和延迟等特殊指令。这类指令通常无法对应一个普通的幺正矩阵,因此在 Cqlib 中统一通过 `Directive` 表示。 +除普通量子门外,量子线路中还可能包含测量、重置、屏障和延迟等特殊指令。这类指令通常无法对应一个普通的酉矩阵,因此在 Cqlib 中统一通过 `Directive` 表示。 `Directive` 主要用于描述线路执行过程中的辅助语义。例如,`barrier` 用于约束编译优化过程中的门重排,`measure` 用于将量子态信息读出为经典结果,`reset` 用于将量子比特重新初始化到 |0>,`delay` 则用于保留硬件时间调度中的空闲等待语义。 -常用的非幺正指令可以直接通过 `Circuit` 提供的接口添加到线路中: +常用的非酉指令可以直接通过 `Circuit` 提供的接口添加到线路中: | `Circuit` 方法 | 指令名 | 说明 | |---|---|---| @@ -404,11 +404,11 @@ print(c.classical_values) print(c.classical_vars) ``` -需要注意的是,非幺正指令会影响线路的数学表示和后续处理方式: +需要注意的是,非酉指令会影响线路的数学表示和后续处理方式: -- `barrier` 本身不改变量子态,仅用于表达编译约束。因此,当线路中不包含其他非幺正操作时,带有 `barrier` 的线路仍可用于矩阵转换。 -- `measure`、`measure_into` 和 `measure_bits` 会引入量子测量与经典结果,因此线路不再能用单一幺正矩阵完整表示。 -- `reset` 会将量子比特重新初始化到固定状态,属于非幺正操作,同样不能通过普通幺正矩阵描述。 +- `barrier` 本身不改变量子态,仅用于表达编译约束。因此,当线路中不包含其他非酉操作时,带有 `barrier` 的线路仍可用于矩阵转换。 +- `measure`、`measure_into` 和 `measure_bits` 会引入量子测量与经典结果,因此线路不再能用单一酉矩阵完整表示。 +- `reset` 会将量子比特重新初始化到固定状态,属于非酉操作,同样不能通过普通酉矩阵描述。 - `delay` 主要用于保留硬件执行或调度层面的时间语义。 因此,在进行 `to_matrix()`、线路等价性验证或门级优化时,应先确认线路中是否包含测量、重置、延迟等特殊指令。 diff --git a/docs/documentation/1_cqlib/0_circuit/2_structures.md b/docs/documentation/cn/1_cqlib/0_circuit/2_structures.md similarity index 96% rename from docs/documentation/1_cqlib/0_circuit/2_structures.md rename to docs/documentation/cn/1_cqlib/0_circuit/2_structures.md index 593e0d60..34e8e50a 100644 --- a/docs/documentation/1_cqlib/0_circuit/2_structures.md +++ b/docs/documentation/cn/1_cqlib/0_circuit/2_structures.md @@ -174,7 +174,7 @@ print([q.index for q in second.qubits]) # [0, 1] ``` `Circuit` 中的每一项操作通常表示为 `ValueOperation`,它是线路中的完整操作对象,主要包含以下字段: -- `instruction`:操作对应的 `ValueInstruction`,可以表示普通量子门、非幺正指令或经典控制流结构; +- `instruction`:操作对应的 `ValueInstruction`,可以表示普通量子门、非酉指令或经典控制流结构; - `qubits`:该操作作用的量子比特列表; - `params`:该次操作携带的参数列表,常用于参数化旋转门、自定义门或复合门; - `label`:可选的标签信息,通常用于调试、标记线路层、记录转换来源或辅助后续分析。 @@ -183,7 +183,7 @@ print([q.index for q in second.qubits]) # [0, 1] ## 添加操作 -在线路创建完成后,您可以向 `Circuit` 中持续追加量子门、复合门、非幺正指令或低层操作对象。Cqlib 提供了多种追加操作的方式,以适应不同的使用需求。 +在线路创建完成后,您可以向 `Circuit` 中持续追加量子门、复合门、非酉指令或低层操作对象。Cqlib 提供了多种追加操作的方式,以适应不同的使用需求。 ### 1. 使用便捷门方法 @@ -322,7 +322,7 @@ print([op.instruction.instruction.name for op in flat.operations]) ## 全局相位 -每条 `Circuit` 都包含一个 `global_phase` 属性,用于记录线路整体的全局相位。全局相位不会改变单次测量得到各个计算基态的概率分布,但它是量子线路数学表示的一部分,在矩阵对比、线路等价性判断、编译重写以及某些相位敏感的算法分析中具有重要意义。 +每条 `Circuit` 都包含一个 `global_phase` 属性,用于记录线路整体的全局相位。全局相位不改变各计算基态的测量概率分布,但它是量子线路数学表示的一部分,在矩阵对比、线路等价性判断、编译重写以及某些相位敏感的算法分析中具有重要意义。 在 Cqlib 中,`global_phase` 使用 `Parameter` 表示,因此既可以设置为普通数值,也可以设置为符号参数或参数表达式。 diff --git a/docs/documentation/1_cqlib/0_circuit/3_parameters.md b/docs/documentation/cn/1_cqlib/0_circuit/3_parameters.md similarity index 100% rename from docs/documentation/1_cqlib/0_circuit/3_parameters.md rename to docs/documentation/cn/1_cqlib/0_circuit/3_parameters.md diff --git a/docs/documentation/1_cqlib/0_circuit/4_circuit_analysis.md b/docs/documentation/cn/1_cqlib/0_circuit/4_circuit_analysis.md similarity index 99% rename from docs/documentation/1_cqlib/0_circuit/4_circuit_analysis.md rename to docs/documentation/cn/1_cqlib/0_circuit/4_circuit_analysis.md index 9000e81c..a8da4561 100644 --- a/docs/documentation/1_cqlib/0_circuit/4_circuit_analysis.md +++ b/docs/documentation/cn/1_cqlib/0_circuit/4_circuit_analysis.md @@ -177,7 +177,7 @@ matrix = op.matrix() print(np.allclose(matrix, np.array([[0, 1], [1, 0]], dtype=complex))) ``` -需要注意的是,该接口仅适用于普通幺正操作。 +需要注意的是,该接口仅适用于普通酉操作。 --- diff --git a/docs/documentation/1_cqlib/0_circuit/5_control_flow.md b/docs/documentation/cn/1_cqlib/0_circuit/5_control_flow.md similarity index 99% rename from docs/documentation/1_cqlib/0_circuit/5_control_flow.md rename to docs/documentation/cn/1_cqlib/0_circuit/5_control_flow.md index 7343ca77..6e5338d1 100644 --- a/docs/documentation/1_cqlib/0_circuit/5_control_flow.md +++ b/docs/documentation/cn/1_cqlib/0_circuit/5_control_flow.md @@ -330,7 +330,7 @@ c.switch(ClassicalExpr.uint_literal(2, 1), build_switch) --- -## 校验与作用域 +## 结构校验 与普通线性线路相比,控制流结构对线路内部一致性的要求更高。因此,在构造包含控制流的线路后,建议调用 `validate()` 对线路进行结构校验。 diff --git a/docs/documentation/1_cqlib/1_ir/0_overview.md b/docs/documentation/cn/1_cqlib/1_ir/0_overview.md similarity index 100% rename from docs/documentation/1_cqlib/1_ir/0_overview.md rename to docs/documentation/cn/1_cqlib/1_ir/0_overview.md diff --git a/docs/documentation/1_cqlib/1_ir/1_qcis.md b/docs/documentation/cn/1_cqlib/1_ir/1_qcis.md similarity index 98% rename from docs/documentation/1_cqlib/1_ir/1_qcis.md rename to docs/documentation/cn/1_cqlib/1_ir/1_qcis.md index ab0241eb..b628f1c7 100644 --- a/docs/documentation/1_cqlib/1_ir/1_qcis.md +++ b/docs/documentation/cn/1_cqlib/1_ir/1_qcis.md @@ -73,6 +73,9 @@ print(len(circuit.operations)) ```python from cqlib.ir import qcis +# 先把上文的 QCIS 文本写成待读取的文件 +qcis.dump(qcis.loads(qcis_code), "input.qcis") + circuit = qcis.load("input.qcis") ``` diff --git a/docs/documentation/1_cqlib/1_ir/2_qasm2.md b/docs/documentation/cn/1_cqlib/1_ir/2_qasm2.md similarity index 98% rename from docs/documentation/1_cqlib/1_ir/2_qasm2.md rename to docs/documentation/cn/1_cqlib/1_ir/2_qasm2.md index 75363288..5bdff71c 100644 --- a/docs/documentation/1_cqlib/1_ir/2_qasm2.md +++ b/docs/documentation/cn/1_cqlib/1_ir/2_qasm2.md @@ -93,6 +93,9 @@ cx q[0],q[1]; ```python from cqlib.ir import qasm2 +# 先把上文的 QASM 文本写成待读取的文件 +qasm2.dump(qasm2.loads(qasm_code), "input.qasm") + circuit = qasm2.load("input.qasm") qasm2.dump(circuit, "output.qasm") ``` diff --git a/docs/documentation/1_cqlib/1_ir/3_qasm3.md b/docs/documentation/cn/1_cqlib/1_ir/3_qasm3.md similarity index 98% rename from docs/documentation/1_cqlib/1_ir/3_qasm3.md rename to docs/documentation/cn/1_cqlib/1_ir/3_qasm3.md index 4bce40b5..320ab40a 100644 --- a/docs/documentation/1_cqlib/1_ir/3_qasm3.md +++ b/docs/documentation/cn/1_cqlib/1_ir/3_qasm3.md @@ -83,6 +83,9 @@ cx q[0],q[1]; ```python from cqlib.ir import qasm3 +# 先把上文的 QASM 文本写成待读取的文件 +qasm3.dump(qasm3.loads(qasm_code), "input.qasm") + circuit = qasm3.load("input.qasm") qasm3.dump(circuit, "output.qasm") ``` diff --git a/docs/documentation/1_cqlib/1_ir/4_conversion_workflow.md b/docs/documentation/cn/1_cqlib/1_ir/4_conversion_workflow.md similarity index 100% rename from docs/documentation/1_cqlib/1_ir/4_conversion_workflow.md rename to docs/documentation/cn/1_cqlib/1_ir/4_conversion_workflow.md diff --git a/docs/documentation/1_cqlib/2_device/0_overview.md b/docs/documentation/cn/1_cqlib/2_device/0_overview.md similarity index 100% rename from docs/documentation/1_cqlib/2_device/0_overview.md rename to docs/documentation/cn/1_cqlib/2_device/0_overview.md diff --git a/docs/documentation/1_cqlib/2_device/1_topology.md b/docs/documentation/cn/1_cqlib/2_device/1_topology.md similarity index 89% rename from docs/documentation/1_cqlib/2_device/1_topology.md rename to docs/documentation/cn/1_cqlib/2_device/1_topology.md index f222b355..b83098c5 100644 --- a/docs/documentation/1_cqlib/2_device/1_topology.md +++ b/docs/documentation/cn/1_cqlib/2_device/1_topology.md @@ -78,9 +78,9 @@ print("是否包含比特 2:", topo.contains_qubit(2)) # True print("是否包含比特 9:", topo.contains_qubit(9)) # False # 邻接关系查询 -print("比特 1 的后继(出边):", topo.successors(1)) # [Qubit(2)] -print("比特 1 的前驱(入边):", topo.predecessors(1)) # [Qubit(0)] -print("比特 1 的无向邻居:", topo.neighbors_undirected(1)) # [Qubit(0), Qubit(2)] +print("比特 1 的后继(出边):", topo.successors(1)) # [PhysicalQubit(2)] +print("比特 1 的前驱(入边):", topo.predecessors(1)) # [PhysicalQubit(0)] +print("比特 1 的无向邻居:", topo.neighbors_undirected(1)) # [PhysicalQubit(0), PhysicalQubit(2)] print("比特 1 的出度:", topo.out_degree(1)) # 1 print("比特 1 的入度:", topo.in_degree(1)) # 1 @@ -93,7 +93,7 @@ print("无向边列表:", edges) - successors(q) 返回从 q 出发通过有向边可直接到达的比特列表 - predecessors(q) 返回有向边指向 q 的比特列表 - -eighbors_undirected(q) 合并两个方向,重复节点只出现一次 +neighbors_undirected(q) 合并两个方向,重复节点只出现一次 - contains_qubit(q) 对不存在的比特返回 False,不会抛出异常 - out_degree(q) 和 in_degree(q) 对不存在的比特返回 0 @@ -116,7 +116,7 @@ print("添加后耦合数:", topo.num_couplings) # 4 # 移除去向耦合(仅移除指定方向) topo.remove_couplings([(2, 3)]) -print("移除后耦合数:", topo.num_couplings) # 2(移除了耦合 (3,4)) +print("移除后耦合数:", topo.num_couplings) # 3(移除了耦合 (2,3)) # 移除比特(连带所有关联耦合一起移除) topo.remove_qubits([4]) @@ -125,12 +125,12 @@ print("移除后耦合数:", topo.num_couplings) # 2(移除了耦合 (3,4)) ``` **边界条件**: -- dd_qubits([...]):添加已有比特会抛出 ValueError -- dd_couplings([...]):如果耦合端点不在拓扑中,或添加已存在的耦合,抛出 ValueError +- add_qubits([...]):添加已有比特会抛出 ValueError +- add_couplings([...]):如果耦合端点不在拓扑中,或添加已存在的耦合,抛出 ValueError - -emove_qubits([...]):如果比特不存在,抛出 ValueError +remove_qubits([...]):如果比特不存在,抛出 ValueError - -emove_couplings([...]) 的 couplings 参数使用 (control, target) 二元组(**不含名称**) +remove_couplings([...]) 的 couplings 参数使用 (control, target) 二元组(**不含名称**) - 移除一个比特会自动移除其所有入射和出射耦合边 --- diff --git a/docs/documentation/1_cqlib/2_device/2_device.md b/docs/documentation/cn/1_cqlib/2_device/2_device.md similarity index 90% rename from docs/documentation/1_cqlib/2_device/2_device.md rename to docs/documentation/cn/1_cqlib/2_device/2_device.md index 068a4c1f..3ca4dd51 100644 --- a/docs/documentation/1_cqlib/2_device/2_device.md +++ b/docs/documentation/cn/1_cqlib/2_device/2_device.md @@ -80,13 +80,13 @@ q0.frequency = 5.1 # 频率(GHz) q0.prob_meas0_prep1 = 0.02 # P(测到 0 | 制备为 1) q0.prob_meas1_prep0 = 0.01 # P(测到 1 | 制备为 0) -# 添加本原单比特门(注意:native_instructions 是列表属性,通过 append 添加) +# 添加本原单比特门(注意:必须用 add_native_instruction(),向 native_instructions 追加不会生效) x_prop = InstructionProp( Instruction.from_standard_gate(StandardGate.X), error_rate=0.001, ) x_prop.length = 20.0 # 门时长(纳秒) -q0.native_instructions.append(x_prop) +q0.add_native_instruction(x_prop) device.add_qubit_properties(0, q0) @@ -106,8 +106,8 @@ print("比特 0 局部属性已注入") **注意**: - InstructionProp 构造器的第一个参数必须是 Instruction 对象,使用 Instruction.from_standard_gate(StandardGate.X) 创建。不允许直接传入 StandardGate.X -- QubitProp.native_instructions 是只读列表属性,不能直接赋值(q0.native_instructions = [...] 会抛出 AttributeError),应通过 .append() 添加元素 -- EdgeProp.native_instructions 也是只读属性,需通过 dd_native_instruction() 方法添加 +- QubitProp.native_instructions 是只读属性,每次读取都返回新列表:既不能直接赋值(q0.native_instructions = [...] 会抛出 AttributeError),对它调用 .append() 也不会生效(改的是临时副本),必须用 add_native_instruction() 添加元素 +- EdgeProp.native_instructions 也是只读属性,需通过 add_native_instruction() 方法添加 --- @@ -160,7 +160,7 @@ device = Device("dev", [0, 1, 2], topo) device.invalid_qubits = [2] # 注意:使用列表,不是集合 print("可用比特数:", device.num_usable_qubits) # 2 -print("可用比特列表:", device.usable_qubits) # [Qubit(0), Qubit(1)] +print("可用比特列表:", device.usable_qubits) # [PhysicalQubit(0), PhysicalQubit(1)] print("比特 2 是否可用:", device.is_usable_qubit(2)) # False ``` diff --git a/docs/documentation/1_cqlib/2_device/3_layout.md b/docs/documentation/cn/1_cqlib/2_device/3_layout.md similarity index 92% rename from docs/documentation/1_cqlib/2_device/3_layout.md rename to docs/documentation/cn/1_cqlib/2_device/3_layout.md index 69f5e370..29e49ab4 100644 --- a/docs/documentation/1_cqlib/2_device/3_layout.md +++ b/docs/documentation/cn/1_cqlib/2_device/3_layout.md @@ -52,10 +52,10 @@ print("物理比特列表:", layout.physical_qubits) print("空闲物理比特:", layout.vacant_physical_qubits) # 正向查询:逻辑 → 物理 -print("逻辑 0 映射到物理:", layout.get_physical(0)) # Qubit(11) +print("逻辑 0 映射到物理:", layout.get_physical(0)) # P11 # 反向查询:物理 → 逻辑 -print("物理 11 映射到逻辑:", layout.get_logical(11)) # Qubit(0) +print("物理 11 映射到逻辑:", layout.get_logical(11)) # L0 # 查询物理比特是否空闲 print("物理 10 是否空闲:", layout.is_physical_vacant(10)) # False(被逻辑 1 占用) @@ -88,18 +88,18 @@ print("绑定后空闲:", layout.num_vacant_physical) # 10 # unbind:解绑逻辑比特,释放物理比特 released = layout.unbind(0) -print("解绑后释放:", released) # Qubit(11) +print("解绑后释放:", released) # P11 print("解绑后空闲:", layout.num_vacant_physical) # 11 # swap_physical:交换两个物理比特上承载的逻辑比特(核心路由操作) layout3 = Layout.from_pairs([(0, 11), (1, 12)], physical_count=13) layout3.swap_physical(11, 12) -print("SWAP 后物理 11→逻辑:", layout3.get_logical(11)) # Qubit(1) -print("SWAP 后物理 12→逻辑:", layout3.get_logical(12)) # Qubit(0) +print("SWAP 后物理 11→逻辑:", layout3.get_logical(11)) # L1 +print("SWAP 后物理 12→逻辑:", layout3.get_logical(12)) # L0 ``` **边界条件**: -- ind(logical, physical):如果 physical 已被占用,或 logical 已绑定,抛出 ValueError +- bind(logical, physical):如果 physical 已被占用,或 logical 已绑定,抛出 ValueError - unbind(logical):如果 logical 未绑定,抛出 ValueError - swap_physical(a, b):如果 或 不在布局中,抛出 ValueError。允许其中一个为空闲比特(相当于移动逻辑比特) @@ -127,6 +127,8 @@ try: print("SWAP 操作成功") print("物理 11→逻辑:", layout.get_logical(11)) print("物理 12→逻辑:", layout.get_logical(12)) +except ValueError as e: + print("SWAP 操作失败:", e) ``` --- diff --git a/docs/documentation/1_cqlib/2_device/4_noise.md b/docs/documentation/cn/1_cqlib/2_device/4_noise.md similarity index 90% rename from docs/documentation/1_cqlib/2_device/4_noise.md rename to docs/documentation/cn/1_cqlib/2_device/4_noise.md index d0ecc6c6..722e5245 100644 --- a/docs/documentation/1_cqlib/2_device/4_noise.md +++ b/docs/documentation/cn/1_cqlib/2_device/4_noise.md @@ -10,11 +10,11 @@ SingleQubitNoise 提供多种静态工厂方法创建不同类型的噪声信道 | 工厂方法 | 信道 | 含义 | |---|---|---| -| it_flip(p) | 比特翻转 | 以概率 p 执行 X 门 | +| bit_flip(p) | 比特翻转 | 以概率 p 执行 X 门 | | phase_flip(p) | 相位翻转 | 以概率 p 执行 Z 门 | | pauli(px, py, pz) | 一般泡利 | 独立指定 X/Y/Z 错误概率 | | depolarizing(p) | 去极化 | 以概率 p 随机施加 X/Y/Z 错误 | -| mplitude_damping(gamma) | 振幅阻尼 | T1 能量弛豫模型 | +| amplitude_damping(gamma) | 振幅阻尼 | T1 能量弛豫模型 | | phase_damping(lambda_) | 相位阻尼 | T2 纯退相干模型 | ```python @@ -38,7 +38,7 @@ print("振幅阻尼有效:", amp.is_valid()) # True **注意**: - SingleQubitNoise 没有 .kind 属性。要区分噪声类型,需自行通过构造方法跟踪 -- pauli(px, py, pz) 要求 px + py + pz <= 1,否则 is_valid() 返回 False +- pauli(px, py, pz) 要求 px + py + pz <= 1,否则构造时抛出 ValueError --- @@ -53,11 +53,11 @@ tq = TwoQubitNoise.depolarizing(0.01) print("去极化 Kraus 形状:", tq.to_kraus()[0].shape) # (4, 4) # 独立噪声(每个比特各自独立施加单比特噪声) -ind = TwoQubitNoise.independent( +bind = TwoQubitNoise.independent( SingleQubitNoise.phase_flip(0.02), SingleQubitNoise.bit_flip(0.03), ) -print("独立噪声 Kraus 形状:", ind.to_kraus()[0].shape) +print("独立噪声 Kraus 形状:", bind.to_kraus()[0].shape) # 关联 Pauli 噪声 corr = TwoQubitNoise.correlated_pauli(Pauli.x(), Pauli.x(), p=0.01) @@ -121,10 +121,10 @@ print("CX 门噪声通道数:", len(errs2)) ``` **说明**: -- dd_single_qubit_error(gate, qubit, noise) 的 gate 参数传入 StandardGate 枚举值(如 StandardGate.X),而非 Instruction 对象 -- dd_two_qubit_error(gate, q0, q1, noise) 同样使用 StandardGate 枚举值 +- add_single_qubit_error(gate, qubit, noise) 的 gate 参数传入 StandardGate 枚举值(如 StandardGate.X),而非 Instruction 对象 +- add_two_qubit_error(gate, q0, q1, noise) 同样使用 StandardGate 枚举值 - OperationKey.new_single(gate, q0) 和 -ew_double(gate, q0, q1) 使用 StandardGate +new_double(gate, q0, q1) 使用 StandardGate - get_single_qubit_errors(key) 返回 list[SingleQubitNoise] | None,无匹配时返回 None --- diff --git a/docs/documentation/1_cqlib/2_device/5_result.md b/docs/documentation/cn/1_cqlib/2_device/5_result.md similarity index 94% rename from docs/documentation/1_cqlib/2_device/5_result.md rename to docs/documentation/cn/1_cqlib/2_device/5_result.md index c2b2eccb..acea8ece 100644 --- a/docs/documentation/1_cqlib/2_device/5_result.md +++ b/docs/documentation/cn/1_cqlib/2_device/5_result.md @@ -28,8 +28,8 @@ print("索引构造:", o3.to_bitstring(3)) # "101" **说明**: - 字符串只能包含 '0' 和 '1',包含其他字符会抛出 ValueError -- o_bitstring(num_qubits) 如果 -um_qubits 大于实际宽度,高位补零 +- to_bitstring(num_qubits) 如果 +num_qubits 大于实际宽度,高位补零 --- @@ -61,7 +61,7 @@ print("cancelled:", x.kind, "终态?", x.is_terminal()) **说明**: - Status.kind 是属性(不是方法),返回字符串,如 "completed"、"failed" -- is_terminal() 对 completed、ailed、cancelled 返回 True +- is_terminal() 对 completed、failed、cancelled 返回 True - is_success() 仅对 completed 返回 True - error_msg 和 error_code 仅在 kind == "failed" 时有值,否则返回 None @@ -92,7 +92,7 @@ print("后端名称:", result.backend) ### 从计数直接构造 -如果已有测量结果,可使用 rom_counts 一步完成创建和填充: +如果已有测量结果,可使用 from_counts 一步完成创建和填充: ```python from cqlib.device import ExecutionResult diff --git a/docs/documentation/1_cqlib/3_qis/0_overview.md b/docs/documentation/cn/1_cqlib/3_qis/0_overview.md similarity index 96% rename from docs/documentation/1_cqlib/3_qis/0_overview.md rename to docs/documentation/cn/1_cqlib/3_qis/0_overview.md index 5d2eda87..cd2bacce 100644 --- a/docs/documentation/1_cqlib/3_qis/0_overview.md +++ b/docs/documentation/cn/1_cqlib/3_qis/0_overview.md @@ -154,6 +154,6 @@ QIS 数值结果不能替代线路可视化和单元测试。推荐在关键算 ## 下一步 -·[Statevector 纯态模拟](1_statevector.md):先用理想纯态检查门序列、概率分布、采样和可观测量期望值。 -·[DensityMatrix 混态模拟](2_density_matrix.md):学习密度矩阵、Kraus 噪声、部分迹和物理性验证。 -·[Pauli、PauliString 与 Hamiltonian](5_pauli_and_hamiltonian.md):掌握 Pauli 表示、哈密顿量建模、期望值计算和 Trotter 演化。 +- [Statevector 纯态模拟](1_statevector.md):先用理想纯态检查门序列、概率分布、采样和可观测量期望值。 +- [DensityMatrix 混态模拟](2_density_matrix.md):学习密度矩阵、Kraus 噪声、部分迹和物理性验证。 +- [Pauli、PauliString 与 Hamiltonian](5_pauli_and_hamiltonian.md):掌握 Pauli 表示、哈密顿量建模、期望值计算和 Trotter 演化。 diff --git a/docs/documentation/1_cqlib/3_qis/1_statevector.md b/docs/documentation/cn/1_cqlib/3_qis/1_statevector.md similarity index 94% rename from docs/documentation/1_cqlib/3_qis/1_statevector.md rename to docs/documentation/cn/1_cqlib/3_qis/1_statevector.md index 4aa1399a..5c6e21d7 100644 --- a/docs/documentation/1_cqlib/3_qis/1_statevector.md +++ b/docs/documentation/cn/1_cqlib/3_qis/1_statevector.md @@ -96,7 +96,7 @@ print(state.probabilities()) ## 直接施加常用量子门 -`Statevector` 提供与标准门相对应的 `apply_*` 方法,包括 Pauli 门、Clifford 门、旋转门、受控门、双比特旋转门、`fSim`、`CCX` 和用户自定义幺正矩阵。 +`Statevector` 提供与标准门相对应的 `apply_*` 方法,包括 Pauli 门、Clifford 门、旋转门、受控门、双比特旋转门、`fSim`、`CCX` 和用户自定义酉矩阵。 ```python import numpy as np @@ -159,7 +159,7 @@ hamiltonian = Hamiltonian.from_list([ print(state.expectation(hamiltonian)) ``` -也可以从可观测量一侧调用 `expectation_statevector(state)`。在 VQE 或 QAOA 中,推荐固定使用一种写法,避免在日志和后处理代码中混淆状态对象与可观测量对象。 +也可以从可观测量一侧调用 `expectation_statevector(state)`。在 VQE 或 QAOA 中,推荐固定使用一种写法,避免在日志和后处理代码中混淆状态对象与可观测量。 --- @@ -191,6 +191,6 @@ print(state.probabilities()) ## 下一步 -·[DensityMatrix 混态模拟](2_density_matrix.md):在需要混态、Kraus 噪声和部分迹时,从纯态模拟切换到密度矩阵。 -·[Pauli、PauliString 与 Hamiltonian](5_pauli_and_hamiltonian.md):学习如何把状态结果连接到可观测量、能量函数和 Trotter 演化。 -·[可视化量子态](../5_visualization/7_state_visualization.md):用 Bloch、state city 和 Pauli vector 图解释纯态模拟结果。 +- [DensityMatrix 混态模拟](2_density_matrix.md):在需要混态、Kraus 噪声和部分迹时,从纯态模拟切换到密度矩阵。 +- [Pauli、PauliString 与 Hamiltonian](5_pauli_and_hamiltonian.md):学习如何把状态结果连接到可观测量、能量函数和 Trotter 演化。 +- [可视化量子态](../5_visualization/7_state_visualization.md):用 Bloch、state city 和 Pauli vector 图解释纯态模拟结果。 diff --git a/docs/documentation/1_cqlib/3_qis/2_density_matrix.md b/docs/documentation/cn/1_cqlib/3_qis/2_density_matrix.md similarity index 93% rename from docs/documentation/1_cqlib/3_qis/2_density_matrix.md rename to docs/documentation/cn/1_cqlib/3_qis/2_density_matrix.md index 58a68647..63c8b013 100644 --- a/docs/documentation/1_cqlib/3_qis/2_density_matrix.md +++ b/docs/documentation/cn/1_cqlib/3_qis/2_density_matrix.md @@ -111,7 +111,7 @@ print(subsystem.data) print(subsystem.probabilities()) ``` -对 Bell 态任意单比特做部分迹,会得到接近最大混合态的局部密度矩阵。这个结果说明单个 qubit 本身没有确定 Bloch 方向,但全局二比特状态仍然具有纠缠相关性。 +对 Bell 态任意单比特做部分迹,会得到接近最大混合态的局部密度矩阵。这个结果说明单个 qubit 本身没有确定 Bloch 方向,但全局双比特状态仍然具有纠缠相关性。 --- @@ -161,6 +161,6 @@ print(density.sample_shots(8)) ## 下一步 -·[DensityMatrixNoise 含噪模拟](3_density_matrix_noise.md):把手动 Kraus 噪声扩展为基于 `NoiseModel` 的自动含噪线路模拟。 -·[量子态指标与熵](6_metrics_entropy.md):用纯度、熵、保真度和纠缠指标解释密度矩阵结果。 -·[可视化量子态](../5_visualization/7_state_visualization.md):用 state city 和 Pauli vector 检查密度矩阵的相干项和 Pauli 展开。 +- [DensityMatrixNoise 含噪模拟](3_density_matrix_noise.md):把手动 Kraus 噪声扩展为基于 `NoiseModel` 的自动含噪线路模拟。 +- [量子态指标与熵](6_metrics_entropy.md):用纯度、熵、保真度和纠缠指标解释密度矩阵结果。 +- [可视化量子态](../5_visualization/7_state_visualization.md):用 state city 和 Pauli vector 检查密度矩阵的相干项和 Pauli 展开。 diff --git a/docs/documentation/1_cqlib/3_qis/3_density_matrix_noise.md b/docs/documentation/cn/1_cqlib/3_qis/3_density_matrix_noise.md similarity index 95% rename from docs/documentation/1_cqlib/3_qis/3_density_matrix_noise.md rename to docs/documentation/cn/1_cqlib/3_qis/3_density_matrix_noise.md index d8a5823d..79c1aaf1 100644 --- a/docs/documentation/1_cqlib/3_qis/3_density_matrix_noise.md +++ b/docs/documentation/cn/1_cqlib/3_qis/3_density_matrix_noise.md @@ -153,6 +153,6 @@ print(simulator.sample_shots(16)) ## 下一步 -·[DensityMatrix 混态模拟](2_density_matrix.md):回到密度矩阵本身,理解 Kraus 通道、部分迹和物理性检查。 -·[可视化执行结果](../5_visualization/6_result_visualization.md):把含噪采样结果画成 histogram 或概率分布,检查主峰和长尾。 -·[设备噪声](../2_device/4_noise.md):了解 `NoiseModel`、门噪声和读出误差在设备模块中的建模方式。 +- [DensityMatrix 混态模拟](2_density_matrix.md):回到密度矩阵本身,理解 Kraus 通道、部分迹和物理性检查。 +- [可视化执行结果](../5_visualization/6_result_visualization.md):把含噪采样结果画成 histogram 或概率分布,检查主峰和长尾。 +- [设备噪声](../2_device/4_noise.md):了解 `NoiseModel`、门噪声和读出误差在设备模块中的建模方式。 diff --git a/docs/documentation/1_cqlib/3_qis/4_stabilizer.md b/docs/documentation/cn/1_cqlib/3_qis/4_stabilizer.md similarity index 93% rename from docs/documentation/1_cqlib/3_qis/4_stabilizer.md rename to docs/documentation/cn/1_cqlib/3_qis/4_stabilizer.md index 9cec8be8..a96e3ecd 100644 --- a/docs/documentation/1_cqlib/3_qis/4_stabilizer.md +++ b/docs/documentation/cn/1_cqlib/3_qis/4_stabilizer.md @@ -146,6 +146,6 @@ print(state.to_stim_format()) ## 下一步 -·[Statevector 纯态模拟](1_statevector.md):当线路包含非 Clifford 门时,用纯态模拟器继续验证。 -·[Pauli、PauliString 与 Hamiltonian](5_pauli_and_hamiltonian.md):理解稳定子生成元背后的 PauliString 表示和 Pauli 期望值。 -·[用文本图调试线路](../5_visualization/1_draw_text.md):在稳定子线路较长时,先用文本图检查门序和控制位。 +- [Statevector 纯态模拟](1_statevector.md):当线路包含非 Clifford 门时,用纯态模拟器继续验证。 +- [Pauli、PauliString 与 Hamiltonian](5_pauli_and_hamiltonian.md):理解稳定子生成元背后的 PauliString 表示和 Pauli 期望值。 +- [用文本图调试线路](../5_visualization/1_draw_text.md):在稳定子线路较长时,先用文本图检查门序和控制位。 diff --git a/docs/documentation/1_cqlib/3_qis/5_pauli_and_hamiltonian.md b/docs/documentation/cn/1_cqlib/3_qis/5_pauli_and_hamiltonian.md similarity index 95% rename from docs/documentation/1_cqlib/3_qis/5_pauli_and_hamiltonian.md rename to docs/documentation/cn/1_cqlib/3_qis/5_pauli_and_hamiltonian.md index 3ab2be66..317c9890 100644 --- a/docs/documentation/1_cqlib/3_qis/5_pauli_and_hamiltonian.md +++ b/docs/documentation/cn/1_cqlib/3_qis/5_pauli_and_hamiltonian.md @@ -188,6 +188,6 @@ circuit = hamiltonian.to_evolution_circuit( ## 下一步 -·[Statevector 纯态模拟](1_statevector.md):把 PauliString 和 Hamiltonian 作用到理想纯态上,验证期望值和能量函数。 -·[DensityMatrix 混态模拟](2_density_matrix.md):把同一组可观测量用于混态和含噪态分析。 -·[量子态指标与熵](6_metrics_entropy.md):在能量之外,继续比较保真度、迹距离、纯度和纠缠指标。 +- [Statevector 纯态模拟](1_statevector.md):把 PauliString 和 Hamiltonian 作用到理想纯态上,验证期望值和能量函数。 +- [DensityMatrix 混态模拟](2_density_matrix.md):把同一组可观测量用于混态和含噪态分析。 +- [量子态指标与熵](6_metrics_entropy.md):在能量之外,继续比较保真度、迹距离、纯度和纠缠指标。 diff --git a/docs/documentation/1_cqlib/3_qis/6_metrics_entropy.md b/docs/documentation/cn/1_cqlib/3_qis/6_metrics_entropy.md similarity index 87% rename from docs/documentation/1_cqlib/3_qis/6_metrics_entropy.md rename to docs/documentation/cn/1_cqlib/3_qis/6_metrics_entropy.md index 35f8bd02..9a27095f 100644 --- a/docs/documentation/1_cqlib/3_qis/6_metrics_entropy.md +++ b/docs/documentation/cn/1_cqlib/3_qis/6_metrics_entropy.md @@ -24,8 +24,8 @@ | `entropy` | `renyi_entropy` | `DensityMatrix`, `alpha` | Rényi 熵 | | `entropy` | `entanglement_entropy_pure` | `Statevector`, 子系统 | 纯态纠缠熵 | | `entropy` | `negativity` | `DensityMatrix`, 子系统 | 负性 | -| `entropy` | `concurrence` | 2-qubit `DensityMatrix` | 两比特 concurrence | -| `entropy` | `entanglement_of_formation` | 2-qubit `DensityMatrix` | 纠缠形成 | +| `entropy` | `concurrence` | two-qubit `DensityMatrix` | 双比特 concurrence | +| `entropy` | `entanglement_of_formation` | two-qubit `DensityMatrix` | 纠缠形成 | --- @@ -91,7 +91,7 @@ state.apply_cx(0, 1) print(entropy.entanglement_entropy_pure(state, [0])) ``` -Bell 态任意一个单比特子系统都接近最大混合态,因此纠缠熵接近 `1` bit。对于乘积态,纠缠熵应接近 `0`。 +Bell 态任意一个单比特子系统的约化密度矩阵恰好是最大混合态 I/2,因此纠缠熵恰好为 `1` bit。对于乘积态,纠缠熵恰好为 `0`。 --- @@ -117,9 +117,9 @@ print(metrics.logarithmic_negativity(density, sys_a=[0])) --- -## 两比特 concurrence 和纠缠形成 +## 双比特 concurrence 和纠缠形成 -`concurrence()` 和 `entanglement_of_formation()` 针对两比特密度矩阵。 +`concurrence()` 和 `entanglement_of_formation()` 针对双比特密度矩阵。 ```python from cqlib.qis import DensityMatrix, entropy @@ -132,7 +132,7 @@ print(entropy.concurrence(state)) print(entropy.entanglement_of_formation(state)) ``` -这两个指标适合两比特纠缠分析。对于更多 qubit 的系统,应改用纠缠熵、负性、对数负性或按子系统划分的其他指标。 +这两个指标适合双比特纠缠分析。对于更多 qubit 的系统,应改用纠缠熵、负性、对数负性或按子系统划分的其他指标。 --- @@ -174,6 +174,6 @@ print(metrics.purity_mixed(noisy_density)) ## 下一步 -·[DensityMatrix 混态模拟](2_density_matrix.md):回到密度矩阵构造、部分迹和物理性验证,理解指标输入从哪里来。 -·[DensityMatrixNoise 含噪模拟](3_density_matrix_noise.md):用含噪模拟生成待比较的混态结果。 -·[可视化量子态](../5_visualization/7_state_visualization.md):把数值指标和 Bloch、state city、Pauli vector 图结合起来解释。 +- [DensityMatrix 混态模拟](2_density_matrix.md):回到密度矩阵构造、部分迹和物理性验证,理解指标输入从哪里来。 +- [DensityMatrixNoise 含噪模拟](3_density_matrix_noise.md):用含噪模拟生成待比较的混态结果。 +- [可视化量子态](../5_visualization/7_state_visualization.md):把数值指标和 Bloch、state city、Pauli vector 图结合起来解释。 diff --git a/docs/documentation/1_cqlib/4_compiler/0_overview.md b/docs/documentation/cn/1_cqlib/4_compiler/0_overview.md similarity index 84% rename from docs/documentation/1_cqlib/4_compiler/0_overview.md rename to docs/documentation/cn/1_cqlib/4_compiler/0_overview.md index 4a176824..79458989 100644 --- a/docs/documentation/1_cqlib/4_compiler/0_overview.md +++ b/docs/documentation/cn/1_cqlib/4_compiler/0_overview.md @@ -134,16 +134,19 @@ Enhanced 严格设备编译会在第一次路由前保存不可变的 pre-routin --- -## CompileConfig 要点 +## 编译参数要点 -| 字段 | 作用 | -|------|------| -| `mode` | `normal` / `enhanced` | -| `device` | 可用比特、拓扑、native gates 与可选标定;单独使用时选择严格 `Device` 目标 | -| `target_basis` | 显式目标门集;与 `device` 同时使用时选择 `TopologyBasis`,不承诺设备原生兼容,也不运行 Pareto beam | -| `initial_layout` | 跳过自动布局,直接用给定映射做 SABRE 路由 | -| `resource_policy` | 分解阶段辅助比特策略 | -| `seed` | 启发式布局/路由随机试次 | +`CompileConfig` 本身只包含 `mode`、`target`、`resource_policy` 三项;其余参数在 `compile()` 入口或 `CompileTarget` 的构造方法上设置。 + +| 参数 | 设置位置 | 作用 | +|------|---------|------| +| `mode` | `CompileConfig` | `normal` / `enhanced` | +| `target` | `CompileConfig` | 目标约束,取值见 [`CompileTarget`](../../../../api/cn/python/4_compile/1_compiler.md) | +| `resource_policy` | `CompileConfig` | 分解阶段辅助比特策略 | +| `device` | `compile()` | 可用比特、拓扑、native gates 与可选标定;单独使用时选择严格 `Device` 目标 | +| `target_basis` | `compile()` | 显式目标门集;与 `device` 同时使用时选择 `TopologyBasis`,不承诺设备原生兼容,也不运行 Pareto beam | +| `initial_layout` | `compile()` 或 `CompileTarget` 的构造方法 | 跳过自动布局,直接用给定映射做 SABRE 路由 | +| `seed` | `compile()` 或 `CompileTarget` 的构造方法 | 启发式布局/路由随机试次 | --- @@ -153,7 +156,7 @@ Enhanced 严格设备编译会在第一次路由前保存不可变的 pre-routin ```python from cqlib import Circuit -from cqlib.compile import CompileConfig, CompileMode, CompilerWorkflow +from cqlib.compile import CompileConfig, CompileMode, CompileTarget, CompilerWorkflow from cqlib.device import Device circuits = [] @@ -168,9 +171,11 @@ circuits.append(c2) config = CompileConfig( mode=CompileMode.enhanced(), - device=Device.line("line-8", 8), - target_basis=["H", "CX", "RZ"], - seed=42, + target=CompileTarget.topology_basis( + Device.line("line-8", 8), + ["H", "CX", "RZ"], + seed=42, + ), ) workflow = CompilerWorkflow(config) diff --git a/docs/documentation/1_cqlib/4_compiler/1_layout.md b/docs/documentation/cn/1_cqlib/4_compiler/1_layout.md similarity index 100% rename from docs/documentation/1_cqlib/4_compiler/1_layout.md rename to docs/documentation/cn/1_cqlib/4_compiler/1_layout.md diff --git a/docs/documentation/1_cqlib/4_compiler/2_sabre_mapping.md b/docs/documentation/cn/1_cqlib/4_compiler/2_sabre_mapping.md similarity index 95% rename from docs/documentation/1_cqlib/4_compiler/2_sabre_mapping.md rename to docs/documentation/cn/1_cqlib/4_compiler/2_sabre_mapping.md index f018d612..75aa6943 100644 --- a/docs/documentation/1_cqlib/4_compiler/2_sabre_mapping.md +++ b/docs/documentation/cn/1_cqlib/4_compiler/2_sabre_mapping.md @@ -1,6 +1,6 @@ # SABRE 路由映射 -SABRE 通过启发式 SWAP 将逻辑两比特门路由到设备允许的物理耦合上。 +SABRE 通过启发式 SWAP 将逻辑双比特门路由到设备允许的物理耦合上。 --- @@ -13,6 +13,7 @@ SABRE 通过启发式 SWAP 将逻辑两比特门路由到设备允许的物理 ```python from cqlib import Circuit +from cqlib.circuit import Instruction, StandardGate from cqlib.compile.transform.layout import LayoutObjective from cqlib.compile.transform.routing import route_sabre, route_with_layout from cqlib.compile.sabre import SabreConfig @@ -22,6 +23,10 @@ circuit = Circuit(3) circuit.cx(0, 2) device = Device.line("line-3", 3) +device.native_gates = [ + Instruction.from_standard_gate(StandardGate.H), + Instruction.from_standard_gate(StandardGate.CX), +] objective = LayoutObjective.topology_only() config = SabreConfig.deterministic_seeded(42) diff --git a/docs/documentation/1_cqlib/4_compiler/3_template_optimization.md b/docs/documentation/cn/1_cqlib/4_compiler/3_template_optimization.md similarity index 100% rename from docs/documentation/1_cqlib/4_compiler/3_template_optimization.md rename to docs/documentation/cn/1_cqlib/4_compiler/3_template_optimization.md diff --git a/docs/documentation/1_cqlib/4_compiler/4_commutative_and_clifford.md b/docs/documentation/cn/1_cqlib/4_compiler/4_commutative_and_clifford.md similarity index 99% rename from docs/documentation/1_cqlib/4_compiler/4_commutative_and_clifford.md rename to docs/documentation/cn/1_cqlib/4_compiler/4_commutative_and_clifford.md index 30d2405f..d7bdc303 100644 --- a/docs/documentation/1_cqlib/4_compiler/4_commutative_and_clifford.md +++ b/docs/documentation/cn/1_cqlib/4_compiler/4_commutative_and_clifford.md @@ -152,7 +152,7 @@ print("after:", len(optimized.operations)) 4. 重写器应用取消、合并或归一化规则; 5. 规范化器清理参数、全局相位和表示细节。 -例如 Z 轴门族的对易规则可以把 `RZ`、`S`、`T`、`Phase` 排列到更容易合并或抵消的位置。`CX` 和 `CZ` 的对易规则则能在两比特门附近移动单比特相位门,减少后续目标门集转换中的冗余。 +例如 Z 轴门族的对易规则可以把 `RZ`、`S`、`T`、`Phase` 排列到更容易合并或抵消的位置。`CX` 和 `CZ` 的对易规则则能在双比特门附近移动单比特相位门,减少后续目标门集转换中的冗余。 --- diff --git a/docs/documentation/1_cqlib/5_visualization/0_overview.md b/docs/documentation/cn/1_cqlib/5_visualization/0_overview.md similarity index 98% rename from docs/documentation/1_cqlib/5_visualization/0_overview.md rename to docs/documentation/cn/1_cqlib/5_visualization/0_overview.md index ef8f7f39..3ee89a26 100644 --- a/docs/documentation/1_cqlib/5_visualization/0_overview.md +++ b/docs/documentation/cn/1_cqlib/5_visualization/0_overview.md @@ -64,7 +64,7 @@ draw_figure(circuit, output_path="assets/bell.png") 2. [生成 PNG 线路图](2_draw_figure.md):为 Notebook、文档站和报告生成图形文件。 3. [Notebook 与文档集成](3_notebook_and_docs.md):在 Notebook 和 Markdown 中保存、引用可视化结果。 4. [复杂线路的可视化策略](4_visualization_practices.md):处理参数化线路、映射前后对比和大线路展示。 -5. [控制流与特殊线路结构](5_control_flow_and_special.md):阅读动态控制流、非幺正指令和自定义门图形。 +5. [控制流与特殊线路结构](5_control_flow_and_special.md):阅读动态控制流、非酉指令和自定义门图形。 6. [可视化执行结果](6_result_visualization.md):用柱状图和概率分布查看采样结果。 7. [可视化量子态](7_state_visualization.md):用 Bloch、state city 和 Pauli vector 理解状态。 diff --git a/docs/documentation/1_cqlib/5_visualization/1_draw_text.md b/docs/documentation/cn/1_cqlib/5_visualization/1_draw_text.md similarity index 100% rename from docs/documentation/1_cqlib/5_visualization/1_draw_text.md rename to docs/documentation/cn/1_cqlib/5_visualization/1_draw_text.md diff --git a/docs/documentation/1_cqlib/5_visualization/2_draw_figure.md b/docs/documentation/cn/1_cqlib/5_visualization/2_draw_figure.md similarity index 100% rename from docs/documentation/1_cqlib/5_visualization/2_draw_figure.md rename to docs/documentation/cn/1_cqlib/5_visualization/2_draw_figure.md diff --git a/docs/documentation/1_cqlib/5_visualization/3_notebook_and_docs.md b/docs/documentation/cn/1_cqlib/5_visualization/3_notebook_and_docs.md similarity index 100% rename from docs/documentation/1_cqlib/5_visualization/3_notebook_and_docs.md rename to docs/documentation/cn/1_cqlib/5_visualization/3_notebook_and_docs.md diff --git a/docs/documentation/1_cqlib/5_visualization/4_visualization_practices.md b/docs/documentation/cn/1_cqlib/5_visualization/4_visualization_practices.md similarity index 96% rename from docs/documentation/1_cqlib/5_visualization/4_visualization_practices.md rename to docs/documentation/cn/1_cqlib/5_visualization/4_visualization_practices.md index dd44c5f5..3e6c7a6d 100644 --- a/docs/documentation/1_cqlib/5_visualization/4_visualization_practices.md +++ b/docs/documentation/cn/1_cqlib/5_visualization/4_visualization_practices.md @@ -101,7 +101,12 @@ original.h(0) original.cx(0, 2) original.cx(1, 2) -result = compile(original, device=Device.line("line-3", 3), seed=42) +result = compile( + original, + device=Device.line("line-3", 3), + target_basis=["H", "CX"], + seed=42, +) mapped = result.circuit print("before") @@ -205,6 +210,6 @@ print(state.probabilities()) ## 下一步 -- [控制流与特殊线路结构](5_control_flow_and_special.md):把同样的读图方法用于动态控制流、非幺正指令和自定义门。 +- [控制流与特殊线路结构](5_control_flow_and_special.md):把同样的读图方法用于动态控制流、非酉指令和自定义门。 - [可视化执行结果](6_result_visualization.md):在线路结构确认后,用 histogram 和 distribution 检查采样结果。 - [可视化量子态](7_state_visualization.md):需要解释状态本身时,用 Bloch、state city 和 Pauli vector 补充结构图。 diff --git a/docs/documentation/1_cqlib/5_visualization/5_control_flow_and_special.md b/docs/documentation/cn/1_cqlib/5_visualization/5_control_flow_and_special.md similarity index 88% rename from docs/documentation/1_cqlib/5_visualization/5_control_flow_and_special.md rename to docs/documentation/cn/1_cqlib/5_visualization/5_control_flow_and_special.md index 242e5d1a..edb45803 100644 --- a/docs/documentation/1_cqlib/5_visualization/5_control_flow_and_special.md +++ b/docs/documentation/cn/1_cqlib/5_visualization/5_control_flow_and_special.md @@ -1,6 +1,6 @@ # 控制流与特殊线路结构 -除了基础门和常规参数化线路,Cqlib 的线路图也可以显示动态控制流、非幺正指令和自定义门。遇到这类线路时,读图重点不只是门顺序,还包括控制流块覆盖了哪些 qubit lane、分支在哪里结束、循环中是否存在 `break` / `continue`,以及特殊门标签是否保留了算法含义。 +除了基础门和常规参数化线路,Cqlib 的线路图也可以显示动态控制流、非酉指令和自定义门。遇到这类线路时,读图重点不只是门顺序,还包括控制流块覆盖了哪些 qubit lane、分支在哪里结束、循环中是否存在 `break` / `continue`,以及特殊门标签是否保留了算法含义。 以下示例只生成线路图,不执行真实硬件任务。 @@ -98,7 +98,7 @@ draw_figure(dynamic, fold=10, output_path="assets/dynamic_control_flow.png") ```python from cqlib import Circuit -from cqlib.circuit import ClassicalExpr, ClassicalType +from cqlib.circuit import ClassicalExpr, ClassicalType, Instruction, StandardGate from cqlib.compile import compile from cqlib.device import Device @@ -125,7 +125,13 @@ def loop_body(body): dynamic.while_(keep_running.expr(), loop_body) -compiled = compile(dynamic, device=Device.line("line-2", 2), seed=42) +device = Device.line("line-2", 2) +device.native_gates = [ + Instruction.from_standard_gate(StandardGate.H), + Instruction.from_standard_gate(StandardGate.X), + Instruction.from_standard_gate(StandardGate.Z), +] +compiled = compile(dynamic, device=device, seed=42) ``` 布局和路由会把控制流体作为结构化子线路处理,并递归处理 body 中的门和控制转移标记。线路图只表达结构、分支位置和控制流边界,不表达一次运行时实际走哪条路径,也不估计分支概率或循环次数。 @@ -162,7 +168,7 @@ draw_figure(special, output_path="assets/special_directives_and_gates.png") - `barrier` 是竖线,用于保留分段或调度边界,不表示量子门; - `delay` 会显示等待时长,适合检查硬件时序或空闲段; -- `reset` 显示为重新置到 `|0>`,它是非幺正指令; +- `reset` 显示为重新置到 `|0>`,它是非酉指令; - `fSim` 是双比特参数门,图中会跨越两个作用 qubit; - 多控制门按“控制位在前、目标位在后”的顺序绘制; - 自定义 `UnitaryGate` 优先显示用户给出的 label,例如这里的 `Oracle`。 diff --git a/docs/documentation/1_cqlib/5_visualization/6_result_visualization.md b/docs/documentation/cn/1_cqlib/5_visualization/6_result_visualization.md similarity index 98% rename from docs/documentation/1_cqlib/5_visualization/6_result_visualization.md rename to docs/documentation/cn/1_cqlib/5_visualization/6_result_visualization.md index 10229b0a..fde1929e 100644 --- a/docs/documentation/1_cqlib/5_visualization/6_result_visualization.md +++ b/docs/documentation/cn/1_cqlib/5_visualization/6_result_visualization.md @@ -43,7 +43,7 @@ plot_histogram( ![Bell-state counts](assets/bell_counts.png) -计数图适合展示真实 shot 数。比较不同 shot 设置、硬件任务或误差缓解前后的原始采样量时,优先使用 histogram。读图时先看主峰,再看是否存在本不应出现的 bitstring。 +计数图适合展示真实 shot 数。比较不同 shot 设置、硬件任务或错误缓解前后的原始采样量时,优先使用 histogram。读图时先看主峰,再看是否存在本不应出现的 bitstring。 --- diff --git a/docs/documentation/1_cqlib/5_visualization/7_state_visualization.md b/docs/documentation/cn/1_cqlib/5_visualization/7_state_visualization.md similarity index 97% rename from docs/documentation/1_cqlib/5_visualization/7_state_visualization.md rename to docs/documentation/cn/1_cqlib/5_visualization/7_state_visualization.md index c4f503f0..539b52ea 100644 --- a/docs/documentation/1_cqlib/5_visualization/7_state_visualization.md +++ b/docs/documentation/cn/1_cqlib/5_visualization/7_state_visualization.md @@ -115,7 +115,7 @@ plot_state_paulivec(density, output_path="assets/plus_density_paulivec.png") ![Density-matrix Pauli vector](assets/plus_density_paulivec.png) -使用状态图时,需要先明确输入是状态向量还是密度矩阵。前者通常表示理想纯态,后者可以表达混态和噪声后的状态。 +使用状态图时,需要先明确输入是态矢量还是密度矩阵。前者通常表示理想纯态,后者可以表达混态和噪声后的状态。 下面构造一个简单混态,展示 state city 如何暴露对角结构: @@ -182,7 +182,7 @@ Bell 态的 state city 图如下: 多比特 state city 和 Pauli vector 的横轴标签依赖 bit order。需要与论文、硬件后端或其他 SDK 的显示习惯对齐时,可以用 `reverse_bits=True` 生成对照图。 -下面构造一个非对称的 2-qubit 对角密度矩阵,使 `01` 和 `10` 的权重不同: +下面构造一个非对称的 two-qubit 对角密度矩阵,使 `01` 和 `10` 的权重不同: ```python asymmetric = DensityMatrix.from_density_matrix( diff --git a/docs/documentation/1_cqlib/5_visualization/assets/ansatz_decomposed.png b/docs/documentation/cn/1_cqlib/5_visualization/assets/ansatz_decomposed.png similarity index 100% rename from docs/documentation/1_cqlib/5_visualization/assets/ansatz_decomposed.png rename to docs/documentation/cn/1_cqlib/5_visualization/assets/ansatz_decomposed.png diff --git a/docs/documentation/1_cqlib/5_visualization/assets/ansatz_modules.png b/docs/documentation/cn/1_cqlib/5_visualization/assets/ansatz_modules.png similarity index 100% rename from docs/documentation/1_cqlib/5_visualization/assets/ansatz_modules.png rename to docs/documentation/cn/1_cqlib/5_visualization/assets/ansatz_modules.png diff --git a/docs/documentation/1_cqlib/5_visualization/assets/ansatz_overview.png b/docs/documentation/cn/1_cqlib/5_visualization/assets/ansatz_overview.png similarity index 100% rename from docs/documentation/1_cqlib/5_visualization/assets/ansatz_overview.png rename to docs/documentation/cn/1_cqlib/5_visualization/assets/ansatz_overview.png diff --git a/docs/documentation/1_cqlib/5_visualization/assets/ansatz_structure.png b/docs/documentation/cn/1_cqlib/5_visualization/assets/ansatz_structure.png similarity index 100% rename from docs/documentation/1_cqlib/5_visualization/assets/ansatz_structure.png rename to docs/documentation/cn/1_cqlib/5_visualization/assets/ansatz_structure.png diff --git a/docs/documentation/1_cqlib/5_visualization/assets/ansatz_with_params.png b/docs/documentation/cn/1_cqlib/5_visualization/assets/ansatz_with_params.png similarity index 100% rename from docs/documentation/1_cqlib/5_visualization/assets/ansatz_with_params.png rename to docs/documentation/cn/1_cqlib/5_visualization/assets/ansatz_with_params.png diff --git a/docs/documentation/1_cqlib/5_visualization/assets/asymmetric_paulivec.png b/docs/documentation/cn/1_cqlib/5_visualization/assets/asymmetric_paulivec.png similarity index 100% rename from docs/documentation/1_cqlib/5_visualization/assets/asymmetric_paulivec.png rename to docs/documentation/cn/1_cqlib/5_visualization/assets/asymmetric_paulivec.png diff --git a/docs/documentation/1_cqlib/5_visualization/assets/asymmetric_paulivec_report.png b/docs/documentation/cn/1_cqlib/5_visualization/assets/asymmetric_paulivec_report.png similarity index 100% rename from docs/documentation/1_cqlib/5_visualization/assets/asymmetric_paulivec_report.png rename to docs/documentation/cn/1_cqlib/5_visualization/assets/asymmetric_paulivec_report.png diff --git a/docs/documentation/1_cqlib/5_visualization/assets/asymmetric_paulivec_reverse_bits.png b/docs/documentation/cn/1_cqlib/5_visualization/assets/asymmetric_paulivec_reverse_bits.png similarity index 100% rename from docs/documentation/1_cqlib/5_visualization/assets/asymmetric_paulivec_reverse_bits.png rename to docs/documentation/cn/1_cqlib/5_visualization/assets/asymmetric_paulivec_reverse_bits.png diff --git a/docs/documentation/1_cqlib/5_visualization/assets/asymmetric_state_city.png b/docs/documentation/cn/1_cqlib/5_visualization/assets/asymmetric_state_city.png similarity index 100% rename from docs/documentation/1_cqlib/5_visualization/assets/asymmetric_state_city.png rename to docs/documentation/cn/1_cqlib/5_visualization/assets/asymmetric_state_city.png diff --git a/docs/documentation/1_cqlib/5_visualization/assets/asymmetric_state_city_reverse_bits.png b/docs/documentation/cn/1_cqlib/5_visualization/assets/asymmetric_state_city_reverse_bits.png similarity index 100% rename from docs/documentation/1_cqlib/5_visualization/assets/asymmetric_state_city_reverse_bits.png rename to docs/documentation/cn/1_cqlib/5_visualization/assets/asymmetric_state_city_reverse_bits.png diff --git a/docs/documentation/1_cqlib/5_visualization/assets/bell.png b/docs/documentation/cn/1_cqlib/5_visualization/assets/bell.png similarity index 100% rename from docs/documentation/1_cqlib/5_visualization/assets/bell.png rename to docs/documentation/cn/1_cqlib/5_visualization/assets/bell.png diff --git a/docs/documentation/1_cqlib/5_visualization/assets/bell_bloch_multivector.png b/docs/documentation/cn/1_cqlib/5_visualization/assets/bell_bloch_multivector.png similarity index 100% rename from docs/documentation/1_cqlib/5_visualization/assets/bell_bloch_multivector.png rename to docs/documentation/cn/1_cqlib/5_visualization/assets/bell_bloch_multivector.png diff --git a/docs/documentation/1_cqlib/5_visualization/assets/bell_counts.png b/docs/documentation/cn/1_cqlib/5_visualization/assets/bell_counts.png similarity index 100% rename from docs/documentation/1_cqlib/5_visualization/assets/bell_counts.png rename to docs/documentation/cn/1_cqlib/5_visualization/assets/bell_counts.png diff --git a/docs/documentation/1_cqlib/5_visualization/assets/bell_distribution.png b/docs/documentation/cn/1_cqlib/5_visualization/assets/bell_distribution.png similarity index 100% rename from docs/documentation/1_cqlib/5_visualization/assets/bell_distribution.png rename to docs/documentation/cn/1_cqlib/5_visualization/assets/bell_distribution.png diff --git a/docs/documentation/1_cqlib/5_visualization/assets/bell_dominant_outcomes.png b/docs/documentation/cn/1_cqlib/5_visualization/assets/bell_dominant_outcomes.png similarity index 100% rename from docs/documentation/1_cqlib/5_visualization/assets/bell_dominant_outcomes.png rename to docs/documentation/cn/1_cqlib/5_visualization/assets/bell_dominant_outcomes.png diff --git a/docs/documentation/1_cqlib/5_visualization/assets/bell_gate.png b/docs/documentation/cn/1_cqlib/5_visualization/assets/bell_gate.png similarity index 100% rename from docs/documentation/1_cqlib/5_visualization/assets/bell_gate.png rename to docs/documentation/cn/1_cqlib/5_visualization/assets/bell_gate.png diff --git a/docs/documentation/1_cqlib/5_visualization/assets/bell_gate_decomposed.png b/docs/documentation/cn/1_cqlib/5_visualization/assets/bell_gate_decomposed.png similarity index 100% rename from docs/documentation/1_cqlib/5_visualization/assets/bell_gate_decomposed.png rename to docs/documentation/cn/1_cqlib/5_visualization/assets/bell_gate_decomposed.png diff --git a/docs/documentation/1_cqlib/5_visualization/assets/bell_initial_state.png b/docs/documentation/cn/1_cqlib/5_visualization/assets/bell_initial_state.png similarity index 100% rename from docs/documentation/1_cqlib/5_visualization/assets/bell_initial_state.png rename to docs/documentation/cn/1_cqlib/5_visualization/assets/bell_initial_state.png diff --git a/docs/documentation/1_cqlib/5_visualization/assets/bell_report_counts.png b/docs/documentation/cn/1_cqlib/5_visualization/assets/bell_report_counts.png similarity index 100% rename from docs/documentation/1_cqlib/5_visualization/assets/bell_report_counts.png rename to docs/documentation/cn/1_cqlib/5_visualization/assets/bell_report_counts.png diff --git a/docs/documentation/1_cqlib/5_visualization/assets/bell_reverse_bits.png b/docs/documentation/cn/1_cqlib/5_visualization/assets/bell_reverse_bits.png similarity index 100% rename from docs/documentation/1_cqlib/5_visualization/assets/bell_reverse_bits.png rename to docs/documentation/cn/1_cqlib/5_visualization/assets/bell_reverse_bits.png diff --git a/docs/documentation/1_cqlib/5_visualization/assets/bell_state_city.png b/docs/documentation/cn/1_cqlib/5_visualization/assets/bell_state_city.png similarity index 100% rename from docs/documentation/1_cqlib/5_visualization/assets/bell_state_city.png rename to docs/documentation/cn/1_cqlib/5_visualization/assets/bell_state_city.png diff --git a/docs/documentation/1_cqlib/5_visualization/assets/bloch_x.png b/docs/documentation/cn/1_cqlib/5_visualization/assets/bloch_x.png similarity index 100% rename from docs/documentation/1_cqlib/5_visualization/assets/bloch_x.png rename to docs/documentation/cn/1_cqlib/5_visualization/assets/bloch_x.png diff --git a/docs/documentation/1_cqlib/5_visualization/assets/deep_folded.png b/docs/documentation/cn/1_cqlib/5_visualization/assets/deep_folded.png similarity index 100% rename from docs/documentation/1_cqlib/5_visualization/assets/deep_folded.png rename to docs/documentation/cn/1_cqlib/5_visualization/assets/deep_folded.png diff --git a/docs/documentation/1_cqlib/5_visualization/assets/dynamic_control_flow.png b/docs/documentation/cn/1_cqlib/5_visualization/assets/dynamic_control_flow.png similarity index 100% rename from docs/documentation/1_cqlib/5_visualization/assets/dynamic_control_flow.png rename to docs/documentation/cn/1_cqlib/5_visualization/assets/dynamic_control_flow.png diff --git a/docs/documentation/1_cqlib/5_visualization/assets/mapped_after.png b/docs/documentation/cn/1_cqlib/5_visualization/assets/mapped_after.png similarity index 100% rename from docs/documentation/1_cqlib/5_visualization/assets/mapped_after.png rename to docs/documentation/cn/1_cqlib/5_visualization/assets/mapped_after.png diff --git a/docs/documentation/1_cqlib/5_visualization/assets/mapped_before.png b/docs/documentation/cn/1_cqlib/5_visualization/assets/mapped_before.png similarity index 100% rename from docs/documentation/1_cqlib/5_visualization/assets/mapped_before.png rename to docs/documentation/cn/1_cqlib/5_visualization/assets/mapped_before.png diff --git a/docs/documentation/1_cqlib/5_visualization/assets/mixed_state_city.png b/docs/documentation/cn/1_cqlib/5_visualization/assets/mixed_state_city.png similarity index 100% rename from docs/documentation/1_cqlib/5_visualization/assets/mixed_state_city.png rename to docs/documentation/cn/1_cqlib/5_visualization/assets/mixed_state_city.png diff --git a/docs/documentation/1_cqlib/5_visualization/assets/plus_bloch.png b/docs/documentation/cn/1_cqlib/5_visualization/assets/plus_bloch.png similarity index 100% rename from docs/documentation/1_cqlib/5_visualization/assets/plus_bloch.png rename to docs/documentation/cn/1_cqlib/5_visualization/assets/plus_bloch.png diff --git a/docs/documentation/1_cqlib/5_visualization/assets/plus_density_city.png b/docs/documentation/cn/1_cqlib/5_visualization/assets/plus_density_city.png similarity index 100% rename from docs/documentation/1_cqlib/5_visualization/assets/plus_density_city.png rename to docs/documentation/cn/1_cqlib/5_visualization/assets/plus_density_city.png diff --git a/docs/documentation/1_cqlib/5_visualization/assets/plus_density_paulivec.png b/docs/documentation/cn/1_cqlib/5_visualization/assets/plus_density_paulivec.png similarity index 100% rename from docs/documentation/1_cqlib/5_visualization/assets/plus_density_paulivec.png rename to docs/documentation/cn/1_cqlib/5_visualization/assets/plus_density_paulivec.png diff --git a/docs/documentation/1_cqlib/5_visualization/assets/plus_paulivec.png b/docs/documentation/cn/1_cqlib/5_visualization/assets/plus_paulivec.png similarity index 100% rename from docs/documentation/1_cqlib/5_visualization/assets/plus_paulivec.png rename to docs/documentation/cn/1_cqlib/5_visualization/assets/plus_paulivec.png diff --git a/docs/documentation/1_cqlib/5_visualization/assets/plus_state_city.png b/docs/documentation/cn/1_cqlib/5_visualization/assets/plus_state_city.png similarity index 100% rename from docs/documentation/1_cqlib/5_visualization/assets/plus_state_city.png rename to docs/documentation/cn/1_cqlib/5_visualization/assets/plus_state_city.png diff --git a/docs/documentation/1_cqlib/5_visualization/assets/qaoa_cost.png b/docs/documentation/cn/1_cqlib/5_visualization/assets/qaoa_cost.png similarity index 100% rename from docs/documentation/1_cqlib/5_visualization/assets/qaoa_cost.png rename to docs/documentation/cn/1_cqlib/5_visualization/assets/qaoa_cost.png diff --git a/docs/documentation/1_cqlib/5_visualization/assets/qaoa_mixer.png b/docs/documentation/cn/1_cqlib/5_visualization/assets/qaoa_mixer.png similarity index 100% rename from docs/documentation/1_cqlib/5_visualization/assets/qaoa_mixer.png rename to docs/documentation/cn/1_cqlib/5_visualization/assets/qaoa_mixer.png diff --git a/docs/documentation/1_cqlib/5_visualization/assets/qaoa_prepare.png b/docs/documentation/cn/1_cqlib/5_visualization/assets/qaoa_prepare.png similarity index 100% rename from docs/documentation/1_cqlib/5_visualization/assets/qaoa_prepare.png rename to docs/documentation/cn/1_cqlib/5_visualization/assets/qaoa_prepare.png diff --git a/docs/documentation/1_cqlib/5_visualization/assets/search_hamming_counts.png b/docs/documentation/cn/1_cqlib/5_visualization/assets/search_hamming_counts.png similarity index 100% rename from docs/documentation/1_cqlib/5_visualization/assets/search_hamming_counts.png rename to docs/documentation/cn/1_cqlib/5_visualization/assets/search_hamming_counts.png diff --git a/docs/documentation/1_cqlib/5_visualization/assets/special_directives_and_gates.png b/docs/documentation/cn/1_cqlib/5_visualization/assets/special_directives_and_gates.png similarity index 100% rename from docs/documentation/1_cqlib/5_visualization/assets/special_directives_and_gates.png rename to docs/documentation/cn/1_cqlib/5_visualization/assets/special_directives_and_gates.png diff --git a/docs/documentation/1_cqlib/6_error_mitigation/0_overview.md b/docs/documentation/cn/1_cqlib/6_error_mitigation/0_overview.md similarity index 93% rename from docs/documentation/1_cqlib/6_error_mitigation/0_overview.md rename to docs/documentation/cn/1_cqlib/6_error_mitigation/0_overview.md index d7792fbd..2a8aae16 100644 --- a/docs/documentation/1_cqlib/6_error_mitigation/0_overview.md +++ b/docs/documentation/cn/1_cqlib/6_error_mitigation/0_overview.md @@ -1,12 +1,12 @@ -# 误差缓解 +# 错误缓解 -Cqlib Python 绑定以 **`cqlib.error_mitigation`** 提供期望值层面的误差缓解能力,当前支持: +Cqlib Python 绑定以 **`cqlib.error_mitigation`** 提供期望值层面的错误缓解能力,当前支持: - **零噪声外推(ZNE)**:通过门折叠构造不同噪声强度下的线路族,再外推到零噪声极限; - **虚拟蒸馏(Virtual Distillation)**:通过 copy-swap 线路估计 `Tr(O ρ^M) / Tr(ρ^M)`; - **统一流水线(`ErrorMitigation`)**:按 `run()` → `get_mitigated()` 顺序封装上述方法。 -误差缓解用于降低**观测量期望值**的系统性偏差,不是量子纠错;通常会增加线路执行次数,且需要用户自行提供 **estimator**(后端或模拟器回调)。 +错误缓解用于降低**观测量期望值**的系统性偏差,不是量子纠错;通常会增加线路执行次数,且需要用户自行提供 **estimator**(后端或模拟器回调)。 --- @@ -127,7 +127,7 @@ MitigatedResult(expectation, variance?) - 本模块聚焦 **ZNE** 与 **Virtual Distillation**; - **读出误差矫正** 不在此模块,见 Tianyan 章节 [`5_readout_mitigation.md`](../7_tianyan/5_readout_mitigation.md); -- 每个 `ErrorMitigation` 实例只能 `run()` 一次、`get_mitigated()` 一次。 +- 每个 `ErrorMitigation` 实例只能 `run()` 一次;`get_mitigated()` 可对同一批估计值重复执行,比较不同后处理参数。 --- diff --git a/docs/documentation/1_cqlib/6_error_mitigation/1_zne.md b/docs/documentation/cn/1_cqlib/6_error_mitigation/1_zne.md similarity index 100% rename from docs/documentation/1_cqlib/6_error_mitigation/1_zne.md rename to docs/documentation/cn/1_cqlib/6_error_mitigation/1_zne.md diff --git a/docs/documentation/1_cqlib/6_error_mitigation/2_virtual_distillation.md b/docs/documentation/cn/1_cqlib/6_error_mitigation/2_virtual_distillation.md similarity index 98% rename from docs/documentation/1_cqlib/6_error_mitigation/2_virtual_distillation.md rename to docs/documentation/cn/1_cqlib/6_error_mitigation/2_virtual_distillation.md index fa712be4..9544d1e8 100644 --- a/docs/documentation/1_cqlib/6_error_mitigation/2_virtual_distillation.md +++ b/docs/documentation/cn/1_cqlib/6_error_mitigation/2_virtual_distillation.md @@ -52,7 +52,7 @@ vd = em.VirtualDistillation(circuit, copies=2) copy_swap = vd.build_copy_swap_circuit() ``` -- 输出线路宽度为 `copies`; +- 输出线路宽度为 `copies` × 基础线路宽度; - 内部通过 pairwise `SWAP` 耦合各拷贝寄存器; - 原始 `circuit` 不会被修改。 diff --git a/docs/documentation/1_cqlib/6_error_mitigation/3_unified_api.md b/docs/documentation/cn/1_cqlib/6_error_mitigation/3_unified_api.md similarity index 85% rename from docs/documentation/1_cqlib/6_error_mitigation/3_unified_api.md rename to docs/documentation/cn/1_cqlib/6_error_mitigation/3_unified_api.md index 53dce359..1da35aa8 100644 --- a/docs/documentation/1_cqlib/6_error_mitigation/3_unified_api.md +++ b/docs/documentation/cn/1_cqlib/6_error_mitigation/3_unified_api.md @@ -19,14 +19,30 @@ from cqlib.qis import Hamiltonian, PauliString 每个 `ErrorMitigation` 实例遵循固定生命周期: ```text -创建 → run() → get_mitigated() → 结束 +创建 → run() → get_mitigated()(可重复) ``` | 规则 | 说明 | |------|------| | `run()` 只能调用一次 | 重复调用抛出 `ErrorMitigationError` | | 必须先 `run()` 再 `get_mitigated()` | 否则抛出 `ErrorMitigationError` | -| `get_mitigated()` 只能调用一次 | 重复调用抛出 `ErrorMitigationError` | +| `get_mitigated()` 可重复调用 | 每次使用同一批估计值,不再次调用 estimator | + +可以在同一个实例上比较不同的外推设置: + +```python +# 接续下方 ZNE 示例,成功执行 mitigation.run(...) 后: +linear = mitigation.get_mitigated( + em.ProcessArgs.zne(em.ExtrapolateMethod.polynomial(), degree=1) +) +quadratic = mitigation.get_mitigated( + em.ProcessArgs.zne(em.ExtrapolateMethod.polynomial(), degree=2) +) +``` + +后处理参数错误不会消耗已经采集的估计值,修正参数后可以重试。 +成功后也可以再次处理;`run()` 仍然拒绝重复采集。 +内部只保留后处理需要的统计量,生成的折叠线路和副本线路不会长期保留。 --- diff --git a/docs/documentation/1_cqlib/7_tianyan/0_overview.md b/docs/documentation/cn/1_cqlib/7_tianyan/0_overview.md similarity index 100% rename from docs/documentation/1_cqlib/7_tianyan/0_overview.md rename to docs/documentation/cn/1_cqlib/7_tianyan/0_overview.md diff --git a/docs/documentation/1_cqlib/7_tianyan/1_auth_config.md b/docs/documentation/cn/1_cqlib/7_tianyan/1_auth_config.md similarity index 100% rename from docs/documentation/1_cqlib/7_tianyan/1_auth_config.md rename to docs/documentation/cn/1_cqlib/7_tianyan/1_auth_config.md diff --git a/docs/documentation/1_cqlib/7_tianyan/2_backend_device.md b/docs/documentation/cn/1_cqlib/7_tianyan/2_backend_device.md similarity index 100% rename from docs/documentation/1_cqlib/7_tianyan/2_backend_device.md rename to docs/documentation/cn/1_cqlib/7_tianyan/2_backend_device.md diff --git a/docs/documentation/1_cqlib/7_tianyan/3_task_result.md b/docs/documentation/cn/1_cqlib/7_tianyan/3_task_result.md similarity index 97% rename from docs/documentation/1_cqlib/7_tianyan/3_task_result.md rename to docs/documentation/cn/1_cqlib/7_tianyan/3_task_result.md index ee50d54b..d2bcf7ce 100644 --- a/docs/documentation/1_cqlib/7_tianyan/3_task_result.md +++ b/docs/documentation/cn/1_cqlib/7_tianyan/3_task_result.md @@ -6,6 +6,8 @@ Python 绑定中的任务提交接口接收 QCIS 字符串列表: +下文示例中的 `backend` 为 `TianyanBackend` 实例,获取方式见 [后端与设备配置](2_backend_device.md)。 + ```python qcis = "H Q1\nM Q1" task = backend.run([qcis], shots=1000) diff --git a/docs/documentation/1_cqlib/7_tianyan/4_qcis_ir_workflow.md b/docs/documentation/cn/1_cqlib/7_tianyan/4_qcis_ir_workflow.md similarity index 100% rename from docs/documentation/1_cqlib/7_tianyan/4_qcis_ir_workflow.md rename to docs/documentation/cn/1_cqlib/7_tianyan/4_qcis_ir_workflow.md diff --git a/docs/documentation/1_cqlib/7_tianyan/5_readout_mitigation.md b/docs/documentation/cn/1_cqlib/7_tianyan/5_readout_mitigation.md similarity index 95% rename from docs/documentation/1_cqlib/7_tianyan/5_readout_mitigation.md rename to docs/documentation/cn/1_cqlib/7_tianyan/5_readout_mitigation.md index 0e1c5ef2..a356909d 100644 --- a/docs/documentation/1_cqlib/7_tianyan/5_readout_mitigation.md +++ b/docs/documentation/cn/1_cqlib/7_tianyan/5_readout_mitigation.md @@ -2,6 +2,8 @@ 真实量子设备的测量存在读取误差。`cqlib-tianyan` 在获取结果时可以利用设备校准数据对测量计数做读取误差矫正。该能力由 `CalibrationMode` 控制。 +下文示例中的 `backend` 为 `TianyanBackend` 实例,获取方式见 [后端与设备配置](2_backend_device.md)。 + 对应导入: ```python @@ -177,6 +179,6 @@ device = backend.device_config() ## 下一步 -- [Cqlib 教程总览](../../README.md):回到教程目录,继续查看其他 Cqlib 模块。 +- [Cqlib 教程入口](../../0_get_started/0_introduction.md):回到教程起点,继续查看其他 Cqlib 模块。 - [QCIS 格式说明](../1_ir/1_qcis.md):了解 QCIS 文本格式、导入导出接口和支持边界。 - [设备模块](../2_device/0_overview.md):了解设备拓扑、设备配置、噪声模型和执行结果对象。 diff --git a/docs/documentation/en/0_get_started/0_introduction.md b/docs/documentation/en/0_get_started/0_introduction.md new file mode 100644 index 00000000..7a428929 --- /dev/null +++ b/docs/documentation/en/0_get_started/0_introduction.md @@ -0,0 +1,74 @@ +# Introduction to Cqlib + +## Welcome to Cqlib! + +Cqlib is a quantum computing software development kit (SDK) independently developed by China Telecom Quantum Group and built for engineering practice. It provides a unified, stable and high-performance programming interface and representation capability for quantum circuits. + +This documentation set covers the [installation tutorial](1_installation.md), the [quickstart](2_quickstart.md), [scenario-based guides](../1_cqlib/0_circuit/0_overview.md) and the [API reference](../../../api/en/python/0_overview.md) for multiple programming languages, aiming to make the construction, execution and visualization of quantum computing tasks smoother and to improve development efficiency and the overall experience. + +--- + + +## Why choose Cqlib? + +- **Extreme performance, Rust-driven** + The core layer is built in Rust, ensuring high-concurrency processing capability and memory safety for [circuit compilation](../1_cqlib/4_compiler/0_overview.md) and [intermediate representation](../1_cqlib/1_ir/0_overview.md) conversion, and providing a solid foundation for large-scale quantum circuit simulation and complex logic processing. + +- **Native ecosystem, QCIS integration** + Deep integration with the independently developed quantum instruction set [QCIS](../1_cqlib/1_ir/1_qcis.md), supporting bidirectional import and export between circuits and QCIS text. + +- **Multi-language coordination, developer-friendly** + Through the jointly designed, highly abstracted [Python interface](../../../api/en/python/0_overview.md) and low-level [C interface](../../../api/en/c/0_overview.md), algorithm development efficiency and system integration stability are both addressed, supporting cross-platform and cross-language mixed programming scenarios. + +- **Unified architecture, flexible extension** + A unified [intermediate representation](../1_cqlib/1_ir/0_overview.md) supports the construction of maintainable and extensible quantum software systems, covering the full path from prototype validation to enterprise application development. + +--- + +## What programming languages does Cqlib use? + +Cqlib uses three languages, Rust, Python and C, and follows the design idea of "a unified core with layered interfaces", ensuring consistency of capability while accommodating the usage habits of different development scenarios: + +- **[Rust](../../../api/en/rust/0_overview.md)** + As the core implementation layer, it carries key capabilities such as quantum circuits, gates and instructions, the parameter system, intermediate representation, device modeling, quantum information, error mitigation and compilation and optimization. + +- **[Python](../../../api/en/python/0_overview.md)** + Aimed at algorithm development and rapid prototyping, providing a more efficient and intuitive Python interface, convenient for interactive use and fast iteration. + +- **[C](../../../api/en/c/0_overview.md)** + Aimed at system integration and cross-language invocation, providing a stable C interface, convenient for deployment in heterogeneous projects. + +--- + +## What can Cqlib do? + +The core capabilities of Cqlib consist of the following eight modules: + +- [Quantum Circuit](../1_cqlib/0_circuit/0_overview.md) + For creating and managing quantum circuits; supports basic functionality such as adding quantum gates, parameterized circuits, control flow, circuit composition, matrix conversion and structural analysis. It is the main entry point for building quantum programs with Cqlib. +- [Intermediate Representation](../1_cqlib/1_ir/0_overview.md) + For converting between circuit objects and text formats; supports the QCIS, OpenQASM 2.0 and OpenQASM 3.0 formats, facilitating circuit storage, exchange across toolchains and subsequent compilation. +- [Device](../1_cqlib/2_device/0_overview.md) + For describing information related to quantum devices, including device topology, the mapping from logical qubits to physical qubits, calibration properties, noise models and execution results, providing the data foundation for backend adaptation and noise-aware compilation. +- [Quantum Information](../1_cqlib/3_qis/0_overview.md) + Provides tools related to quantum states, operators and quantum information processing, including statevector, density matrix, stabilizer simulation, Pauli operators, Hamiltonian, quantum evolution and information metrics. +- [Compilation and optimization](../1_cqlib/4_compiler/0_overview.md) + For transforming high-level quantum circuits into circuit forms better suited to execution on a target device; supports gate decomposition, topological layout, routing mapping, template matching, swap optimization and Clifford-related optimization. +- [Visualization](../1_cqlib/5_visualization/0_overview.md) + For displaying the structure of quantum circuits; supports Unicode text diagrams and SVG rendering, making it easy to inspect circuit hierarchy, gate operation order and overall structure. +- [Error Mitigation](../1_cqlib/6_error_mitigation/0_overview.md) + For reducing the impact of noise on quantum computation results; provides error mitigation capabilities such as zero-noise extrapolation (ZNE) and virtual distillation, suitable for noisy simulation or for processing results from real device experiments. +- [Tianyan quantum cloud platform client](../1_cqlib/7_tianyan/0_overview.md) + For connecting to the Tianyan quantum cloud platform; supports platform authentication, backend queries, device configuration retrieval, QCIS task submission, task status polling, execution result parsing and readout error correction, suitable for submitting locally built circuits to cloud real devices or platform backends. + +--- + + +## Next steps + +To start using Cqlib, the following reading order is recommended: + +- [Installation guide](1_installation.md): installation and setup in a single step +- [Quickstart](2_quickstart.md): the first quantum circuit, from "0" to "1" +- [Quantum Circuit](../1_cqlib/0_circuit/0_overview.md): learn about the basic module for describing quantum programs in Cqlib. +- [Tianyan quantum cloud platform client](../1_cqlib/7_tianyan/0_overview.md): learn how to log in to the platform, select a backend, submit QCIS tasks and obtain cloud execution results. diff --git a/docs/documentation/en/0_get_started/1_installation.md b/docs/documentation/en/0_get_started/1_installation.md new file mode 100644 index 00000000..74a39b3a --- /dev/null +++ b/docs/documentation/en/0_get_started/1_installation.md @@ -0,0 +1,142 @@ +# Cqlib Installation and Environment Setup + +This guide covers the installation of Cqlib and the setup of a development environment. + +Before installing Cqlib, ensure that the environment meets the following conditions: + +- Python environment: Python 3.10 – 3.14 is supported (a 64-bit build is recommended); +- Operating system: + - Linux + - macOS + - Windows + +--- + +## Option A: quick installation with `pip` (recommended) + +For most users, installing directly with `pip` is recommended. + + +> Applies to: algorithm development, prototype validation. +> Note: this option already includes prebuilt binaries; no Rust or C compiler is required. + +### Step 1: create and activate a virtual environment + +Working in an isolated environment is recommended to avoid dependency conflicts: + +```bash +python -m venv cqlib-env + +# activate the environment (Windows) +cqlib-env\Scripts\activate +# activate the environment (Linux/macOS) +source cqlib-env/bin/activate +``` + +### Step 2: one-step installation with `pip` + +```bash +pip install cqlib +``` + +### Step 3: verify the installation + +After installation completes, verify it as follows: + +```bash +python -m pip show cqlib +``` +If the version information is printed correctly, the installation succeeded. + +--- + +## Option B: build from source (for developers) + +To take part in the development of Cqlib, or to use the latest features that have not yet been released, build from source with the following method. + +> Applies to: customized development, code contributions or the latest features. +> Note: this option requires a local compilation environment. + +### Step 1: set up the compilation toolchain + +Before building, install the following required components according to the operating system: + +- Install the Rust toolchain (Stable 1.89+): + + - Windows: visit the official website: https://www.rust-lang.org/tools/install to download and run the installer. + - Linux/macOS: run `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh` + +- Install a C compiler: + - Windows: install the Visual Studio Build Tools and select "Desktop development with C++". + - Linux: install `build-essential` (Ubuntu/Debian) or `base-devel` (Arch). + - macOS: run `xcode-select --install` in the terminal. + +After installation completes, verify with the following commands: + +```bash +rustc --version +cargo --version +``` +If the version numbers are printed correctly, Rust was installed successfully. + +### Step 2: obtain the source code + +Clone the repository from Gitee to the local machine: +```bash +git clone https://gitee.com/zdxlz/cqlib2.git +cd cqlib +``` + +### Step 3: build and install the Python bindings + +Use the `maturin` cross-language tool to compile the Rust core into a Python module: + +```bash +# 1. install the build tools +pip install -U maturin + +# 2. compile and install into the current environment +# the --release flag ensures the best runtime performance +maturin develop --release -m crates/binding-python/Cargo.toml +``` +This command installs the locally built version into the current virtual environment. + +### Step 4: build the Rust core and the C interface (optional) + +```bash +# build the core library +cargo build --release + +# build the C interface ABI (optional) +cargo build -p binding-c --release +``` + +--- + +## Optional module: install the Tianyan quantum cloud platform client + +To submit local circuits to the Tianyan quantum cloud platform for execution, install `cqlib-tianyan` as well. This module is independent of the Cqlib core package and is responsible for platform authentication, backend queries, task submission, task polling and result parsing. + +```bash +pip install cqlib-tianyan +``` + +After installation, verify it as follows: + +```python +from cqlib_tianyan import TianyanPlatform +print(TianyanPlatform) +``` + +`cqlib-tianyan` is usually used together with `cqlib`: build circuits with `cqlib.circuit`, export QCIS with `cqlib.ir.qcis`, and finally submit to a cloud backend through `cqlib_tianyan`. + +--- + +## Next steps + +After installation, the following content is recommended for further reading: + +- [Quickstart](2_quickstart.md): the first quantum circuit, from "0" to "1" +- [Quantum Circuit](../1_cqlib/0_circuit/0_overview.md): learn about the basic module for describing quantum programs in Cqlib. +- [Quantum gates and instructions](../1_cqlib/0_circuit/1_gates.md): learn about built-in gates, custom gates, composite gates and non-unitary instructions. +- [Tianyan quantum cloud platform client](../1_cqlib/7_tianyan/0_overview.md): learn about cloud backend access, task submission and result retrieval. diff --git a/docs/documentation/en/0_get_started/2_quickstart.md b/docs/documentation/en/0_get_started/2_quickstart.md new file mode 100644 index 00000000..8f3c04a8 --- /dev/null +++ b/docs/documentation/en/0_get_started/2_quickstart.md @@ -0,0 +1,122 @@ +# Quickstart + +This guide builds a minimal working quantum circuit with Cqlib and completes an end-to-end "engineering loop" workflow. + +Before starting, ensure that the environment is ready: + +- Python environment: Python 3.10 – 3.14 is supported +- Cqlib is installed and the environment is configured (for installation, see "[Cqlib Installation and Environment Setup](../0_get_started/1_installation.md)") + +--- + +## The first quantum circuit: Bell state + +This section uses a minimal but complete Bell state example to introduce the basic workflow of building a quantum circuit with Cqlib. The example covers how to create a circuit, add quantum gates and view the circuit structure, and further operations such as state simulation, measurement sampling, circuit visualization and IR export. + +The Bell state is one of the most common two-qubit entangled states in quantum computing, usually used to demonstrate the basic concepts of superposition and entanglement. Constructing a Bell state takes the following two steps: + +- Apply an `H` gate to qubit `0` to put it into a superposition state; +- Apply a CX gate with qubit `0` as the control and qubit `1` as the target, thereby establishing entanglement between the two qubits. + +Ideally, the resulting quantum state is: + +```text +(|00> + |11>) / sqrt(2) +``` + +## 1. Create a circuit + +First create a two-qubit quantum circuit and append an `H` gate and a `CX` gate to it in order: + +```python +from cqlib import Circuit + +circuit = Circuit(2) +circuit.h(0) +circuit.cx(0, 1) + +print(circuit.num_qubits) # 2 +print(len(circuit)) # 2 +print(circuit.operations) # view the underlying Operation list +``` + +## 2. View the circuit as a text diagram + +Render the quantum circuit as a text diagram to view the circuit structure: + +```python +from cqlib.visualization import draw_text + +print(draw_text(circuit)) +``` + +## 3. Convert to a matrix for verification + +For small circuits made up purely of quantum gates, the whole circuit can be converted into a complete unitary matrix to verify its mathematical behavior: + +```python +matrix = circuit.to_matrix() +print(matrix) +``` + +## 4. Export OpenQASM 2.0 / 3.0 + +Cqlib also provides export interfaces for OpenQASM 2.0 and OpenQASM 3.0: + +```python +from cqlib.ir import qasm2, qasm3 + +print(qasm2.dumps(circuit)) +print(qasm3.dumps(circuit)) +``` + +## 5. Statevector simulation + +Use statevector simulation to view the quantum state distribution after the circuit is applied: + +```python +from cqlib.qis import Statevector + +sv = Statevector.from_circuit(circuit) +print(sv.data) +print(sv.probabilities()) +``` + +For a Bell state circuit, the result of `probabilities()` should ideally be close to: + +```text +[0.5, 0.0, 0.0, 0.5] +``` + +This means that measurement yields only the two outcomes `00` and `11`, with equal probability, while the probabilities of `01` and `10` are close to `0`, reflecting the entanglement correlation between the two qubits in the Bell state. + +## 6. Sampling measurement + +Once the statevector is obtained, repeated sampling can be performed to simulate the statistical results of an actual measurement process: + +```python +shots = sv.sample_shots(1000) +counts = {} +for outcome in shots: + bitstring = outcome.to_bitstring(2) + counts[bitstring] = counts.get(bitstring, 0) + 1 + +print(counts) +``` + +The output is similar to: + +```text +{'00': 506, '11': 494} +``` + +Because the sampling process is random, the counts obtained from each run are not exactly the same. For an ideal Bell state, however, after a large number of samples the occurrence counts of `00` and `11` should be roughly equal, while `01` and `10` usually do not appear or have a probability close to zero. + +--- + +## Next steps + +- [Quantum Circuit](../1_cqlib/0_circuit/0_overview.md): learn about the basic module for describing quantum programs in Cqlib. +- [Quantum gates and instructions](../1_cqlib/0_circuit/1_gates.md): learn about built-in gates, custom gates, composite gates and non-unitary instructions. +- [Circuit structure and construction](../1_cqlib/0_circuit/2_structures.md): understand the lifecycle, indexing, composition and operation representation of `Circuit`. +- [Tianyan quantum cloud platform client](../1_cqlib/7_tianyan/0_overview.md): to submit circuits to a cloud backend for execution, continue with QCIS export, backend selection, task submission and result retrieval. diff --git a/docs/documentation/en/1_cqlib/0_circuit/0_overview.md b/docs/documentation/en/1_cqlib/0_circuit/0_overview.md new file mode 100644 index 00000000..326b05f2 --- /dev/null +++ b/docs/documentation/en/1_cqlib/0_circuit/0_overview.md @@ -0,0 +1,154 @@ +# Quantum Circuit + +`cqlib.circuit` is the basic module for describing quantum programs in Cqlib. It is responsible for expressing qubits, gate operations, parameter expressions, composite circuits, non-unitary instructions, measurement results and dynamic control flow driven by classical expressions. The later [IR conversion](../1_ir/0_overview.md), [QIS simulation](../3_qis/0_overview.md), [compilation and optimization](../4_compiler/0_overview.md), [device mapping](../2_device/0_overview.md) and [visualization](../5_visualization/0_overview.md) modules usually take `Circuit` as input or as an intermediate representation. + +--- + +## Core abstractions + +`cqlib.circuit` builds around `Circuit` a set of cooperating objects for describing the complete circuit structure, from basic quantum gates to dynamic circuit control flow. + +| Abstraction | Role | Common entry points | +|---|---|---| +| `Qubit` | Qubit identifier, storing only a non-negative index | `Qubit(0)`| +| `Circuit` | Quantum circuit container, storing qubits, parameters, classical values and the operation sequence | `Circuit(...)` | +| `StandardGate` | Set of standard gates built into Cqlib, including Pauli, Clifford, rotation and two-qubit gates | `StandardGate.H`, `StandardGate.RX(theta)` | +| `MCGate` | Multi-controlled gate, promoting a standard gate into a gate with multiple control qubits | `MCGate(2, StandardGate.X())` | +| `UnitaryGate` | User-defined unitary gate, which can be defined by a numeric matrix, a symbolic matrix or an immutable sub-circuit | `UnitaryGate("Oracle", 2)` | +| `CircuitGate` | Composite gate obtained by converting a sub-circuit | `sub.to_gate("Block")` | +| `Directive` | Non-unitary instructions, such as barrier, measure and reset | `circuit.measure(0)` | +| `Parameter` | Symbolic parameter expression | `Parameter("theta")` | +| `ClassicalType` / `ClassicalExpr` | Classical types and classical expressions in dynamic circuits | `ClassicalType.bit()`, `m.expr()` | +| `ValueOperation` | Complete operation representation: instruction + qubits + parameters + optional label | `circuit.operations[0]` | + +--- + +## Core functionality + +### 1. Static circuits + +A static circuit is the most basic way of using the module. When the circuit width is fixed, quantum gates and instructions are appended one after another in execution order. + +```python +from cqlib import Circuit + +c = Circuit(3) +c.h(0) +c.cx(0, 1) +c.rzz(1, 2, 0.25) +c.barrier([0, 1, 2]) +c.reset(2) +``` + +Static circuits apply to scenarios such as algorithm prototype validation, compilation and optimization, QIS simulation and IR export. For a purely unitary circuit that contains no measurement, reset or control flow, the circuit can be further converted into a matrix representation for small-scale circuit validation. + +### 2. Parameterized circuits + +Parameterized circuits use `Parameter` as an angle or expression placeholder, and are commonly used in scenarios such as VQE, QAOA, quantum machine learning, parameter sweeps and gradient computation. + +```python +from cqlib import Circuit, Parameter + +theta = Parameter("theta") +phi = Parameter("phi") + +c = Circuit(2) +c.rx(0, theta) +c.ry(1, phi) +c.cx(0, 1) + +print(c.symbols) # ['theta', 'phi'] + +bound = c.assign_parameters({"theta": 0.1, "phi": 0.2}) +print(bound.to_matrix()) +``` + +Note that `assign_parameters()` returns a new bound circuit and does not modify the original parameterized circuit template. The same parameterized circuit can therefore be reused many times, for sweeps and optimization at different parameter points. + +### 3. Sub-circuits and composite gates + +Real quantum algorithms are usually composed of several reusable circuit modules. Cqlib supports converting a `Circuit` into a `CircuitGate`, which can then be appended to other circuits like an ordinary quantum gate. + +```python +from cqlib import Circuit + +bell = Circuit(2) +bell.h(0) +bell.cx(0, 1) + +bell_gate = bell.to_gate("Bell") + +main = Circuit(4) +main.append_circuit_gate(bell_gate, [0, 1]) +main.append_circuit_gate(bell_gate, [2, 3]) + +flat = main.decompose() +``` + +`CircuitGate` is suitable for expressing reusable modules in an algorithm, such as entanglement blocks, feature-mapping blocks and oracle subroutines. In addition, `decompose()` can expand a composite gate into basic operations, which facilitates later matrix validation, compilation and optimization or IR export. + +### 4. Custom gates and multi-controlled gates + +When the built-in gate set is not enough to describe a certain algorithm unit, `UnitaryGate` can be used to define a custom unitary gate, and `MCGate` can be used to construct a multi-controlled gate. + +```python +import numpy as np +from cqlib import Circuit +from cqlib.circuit import MCGate, StandardGate, UnitaryGate + +c = Circuit(3) + +controlled_h = MCGate(2, StandardGate.H()) +c.append_mc_gate(controlled_h, [0, 1, 2]) + +custom_x = UnitaryGate("CustomX", 1).with_matrix([[0, 1], [1, 0]]) +c.append_unitary_gate(custom_x, [2]) +``` + +Multi-controlled gates and custom gates are commonly used in algorithm libraries, oracle construction, controlled subroutines and hardware-specific instruction modeling. Note that when a custom unitary gate is used, the matrix dimension must match the number of qubits it acts on, and the matrix must satisfy the unitarity requirement. + +### 5. Dynamic circuits + +Dynamic circuits allow mid-circuit measurement during circuit execution and control subsequent operations according to measurement results or classical variables. Here, Cqlib describes such circuits through `ClassicalType`, `ClassicalExpr` and structured control flow interfaces. + +```python +from cqlib import Circuit +from cqlib.circuit import ClassicalExpr, ClassicalType + +c = Circuit(1) +measurement = c.measure(0) + +condition = measurement.expr().to_bool() +c.if_(condition, lambda body: body.x(0)) + +flag = c.var(ClassicalType.bool()) +c.store(flag, ClassicalExpr.bool_literal(True)) +c.while_(flag.expr(), lambda body: body.break_loop()) + +c.validate() +``` + +Note that a dynamic circuit usually cannot be represented as a single unitary matrix, so `to_matrix()` cannot be used directly for whole-circuit matrix conversion. For dynamic circuits, structured validation, IR conversion or a backend flow that supports dynamic execution semantics should be preferred. + +### 6. Circuit analysis and transformation + +After the circuit has been constructed, operations such as parameter binding, inversion, decomposition, numeric matrix conversion or symbolic matrix conversion can be performed further. + +```python +from cqlib import Circuit, Parameter + +theta = Parameter("theta") +c = Circuit(1) +c.rx(0, theta) + +inverse = c.inverse() +symbolic = c.to_symbolic_matrix() +numeric = c.assign_parameters({"theta": 0.3}).to_matrix() +``` +--- + +## Next steps + +- [Quantum gates and instructions](1_gates.md): learn about built-in gates, custom gates, composite gates and non-unitary instructions. +- [Circuit structure and construction](2_structures.md): understand the lifecycle, indexing, composition and operation representation of `Circuit`. +- [Parameter system](3_parameters.md): learn about parameter expressions, parameter binding, expression simplification, symbolic differentiation and symbolic matrices. diff --git a/docs/documentation/en/1_cqlib/0_circuit/1_gates.md b/docs/documentation/en/1_cqlib/0_circuit/1_gates.md new file mode 100644 index 00000000..0ce576c7 --- /dev/null +++ b/docs/documentation/en/1_cqlib/0_circuit/1_gates.md @@ -0,0 +1,451 @@ +# Quantum gates and instructions + +`cqlib.circuit` provides a rich set of quantum gates and circuit instructions for constructing various quantum programs, from basic quantum circuits to parameterized algorithm modules. This page describes in detail the basic usage of quantum gates and instructions in Cqlib, including standard gates, parameterized gates, multi-qubit gates, multi-controlled gates, custom unitary gates, sub-circuit gates and non-unitary instructions such as measurement, reset and barrier. + +This page covers how to select the appropriate gate type according to algorithm requirements and add it to a quantum circuit correctly. Common gates can be added quickly through the convenience methods provided by `Circuit`, and more complex gate operations can also be described explicitly with objects such as `StandardGate`, `MCGate`, `UnitaryGate` and `CircuitGate`. + +--- + +## Standard gates + +Standard gates are the basic gate set built into Cqlib, covering common single-qubit gates, multi-qubit gates, parameterized rotation gates and some hardware-related gates. Standard gates can be applied in the following two ways: + +1. Call the shortcut methods provided by `Circuit` directly; +2. Construct a `StandardGate` object explicitly and append it to the circuit through `append_gate()`. + +```python +from cqlib import Circuit +from cqlib.circuit import StandardGate + +c = Circuit(1) +c.h(0) +c.append_gate(StandardGate.X(), [0]) +c.append_gate(StandardGate.RZ(0.5), [0], label="phase-correction") +``` + +`StandardGate` objects support operations such as attribute queries, parameter binding, matrix computation, inversion and conversion into a multi-controlled gate. + +```python +from cqlib.circuit import StandardGate + +gate = StandardGate.RX(0.25) + +print(gate.num_qubits) # 1 +print(gate.num_params) # 1 +print(gate.num_ctrl_qubits) # 0 +print(gate.params) # [Parameter(0.25)] +print(gate.matrix().shape) # (2, 2) + +inverse_gate = gate.inverse() +controlled = StandardGate.X().control(2) +``` + +The commonly used standard gates are introduced below by category, according to the number of qubits a gate acts on and its parameter form. + + +### 1. Single-qubit non-parametric gates + +Single-qubit non-parametric gates are the most basic class of standard gates for constructing quantum circuits. They act on a single qubit and require no angle parameter, and the matrix form and effect of the gate are already fixed at definition time. + +These gates perform basic transformations of quantum states, for example using the `H` gate to construct a superposition state, the `X` gate to flip a bit, the `Z`, `S` and `T` gates to adjust the phase, or the Clifford gate set to construct basic circuits that are convenient for analysis and compilation and optimization. + +In actual use, single-qubit non-parametric gates usually serve as the basic building blocks of more complex circuits. They can be used alone for state preparation, phase correction and circuit debugging, or combined with two-qubit gates and parameterized rotation gates to construct Bell states, GHZ states, variational circuits, error-checking circuits and hardware native gate decomposition results. + +| `Circuit` method | `StandardGate` | Description | +|---|---|---| +| `i(q)` | `I` | Identity gate | +| `h(q)` | `H` | Hadamard gate | +| `x(q)` | `X` | Pauli-X / NOT gate | +| `y(q)` | `Y` | Pauli-Y gate | +| `z(q)` | `Z` | Pauli-Z gate | +| `s(q)` | `S` | Phase gate | +| `sdg(q)` | `SDG` | Inverse of the `S` gate | +| `t(q)` | `T` | `pi/4` phase gate | +| `tdg(q)` | `TDG` | Inverse of the `T` gate | +| `x2p(q)` | `X2P` | `sqrt(X)`, by convention `X^(+1/2)` | +| `x2m(q)` | `X2M` | Inverse of `sqrt(X)` | +| `y2p(q)` | `Y2P` | `sqrt(Y)` | +| `y2m(q)` | `Y2M` | Inverse of `sqrt(Y)` | + +```python +from cqlib import Circuit + +c = Circuit(1) +c.h(0) +c.x(0) +c.sdg(0) +c.t(0) +c.x2p(0) +c.y2m(0) + +print([op.instruction.instruction.name for op in c.operations]) +``` + +Note that `i(q)` and `delay(q, duration)` have different semantics. `i(q)` denotes the standard identity gate in a quantum circuit; `delay(q, duration)` denotes idle waiting on the hardware timeline, and is usually used to preserve scheduling or timing semantics. + +### 2. Single-qubit parameterized gates + +Single-qubit parameterized gates are parameterized quantum gates applied on a single qubit, usually controlling the rotation axis and rotation magnitude of a quantum state on the Bloch sphere through one or more angle parameters. Unlike fixed gates such as `H` and `X`, the actual effect of a parameterized gate is determined by the numeric or symbolic parameters passed in, so the circuit behavior can be changed by adjusting the parameters while the circuit structure stays unchanged. + +In Cqlib, a single-qubit parameterized gate accepts either an ordinary numeric parameter or a Parameter expression. The former is suitable for constructing circuits whose angles are already determined; the latter is suitable for constructing parameterized circuit templates and, in a later algorithm flow, for parameter binding, parameter sweeps, optimization iterations or symbolic matrix validation. + +Parameterized gates are widely used in scenarios such as VQE, QAOA, quantum machine learning, variational circuit design and hardware calibration. For example, in variational quantum algorithms the circuit structure usually stays unchanged, while the optimizer keeps updating the parameter values in rotation gates to search for a better quantum state or objective function value. + +| `Circuit` method | `StandardGate` | Parameters | Description | +|---|---|---|---| +| `rx(q, theta)` | `RX` | 1 | Rotation around the X axis | +| `ry(q, theta)` | `RY` | 1 | Rotation around the Y axis | +| `rz(q, theta)` | `RZ` | 1 | Rotation around the Z axis | +| `phase(q, lambda_)` | `Phase` | 1 | Phase gate | +| `u(q, theta, phi, lambda_)` | `U` | 3 | General single-qubit gate | +| `xy(q, theta)` | `XY` | 1 | Single-qubit gate of the XY family | +| `xy2p(q, theta)` | `XY2P` | 1 | Positive half-angle variant of `XY` | +| `xy2m(q, theta)` | `XY2M` | 1 | Negative half-angle variant of `XY` | +| `rxy(q, theta, phi)` | `RXY` | 2 | Specifies the rotation axis in the XY plane | + +```python +from cqlib import Circuit, Parameter + +theta = Parameter("theta") +phi = Parameter("phi") + +c = Circuit(1) +c.rx(0, theta) +c.ry(0, 0.25) +c.rz(0, 2 * theta + phi) +c.phase(0, phi) +c.u(0, theta, 0.1, phi) +c.rxy(0, theta, phi) + +print(c.symbols) +bound = c.assign_parameters({"theta": 0.3, "phi": 0.5}) +print(bound.to_matrix()) +``` + +If the circuit still contains unbound symbolic parameters, a numeric matrix cannot be obtained directly with `to_matrix()`. In this case, bind the parameters first through `assign_parameters()`, or use `to_symbolic_matrix()` to keep the symbolic expressions. + +`StandardGate.GPhase` is used to represent the global phase. At the circuit level, a more commonly used approach is to set the global phase of `Circuit`: + +```python +from cqlib import Circuit, Parameter + +c = Circuit(1) +c.set_global_phase(Parameter("alpha")) +print(c.global_phase) +``` + +### 3. Two-qubit and three-qubit gates + +Two-qubit and three-qubit gates describe the interaction between multiple qubits, and are core components for constructing entangled states, controlled logic and the core structures of quantum algorithms. Unlike single-qubit gates, which only change the state of a single qubit, multi-qubit gates can establish correlations between different qubits: for example, `CX`, `CZ` and other controlled gates construct Bell states, GHZ states and various entangled circuits, and `SWAP` can adjust the logical positions between qubits. + +In actual quantum algorithms, multi-qubit gates usually play the role of "connecting" the information of different qubits. For example, QAOA commonly uses two-qubit parameterized gates such as `RZZ` to express the interaction terms in the problem Hamiltonian; quantum Fourier transform and phase estimation algorithms use controlled-phase operations; and error-correction and verification circuits often use gates such as `CX` and `CCX` to implement conditional flips and ancilla control. + +| `Circuit` method | `StandardGate` | Parameters | Description | +|---|---|---|---| +| `cx(control, target)` | `CX` | 0 | Controlled X gate, CNOT | +| `cy(control, target)` | `CY` | 0 | Controlled Y gate | +| `cz(control, target)` | `CZ` | 0 | Controlled Z gate | +| `swap(a, b)` | `SWAP` | 0 | Swaps the states of two qubits | +| `ccx(c1, c2, target)` | `CCX` | 0 | Toffoli gate | +| `rxx(a, b, theta)` | `RXX` | 1 | `XX` Pauli rotation | +| `ryy(a, b, theta)` | `RYY` | 1 | `YY` Pauli rotation | +| `rzz(a, b, theta)` | `RZZ` | 1 | `ZZ` Pauli rotation | +| `rzx(a, b, theta)` | `RZX` | 1 | `ZX` Pauli rotation | +| `crx(control, target, theta)` | `CRX` | 1 | Controlled `RX` gate| +| `cry(control, target, theta)` | `CRY` | 1 | Controlled `RY` gate| +| `crz(control, target, theta)` | `CRZ` | 1 | Controlled `RZ` gate| +| `fsim(a, b, theta, phi)` | `FSIM` | 2 | fSim two-qubit gate | + +```python +from cqlib import Circuit, Parameter + +theta = Parameter("theta") +phi = Parameter("phi") + +c = Circuit(3) +c.cx(0, 1) +c.swap(1, 2) +c.ccx(0, 1, 2) +c.rzz(0, 2, theta) +c.crx(1, 2, 0.25) +c.fsim(0, 1, theta, phi) +``` + +When adding a multi-qubit gate, Cqlib performs validity checks on the operands, including whether the target qubits have already been registered in the circuit, whether the same operation references the same qubit more than once, and whether the number of parameters passed in is consistent with the gate definition. If a check fails, the system raises `CircuitError` or `ParameterError` to prompt the user to correct the qubit index, the operand or the parameter configuration. + +--- + +## Gate matrices and gate inversion + +In quantum circuit analysis, algorithm validation and compilation conversion, the matrix representation of a gate is an important basis for understanding its mathematical effect. Cqlib supports computing matrices directly for standard gates and multi-controlled gates, so that the dimension of a gate can be checked, the unitarity of a gate verified, and the specific effect of a gate on a quantum state analyzed. + +When computing a matrix, note the following: + +- For a gate that contains no symbolic parameter, `matrix()` can be called directly to obtain its numeric matrix. +- For a gate that contains symbolic parameters, concrete numeric parameters need to be provided when computing the matrix, or the gate object needs to be made numeric first through parameter binding before the matrix is computed. + +This preserves the flexibility of parameterized gates in algorithm templates and also gives a definite matrix result when numeric validation is needed. + +```python +import numpy as np +from cqlib import Parameter +from cqlib.circuit import StandardGate + +h = StandardGate.H() +h_matrix = h.matrix() +print(h_matrix) + +theta = Parameter("theta") +rx = StandardGate.RX(theta) +rx_matrix = rx.matrix([np.pi / 2]) + +print(rx_matrix) +``` + +In addition, Cqlib also supports performing inversion on invertible gates. + +- For common self-inverse gates, the inversion result is the same as the original gate. +- For parameterized gates such as rotation gates, inversion usually amounts to negating the rotation angle. + +```python +from cqlib.circuit import StandardGate + +h_inverse = StandardGate.H.inverse() +print(h_inverse) # H + +rx_inverse = StandardGate.RX(0.5).inverse() +print(rx_inverse.params[0].evaluate()) # -0.5 + +s_inverse = StandardGate.S.inverse() +t_inverse = StandardGate.T.inverse() + +print(s_inverse) # SDG +print(t_inverse) # TDG +``` + +The inversion rules of common gates are as follows: + +- `H`, `X`, `Y`, `Z`, `CX`, `CY`, `CZ`, `SWAP` and `CCX` are self-inverse gates. +- `S` and `SDG` are mutual inverses, and `T` and `TDG` are mutual inverses. +- The inverse of a rotation gate is usually obtained by negating the angle, for example `RX(theta)^† = RX(-theta)`. +- The inverse of `U(theta, phi, lambda)` transforms the parameters according to the matrix definition. +- `Barrier` does not change the quantum state and can be regarded as its own inverse; `Measure` and `Reset` have no ordinary unitary inverse operation. + +--- + +## Multi-controlled gates + +`MCGate` is used to extend an existing `StandardGate` into a multi-controlled gate, that is, to add one or more control qubits on top of the original gate. The target gate acts on the corresponding target qubit only when all control qubits satisfy the specified control condition. This mechanism is commonly used to construct Toffoli gates, multi-controlled phase gates, oracles, conditional flip operations and controlled subroutines in quantum algorithms. + +When appending an `MCGate` to a circuit, the qubit order needs to be passed in according to the convention: **all control qubits first, then the target qubits that the target gate acts on**. + +For example, for a three-control `X` gate, the first three qubits are control qubits and the last qubit is the target qubit. This keeps the semantics of the multi-controlled gate clear and also facilitates later gate decomposition, compilation and optimization and hardware mapping. + + +```python +from cqlib import Circuit, Parameter +from cqlib.circuit import MCGate, StandardGate + +c = Circuit(4) + +mcx = MCGate(3, StandardGate.X()) +c.append_mc_gate(mcx, [0, 1, 2, 3]) + +theta = Parameter("theta") +mcrz = MCGate(2, StandardGate.RZ(theta)) +c.append_mc_gate(mcrz, [0, 1, 2]) + +print(c[0].instruction.instruction.name) # C3-X +print(c[1].params) # [theta] +``` + +Besides constructing an `MCGate` explicitly, the `control()` method can also be called directly on a standard gate object to generate a multi-controlled gate. + +```python +from cqlib.circuit import StandardGate + +ccx = StandardGate.X().control(2) +controlled_cx = StandardGate.CX().control(1) +``` + +`StandardGate.CX().control(1)` means adding one more control qubit on top of the existing `CX` gate, so its total number of control qubits is 2, which is equivalent to the three-qubit `CCX` in control structure. + +--- + +## Custom unitary gates + +When an algorithm needs a unitary operation outside the built-in standard gates, `UnitaryGate` can be used to define a custom unitary gate. Custom unitary gates are suitable for representing special oracles in an algorithm, problem-related transforms, hardware-specific gates and quantum operations with a known matrix form. + +When defining a `UnitaryGate`, the gate name and the number of qubits it acts on need to be specified explicitly. If the gate is defined with a matrix, the matrix dimension must be `2^n × 2^n`, where `n` denotes the number of qubits the gate acts on, that is, `num_qubits`. Cqlib checks whether the matrix dimension matches the number of qubits according to the gate definition. + +```python +import numpy as np +from cqlib import Circuit +from cqlib.circuit import UnitaryGate + +h_matrix = np.array([[1, 1], [1, -1]], dtype=complex) / np.sqrt(2) +custom_h = UnitaryGate("CustomH", 1).with_matrix(h_matrix) + +c = Circuit(2) +c.append_unitary_gate(custom_h, [0]) +``` + +Besides a numeric matrix, a custom unitary gate can also be defined with a symbolic matrix. A symbolic matrix is suitable for describing a gate family with parameters: the symbolic form of the gate can be defined first, and concrete parameter values passed in later from different circuits or different positions. This approach is commonly used in parameterized oracles, tunable phase gates, algorithm templates and symbolic validation scenarios. + +```python +from cqlib import Circuit, Parameter +from cqlib.circuit import SymbolicComplex, SymbolicMatrix, UnitaryGate + +theta = Parameter("theta") +phase = SymbolicComplex.exp_i(theta) + +matrix = SymbolicMatrix( + [ + [SymbolicComplex.one(), SymbolicComplex.zero()], + [SymbolicComplex.zero(), phase], + ] +) + +gate = UnitaryGate("SymbolicPhase", 1, num_params=1).with_symbolic_matrix( + matrix, + ["theta"], +) + +c = Circuit(1) +c.append_unitary_gate(gate, [0], [0.25]) +``` + +Note that `UnitaryGate` is better suited to quantum operations that already have a clear matrix definition. If the goal is only to add an existing sub-circuit to other circuits as a reusable module, `CircuitGate` is usually more recommended. `CircuitGate` can be obtained by encapsulating a sub-circuit directly, which both avoids writing a matrix by hand and facilitates later decomposition, parameter binding and circuit structure analysis. + +--- + +## Sub-circuit gate `CircuitGate` + +`CircuitGate` is used to encapsulate an existing sub-circuit as a reusable composite gate. Unlike `UnitaryGate`, which describes gate behavior through a matrix, `CircuitGate` keeps the structural information of the sub-circuit, so it is better suited to expressing algorithm modules, oracles, ansatz blocks, repeated circuit structures and other quantum program fragments that need to be reused in multiple positions. + +In actual use, a sub-circuit can be constructed first and then converted into a `CircuitGate` through `to_gate(name)`. The resulting composite gate can be appended to other circuits like an ordinary gate, and can be expanded back into the original sub-circuit operations through `decompose()` when needed, which facilitates later circuit analysis, parameter binding, compilation and optimization or IR export. + +```python +from cqlib import Circuit, Parameter + +theta = Parameter("theta") + +sub = Circuit(1) +sub.rx(0, theta) +sub.rz(0, theta / 2) + +block = sub.to_gate("ParamBlock") + +main = Circuit(2) +main.append_circuit_gate(block, [0], [0.3]) +main.append_circuit_gate(block, [1], [0.7]) + +decomposed = main.decompose() +print([op.instruction.instruction.name for op in decomposed.operations]) +``` + +For a `CircuitGate` with parameters, concrete parameter values can be passed in when it is appended to a circuit. The parameters are bound positionally in the order of the symbols recorded in `gate.symbols`; if `params` is not passed, the original symbolic parameters in the sub-circuit are kept, so the composite gate remains in parameterized form. + +```python +print(block.name) +print(block.num_qubits) +print(block.num_params) +print(block.symbols) +``` + +Besides `to_gate(name)`, a composite gate can also be constructed explicitly with `FrozenCircuit` and `CircuitGate`. + +```python +from cqlib import Circuit +from cqlib.circuit import CircuitGate, FrozenCircuit + +sub = Circuit(1) +sub.h(0) + +frozen = FrozenCircuit(sub.qubits, sub.operations) +gate = CircuitGate("HadamardBlock", frozen) +``` + +--- + +## Directive: non-unitary instructions + +Besides ordinary quantum gates, a quantum circuit may also contain special instructions such as measurement, reset, barrier and delay. Such instructions usually cannot correspond to an ordinary unitary matrix, so they are represented uniformly by `Directive` in Cqlib. + +`Directive` is mainly used to describe auxiliary semantics during circuit execution. For example, `barrier` constrains gate reordering during compilation and optimization, `measure` reads out quantum state information as a classical result, `reset` reinitializes a qubit to |0>, and `delay` preserves the idle-wait semantics in hardware time scheduling. + +Common non-unitary instructions can be added to a circuit directly through the interfaces provided by `Circuit`: + +| `Circuit` method | Instruction name | Description | +|---|---|---| +| `barrier(qubits)` | `Barrier` | Prevents the compiler from reordering gates across the specified qubits | +| `measure(qubit)` | `measure_bit` | Measures a single qubit and returns a `Measurement` | +| `measure_bits(qubits)` | `measure_bits` | Measures multiple qubits and returns a bit-vector measurement value | +| `reset(qubit)` | `Reset` | Resets a qubit | +| `delay(qubit, duration)` | `delay` | Keeps idle on the hardware timeline | + +```python +from cqlib import Circuit, Parameter +from cqlib.circuit import ClassicalType + +c = Circuit(2) +c.h(0) +c.barrier([0, 1]) + +readout = c.measure(0) +bit_var = c.var(ClassicalType.bit()) +c.measure_into(1, bit_var) + +c.reset(0) +c.delay(1, Parameter("tau")) + +print(readout.width) +print(c.classical_values) +print(c.classical_vars) +``` + +Note that non-unitary instructions affect the mathematical representation of a circuit and the way it is processed later: + +- `barrier` itself does not change the quantum state and is only used to express a compilation constraint. Therefore, when a circuit contains no other non-unitary operation, a circuit with `barrier` can still be used for matrix conversion. +- `measure`, `measure_into` and `measure_bits` introduce quantum measurement and classical results, so the circuit can no longer be represented completely by a single unitary matrix. +- `reset` reinitializes a qubit to a fixed state; it is a non-unitary operation and likewise cannot be described by an ordinary unitary matrix. +- `delay` is mainly used to preserve time semantics at the hardware execution or scheduling level. + +Therefore, before performing `to_matrix()`, circuit equivalence validation or gate-level optimization, first confirm whether the circuit contains special instructions such as measurement, reset and delay. + +--- + +## Low-level instructions and ValueOperation + +In some lower-level or more automated development scenarios, Cqlib supports constructing `Instruction` and `ValueOperation` explicitly, which provides a more flexible development approach. For example, when writing a circuit deserializer, an IR converter, compilation and optimization tests, an automated circuit generation tool or a custom front-end interface, this approach can be used to describe directly the instruction type, acting qubits, parameter list and label information of a certain operation. + +```python +from cqlib import Circuit, Qubit +from cqlib.circuit import Instruction, StandardGate, ValueOperation + +instruction = Instruction.from_standard_gate(StandardGate.H()) +operation = ValueOperation.from_standard_gate( + StandardGate.RX(0.25), + [Qubit(0)], + label="rx-layer-0", +) + +c = Circuit(1) +c.append(operation) + +print(c[0].instruction) +print(c[0].params) +print(c[0].label) +``` + +Semantically, `Instruction` is used to describe "what type of instruction is executed", while `ValueOperation` is used to describe one concrete application of this instruction in a circuit, including which qubits it acts on, which parameters it uses, and whether it carries an extra label. + +This distinction allows Cqlib to reuse a unified data model across high-level circuit construction, low-level operation representation, IR conversion, compilation and optimization and dynamic control flow handling. + +--- + +## Next steps + +- [Circuit structure and construction](2_structures.md): understand the lifecycle, indexing, composition and operation representation of `Circuit`. +- [Parameter system](3_parameters.md): learn about parameter expressions, parameter binding, expression simplification, symbolic differentiation and symbolic matrices. +- [Circuit analysis and transformation](4_circuit_analysis.md): use tools such as inversion, decomposition, matrix conversion and operation inspection. diff --git a/docs/documentation/en/1_cqlib/0_circuit/2_structures.md b/docs/documentation/en/1_cqlib/0_circuit/2_structures.md new file mode 100644 index 00000000..4b95f6f7 --- /dev/null +++ b/docs/documentation/en/1_cqlib/0_circuit/2_structures.md @@ -0,0 +1,350 @@ +# Circuit structure and construction + +`Circuit` is the core circuit container in the `cqlib.circuit` module, used to represent a complete quantum program. It records qubits and the circuit operations in order, and is also responsible for maintaining parameter expressions, the global phase, classical variables, measurement results and the classical handle namespace required by dynamic circuits. Therefore, `Circuit` is both the main entry point for constructing quantum circuits and an important data foundation for later processes such as IR conversion, compilation and optimization, device mapping and result analysis. + +Structurally, a `Circuit` usually contains the following information: + +* A qubit list, describing the logical qubits that can be operated on in the circuit; +* An operation list in append order, recording quantum gates, instructions and control flow structures; +* The free parameters in the circuit, supporting parameterized circuits, parameter binding and variational algorithms; +* The global phase, preserving the overall phase information of the circuit; +* The classical variables and classical values used in dynamic circuits, describing measurement results and classical control logic; +* The classical handle namespace identified by `CircuitId`, ensuring consistency of classical variables and measurement values within the circuit. + +Together, these structures form the basic representation of a Cqlib quantum circuit, so that the circuit keeps a consistent data semantics across different stages such as construction, composition, parameter binding, matrix conversion, IR export, compilation and optimization and dynamic control flow analysis. + + +--- + +## Creating a circuit + +`Circuit` supports several ways of creating a circuit. Either the number of qubits can be specified directly, or an explicit list of qubit indices or `Qubit` objects can be passed in. Different ways suit different modeling needs: when the qubit numbering is contiguous, passing an integer directly is the most concise; when a specific logical numbering needs to be preserved or aligned with the bit numbering in an external system, an integer index list or explicit `Qubit` objects can be used. + +### 1. Using a qubit count + +The most common way is to pass an integer `n` to `Circuit`. Cqlib then creates `n` logical qubits automatically and assigns indices in the order `0..n-1`. + +```python +from cqlib import Circuit + +c = Circuit(3) +print(c.num_qubits) # 3 +print([q.index for q in c.qubits]) # [0, 1, 2] +``` + +In the example above, `Circuit(3)` creates a circuit containing 3 qubits, with the corresponding logical qubit indices `0`, `1` and `2`. When gate operations are added later, these integer indices can be used directly to specify the operands of a gate, for example `c.h(0)` or `c.cx(0, 1)`. + +In addition, `Circuit(0)` is also a legal construction, representing a zero-qubit circuit that contains no qubits and no operations. Zero-qubit circuits are usually used for boundary testing, recursive construction, as an initial placeholder when generating circuits programmatically, or to verify the behavior of an interface under empty input. + +```python +from cqlib import Circuit + +empty = Circuit(0) +print(empty.width) # 0 +print(len(empty)) # 0 +print(empty.to_matrix()) +``` + +### 2. Using an integer index list + +When the logical qubit numbering of a circuit is not the contiguous `0..n-1`, or needs to stay consistent with the numbering in external data, a device mapping or an algorithm model, an integer index list can be passed directly to `Circuit`. + +```python +from cqlib import Circuit + +c = Circuit([2, 0, 5]) +print([q.index for q in c.qubits]) # [2, 0, 5] + +c.h(2) +c.cx(2, 5) +``` + +Note that Cqlib preserves the qubit order of the passed list. That order affects not only the value returned by `c.qubits`, but also the way qubits are arranged when a matrix is constructed by default. Unless `qubits_order` is specified explicitly during matrix conversion, Cqlib interprets each qubit in the order of `c.qubits` as stored inside the circuit. + + +### 3. Using `Qubit` objects + +Besides integer indices, Cqlib also supports creating `Qubit` objects explicitly for circuit construction. `Qubit` is a lightweight handle representing a logical qubit in `Cqlib`, mainly used to wrap a non-negative integer index. + +```python +from cqlib import Circuit, Qubit + +q0 = Qubit(10) +q1 = Qubit(11) + +c = Circuit([q0, q1]) +c.h(q0) +c.cx(q0, q1) + +print(q0.index) +print(q0 == Qubit(10)) +``` + +--- + +## Adding qubits + +In some scenarios, a smaller circuit may need to be constructed first, and the circuit width then extended according to later algorithm logic, sub-circuit composition or programmatic generation results. `add_qubits()` can then be used to add new qubits to the circuit. + +`add_qubits()` adds new qubits to the current circuit while keeping the existing operation sequence. The new qubits do not affect the gate operations that already exist, but they extend the qubit list of the circuit and affect the scope of operations that can be added later as well as the dimension of the circuit matrix. + +```python +from cqlib import Circuit + +c = Circuit(1) +c.h(0) + +c.add_qubits([2, 4]) +print(c.num_qubits) # 3 +print([q.index for q in c.qubits]) # [0, 2, 4] +print(len(c.operations)) # 1 + +c.cx(0, 2) +``` + +In the example above, the circuit initially contains only qubit `0`, and one `H` gate has already been added. After `add_qubits([2, 4])` is called, qubits with indices `2` and `4` are added to the circuit, the original `H` gate is still kept, and operations can continue to be added on the new qubits later. + +Note the following when using `add_qubits()`: + +- A new qubit must not duplicate an existing qubit in the circuit, otherwise a circuit structure error is raised; +- Existing operations are not rewritten or remapped; new qubits only affect later operations; +- The qubit order of a circuit affects the bit arrangement used when a matrix is constructed by default; +- New qubits expand the circuit width, so the matrix dimension grows accordingly during circuit matrix conversion. + +--- + +## Querying circuit information + +When constructing or debugging a quantum circuit, Cqlib supports inspecting basic structural information such as the number of qubits, the operations already added, the free parameters and the classical handle namespace. `Circuit` provides a set of common attributes that help to understand the current state of a circuit quickly and provide necessary information for later parameter binding, circuit composition, matrix conversion or IR export. + +```python +from cqlib import Circuit, Parameter + +theta = Parameter("theta") +c = Circuit(2) +c.rx(0, theta) +c.cx(0, 1) + +print(c.id) # the CircuitId handle identifier of the current circuit +print(c.num_qubits) # 2 +print(c.width) # 2 +print(c.qubits) # [Qubit(0), Qubit(1)] +print(c.parameters) # [theta] +print(c.symbols) # ['theta'] +print(c.operations) # [ValueOperation(...), ...] +print(len(c)) # 2 +``` + +Here, both `num_qubits` and `width` denote the number of qubits the circuit contains; `qubits` returns the list of qubits registered in the circuit; `operations` returns the operation list in append order; and `len(c)` returns the number of operations in the current circuit. For scenarios that involve dynamic circuit structures, `id` denotes the classical handle namespace of the current circuit, used to distinguish classical variables and measurement results in different circuits. + +Note that `parameters` and `symbols` do not have exactly the same meaning. `parameters` returns the parameter expression objects registered in the circuit, while `symbols` returns the free symbol names contained in those expressions. + +```python +from cqlib import Circuit, Parameter + +theta = Parameter("theta") +phi = Parameter("phi") + +c = Circuit(1) +c.rz(0, 2 * theta + phi) + +print([str(p) for p in c.parameters]) # ['phi + 2*theta'] +print(c.symbols) # ['theta', 'phi'] +``` + +--- + +## Operation sequence and indexing + +`Circuit` maintains an ordered operation sequence in the order in which operations are appended to the circuit. The complete operation list can be viewed through the `operations` attribute, and the operation at a given position can be accessed with `operation(index)` or `circuit[index]`. This mechanism applies to circuit inspection, debugging, converter development and structural comparison before and after compilation. + +```python +from cqlib import Circuit + +c = Circuit(2) +c.h(0) +c.cx(0, 1) + +first = c[0] +second = c.operation(1) + +print(first.instruction.instruction.name) # H +print(second.instruction.instruction.name) # CX +print([q.index for q in second.qubits]) # [0, 1] +``` + +Each operation in a `Circuit` is usually represented as a `ValueOperation`, which is the complete operation object in a circuit and mainly contains the following fields: +- `instruction`: the `ValueInstruction` corresponding to the operation, which can represent an ordinary quantum gate, a non-unitary instruction or a classical control flow structure; +- `qubits`: the list of qubits this operation acts on; +- `params`: the parameter list carried by this operation, commonly used for parameterized rotation gates, custom gates or composite gates; +- `label`: optional label information, usually used for debugging, marking circuit layers, recording the transformation source or assisting later analysis. + +--- + +## Adding operations + +After a circuit is created, quantum gates, composite gates, non-unitary instructions or low-level operation objects can be appended to `Circuit` continuously. Cqlib provides several ways of appending operations to suit different usage needs. + +### 1. Using convenience gate methods + +Using the convenience gate methods provided by `Circuit` is the most common way of constructing a circuit. Such methods take qubit indices and necessary parameters as input directly, with clear semantics and concise code, and are suitable for algorithm prototypes and everyday quantum circuit development. + +```python +from cqlib import Circuit + +c = Circuit(2) +c.h(0) +c.cx(0, 1) +c.rzz(0, 1, 0.25) +``` + +### 2. Using explicit gate objects + +When gate objects need to be stored, gate attributes checked, labels added, or gate operations generated in batches through program logic, a `gate` object can be constructed explicitly first and then appended to the circuit through the corresponding `append_*` method. + +```python +from cqlib import Circuit +from cqlib.circuit import MCGate, StandardGate + +c = Circuit(3) +c.append_gate(StandardGate.H(), [0]) +c.append_gate(StandardGate.RZ(0.25), [1], label="rz-calibrated") + +mcx = MCGate(2, StandardGate.X()) +c.append_mc_gate(mcx, [0, 1, 2]) +``` + +The commonly used explicit append methods are as follows: + +| Method | Purpose | +|---|---| +| `append_gate(gate, qubits, label=None)` | Appends a `StandardGate` | +| `append_mc_gate(gate, qubits, label=None)` | Appends an `MCGate` | +| `append_unitary_gate(gate, qubits, params=None)` | Appends a `UnitaryGate` | +| `append_circuit_gate(gate, qubits, params=None)` | Appends a `CircuitGate` | + +### 3. Using a low-level `ValueOperation` + +In lower-level development scenarios, a complete `ValueOperation` can also be constructed and appended directly. + +```python +from cqlib import Circuit, Qubit +from cqlib.circuit import StandardGate, ValueOperation + +operation = ValueOperation.from_standard_gate( + StandardGate.RX(0.5), + [Qubit(0)], + label="manual-rx", +) + +c = Circuit(1) +c.append(operation) +print(c[0].label) +``` + +--- + +## Rebuilding a circuit from operations + +Besides the usual step-by-step construction, Cqlib also provides the `Circuit.from_operations()` interface, which rebuilds a circuit from an existing qubit list and operation sequence. This interface is a relatively low-level circuit construction entry point, usually used to restore a complete circuit from serialized data, IR conversion results, compiler intermediate results or test cases. + +Unlike calling methods such as `h()`, `cx()` and `append_gate()` to append operations step by step, `from_operations()` applies to scenarios where the qubits required by the circuit and the `ValueOperation` operation list are already prepared. Cqlib regenerates a `Circuit` object from this information and preserves the original operation order. + +```python +from cqlib import Circuit + +source = Circuit(2) +source.h(0) +source.cx(0, 1) + +restored = Circuit.from_operations(source.qubits, source.operations) + +print(restored.num_qubits) +print([op.instruction.instruction.name for op in restored.operations]) +``` +--- + +## Composing circuits + +When constructing a complex quantum program, several smaller circuits usually need to be composed into one complete circuit. `compose()` appends the operations of another circuit to the end of the current circuit, achieving sequential composition between circuit modules. + +By default, `compose()` composes according to the qubit numbering in the two circuits; if a sub-circuit needs to be mapped to specific qubit positions in the main circuit, the position remapping relation can be specified explicitly through the `qubits` parameter. + +```python +from cqlib import Circuit + +main = Circuit(3) +main.h(0) + +sub = Circuit(2) +sub.cx(0, 1) + +main.compose(sub, [1, 2]) + +print([op.instruction.instruction.name for op in main.operations]) +print([q.index for q in main[1].qubits]) # [1, 2] +``` + +Note that the following rules need to be followed when using `compose()`: + +- If the qubits parameter is passed, its length must match other.num_qubits; +- The i-th qubit in other is mapped to the main-circuit qubit specified by qubits[i]; +- The operation order after composition is "the operations already in the current circuit first, the operations of other afterwards"; + +--- + +## Sub-circuit encapsulation + +When constructing complex quantum algorithms, many circuit fragments are reused multiple times, such as rotation layers, entanglement layers, oracles, ansatz blocks or specific algorithm submodules. For such reusable structures, Cqlib supports using `to_gate(name)` to encapsulate the current circuit as a `CircuitGate`, and then appending the resulting composite gate to other circuits. + +The composite gate can be invoked multiple times at different qubit positions of the main circuit, and can be expanded back into the original circuit operations through `decompose()` when needed. + +```python +from cqlib import Circuit, Parameter + +theta = Parameter("theta") + +block = Circuit(1) +block.rx(0, theta) +block.rz(0, theta / 2) + +gate = block.to_gate("RotationBlock") + +main = Circuit(2) +main.append_circuit_gate(gate, [0], [0.2]) +main.append_circuit_gate(gate, [1], [0.4]) + +flat = main.decompose() +print([op.instruction.instruction.name for op in flat.operations]) +``` + +--- + +## Global phase + +Every `Circuit` contains a `global_phase` attribute, used to record the overall global phase of the circuit. The global phase does not change the measurement probability distribution of the computational basis states, but it is part of the mathematical representation of a quantum circuit and is of great significance in matrix comparison, circuit equivalence determination, compilation rewriting and certain phase-sensitive algorithm analyses. + +In Cqlib, `global_phase` is represented with `Parameter`, so it can be set to an ordinary numeric value, a symbolic parameter or a parameter expression. + +```python +from cqlib import Circuit, Parameter + +c = Circuit(1) +print(c.global_phase.is_zero()) + +c.set_global_phase(0.25) +print(c.global_phase.evaluate({})) # 0.25 + +alpha = Parameter("alpha") +c.set_global_phase(alpha) +print(c.global_phase) +print(c.symbols) +``` + +--- + +## Next steps + +- [Parameter system](3_parameters.md): learn about parameter expressions, parameter binding, expression simplification, symbolic differentiation and symbolic matrices. +- [Circuit analysis and transformation](4_circuit_analysis.md): use tools such as inversion, decomposition, matrix conversion and operation inspection. +- [Control flow](5_control_flow.md): use measurement results, classical variables and structured control flow to construct dynamic circuits. diff --git a/docs/documentation/en/1_cqlib/0_circuit/3_parameters.md b/docs/documentation/en/1_cqlib/0_circuit/3_parameters.md new file mode 100644 index 00000000..42483d8a --- /dev/null +++ b/docs/documentation/en/1_cqlib/0_circuit/3_parameters.md @@ -0,0 +1,467 @@ +# Parameter system + +`Parameter` is the core type in Cqlib for representing numeric parameters, symbolic variables and parameter expressions, and is commonly used to describe quantum gate angles, circuit global phases, custom symbolic gates and tunable elements in symbolic matrices. With `Parameter`, a circuit template with symbolic parameters can be constructed first, and then bound, evaluated and validated with different parameter values in a later algorithm flow. + +`Parameter` can represent a definite constant, a symbolic variable that has not yet been assigned, or a more complex expression composed through arithmetic operations and mathematical functions. This allows Cqlib to perform multiple parameter bindings, parameter sweeps, optimization iterations or symbolic analyses on the same parameterized circuit while the circuit structure stays unchanged. + +--- + +## Creating a `Parameter` + +`Parameter` can be created from a symbol name, a numeric value, an expression string or a built-in constant constructor. Different ways of creating it suit different scenarios: +- A symbol name is suitable for defining parameters to be optimized; +- A numeric value is suitable for representing a determined angle; +- An expression string is suitable for parsing a parameter expression from a configuration file or external input; +- A constant constructor is used to represent mathematical constants such as `pi` and `e` uniformly. + +### 1. Creating from a symbol name + +The most common way is to create a symbolic parameter using a string. A symbolic parameter represents a variable that has not yet been assigned, and is usually used for parameterized quantum gates, the global phase, symbolic matrices or parameters to be optimized in variational algorithms. + +```python +from cqlib import Parameter + +theta = Parameter("theta") +phi = Parameter("phi") + +print(theta.symbols) # ['theta'] +print(theta.as_symbol()) # theta +``` + +Note that the passed string is parsed as a parameter expression. Therefore, if the string does not conform to the parameter expression syntax, Cqlib raises `ParameterError` to prompt the user to check the variable name or the expression format. + +### 2. Creating from a numeric value + +If the parameter value is already determined, an integer or floating-point number can be used directly to create a numeric parameter. Numeric parameters are usually used for rotation gates with fixed angles, a fixed global phase, or for constructing definite parameter objects in tests and examples. + +```python +from cqlib import Parameter + +a = Parameter(1) +b = Parameter(0.25) + +print(a.is_constant()) # True +print(b.evaluate({})) # 0.25 +``` + +A `Parameter` created from a numeric value is a constant expression and does not depend on any symbol binding, so it can be evaluated directly. + +### 3. Creating from an expression string + +When a parameter expression comes from a configuration file, user input, serialized data or an external tool, `Parameter.from_expression()` can be used to parse the expression from a string. + +```python +from cqlib import Parameter + +expr = Parameter.from_expression("2*x + sin(y) + pi/2") + +print(expr.symbols) # ['x', 'y'] +print(expr.evaluate({"x": 0.1, "y": 0.2})) +``` + +An expression string can contain variables, constants, arithmetic operations and common mathematical functions. After parsing, the result is still a `Parameter` object, which can continue to be used for quantum gate parameters, the global phase, symbolic matrices or other parameter expression compositions. + +The currently supported expression elements include: + +- Numbers; +- The constants `pi` and `e`; +- Variable names; +- The operators `+`, `-`, `*`, `/`, `**`; +- Parentheses; +- Functions such as `sin`, `cos`, `tan`, `exp`, `sqrt`, `ln` and `log`. + +### 4. Constant constructors + +For common mathematical constants, Cqlib provides dedicated constant constructors, such as `Parameter.pi()` and `Parameter.e()`. Using these constructors avoids typing approximate floating-point numbers by hand, thus improving the readability and consistency of expressions. + +```python +from cqlib import Parameter + +pi = Parameter.pi() +e = Parameter.e() + +print(pi.evaluate({})) +print(e.evaluate({})) +``` + +--- + +## Arithmetic expressions + +`Parameter` supports common Python arithmetic operations, so symbolic parameters, constants and parameter expressions can be combined like ordinary numeric values. These operations make it convenient to construct gate angles, the global phase, symbolic matrix elements or parameter relations that need to be reused in an algorithm. + +Note that a `Parameter` expression is an immutable object. Every arithmetic operation returns a new `Parameter` and does not modify the original parameter object. Therefore, the same symbolic parameter can be reused safely, and multiple different expressions can be constructed based on it. + +```python +from cqlib import Parameter + +theta = Parameter("theta") +phi = Parameter("phi") + +expr1 = theta + phi +expr2 = 2 * theta - phi / 3 +expr3 = -(theta + 1) +expr4 = theta ** 2 +expr5 = theta.pow(Parameter(0.5)) + +print(expr1) +print(expr2) +print(expr3) +print(expr4) +print(expr5) +``` + +`Parameter` also supports mixed operations between numeric values and symbolic parameters. A number can appear on the left or the right side of an expression, and Cqlib converts it automatically into the corresponding constant parameter expression. + + +--- + +## Mathematical functions + +Besides basic arithmetic operations, `Parameter` also provides a set of common mathematical functions for constructing more complex parameter expressions. These functions can act on a single symbolic parameter or on a composite expression formed by several parameters, and are suitable for describing parameterized gate angles, symbolic matrix elements, parameter transformation relations in an algorithm, and expressions that need differentiation or simplification. + +The commonly used mathematical functions are as follows: + +| Method | Description | +|---|---| +| `sin()` / `cos()` / `tan()` | Trigonometric functions | +| `asin()` / `acos()` / `atan()` | Inverse trigonometric functions | +| `sinh()` / `cosh()` / `tanh()` | Hyperbolic functions | +| `exp()` | Exponential function | +| `ln()` | Natural logarithm | +| `log(base=None)` | Logarithm; when `base=None`, it is treated as the natural logarithm | +| `sqrt()` | Square root | +| `abs()` | Absolute value | +| `floor()` / `ceil()` / `round()` | Rounding-related functions | + +```python +from cqlib import Parameter + +x = Parameter("x") +y = Parameter("y") + +expr = (2 * x + y).sin().exp() +grad = expr.derivative("x") + +print(expr) +print(grad) +``` + +The mathematical functions above support chained calls and can also be combined with arithmetic operations such as addition, subtraction, multiplication, division and exponentiation. Because every function call returns a new `Parameter` object and the original parameter is not modified, an existing symbolic parameter can be reused safely to construct many different expressions. + +--- + +## Evaluation + +`evaluate(bindings)` is used to compute a `Parameter` expression into a concrete floating-point result. For an expression that contains free symbols, the numeric value corresponding to each symbol needs to be provided through the `bindings` parameter; Cqlib can perform numeric evaluation only after all free symbols in the expression have been bound. + +```python +from cqlib import Parameter + +theta = Parameter("theta") +expr = (2 * theta + 1).sin() + +value = expr.evaluate({"theta": 0.5}) +print(value) +``` + +If the expression itself contains no free symbols, for example when it is formed by constants or mathematical constants, an empty dictionary can be passed, or the `bindings` parameter can be omitted. + +```python +from cqlib import Parameter + +expr = Parameter.pi() / 2 + +print(expr.evaluate({})) +print(expr.evaluate()) +``` + +Note that `evaluate()` requires the expression to produce a valid result in the real domain. Cqlib usually raises `ParameterError` in the following cases: +- The expression contains unbound free symbols; +- The bindings dictionary is missing a necessary parameter name; +- A binding value contains an illegal numeric value such as `NaN` or infinity; +- The expression cannot be evaluated in the real domain. + +Therefore, before a parameter expression is used for circuit matrix computation, parameter binding or validation of optimizer results, confirm first that all free symbols are bound correctly and that the binding values are within the legal numeric range. + +--- + +## Simplification + +`simplify()` is used to perform algebraic simplification on a `Parameter` expression and returns a new parameter expression. This interface is commonly used to reduce redundant expressions, canonicalize parameter forms, and tidy up expression structures before parameter binding, symbolic matrix construction or circuit analysis. + +Note that `simplify()` does not modify the original expression, but generates a new `Parameter` object. This ensures that the original parameter expression is reused safely in multiple circuit templates or algorithm flows. + +```python +from cqlib import Parameter + +theta = Parameter("theta") + +examples = [ + theta + 0, + theta * 1, + theta - theta, + (Parameter(0)).sin(), + (Parameter(1)).ln(), +] + +for expr in examples: + print(expr, "=>", expr.simplify()) +``` + +--- + +## Differentiation + +`derivative(var)` is used to perform symbolic differentiation of a `Parameter` expression with respect to a specified symbolic variable, and returns a new `Parameter` expression. This interface deals with differentiation relations at the level of classical parameter expressions, and is suitable for analyzing the dependence of gate angles, the global phase, symbolic matrix elements or other parameter combination relations on a certain variable. + +```python +from cqlib import Parameter + +theta = Parameter("theta") +phi = Parameter("phi") + +expr = (2 * theta + phi).sin() + +d_theta = expr.derivative("theta").simplify() +d_phi = expr.derivative("phi").simplify() + +print(d_theta) +print(d_phi) +print(d_theta.evaluate({"theta": 0.3, "phi": 0.4})) +``` + +Note that `Parameter.derivative()` only deals with the classical symbolic expression itself and does not directly compute the gradient of a quantum circuit with respect to some objective function. The overall circuit gradient usually also depends on the quantum state evolution, the measured observables, the simulator or backend execution results, as well as parameter-shift rules, the adjoint method or other quantum gradient computation methods. + +--- + +## Replacement and substitution + +In parameterized circuits and symbolic expression handling, a symbol in an expression sometimes needs to be replaced by another parameter expression. Here, Cqlib provides the `replace()` and `substitute()` interfaces for symbol-level replacement in `Parameter` expressions. + +### 1. `replace()` + +`replace()` is used to replace a single symbol in an expression. The name of the symbol to be replaced needs to be specified, together with the `Parameter` expression that replaces it. + +```python +from cqlib import Parameter + +x = Parameter("x") +y = Parameter("y") + +expr = x + 2 +new_expr = expr.replace("x", 3 * y) + +print(new_expr) +``` + +### 2. `substitute()` + +`substitute()` is used to replace multiple symbols at the same time, and is suitable for completing batch symbol substitution in one operation. A dictionary can be used to specify the correspondence between multiple symbols and replacement expressions. + +```python +from cqlib import Parameter + +x = Parameter("x") +y = Parameter("y") +z = Parameter("z") + +expr = x * y + 1 +new_expr = expr.substitute({"x": z + 1, "y": Parameter(2)}) + +print(new_expr.simplify()) +``` + +Note in particular that `replace()` and `substitute()` perform symbolic expression replacement; the replacement value should be a `Parameter` object or an expression convertible to `Parameter`, rather than a `float` result used directly for numeric evaluation. + +--- + +## Equivalence checking + +In parameterized circuits, symbolic matrices and compilation and optimization, it is sometimes necessary to determine whether two `Parameter` expressions represent the same mathematical meaning. Here, Cqlib provides a fairly conservative equivalence checking interface. "Conservative" means that when the interface returns `True`, the two expressions can be regarded as provably equivalent under the current rules and tolerance; when the interface returns `False`, it does not necessarily mean that the two expressions are definitely not equivalent — it may also mean only that the current symbolic rules cannot prove them equal. + +```python +from cqlib import Parameter + +x = Parameter("x") + +a = (x + 0).simplify() +b = x + +print(a.provably_equal(b)) +``` + +| Method | Description | +|---|---| +| `provably_equal(other, tolerance=1e-12)` | Conservatively determines, within the given tolerance, whether two expressions can be proven equal | +| `provably_equal_modulo(other, modulus, tolerance=1e-12)` | Determines whether two expressions are equivalent under the specified modulus | + +Note that equivalence checking usually depends on expression simplification, algebraic rules and numeric tolerance. For expressions with a more complex structure, call `simplify()` first and then perform equivalence checking. + +--- + +## State checking methods + +`Parameter` provides a set of state checking methods, used to determine whether an expression contains free symbols, whether it is a constant, whether it is equal to a particular value, or whether it can be treated as a single symbol. These methods are commonly used in scenarios such as parameter validation, compilation and optimization, circuit canonicalization, pre-binding checks and symbolic matrix handling. + +Through these interfaces, whether a parameter expression meets the requirements of the current flow can be confirmed in advance before matrix computation, circuit conversion or backend execution. For example, before numeric matrix computation it is usually necessary to confirm that no unbound symbol exists in the circuit; during compilation and optimization it may be necessary to identify whether the global phase is zero, or to determine whether a certain rotation angle can be eliminated. + +The commonly used state checking methods are as follows: + +| Method | Description | +|---|---| +| `symbols` | Returns the list of free symbols contained in the expression | +| `canonicalized()` | Returns the canonicalized form used for parameter interning and internal comparison | +| `is_exact_zero()` | Determines whether the expression represents the constant `0` exactly | +| `is_constant()` | Determines whether the expression contains no free variables | +| `is_zero()` | Determines whether the expression can be evaluated to `0` under the current conditions | +| `is_one()` | Determines whether the expression can be evaluated to `1` under the current conditions | +| `as_symbol()` | If the expression is exactly a single symbol, returns that symbol name; otherwise returns `None` | + +```python +from cqlib import Parameter + +theta = Parameter("theta") +zero = Parameter(0) + +print(theta.is_constant()) # False +print(theta.as_symbol()) # theta +print(zero.is_exact_zero()) # True +print((theta - theta).simplify().is_exact_zero()) +``` + +--- + +## Using parameters in a circuit + +In Cqlib, a parameterized quantum gate can accept an ordinary numeric parameter or a `Parameter` expression as input. Numeric parameters are suitable for constructing circuits whose angles are already determined; `Parameter` expressions are suitable for constructing parameterized circuit templates and, in a later algorithm flow, for parameter binding, parameter sweeps or optimization iterations. + +```python +from cqlib import Circuit, Parameter + +theta = Parameter("theta") +phi = Parameter("phi") + +c = Circuit(2) +c.rx(0, theta) +c.ry(1, phi) +c.rzz(0, 1, 2 * theta - phi) + +print(c.parameters) +print(c.symbols) +``` + +--- + +## Parameter binding + +Parameter binding means replacing the symbolic parameters in a circuit with concrete values, producing a circuit that can be used further for matrix computation, simulation execution, IR export or backend running. Cqlib completes parameter binding through `assign_parameters()`. This interface accepts a mapping from parameter names to values, such as `dict[str, float]`, and returns a new bound circuit. + +```python +from cqlib import Circuit, Parameter + +theta = Parameter("theta") +phi = Parameter("phi") + +c = Circuit(1) +c.rx(0, theta) +c.rz(0, theta + phi) + +bound = c.assign_parameters({"theta": 0.25, "phi": 0.5}) + +print(bound[0].params) +print(bound[1].params) +print(bound.parameters) +``` + +Cqlib also supports partial parameter binding. If the bindings dictionary provides values for only some of the symbols, the symbols that are bound are replaced by numeric values, and the unbound symbols continue to be kept in the circuit. + +```python +partial = c.assign_parameters({"theta": 0.25}) + +print(partial[0].params) # [0.25] +print(partial[1].params) # [Parameter("0.25 + phi")] +print(partial.symbols) # ['phi'] +``` + +--- + +## Relation to matrix conversion + +When a parameterized circuit undergoes matrix conversion, there are two cases, the numeric matrix and the symbolic matrix: +- A numeric matrix requires all parameters in the circuit to be bound to concrete values; +- A symbolic matrix can keep parameter expressions, and is used for symbolic analysis and validation of small-scale circuits. + +### 1. Numeric matrix + +`to_matrix()` is used to convert a quantum circuit into a numeric matrix. Because every element of a numeric matrix must be a definite complex value, all symbolic parameters in the circuit need to be bound to numeric values before `to_matrix()` is called. + +```python +from cqlib import Circuit, Parameter + +theta = Parameter("theta") + +c = Circuit(1) +c.rx(0, theta) + +bound = c.assign_parameters({"theta": 0.3}) +matrix = bound.to_matrix() +``` + +### 2. Symbolic matrix + +To keep the parameter structure in the circuit matrix, `to_symbolic_matrix()` can be used. This interface generates a symbolic matrix whose elements may contain `Parameter` expressions, and is suitable for small-scale parameterized circuit validation, symbolic gate definition and compilation rule checking. + +```python +from cqlib import Circuit, Parameter + +theta = Parameter("theta") + +c = Circuit(1) +c.rz(0, theta) + +symbolic = c.to_symbolic_matrix() +print(symbolic.shape) +print(symbolic.symbols) + +numeric = symbolic.evaluate({"theta": 0.5}) +``` + +--- + +## Using parameters in custom symbolic gates + +Cqlib supports using parameters in custom symbolic gates. Through `SymbolicComplex` and `SymbolicMatrix`, a matrix containing `Parameter` expressions can be defined, and this matrix can be used as the symbolic matrix representation of a `UnitaryGate`. + +```python +from cqlib import Circuit, Parameter +from cqlib.circuit import SymbolicComplex, SymbolicMatrix, UnitaryGate + +theta = Parameter("theta") + +matrix = SymbolicMatrix( + [ + [SymbolicComplex.one(), SymbolicComplex.zero()], + [SymbolicComplex.zero(), SymbolicComplex.exp_i(theta)], + ] +) + +gate = UnitaryGate("SymbolicPhase", 1, num_params=1).with_symbolic_matrix( + matrix, + ["theta"], +) + +c = Circuit(1) +c.append_unitary_gate(gate, [0], [0.25]) +``` + +Note that `params` in `with_symbolic_matrix(matrix, params)` is used to define the parameter order of the custom gate. When `append_unitary_gate()` is called in a circuit, the parameters passed in are bound to the parameter names in the symbolic matrix in this order. + +--- + +## Next steps + +- [Circuit analysis and transformation](4_circuit_analysis.md): use tools such as inversion, decomposition, matrix conversion and operation inspection. +- [Control flow](5_control_flow.md): use measurement results, classical variables and structured control flow to construct dynamic circuits. +- [Intermediate Representation](../1_ir/0_overview.md): understand the bidirectional conversion flow between Circuit and IR. + diff --git a/docs/documentation/en/1_cqlib/0_circuit/4_circuit_analysis.md b/docs/documentation/en/1_cqlib/0_circuit/4_circuit_analysis.md new file mode 100644 index 00000000..ef2e64cf --- /dev/null +++ b/docs/documentation/en/1_cqlib/0_circuit/4_circuit_analysis.md @@ -0,0 +1,257 @@ +# Circuit analysis and transformation + +After Circuit construction is complete, the circuit usually still needs to go through further analysis, validation and transformation processes. Depending on actual needs, the operation sequence can be inspected, gate types counted, symbolic parameters bound, composite gates expanded, an inverse circuit generated, the circuit converted into a matrix representation, or the circuit structure checked for consistency. These capabilities are an important foundation connecting circuit construction, algorithm validation, IR conversion, compilation and optimization, and backend execution. + +This page gives a systematic introduction to the common interfaces related to circuit analysis and structure transformation in `cqlib.circuit`, and explains how a circuit is inspected, reused, transformed and validated after construction. + +--- + +## Inspecting the operation sequence + +`Circuit.operations` returns the ordered list of `ValueOperation` objects in a circuit. The list is kept in the order in which the operations were appended to the circuit. + +Each `ValueOperation` can be further broken down into: +- `instruction` describes the gate, instruction or control flow structure corresponding to the operation; +- `qubits` denotes the qubits this operation acts on; +- `params` denotes the parameters carried by this operation; +- `label` records a debugging label or the transformation source. + +```python +from cqlib import Circuit, Parameter + +theta = Parameter("theta") + +c = Circuit(2) +c.h(0) +c.cx(0, 1) +c.rz(1, theta) + +for index, op in enumerate(c.operations): + if op.instruction.is_instruction: + instruction = op.instruction.instruction + name = instruction.name + else: + name = op.instruction.classical_control.kind + + qubits = [q.index for q in op.qubits] + print(index, name, qubits, op.params, op.label) +``` + +Besides iterating over the complete operation list, a single operation can also be accessed by index. Both `circuit[i]` and `circuit.operation(i)` can be used to obtain the `ValueOperation` at a specified position. + +```python +first = c[0] +second = c.operation(1) + +print(first.instruction.instruction.name) # H +print(second.instruction.instruction.name) # CX +``` + +--- + +## Collecting circuit statistics + +During circuit analysis and compilation, it is usually necessary to count the gate types, the number of operations or the occurrences of a particular instruction in a circuit. Such statistics can be used to assess the circuit size, compare the changes before and after optimization, or verify in tests whether a compilation pass produced the expected result. + +In Cqlib, the required statistics logic can be implemented through `Circuit.operations`. Because `operations` keeps the complete operation sequence of a circuit, instruction names, acting qubits, parameters and control flow kinds can be extracted by iterating over the operation list. + +```python +from collections import Counter +from cqlib import Circuit + +c = Circuit(3) +c.h(0) +c.cx(0, 1) +c.cx(1, 2) +c.rzz(0, 2, 0.5) + +names = [] +for op in c.operations: + if op.instruction.is_instruction: + names.append(op.instruction.instruction.name) + else: + names.append(op.instruction.classical_control.kind) + +print(Counter(names)) +``` + +--- + +## Generating the inverse circuit + +`inverse()` generates the inverse circuit corresponding to the current circuit. For a circuit that contains only invertible quantum gates, its inverse circuit represents the quantum evolution process opposite to that of the original circuit. Specifically, Cqlib reverses the operation order in the original circuit and replaces each operation with the corresponding inverse operation, thus obtaining a new `Circuit`. + +```python +import numpy as np +from cqlib import Circuit + +c = Circuit(2) +c.h(0) +c.cx(0, 1) +c.rz(1, 0.25) + +inv = c.inverse() + +product = inv.to_matrix() @ c.to_matrix() +print(np.allclose(product, np.eye(4), atol=1e-10)) + +print([op.instruction.instruction.name for op in c.operations]) +print([op.instruction.instruction.name for op in inv.operations]) +``` + +Note the following: + +- `inverse()` does not modify the original circuit, but returns a new `Circuit`; +- `Barrier` does not change the quantum state and can be regarded as its own inverse, so it is kept; +- Non-invertible structures such as `Measure`, `Reset` and classical control flow cannot generate an ordinary inverse circuit. + +```python +from cqlib import Circuit +from cqlib.circuit import CircuitError + +c = Circuit(1) +c.measure(0) + +try: + c.inverse() +except CircuitError: + print("measurement is not invertible") +``` + +--- + +## Decomposing composite gates + +`decompose()` expands the composite gates in a circuit and returns a new `Circuit`. When a circuit contains a composite gate represented by `CircuitGate`, `decompose()` replaces it with the original operation sequence defined inside the composite gate. This interface is commonly used in matrix validation, IR export, compilation and backend adaptation flows that do not support composite gates. + +```python +from cqlib import Circuit + +sub = Circuit(2) +sub.h(0) +sub.cx(0, 1) +bell = sub.to_gate("Bell") + +main = Circuit(2) +main.append_circuit_gate(bell, [0, 1]) + +print(len(main)) # 1 + +flat = main.decompose() +print(len(flat)) # 2 +print([op.instruction.instruction.name for op in flat.operations]) +``` + +For a parameterized composite gate, `decompose()` binds and expands according to the positional parameters passed in when the composite gate was appended. + +```python +from cqlib import Circuit, Parameter + +theta = Parameter("theta") + +sub = Circuit(1) +sub.rx(0, theta) +block = sub.to_gate("RxBlock") + +main = Circuit(1) +main.append_circuit_gate(block, [0], [0.75]) + +flat = main.decompose() +print(flat[0].params) # [0.75] +``` + +--- + +## Matrix of a single operation + +Cqlib supports computing the matrix of a single `ValueOperation`. `ValueOperation.matrix()` obtains the matrix representation corresponding to one concrete operation, and is suitable for gate-level testing, compilation rule validation, gate decomposition result checking and local operation analysis. + +```python +import numpy as np +from cqlib import Qubit +from cqlib.circuit import StandardGate, ValueOperation + +op = ValueOperation.from_standard_gate(StandardGate.X(), [Qubit(0)]) +matrix = op.matrix() + +print(np.allclose(matrix, np.array([[0, 1], [1, 0]], dtype=complex))) +``` + +Note that this interface applies only to ordinary unitary operations. + +--- + +## Converting to a gate + +In quantum algorithm development, a circuit that has already been constructed usually needs to serve as a reusable module invoked multiple times in other circuits. `to_gate(name)` encapsulates the current circuit as a `CircuitGate`, thus converting a sub-circuit into a composite gate with a name. + +This approach is suitable for expressing structured algorithm modules, such as state preparation modules, oracles, ansatz blocks, entanglement layers or reused circuit fragments. Unlike appending a circuit directly with `compose()`, `to_gate()` keeps the module boundary, making the main circuit clearer in structure and also facilitating later composite gate decomposition, parameter binding, IR export and compilation. + +```python +from cqlib import Circuit + +sub = Circuit(1) +sub.h(0) + +gate = sub.to_gate("HadamardBlock") + +main = Circuit(1) +main.append_circuit_gate(gate, [0]) +``` + +When a later analysis flow needs to inspect the internal structure of a composite gate, `decompose()` can be called to expand the composite gate into the original operation sequence. + +```python +flat = main.decompose() +print(flat.to_matrix()) +``` + +--- + +## Circuit composition and remapping + +`compose()` appends the operations of one circuit to another circuit. Through this interface, several circuit fragments can be concatenated in order into one complete circuit, and qubits can also be remapped during appending so that the logical qubits in a sub-circuit act on specified qubits in the main circuit. + +```python +from cqlib import Circuit + +prefix = Circuit(3) +prefix.h(0) + +block = Circuit(2) +block.cx(0, 1) + +prefix.compose(block, [1, 2]) + +print([op.instruction.instruction.name for op in prefix.operations]) +print([q.index for q in prefix[1].qubits]) +``` + +--- + +## Structure validation + +`validate()` checks the internal consistency of a circuit object and helps to detect potential structural problems as early as possible before later analysis, transformation, compilation or execution: +- For an ordinary static circuit, validation usually focuses on whether qubit references, operation objects and parameter structures are consistent; +- For a dynamic circuit that contains measurement, classical variables and control flow, validation further checks whether classical handles, measurement result references and control flow structures meet the circuit semantic requirements. + +```python +from cqlib import Circuit +from cqlib.circuit import ClassicalExpr + +c = Circuit(1) +c.measure(0) +c.if_(ClassicalExpr.bool_literal(True), lambda body: body.x(0)) + +c.validate() +``` + +--- + + + +## Next steps + +- [Control flow](5_control_flow.md): use measurement results, classical variables and structured control flow to construct dynamic circuits. +- [Intermediate Representation](../1_ir/0_overview.md): understand the bidirectional conversion flow between Circuit and IR. +- [QCIS support](../1_ir/1_qcis.md): export a Cqlib circuit as QCIS instructions or load a circuit from a QCIS file. diff --git a/docs/documentation/en/1_cqlib/0_circuit/5_control_flow.md b/docs/documentation/en/1_cqlib/0_circuit/5_control_flow.md new file mode 100644 index 00000000..2d17a22c --- /dev/null +++ b/docs/documentation/en/1_cqlib/0_circuit/5_control_flow.md @@ -0,0 +1,353 @@ +# Control flow + +Control flow is used to describe conditional branches, loop structures and multi-branch selection logic in a quantum circuit. Unlike an ordinary circuit that only applies quantum gates in a fixed order, control flow can keep structured semantics in a unified circuit representation, such as "execute a section of the circuit when a certain classical condition is met", "repeat a section of the circuit according to a counter variable" or "select a different branch according to an integer value". + +This page covers how to construct classical expressions, how to organize circuit structures with interfaces such as `if_`, `if_else`, `while_`, `for_uint` and `switch`, and how to perform basic validation and structural checking after construction. + +--- + +## Classical types + +`ClassicalType` is used to describe the type information of classical values in control flow. Through explicit type definitions, whether the types of conditional expressions, loop variables, branch targets and ordinary classical expressions match can be checked during circuit construction. + +The commonly used classical types include the following categories: + +| Type constructor | Description | Typical use | +|---|---|---| +| `ClassicalType.bit()` | A single bit, with the value `0` or `1` | Single-qubit classical result, bit expression | +| `ClassicalType.bool()` | A logical boolean value | `if` and `while` conditions | +| `ClassicalType.uint(width)` | An unsigned integer of the specified width | Counter, loop variable, `switch` target | +| `ClassicalType.bit_vec(width)` | A bit vector of the specified width | Bit vector expression, bit packing, bit concatenation result | + +```python +from cqlib.circuit import ClassicalType + +bit_ty = ClassicalType.bit() +bool_ty = ClassicalType.bool() +u3_ty = ClassicalType.uint(3) +bits_ty = ClassicalType.bit_vec(4) + +print(bit_ty.width) # 1 +print(u3_ty.width) # 3 +``` + +Type objects can also be used to create commonly used literal expressions. The following example creates the `0` and `1` literals of a 3-bit unsigned integer type: + +```python +zero = ClassicalType.uint(3).zero_literal() +one = ClassicalType.uint(3).one_literal() +``` + +--- + +## Classical variables + +`Circuit.var(type)` is used to allocate a mutable classical variable in a circuit. Every classical variable has explicit type information, such as `Bool`, `UInt` or `BitVec`, and is bound to the `Circuit` object that created it. + +Classical variables are usually used to store and reference classical state in control flow, for example recording a branch flag, maintaining a loop counter, or serving as the target value of `switch` branch determination. + +```python +from cqlib import Circuit +from cqlib.circuit import ClassicalExpr, ClassicalType + +c = Circuit(1) + +flag = c.var(ClassicalType.bool()) +counter = c.var(ClassicalType.uint(4)) + +c.store(flag, ClassicalExpr.bool_literal(True)) +c.store(counter, ClassicalExpr.uint_literal(4, 3)) + +print(flag.index) +print(flag.ty) +print(flag.circuit_id == c.id) +``` + +`store(target, value)` is used to write a classical expression to a classical variable. When writing, the type of `value` must be compatible with `target.ty`; otherwise the classical data semantics in the circuit are broken. + +--- + +## Classical expressions + +`ClassicalExpr` is used to describe the classical computation logic in control flow. It can represent expression structures such as literals, variable reads, logical operations, comparison operations, conditional selection, bit extraction and bit concatenation. + +`ClassicalExpr` can be understood as a kind of "classical expression syntax tree". It does not perform the computation immediately itself, but records the classical computation relations and takes part in later validation, transformation and compilation processing as part of the circuit structure. + +### 1. Literals + +Literals are used to construct fixed classical values directly, such as a boolean value, a single bit, an unsigned integer or a bit vector. + +```python +from cqlib.circuit import ClassicalExpr + +truth = ClassicalExpr.bool_literal(True) +bit_one = ClassicalExpr.bit_literal(True) +u3_five = ClassicalExpr.uint_literal(3, 5) +bits = ClassicalExpr.bit_vec_literal(4, 0b1010) +``` + +Here, `uint_literal(width, value)` and `bit_vec_literal(width, value)` need the bit width to be specified explicitly. The bit width is part of the classical type and affects the legality of later comparison, loop and concatenation operations. + +### 2. Reading from a variable + +A classical variable is itself a handle that can be written to and referenced. If the current value of a variable needs to be read in an expression, `ClassicalExpr.var(var)` can be used to convert the variable into an expression, or the `expr()` method provided by the variable handle can be called directly. + +```python +from cqlib import Circuit +from cqlib.circuit import ClassicalExpr, ClassicalType + +c = Circuit(1) + +flag = c.var(ClassicalType.bool()) +flag_expr = ClassicalExpr.var(flag) + +# equivalent shorthand +same_flag_expr = flag.expr() +``` + +### 3. Logical operations + +`ClassicalExpr` supports common logical operations. The operators `~`, `&`, `|` and `^` correspond to logical NOT, AND, OR and XOR respectively. + +```python +from cqlib.circuit import ClassicalExpr + +a = ClassicalExpr.bool_literal(True) +b = ClassicalExpr.bool_literal(False) + +expr = a | b +simplified = expr.simplified() +print(simplified.is_bool_true()) # True +``` + +### 4. Comparison + +To construct a comparison condition, the static methods provided by `ClassicalExpr` should be used. + +```python +from cqlib.circuit import ClassicalExpr + +x = ClassicalExpr.uint_literal(3, 2) +y = ClassicalExpr.uint_literal(3, 5) + +cond1 = ClassicalExpr.lt(x, y) +cond2 = ClassicalExpr.equal(x, y) +cond3 = ClassicalExpr.not_equal(x, y) +cond4 = ClassicalExpr.ge(y, x) +``` + +The supported comparison methods include: + +- `equal(lhs, rhs)` +- `not_equal(lhs, rhs)` +- `lt(lhs, rhs)` +- `le(lhs, rhs)` +- `gt(lhs, rhs)` +- `ge(lhs, rhs)` + +The comparison result is usually a `Bool` expression and can be used directly as the condition of `if_`, `if_else` or `while_`. + +### 5. Selection, extraction and concatenation + +Besides basic logical and comparison operations, `ClassicalExpr` also supports operations such as conditional selection, bit extraction and bit concatenation, which can be used to construct more complex classical control logic. + +```python +from cqlib.circuit import ClassicalExpr + +cond = ClassicalExpr.bool_literal(True) +a = ClassicalExpr.uint_literal(3, 1) +b = ClassicalExpr.uint_literal(3, 7) + +selected = ClassicalExpr.select(cond, a, b) + +bits = ClassicalExpr.bit_vec_literal(4, 0b1010) +low_bit = bits.extract_bit(0) +middle = bits.extract_bits(offset=1, width=2) + +packed = ClassicalExpr.pack_bits( + [ + ClassicalExpr.bit_literal(True), + ClassicalExpr.bit_literal(False), + ] +) +``` + +Here: +- `select(cond, a, b)` means selecting between two expressions according to a boolean condition: when `cond` is true, `a` is selected, otherwise `b` is selected; +- `extract_bit()` is used to extract a single bit from a `BitVec`, and `extract_bits()` is used to extract a contiguous multi-bit fragment; +- `pack_bits(bits)` is used to pack multiple Bit expressions into one BitVec expression. + +--- + +## `if_` conditional branch + +`if_(condition, body)` is used to append a conditional control structure that does not contain an `else` branch to a circuit. This interface means that when the given condition is met, the circuit operations in the branch body are executed; when the condition is not met, the branch body is skipped. + +Here: +- `condition` must be a `ClassicalExpr` expression of type `Bool`, used to describe the condition determination logic; +- `body` is a callback function, used to construct the circuit content to be executed when the condition is met. The callback function receives a temporary circuit builder, in which quantum gates or other supported operations can continue to be appended. + +```python +from cqlib import Circuit +from cqlib.circuit import ClassicalExpr + +c = Circuit(1) + +c.if_( + ClassicalExpr.bool_literal(True), + lambda body: body.x(0), +) +``` + +For a simple branch containing only one operation, the `lambda` form can be used to make the code more concise. For a branch body containing multiple operations, an ordinary function definition is recommended, to improve code readability and later maintainability. + +```python +def then_body(body): + body.h(0) + body.x(0) + +c.if_(ClassicalExpr.bool_literal(True), then_body) +``` + +Control flow callbacks adopt an atomic construction approach. That is, the branch body is committed to the circuit only after the callback function has completed successfully; if an exception is raised during callback execution, Cqlib abandons this branch body construction, avoiding an incomplete or inconsistent control flow structure left in the circuit. + +```python +def failing_body(body): + body.x(0) + raise RuntimeError("construction failed") + +try: + c.if_(ClassicalExpr.bool_literal(True), failing_body) +except RuntimeError: + pass +``` + +--- + +## `if_else` conditional branch + +`if_else(condition, then_body, else_body)` is used to construct a conditional control structure with an `else` branch in a circuit. This interface means that when the `condition` is true, the `then_body` branch is executed; when the condition is false, the `else_body` branch is executed. Both branches are constructed through callback functions and are stored in the circuit as part of the same conditional control structure. + +```python +from cqlib import Circuit +from cqlib.circuit import ClassicalExpr + +c = Circuit(1) + +c.if_else( + ClassicalExpr.bool_literal(False), + lambda then_body: then_body.x(0), + lambda else_body: else_body.z(0), +) + +control = c[0].instruction.classical_control +print(control.kind) # if +``` + +Like `if_`, the two branches of `if_else` also adopt an atomic construction approach. The related control flow structure is committed to the circuit only after both callback functions have completed construction successfully; if either branch raises an exception during construction, this control flow construction is abandoned, avoiding an incomplete or inconsistent branch structure left in the circuit. + +--- + +## `while_` loop + +`while_(condition, body)` is used to construct a loop structure based on a boolean condition in a circuit. This interface means that when the `condition` is met, the operations defined in the loop body `body` are executed; before each iteration begins, whether to continue is determined according to the condition expression. + +Here: +- `condition` must be a `ClassicalExpr` expression of type `Bool`; +- `body` is a callback function, used to describe the circuit operations in the loop body. The callback function receives a temporary circuit builder, in which quantum gates, control flow jumps or other supported operations can be appended. + +```python +from cqlib import Circuit +from cqlib.circuit import ClassicalExpr + +c = Circuit(1) + +c.while_( + ClassicalExpr.bool_literal(True), + lambda body: body.break_loop(), +) +``` + +In the loop body, the following jump operations can be used to control the loop execution flow: + +- `break_loop()`: exits the innermost loop; +- `continue_loop()`: skips the remaining operations in the current loop body and proceeds to the next determination of the innermost loop. + +```python +def loop_body(body): + body.x(0) + body.continue_loop() + +c = Circuit(1) +c.while_(ClassicalExpr.bool_literal(True), loop_body) +``` + +--- + +## `for_uint` loop + +`for_uint(var, start, stop, step, body)` is used to construct a loop structure based on an unsigned integer variable in a circuit, with the loop range `[start, stop)`: it starts from `start`, increments by `step` each iteration, and ends the loop when the loop variable reaches or exceeds `stop`. + +```python +from cqlib import Circuit +from cqlib.circuit import ClassicalExpr, ClassicalType + +c = Circuit(1) +loop_var = c.var(ClassicalType.uint(3)) + +def body(builder, index_expr): + # index_expr is a UInt expression that reads loop_var + builder.rx(0, 0.25) + +c.for_uint( + loop_var, + ClassicalExpr.uint_literal(3, 0), + ClassicalExpr.uint_literal(3, 3), + ClassicalExpr.uint_literal(3, 1), + body, +) +``` + +--- + +## `switch` multi-branch selection + +`switch(target, build)` is used to construct a multi-branch control structure based on integer value matching in a circuit. This interface selects one of several registered branches for execution according to the value of the `target` expression. `build` is a callback function, used to register the different integer matching branches and an optional default branch. + +```python +from cqlib import Circuit +from cqlib.circuit import ClassicalExpr + +c = Circuit(1) + +def build_switch(builder): + builder.value(0, lambda body: body.x(0)) + builder.value(1, lambda body: body.z(0)) + builder.default(lambda body: body.h(0)) + +c.switch(ClassicalExpr.uint_literal(2, 1), build_switch) +``` + +--- + +## Structure validation + +Compared with an ordinary linear circuit, a control flow structure has higher requirements on the internal consistency of the circuit. Therefore, after a circuit containing control flow is constructed, calling `validate()` to perform structural validation on the circuit is recommended. + +```python +from cqlib import Circuit +from cqlib.circuit import ClassicalExpr + +c = Circuit(1) +c.if_(ClassicalExpr.bool_literal(True), lambda body: body.x(0)) + +c.validate() +``` + +--- + +## Next steps + +- [Intermediate Representation](../1_ir/0_overview.md): understand the bidirectional conversion flow between Circuit and IR, supporting circuit persistence, cross-toolchain exchange and later compilation. +- [QCIS support](../1_ir/1_qcis.md): export a Cqlib circuit as QCIS instructions or load a circuit from a QCIS file. +- [OpenQASM 2.0 support](../1_ir/2_qasm2.md): import and export between Cqlib circuits and the OpenQASM 2.0 format. diff --git a/docs/documentation/en/1_cqlib/1_ir/0_overview.md b/docs/documentation/en/1_cqlib/1_ir/0_overview.md new file mode 100644 index 00000000..f04c83ca --- /dev/null +++ b/docs/documentation/en/1_cqlib/1_ir/0_overview.md @@ -0,0 +1,125 @@ +# Intermediate Representation (IR) overview + +IR (Intermediate Representation) connects the `Circuit` objects inside Cqlib with external text formats. It is not a standalone algorithm module, but the protocol layer through which a quantum program flows between construction, storage, cross-toolchain exchange, hardware submission and debugging reproduction. + +From a user perspective, the IR module mainly solves four kinds of problems: + +- Exporting a `Circuit` built with Cqlib to a standard or hardware-related text format. +- Importing quantum circuit text generated by external tools as a Cqlib `Circuit`. +- Converting between the QCIS, OpenQASM 2.0 and OpenQASM 3.0 formats. +- Providing a unified entry point for subsequent compilation and optimization, visualization, simulation and hardware adaptation. + +## 1. The IR working model of Cqlib + +The core internal object of Cqlib is `Circuit`. An external text format (such as QCIS or OpenQASM) does not bypass `Circuit` to enter the simulator or the compiler directly; it is first parsed into a `Circuit`, and all subsequent modules work on the same set of circuit objects. + +```mermaid +flowchart LR + A["QCIS text"] --> D["cqlib.ir.qcis.loads/load"] + B["OpenQASM 2.0 text"] --> E["cqlib.ir.qasm2.loads/load"] + C["OpenQASM 3.0 text"] --> F["cqlib.ir.qasm3.loads/load"] + D --> G["Cqlib Circuit"] + E --> G + F --> G + G --> H["Compilation and optimization / Visualization / Simulation / Hardware adaptation"] + G --> I["qcis.dumps/dump"] + G --> J["qasm2.dumps/dump"] + G --> K["qasm3.dumps/dump"] +``` + +This means that the core semantics of IR are: + +- `load/loads`: converts an external text format into a `Circuit`. +- `dump/dumps`: converts a `Circuit` into an external text format. +- Format conversion is essentially `format A -> Circuit -> format B`. +- If an external format has insufficient expressive power, exporting returns an explicit error instead of silently losing semantics. + +## 2. Supported formats + +| Format | Python module | Main use | Suitable scenario | Notes | +|---|---|---|---|---| +| QCIS | `cqlib.ir.qcis` | Circuit text oriented to hardware control instructions | Access to independently developed quantum hardware, low-level instruction exchange, QCIS file parsing | Expressive power leans toward the hardware instruction layer, and complex classical control flow is not supported | +| OpenQASM 2.0 | `cqlib.ir.qasm2` | Classical quantum assembly standard | Standard circuit files, benchmark circuits, interoperation with older OpenQASM toolchains | Classical semantics are limited, and `if (creg == int) qop` is mainly supported | +| OpenQASM 3.0 | `cqlib.ir.qasm3` | Modern quantum program text format | Dynamic circuits, measurement assignment, classical variables, control flow, interoperation with modern OpenQASM toolchains | The supported subset is bounded by what a Cqlib `Circuit` can express | + +## 3. Unified API + +All three IR submodules adopt the same API naming: `loads/dumps` for strings and `load/dump` for files. + +| Function | Input | Output | Use | +|---|---|---|---| +| `loads(text)` | Format text string | `Circuit` | Parse a circuit from a string | +| `load(path)` | File path | `Circuit` | Parse a circuit from a file | +| `dumps(circuit)` | `Circuit` | String | Export a circuit as text | +| `dump(circuit, path)` | `Circuit`, file path | `None` | Write a circuit to a file | + +Example: + +```python +from cqlib import Circuit +from cqlib.ir import qasm2, qasm3, qcis + +circuit = Circuit(2) +circuit.h(0) +circuit.cx(0, 1) + +qasm2_text = qasm2.dumps(circuit) +qasm3_text = qasm3.dumps(circuit) +qcis_text = qcis.dumps(circuit) + +restored = qasm3.loads(qasm3_text) +print(restored.num_qubits) +``` + +## 4. Measurement and classical data model of Cqlib IR + +A Cqlib `Circuit` supports dynamic circuits, so measurement is not simply "drawing a measurement gate". Two kinds of classical data are distinguished internally: + +| Concept | Meaning | Common source | User-writable | +|---|---|---|---| +| `ClassicalValue` | Immutable temporary result produced by measurement | `circuit.measure()`, `measure_bits()` | No | +| `ClassicalVar` | Mutable classical storage for users | `circuit.var(...)` | Yes | + +For example: + +```python +from cqlib import Circuit +from cqlib.circuit import ClassicalType + +circuit = Circuit(2) + +# only produces a measurement result value, without specifying a user variable. +measurement = circuit.measure(0) + +# create a user-visible classical bit and write the measurement result into it. +bit = circuit.var(ClassicalType.bit()) +circuit.measure_into(1, bit) +``` + +When exporting to OpenQASM, Cqlib folds internal measurement values into the natural form of the target format as far as possible: + +| Cqlib operation | OpenQASM 3 output preference | +|---|---| +| `measure_into(q, bit)` | `c0 = measure q[0];` | +| `measure_bits_into([0, 1], bitvec)` | `c0 = measure q;` | +| Bare measurement `measure(q)` | Generates `bit[n] meas;` and writes into `meas[i]`, ensuring that the text can be read back | +| Partial register measurement | Outputs the form `c0[i] = measure q[j];`, preserving the order | + +This part is one of the current focuses of IR modification in Cqlib: when exporting QASM3, internal temporary values are no longer exposed directly as meaningless `v0/v1`; instead, measurement targets that can be read back with clear semantics are generated. + +## 5. Choosing a format + +| Goal | Recommended format | Reason | +|---|---|---| +| Interoperation with public datasets or general tools | OpenQASM 2.0 or 3.0 | Common in the ecosystem and supported by many tools | +| Expressing modern dynamic circuits, measurement assignment and classical variables | OpenQASM 3.0 | More complete semantics than 2.0 | +| Hardware-instruction oriented or QCIS file delivery | QCIS | Closer to the hardware instruction format | +| Only storing a simple circuit to be read by old tools | OpenQASM 2.0 | Simple and stable | +| Preserving Cqlib-specific dynamic semantics | OpenQASM 3.0 preferred | Expressive power closer to `Circuit` | + +## 6. Recommended reading order + +1. Read [QCIS support](1_qcis.md) first to understand hardware-instruction-style text. +2. Then read [OpenQASM 2.0 support](2_qasm2.md) to master the most common standard format. +3. Then read [OpenQASM 3.0 support](3_qasm3.md) to understand measurement, classical variables and dynamic circuits. +4. Finally read the [format conversion workflow](4_conversion_workflow.md) to learn how to achieve cross-format interoperation and verification. diff --git a/docs/documentation/en/1_cqlib/1_ir/1_qcis.md b/docs/documentation/en/1_cqlib/1_ir/1_qcis.md new file mode 100644 index 00000000..726c8494 --- /dev/null +++ b/docs/documentation/en/1_cqlib/1_ir/1_qcis.md @@ -0,0 +1,221 @@ +# QCIS support + +QCIS is a quantum circuit text format oriented to hardware instructions and engineering delivery. It describes quantum operations in the form of "one instruction per line", and is suitable for exporting a Cqlib circuit to hardware-related toolchains, or for restoring a `Circuit` from an existing QCIS file for simulation, visualization and recompilation. + +Corresponding Python module: + +```python +from cqlib.ir import qcis +``` + +## 1. API overview + +| Function | Use | Example | +|---|---|---| +| `qcis.loads(text)` | Parse a `Circuit` from a QCIS string | `circuit = qcis.loads("H Q0\nM Q0\n")` | +| `qcis.load(path)` | Parse a `Circuit` from a QCIS file | `circuit = qcis.load("input.qcis")` | +| `qcis.dumps(circuit)` | Export a `Circuit` as a QCIS string | `text = qcis.dumps(circuit)` | +| `qcis.dump(circuit, path)` | Write a `Circuit` to a QCIS file | `qcis.dump(circuit, "output.qcis")` | + +## 2. QCIS text structure + +QCIS is a line-based format. Each line usually consists of three parts: + +```text +OPCODE QUBIT_LIST [PARAMETER_LIST] +``` + +Example: + +```text +H Q0 +CZ Q0 Q1 +RZ Q0 pi/2 +M Q0 Q1 +``` + +Meaning: + +- `H Q0`: applies a Hadamard gate on `Q0`. +- `CZ Q0 Q1`: applies a CZ gate on `Q0` and `Q1`. +- `RZ Q0 pi/2`: applies an RZ gate with parameter `pi/2` on `Q0`. +- `M Q0 Q1`: measures `Q0` and `Q1`. + +QCIS supports comments starting with `//`, and also supports inline comments. + +```text +// prepare Bell state +H Q0 +CZ Q0 Q1 // entangle Q0 and Q1 +``` + +## 3. Loading from a QCIS string + +```python +from cqlib.ir import qcis + +qcis_code = """ +H Q0 +CZ Q0 Q1 +RZ Q0 pi/2 +M Q0 Q1 +""" + +circuit = qcis.loads(qcis_code) +print(circuit.num_qubits) +print(len(circuit.operations)) +``` + +The loaded object is a standard Cqlib `Circuit`, which can continue to be used for visualization, simulation, compilation and optimization or export again. + +## 4. Loading from a QCIS file + +```python +from cqlib.ir import qcis + +# first write the QCIS text above to a file to be read +qcis.dump(qcis.loads(qcis_code), "input.qcis") + +circuit = qcis.load("input.qcis") +``` + +If the file does not exist, an I/O-related exception is raised; if the QCIS content has a syntax error or the gate parameters do not match, a `ValueError` is raised. + +## 5. Exporting a QCIS string + +```python +from cqlib import Circuit +from cqlib.ir import qcis + +circuit = Circuit(2) +circuit.h(0) +circuit.cz(0, 1) +circuit.rz(0, 3.141592653589793 / 2) +circuit.measure(0) +circuit.measure(1) + +text = qcis.dumps(circuit) +print(text) +``` + +Typical output: + +```text +H Q0 +CZ Q0 Q1 +RZ Q0 pi/2 +M Q0 +M Q1 +``` + +## 6. Exporting a QCIS file + +```python +from cqlib.ir import qcis + +qcis.dump(circuit, "output.qcis") +``` + +An I/O exception is raised when writing the file fails; if the circuit contains an instruction that QCIS cannot represent, a `ValueError` is raised. + +## 7. Supported instruction types + +The current QCIS module covers the standard gates and instructions in Cqlib that can be represented as QCIS text. + +| Type | Supported content | +|---|---| +| Single-qubit gates | `H`, `S`, `SD`, `T`, `TD`, `X`, `X2P`, `X2M`, `Y`, `Y2P`, `Y2M`, `Z` | +| Parameterized single-qubit gates | `RX`, `RY`, `RZ`, `RXY`, `U`, `XY`, `XY2P`, `XY2M`, `PHASE` | +| Multi-qubit gates | `CX`, `CY`, `CZ`, `SWAP`, `CCX`, `CRX`, `CRY`, `CRZ`, `RXX`, `RYY`, `RZZ`, `RZX`, `FSIM` | +| Instructions | `M` measurement, `B`/`Barrier` barrier | +| Delay | `I Qn t`, meaning a delay of `t` ticks on `Qn` | + +Alias rules: + +- `SDG` can be loaded and is normalized to `SD` on export. +- `TDG` can be loaded and is normalized to `TD` on export. + +## 8. `I` in QCIS is not an ordinary identity gate + +QCIS `I Qn t` represents a delay instruction, not the identity gate among the Cqlib standard gates. + +```text +I Q0 10 +``` + +The meaning is idling on `Q0` for the specified duration. Cqlib represents it as a `Delay` instruction after loading. On export, if an ordinary identity gate has been placed directly in the circuit, the QCIS dumper refuses to export, avoiding the misinterpretation of an "identity gate with no duration" as a "hardware delay with a duration". + +## 9. Measurement semantics + +QCIS only describes the measurement instruction itself, and does not describe the explicit classical register assignment of OpenQASM. Therefore: + +```text +M Q0 Q1 +``` + +After loading into Cqlib, it becomes a measurement operation in the `Circuit`; if it is subsequently exported to OpenQASM 3, Cqlib automatically supplies a readable classical destination, for example: + +```text +OPENQASM 3.0; +include "stdgates.inc"; + +qubit[2] q; +bit[2] meas; + +meas[0] = measure q[0]; +meas[1] = measure q[1]; +``` + +This step is normal behavior at the format conversion boundary: QCIS has no classical assignment syntax while OpenQASM 3 does, so Cqlib completes the target register on export. + +## 10. Conversion from QCIS to OpenQASM + +```python +from cqlib.ir import qcis, qasm3 + +qcis_code = """ +H Q0 +CZ Q0 Q1 +M Q0 Q1 +""" + +circuit = qcis.loads(qcis_code) +qasm3_text = qasm3.dumps(circuit) +print(qasm3_text) +``` + +This flow is suitable for converting a hardware-side QCIS file into more general OpenQASM 3 text, which is then handed to other tools that support OpenQASM for reading. + +## 11. Unsupported cases or cases requiring prior handling + +QCIS is a hardware-instruction-style format and is not suitable for expressing all high-level circuit semantics. The following content usually cannot be exported to QCIS directly: + +- A `UnitaryGate` in arbitrary matrix form. +- A user-defined `CircuitGate`, unless it is first decomposed into basic gates supported by QCIS. +- Generalized forms of multi-controlled gates, unless they have already been decomposed. +- Complex classical control flow such as `if/else`, `for`, `while` and `switch`. +- Standard identity gates and the global phase `GPhase`. + +Recommended handling: + +```python +compiled = circuit.decompose() +text = qcis.dumps(compiled) +``` + +If it still fails, the decomposed circuit still contains instructions that QCIS cannot express, and compilation and optimization or target gate set mapping is required first. + +## 12. Troubleshooting common errors + +| Symptom | Common cause | Handling | +|---|---|---| +| `ValueError: QCIS parse error` | An unknown gate, an incorrect qubit format or a parameter count mismatch in the text | Check whether the `Q0` form is used, and check the parameter count of each gate | +| An unsupported gate is reported on export | The `Circuit` contains a gate or control flow that QCIS does not support | Call `decompose()` first, then perform target gate set mapping | +| The behavior of the `I` instruction differs from expectation | `I` in QCIS is a delay, not an ordinary identity | If it is only an ordinary identity gate, exporting it to QCIS is not recommended | +| The number of qubits appears to change | Cqlib constructs the circuit according to the maximum qubit index that actually appears | Check whether the QCIS file starts from `Q1` without `Q0` | + +## Next steps + +- [OpenQASM 2.0 support](2_qasm2.md) +- [OpenQASM 3.0 support](3_qasm3.md) +- [Format conversion workflow](4_conversion_workflow.md) diff --git a/docs/documentation/en/1_cqlib/1_ir/2_qasm2.md b/docs/documentation/en/1_cqlib/1_ir/2_qasm2.md new file mode 100644 index 00000000..0cc8abad --- /dev/null +++ b/docs/documentation/en/1_cqlib/1_ir/2_qasm2.md @@ -0,0 +1,226 @@ +# OpenQASM 2.0 support + +OpenQASM 2.0 is a very widely used circuit exchange format in the quantum computing ecosystem. It is suitable for describing static circuits dominated by quantum gates, registers, measurements and simple classical conditions, and it is also the format supported by default in many public benchmarks and old toolchains. + +Corresponding Python module: + +```python +from cqlib.ir import qasm2 +``` + +## 1. API overview + +| Function | Use | Example | +|---|---|---| +| `qasm2.loads(text)` | Parse a `Circuit` from an OpenQASM 2.0 string | `circuit = qasm2.loads(qasm_text)` | +| `qasm2.load(path)` | Parse a `Circuit` from an OpenQASM 2.0 file | `circuit = qasm2.load("input.qasm")` | +| `qasm2.dumps(circuit)` | Export a `Circuit` as an OpenQASM 2.0 string | `text = qasm2.dumps(circuit)` | +| `qasm2.dump(circuit, path)` | Write a `Circuit` to an OpenQASM 2.0 file | `qasm2.dump(circuit, "output.qasm")` | + +## 2. Minimal OpenQASM 2.0 program + +```text +OPENQASM 2.0; +include "qelib1.inc"; +qreg q[2]; +h q[0]; +cx q[0],q[1]; +``` + +Loading in Cqlib: + +```python +from cqlib.ir import qasm2 + +qasm_code = """ +OPENQASM 2.0; +include "qelib1.inc"; +qreg q[2]; +h q[0]; +cx q[0],q[1]; +""" + +circuit = qasm2.loads(qasm_code) +print(circuit.num_qubits) +print(len(circuit.operations)) +``` + +## 3. Handling of `qelib1.inc` + +A common OpenQASM 2.0 program contains: + +```text +include "qelib1.inc"; +``` + +Cqlib handles `qelib1.inc` internally, and this file does not need to exist locally. Even if the include is omitted, Cqlib can recognize common qelib1 gate names; but for compatibility with OpenQASM 2.0 toolchains, the export still writes it out in a canonical way: + +```text +include "qelib1.inc"; +``` + +## 4. Exporting OpenQASM 2.0 from Cqlib + +```python +from cqlib import Circuit +from cqlib.ir import qasm2 + +circuit = Circuit(2) +circuit.h(0) +circuit.cx(0, 1) + +text = qasm2.dumps(circuit) +print(text) +``` + +Typical output: + +```text +// This file is auto-generated by Cqlib v0.1.0. +OPENQASM 2.0; +include "qelib1.inc"; + +qreg q[2]; + +h q[0]; +cx q[0],q[1]; +``` + +The output is normalized, and the spaces, comments, register names or layout of the input file are not guaranteed to be preserved. + +## 5. File reading and writing + +```python +from cqlib.ir import qasm2 + +# first write the QASM text above to a file to be read +qasm2.dump(qasm2.loads(qasm_code), "input.qasm") + +circuit = qasm2.load("input.qasm") +qasm2.dump(circuit, "output.qasm") +``` + +`load` is suitable for reading an external `.qasm` file; `dump` is suitable for saving a Cqlib circuit to a file for submission, archiving or passing to other tools. + +## 6. Parameter expressions + +Cqlib supports the parameter expressions commonly used in OpenQASM 2.0, for example: + +```text +rx(pi) q[0]; +ry(pi/2) q[0]; +rz(3*pi/4) q[0]; +``` + +Loading example: + +```python +from cqlib.ir import qasm2 + +qasm_code = """ +OPENQASM 2.0; +include "qelib1.inc"; +qreg q[1]; +rx(pi) q[0]; +ry(pi/2) q[0]; +rz(3*pi/4) q[0]; +""" + +circuit = qasm2.loads(qasm_code) +``` + +The current loader also accepts expression extensions common in some engineering projects, such as scientific notation without a decimal point, `1e-5`, and functions such as `asin`, `acos` and `atan`. + +## 7. Measurement + +OpenQASM 2.0 uses `creg` to represent a classical register, and the measurement syntax is: + +```text +measure q[0] -> c[0]; +``` + +Complete example: + +```python +from cqlib.ir import qasm2 + +qasm_code = """ +OPENQASM 2.0; +include "qelib1.inc"; +qreg q[2]; +creg c[2]; +h q[0]; +cx q[0],q[1]; +measure q[0] -> c[0]; +measure q[1] -> c[1]; +""" + +circuit = qasm2.loads(qasm_code) +``` + +On export, Cqlib maps internal measurement values to the `creg` form that OpenQASM 2.0 can represent. For a multi-bit measurement such as `measure_bits_into([0, 1], c)`, it is expanded into per-bit measurement: + +```text +measure q[0] -> c0[0]; +measure q[1] -> c0[1]; +``` + +## 8. Custom gates + +OpenQASM 2.0 supports `gate` definitions: + +```text +OPENQASM 2.0; +include "qelib1.inc"; +gate bell a,b { + h a; + cx a,b; +} +qreg q[2]; +bell q[0],q[1]; +``` + +On loading, Cqlib converts a custom gate into a `CircuitGate`. On export, if the `Circuit` contains a custom circuit gate that can be expanded into OpenQASM 2.0, the corresponding `gate` definition is generated as well. + +## 9. Conditional execution + +OpenQASM 2.0 supports limited classical conditions: + +```text +if (c == 1) x q[0]; +``` + +Cqlib supports this form of condition where the register as a whole equals an integer. Note that: + +- OpenQASM 2.0 does not support `else`. +- OpenQASM 2.0 does not support general `while/for/switch`. +- When exporting to QASM2, Cqlib refuses complex control flow that cannot be expressed losslessly. + +If the circuit contains complex dynamic semantics, OpenQASM 3.0 is preferred. + +## 10. Support and limitations + +| Capability | Loading | Export | Description | +|---|---|---|---| +| `qreg`, `creg` | Supported | Supported | Register names are normalized on export | +| qelib1 standard gates | Supported | Supported | `qelib1.inc` is handled internally | +| Parameterized gates | Supported | Supported | Common expressions are supported | +| Custom `gate` | Supported | Supported | Must be representable as an OpenQASM 2.0 gate body | +| `measure`, `reset`, `barrier` | Supported | Supported | Some complex combinations are normalized | +| `if (creg == int) qop` | Supported | Supported | The limited condition of OpenQASM 2.0 | +| `else/while/for/switch` | Not suitable | Lossless export not supported | OpenQASM 3.0 is recommended | +| General classical assignment | Not suitable | Not supported | Insufficient expressive power of OpenQASM 2.0 | + +## 11. Troubleshooting common errors + +| Symptom | Common cause | Handling | +|---|---|---| +| Parsing fails | A missing `OPENQASM 2.0;`, a syntax error or a gate parameter count mismatch | Confirm the format with a minimal example first, then add gates section by section | +| Export fails | The circuit contains control flow or instructions that OpenQASM 2.0 cannot represent | Switch to QASM3, or decompose/remove the complex semantics first | +| Gate names differ after another tool reads the file | Cqlib normalizes some gates, for example the `U1/U2/U3` mapping | Verify by matrix or circuit semantics, not only by the text gate names | +| Measurement register names change | The exporter generates canonical register names | This is normal behavior and does not affect measurement semantics | + +## Next steps + +- [OpenQASM 3.0 support](3_qasm3.md) +- [Format conversion workflow](4_conversion_workflow.md) diff --git a/docs/documentation/en/1_cqlib/1_ir/3_qasm3.md b/docs/documentation/en/1_cqlib/1_ir/3_qasm3.md new file mode 100644 index 00000000..2e700c56 --- /dev/null +++ b/docs/documentation/en/1_cqlib/1_ir/3_qasm3.md @@ -0,0 +1,279 @@ +# OpenQASM 3.0 support + +OpenQASM 3.0 targets modern quantum program design, and is better suited than OpenQASM 2.0 to expressing measurement assignment, classical variables, control flow and more complete dynamic circuit semantics. The `qasm3` module of Cqlib is responsible for bidirectional conversion between OpenQASM 3.0 text and a Cqlib `Circuit`. + +Corresponding Python module: + +```python +from cqlib.ir import qasm3 +``` + +## 1. API overview + +| Function | Use | Example | +|---|---|---| +| `qasm3.loads(text)` | Parse a `Circuit` from an OpenQASM 3.0 string | `circuit = qasm3.loads(qasm_text)` | +| `qasm3.load(path)` | Parse a `Circuit` from an OpenQASM 3.0 file | `circuit = qasm3.load("input.qasm")` | +| `qasm3.dumps(circuit)` | Export a `Circuit` as an OpenQASM 3.0 string | `text = qasm3.dumps(circuit)` | +| `qasm3.dump(circuit, path)` | Write a `Circuit` to an OpenQASM 3.0 file | `qasm3.dump(circuit, "output.qasm")` | + +## 2. Minimal OpenQASM 3.0 program + +```text +OPENQASM 3.0; +include "stdgates.inc"; + +qubit[2] q; + +h q[0]; +cx q[0],q[1]; +``` + +Loading into Cqlib: + +```python +from cqlib.ir import qasm3 + +qasm_code = """ +OPENQASM 3.0; +include "stdgates.inc"; + +qubit[2] q; + +h q[0]; +cx q[0],q[1]; +""" + +circuit = qasm3.loads(qasm_code) +print(circuit.num_qubits) +``` + +Both `OPENQASM 3;` and `OPENQASM 3.0;` can be loaded. + +## 3. Exporting OpenQASM 3.0 from Cqlib + +```python +from cqlib import Circuit +from cqlib.ir import qasm3 + +circuit = Circuit(2) +circuit.h(0) +circuit.cx(0, 1) + +text = qasm3.dumps(circuit) +print(text) +``` + +Typical output: + +```text +OPENQASM 3.0; +include "stdgates.inc"; + +qubit[2] q; + +h q[0]; +cx q[0],q[1]; +``` + +The exporter generates normalized text, and does not preserve the spaces, comments and variable names of the original input. + +## 4. File reading and writing + +```python +from cqlib.ir import qasm3 + +# first write the QASM text above to a file to be read +qasm3.dump(qasm3.loads(qasm_code), "input.qasm") + +circuit = qasm3.load("input.qasm") +qasm3.dump(circuit, "output.qasm") +``` + +A file reading failure raises an I/O exception; a syntax error, a semantic error or an unsupported feature raises a `ValueError`. + +## 5. `stdgates.inc` + +OpenQASM 3.0 standard gates are usually introduced through: + +```text +include "stdgates.inc"; +``` + +Cqlib relies on the OpenQASM 3 frontend to recognize standard library gates. On export, `include "stdgates.inc";` is written out as well, which makes it convenient for external OpenQASM 3 tools to read. + +For extension gates that Cqlib has but `stdgates.inc` does not necessarily provide directly, the exporter generates a gate definition before the main circuit. For example, `x2p`, `rxx`, `rzz`, `rzx`, `fsim` and others may be written as custom gates. + +## 6. Measurement assignment + +Measurement in OpenQASM 3.0 is more natural than in OpenQASM 2.0, because it supports assignment expressions: + +```text +bit c; +c = measure q[0]; +``` + +Multi-bit measurement: + +```text +bit[2] c; +c = measure q; +``` + +Partial bit-vector assignment: + +```text +bit[2] c; +c[0] = measure q[2]; +c[1] = measure q[0]; +``` + +Cqlib currently supports the measurement forms above, and generates a canonical form that can be read back as far as possible on export. + +## 7. Mapping rules from Cqlib measurement to QASM3 + +Cqlib divides measurement into two layers internally: + +- `ClassicalValue`: the immutable result produced by measurement. +- `ClassicalVar`: the mutable classical variable created by users. + +On export to QASM3, the following rules are applied: + +| Cqlib operation | QASM3 output | Description | +|---|---|---| +| `measure_into(q, bit)` | `c0 = measure q[0];` | Single-bit measurement written into a user variable | +| `measure_bits_into([0,1], bitvec)` | `c0 = measure q;` | The whole register is merged into one output when the order is consistent | +| `measure_bits_into([2,0], bitvec)` | `c0[0] = measure q[2];` and `c0[1] = measure q[0];` | Non-contiguous or reordered measurement is split into indexed assignment | +| `measure(q)` | `bit[n] meas; meas[i] = measure q[j];` | A bare measurement generates an explicit register, ensuring that the QASM3 can be read back | + +Example: + +```python +from cqlib import Circuit +from cqlib.ir import qasm3 + +circuit = Circuit(2) +circuit.h(0) +circuit.cx(0, 1) +circuit.measure(0) +circuit.measure(1) + +print(qasm3.dumps(circuit)) +``` + +Output: + +```text +OPENQASM 3.0; +include "stdgates.inc"; + +qubit[2] q; +bit[2] meas; + +h q[0]; +cx q[0],q[1]; +meas[0] = measure q[0]; +meas[1] = measure q[1]; +``` + +The reason for this is: a bare measurement can produce only a temporary value inside Cqlib, but if QASM3 text is to be stored and read back reliably across tools, an explicit classical destination is preferable. The current implementation generates a `meas` register instead of leaking the internal temporary name `v0/v1`. + +## 8. Scalar measurement assignment compatibility + +Common forms in OpenQASM 3.0 can also be loaded: + +```text +OPENQASM 3.0; +qubit[1] q; +bit v; +v = measure q[0]; +``` + +And: + +```text +OPENQASM 3.0; +qubit q; +bit[2] c; +c[0] = measure q; +``` + +Cqlib lowers them into an internal measurement operation plus a classical store operation. + +## 9. reset, barrier and global phase + +Example: + +```text +OPENQASM 3.0; +include "stdgates.inc"; + +qubit[2] q; +reset q[0]; +barrier q[0],q[1]; +gphase(0.25); +``` + +Cqlib supports loading these statements as corresponding `Circuit` operations. On export, equivalent semantics are preserved as far as possible. + +## 10. Custom gates + +OpenQASM 3.0 custom gate example: + +```text +OPENQASM 3.0; +include "stdgates.inc"; + +gate bell a, b { + h a; + cx a, b; +} + +qubit[2] q; +bell q[0], q[1]; +``` + +After loading, Cqlib treats `bell` as a circuit definition gate. On export, if the `Circuit` contains a `CircuitGate` that can be represented as a QASM3 gate body, the corresponding definition is output as well. + +## 11. Control flow support scope + +The QASM3 loader of Cqlib supports part of the control flow that can be mapped to the current `Circuit`: + +| OpenQASM 3 feature | Current support | Description | +|---|---|---| +| `if/else` | Supported | The condition must be convertible into a Cqlib classical expression | +| Static `for` | Supported | For example a fixed range `[0:2]`, which can be expanded statically | +| `switch` | Partial support for exact-value cases | Suitable for simple unsigned integer cases | +| `while` | Limited by the frontend and the expressive power of the IR | A complex runtime loop may be rejected | +| `break/continue` | Restricted | Depends on whether it can be mapped to the current control flow model | + +Recommended principle: if a circuit is mainly used for cross-toolchain exchange, use simple, static and expandable control flow as far as possible; for a complex dynamic program, check whether the target backend supports it first. + +## 12. Current support and limitations + +| Type | Support status | +|---|---| +| `qubit`, `qubit[n]` | Supported | +| `bit`, `bit[n]`, `bool`, `uint[n]` | Common forms supported | +| Standard gates | Supported for gates that can be mapped to a Cqlib `StandardGate` | +| Cqlib extension gates | A gate definition is generated on export | +| Measurement assignment | Common forms of scalar, bit-vector and indexed assignment supported | +| `reset`, `barrier`, `gphase` | Supported | +| Custom gate | Supported for a mappable gate body | +| calibration, pulse, extern, hardware qubit, alias | Currently not supported | +| Arbitrary complex classical arithmetic | Lossless lowering is currently not supported | +| Complex lvalue slicing, multi-dimensional indexing | Currently not supported, or only the necessary subset is supported | + +## 13. Troubleshooting common errors + +| Symptom | Common cause | Handling | +|---|---|---| +| `QASM3 parse error` | A syntax error, a frontend semantic check failure or use of a feature that Cqlib does not support | Verify with a minimal QASM3 program first, then add features step by step | +| `unsupported feature` | Use of calibration, extern, a complex lvalue or an unsupported gate modifier | Rewrite as basic gates, or expand into a basic circuit in an external tool first | +| Export fails | The `Circuit` contains a Delay, Unitary or complex store that QASM3 cannot represent | Call `decompose()` first, or switch to a more suitable format | +| An external tool cannot read the QASM3 exported by Cqlib | The external tool does not yet support some extension gates or OpenQASM 3 subsets | Decompose the extension gates first, or switch to the syntax subset supported by the target tool | +| The measurement register name becomes `meas` | Cqlib generates an explicit target register for a bare measurement | Normal behavior, used to ensure that the text can be read back | + +## Next steps + +- [Format conversion workflow](4_conversion_workflow.md) diff --git a/docs/documentation/en/1_cqlib/1_ir/4_conversion_workflow.md b/docs/documentation/en/1_cqlib/1_ir/4_conversion_workflow.md new file mode 100644 index 00000000..895d7e40 --- /dev/null +++ b/docs/documentation/en/1_cqlib/1_ir/4_conversion_workflow.md @@ -0,0 +1,236 @@ +# Format conversion workflow + +The IR module of Cqlib can serve as a conversion hub between QCIS, OpenQASM 2.0, OpenQASM 3.0 and external frameworks. All conversions follow the same principle: + +```text +External format A -> Cqlib Circuit -> External format B +``` + +In other words, conversion is not string replacement: the input format is first parsed into a structured `Circuit`, and then the target format exporter regenerates the text. + +## 1. Conversion overview + +```mermaid +flowchart LR + A["QCIS"] --> C["Cqlib Circuit"] + B["OpenQASM 2.0"] --> C + D["OpenQASM 3.0"] --> C + E["External circuit tools"] --> F["OpenQASM 2/3 text"] --> C + C --> G["QCIS"] + C --> H["OpenQASM 2.0"] + C --> I["OpenQASM 3.0"] + C --> J["Visualization / Simulation / Compilation / Hardware adaptation"] +``` + +## 2. Converting QCIS to OpenQASM 3.0 + +When to use: converting a hardware-side or existing QCIS file into a modern standard format, for consumption by documentation systems, test tools or other OpenQASM tools. + +```python +from cqlib.ir import qcis, qasm3 + +qcis_code = """ +H Q0 +CX Q0 Q1 +M Q0 Q1 +""" + +circuit = qcis.loads(qcis_code) +qasm3_text = qasm3.dumps(circuit) +print(qasm3_text) +``` + +Typical output: + +```text +OPENQASM 3.0; +include "stdgates.inc"; + +qubit[2] q; +bit[2] meas; + +h q[0]; +cx q[0],q[1]; +meas[0] = measure q[0]; +meas[1] = measure q[1]; +``` + +Here `meas` is the explicit classical register that Cqlib supplies for QCIS measurement. QCIS has no measurement assignment syntax, but QASM3 does, so the exporter generates a target register that can be read back. + +## 3. Converting OpenQASM 3.0 to QCIS + +When to use: converting a standard QASM3 circuit into hardware-related QCIS instructions. + +```python +from cqlib.ir import qasm3, qcis + +qasm3_code = """ +OPENQASM 3.0; +include "stdgates.inc"; + +qubit[2] q; +h q[0]; +cz q[0],q[1]; +""" + +circuit = qasm3.loads(qasm3_code) +qcis_text = qcis.dumps(circuit) +print(qcis_text) +``` + +Output: + +```text +H Q0 +CZ Q0 Q1 +``` + +If the QASM3 contains control flow, custom gates, complex classical assignment or a non-target gate set that QCIS cannot express, decomposition or compilation mapping is required first. + +## 4. Converting OpenQASM 2.0 to OpenQASM 3.0 + +When to use: upgrading an old-format circuit, or converting a QASM2 benchmark into QASM3 to make it convenient to express subsequent dynamic measurement logic. + +```python +from cqlib.ir import qasm2, qasm3 + +qasm2_code = """ +OPENQASM 2.0; +include "qelib1.inc"; +qreg q[2]; +h q[0]; +cx q[0],q[1]; +""" + +circuit = qasm2.loads(qasm2_code) +qasm3_text = qasm3.dumps(circuit) +print(qasm3_text) +``` + +## 5. Converting OpenQASM 3.0 to OpenQASM 2.0 + +When to use: output is needed for an old tool that supports only QASM2. + +```python +from cqlib.ir import qasm3, qasm2 + +qasm3_code = """ +OPENQASM 3.0; +include "stdgates.inc"; + +qubit[2] q; +h q[0]; +cx q[0],q[1]; +""" + +circuit = qasm3.loads(qasm3_code) +qasm2_text = qasm2.dumps(circuit) +print(qasm2_text) +``` + +Note: the conversion succeeds only when the features used by the QASM3 circuit can also be expressed by QASM2. Complex `if/else`, `for`, `switch`, general classical assignment and similar constructs usually cannot be converted into QASM2 losslessly. + +## 6. General way to convert between external tools and Cqlib + +An external quantum circuit tool can exchange circuits with Cqlib in the same way as long as it can import or export OpenQASM text: + +```text +External circuit object -> OpenQASM text -> cqlib.ir.qasm2/qasm3.loads -> Cqlib Circuit +``` + +```text +Cqlib Circuit -> cqlib.ir.qasm2/qasm3.dumps -> OpenQASM text -> External circuit object +``` + +Using OpenQASM text as the tool boundary is recommended, rather than coupling the internal circuit objects of both sides directly. This reduces the complexity of dependencies, and also makes it convenient to save input and output files in CI and perform regression tests. + +If an external tool cannot read some Cqlib extension gates, the circuit can be decomposed into a more basic gate set on the Cqlib side first, and then exported. + +## 7. Verification after conversion + +After conversion, three kinds of verification are recommended. + +### 7.1 Structural check + +```python +print(circuit.num_qubits) +print(len(circuit.operations)) +``` + +### 7.2 Visualization check + +```python +from cqlib.visualization import draw_text + +print(draw_text(circuit)) +``` + +### 7.3 Small-scale unitary equivalence check + +For a small circuit without measurement and dynamic control, matrices can be compared: + +```python +import numpy as np +from cqlib.ir import qasm3 + +before = circuit.to_matrix() +restored = qasm3.loads(qasm3.dumps(circuit)) +after = restored.to_matrix() + +assert np.allclose(before, after) +``` + +A circuit containing measurement or control flow is not suitable for direct comparison of the overall unitary matrix; instead, the operation sequence, measurement targets, sampling results or backend execution results should be checked. + +## 8. Format selection strategy + +| Input source | Target | Recommended route | +|---|---|---| +| A QCIS file | General OpenQASM documentation or test file | `qcis.load -> qasm3.dump` | +| An external circuit tool | Cqlib simulation or compilation | `external tool exports OpenQASM -> cqlib.qasm2/qasm3.loads` | +| A Cqlib circuit | Old toolchain | `qasm2.dumps` | +| A Cqlib circuit | Modern toolchain or dynamic circuit | `qasm3.dumps` | +| A Cqlib circuit | Hardware instruction delivery | Compile to the target gate set first, then `qcis.dumps` | + +## 9. Pre-conversion checklist + +Before exporting, confirm: + +- Whether the circuit contains a gate that the target format does not support. +- Whether there are unbound parameters. +- Whether complex classical control flow is present. +- Whether custom gates need `decompose()` first. +- Whether hardware topology mapping or target gate set decomposition is needed first. +- Whether measurement results need to be preserved, or whether it is purely drawing/simulation. + +Example: + +```python +compiled = circuit.decompose() +text = qasm3.dumps(compiled) +``` + +## 10. Common conversion issues + +| Issue | Root cause | Suggestion | +|---|---|---| +| Extra `bit[n] meas` after converting QCIS to QASM3 | QCIS has no classical assignment, and QASM3 needs an explicit measurement target to ensure that it can be read back | Normal behavior, no need to delete | +| Conversion from QASM3 to QASM2 fails | The QASM3 uses dynamic semantics that QASM2 cannot express | Keep QASM3, or simplify the circuit | +| An external tool fails to read the OpenQASM exported by Cqlib | The target tool does not support some extension gates or syntax subsets | Decompose into basic gates first, or switch to a format supported by the target tool | +| QCIS export fails | The circuit contains a gate, Unitary, control flow or ordinary identity that QCIS does not support | Compile/decompose to a QCIS-supported gate set first | +| The text differs from the original after conversion | The exporter normalizes the text and does not preserve the layout and variable names | Take the circuit semantics as the standard, not exact string equality | + +## 11. Recommended engineering practices + +- Keep both the source format and the target format in the project, for traceability. +- Add unit tests for critical format conversions, covering at least `loads -> dumps -> loads`. +- Use matrix equivalence verification for simple unitary circuits. +- Check the measurement order and the classical target for circuits containing measurement. +- For a hardware submission path, verify the gate set, qubit numbering and measurement output with a small circuit first. + +## Next steps + +- [IR overview](0_overview.md) +- [QCIS support](1_qcis.md) +- [OpenQASM 2.0 support](2_qasm2.md) +- [OpenQASM 3.0 support](3_qasm3.md) diff --git a/docs/documentation/en/1_cqlib/2_device/0_overview.md b/docs/documentation/en/1_cqlib/2_device/0_overview.md new file mode 100644 index 00000000..ca6d804d --- /dev/null +++ b/docs/documentation/en/1_cqlib/2_device/0_overview.md @@ -0,0 +1,114 @@ +# Device overview + +cqlib.device is the data module in Cqlib used to describe the hardware capabilities, noise characteristics and execution results of a quantum backend, and it is located under the cqlib.device package. + +In a quantum computing workflow, Circuit expresses "how the algorithm is done", while device expresses "what the hardware allows". It provides unified underlying support for the following key engineering aspects: + +- **Physical constraint awareness**: defines the hardware topology (which qubits support two-qubit gate coupling and the direction restrictions) +- **High-fidelity modeling**: refined management of qubit coherence time (T1/T2), readout error and gate fidelity +- **Dynamic layout tracking**: maintains the mapping between logical qubits and physical qubits in real time during the compilation and routing stage +- **Noise simulation**: builds channel models usable for quantum noise simulation, with Kraus operator representation +- **Full task lifecycle management**: tracks the complete closed loop of a task from submission, queuing and running to result return + +--- + +## Core objects at a glance + +| Object | Use | Key capabilities | +|---|---|---| +| Topology | Hardware topology graph | Directed coupling modeling, dynamic addition and removal of nodes and edges, adjacency queries, the line factory method | +| Device | Complete hardware description | Calibration parameters with global defaults plus local overrides, factory methods for quick construction (line/bidirectional_line/ring/star/grid/from_edges) | +| QubitProp / EdgeProp / InstructionProp | Calibration property containers | Store the T1/T2, readout error, gate fidelity and duration of qubits and edges | +| Layout | Logical-physical mapping | Bidirectional mapping queries, SWAP routing operations, dynamic binding and unbinding | +| NoiseModel / SingleQubitNoise / TwoQubitNoise / ReadoutError | Noise channels | Bit flip, phase flip, depolarizing, amplitude/phase damping, etc., with Kraus operator export | +| Outcome / Status / ExecutionResult | Execution results | Measurement result encapsulation, task state machine (queued/running/completed/failed/cancelled), probability calculation | + +--- + +## Quick example: from topology to result + +The following example demonstrates the complete workflow of the device module, covering topology definition, device creation, calibration injection, layout mapping, noise configuration and result management. + +```python +from cqlib.circuit import Instruction, StandardGate +from cqlib.device import ( + Device, EdgeProp, ExecutionResult, InstructionProp, Layout, + NoiseModel, OperationKey, QubitProp, ReadoutError, + SingleQubitNoise, Topology, TwoQubitNoise, +) + +# 1) define the hardware topology: qubit list + (control, target, gate name) triples +topo = Topology([0, 1, 2], [(0, 1, "CX"), (1, 2, "CZ")]) + +# 2) create the device and set the global default calibration parameters +device = Device("demo_backend", [0, 1, 2], topo) +device.default_t1 = 50.0 +device.default_t2 = 35.0 +device.default_readout_error = 0.05 +device.default_single_qubit_error = 0.001 +device.default_two_qubit_error = 0.01 + +# 3) inject local calibration parameters (they override the global defaults) +q0_prop = QubitProp(readout_error=0.02) +q0_prop.t1 = 80.0 +q0_prop.t2 = 70.0 +device.add_qubit_properties(0, q0_prop) + +cx_prop = InstructionProp( + Instruction.from_standard_gate(StandardGate.CX), + error_rate=0.015, +) +cx_prop.length = 220.0 +edge_prop = EdgeProp() +edge_prop.add_native_instruction(cx_prop) +device.add_edge_properties(0, 1, edge_prop) + +# 4) layout mapping: logical qubits -> physical qubits +layout = Layout.from_pairs([(0, 11), (1, 10)], physical_count=13) +layout.swap_physical(11, 12) + +# 5) noise model configuration +noise = NoiseModel() +noise.add_readout_error(0, ReadoutError(0.02, 0.01)) +noise.add_single_qubit_error(StandardGate.X, 0, SingleQubitNoise.bit_flip(0.005)) +noise.add_two_qubit_error(StandardGate.CX, 0, 1, TwoQubitNoise.depolarizing(0.02)) + +# 6) task execution result +result = ExecutionResult("task-1", [0, 1], 100, 2, "demo_backend") +result.start() +result.finish({"00": 60, "11": 40}) +result.calc_probabilities() + +# 7) query and verification output +print("device name:", device.name) +print("T1 of qubit 0 (local value):", device.get_t1(0)) +print("T1 of qubit 2 (falls back to the global default):", device.get_t1(2)) +print("number of usable qubits:", device.num_usable_qubits) +print("logical -> physical mapping:", layout.l2p_map) +print("readout error of qubit 0:", noise.get_readout_error(0)) +print("task status:", result.status.kind) +print("probability distribution:", result.probabilities) + +# query noise channels through OperationKey +skey = OperationKey.new_single(StandardGate.X, 0) +qubit_noises = noise.get_single_qubit_errors(skey) +if qubit_noises: + print("number of X gate noise channels:", len(qubit_noises)) +``` + +**Notes**: + +- get_t1(), get_t2() and get_readout_error() follow the query policy of **local value first, falling back to the global default when there is no local value** +- l2p_map returns a mapping dictionary from LogicalQubit to PhysicalQubit +- probabilities is obtained by calc_probabilities() normalizing the raw counts, and the result is dict[str, float] +- All NoiseModel.add_*() methods return None, and raise ValueError when parameter validation fails +- A method involving an instruction parameter (such as Device.single_qubit_error) requires an Instruction object (constructed through Instruction.from_standard_gate()) rather than a StandardGate enum value +- The init_map parameter of Layout requires dict[Qubit, Qubit], so passing {0: 11} directly reports a type error. Layout.from_pairs() is recommended as an alternative + +## Next steps + +- [Topology modeling](1_topology.md): understand the directed graph model of Topology and its instantiation, query and dynamic modification operations +- [Device modeling](2_device.md): master the global default plus local override calibration strategy of Device +- [Layout mapping](3_layout.md): learn the bidirectional logical-physical qubit mapping and SWAP routing operations of Layout +- [Noise model](4_noise.md): understand the use of noise channels such as NoiseModel, SingleQubitNoise, TwoQubitNoise and ReadoutError +- [Execution result and status](5_result.md): become familiar with the complete lifecycle and error handling of Outcome, Status and ExecutionResult diff --git a/docs/documentation/en/1_cqlib/2_device/1_topology.md b/docs/documentation/en/1_cqlib/2_device/1_topology.md new file mode 100644 index 00000000..3a9a057c --- /dev/null +++ b/docs/documentation/en/1_cqlib/2_device/1_topology.md @@ -0,0 +1,157 @@ +# Topology modeling + +Topology is a graph-theoretic abstraction of the physical qubits of a quantum chip and their coupling connectivity. It is the core input of all advanced compilation and routing algorithms: only when the hardware constraints of the chip are accurately known can the compiler find the optimal gate mapping scheme under a limited connectivity path. + +The Topology of Cqlib adopts a **directed graph** model. This means that (0, 1, "CX") is not equivalent to (1, 0, "CX")—this edge only allows the CX gate to be executed when qubit 0 is the control and qubit 1 is the target. Directed modeling accurately reflects the direction dependence of two-qubit gates in real hardware. + +--- + +## Constructing a topology + +### 1. Explicit construction + +Each edge must use a (control, target, name) triple: + +```python +from cqlib.device import Topology + +topo = Topology( + [0, 1, 2], + [ + (0, 1, "CX"), # CX applied in the qubit 0 -> qubit 1 direction + (1, 2, "CZ"), # CZ applied in the qubit 1 -> qubit 2 direction + ], +) + +print("qubit count:", topo.num_qubits) +print("coupling count:", topo.num_couplings) +print("node list:", topo.qubits) +``` + +**Note**: the couplings parameter of the constructor requires (control, target, name) triples, and the two-element form is not supported. + +### 2. Factory methods + +Topology currently provides the line factory, used to create a line topology quickly: + +```python +from cqlib.device import Topology + +# Line topology: 0 -> 1 -> 2 -> 3 (a one-way chain) +line_topo = Topology.line([0, 1, 2, 3]) +print("line coupling count:", line_topo.num_couplings) +``` + +**Notes**: +- Only the line factory method exists on Topology; there is no ring/star/grid/bidirectional_line +- For richer topology structures, the factory methods of Device can be used (see [Device modeling](2_device.md) for details), or bidirectional couplings can be added manually + +### 3. Manually adding bidirectional coupling + +```python +from cqlib.device import Topology + +# Qubits 0 and 1 act as each other's control and target (bidirectional coupling) +topo = Topology([0, 1], [(0, 1, "CX"), (1, 0, "CX")]) +print("supports 0->1:", topo.supports_directed_coupling(0, 1)) # True +print("supports 1->0:", topo.supports_directed_coupling(1, 0)) # True +``` + +--- + +## Topology queries and analysis + +```python +from cqlib.device import Topology + +topo = Topology([0, 1, 2], [(0, 1, "CX"), (1, 2, "CZ")]) + +# Connectivity checks +print("directed coupling 0->1:", topo.supports_directed_coupling(0, 1)) # True +print("directed coupling 1->0:", topo.supports_directed_coupling(1, 0)) # False +print("either direction present:", topo.supports_coupling_either_direction(0, 1)) # True +print("coupling name 0->1:", topo.get_coupling_name(0, 1)) # "CX" +print("coupling name 2->1 (reverse absent):", topo.get_coupling_name(2, 1)) # None + +# Check whether a qubit exists +print("contains qubit 2:", topo.contains_qubit(2)) # True +print("contains qubit 9:", topo.contains_qubit(9)) # False + +# Adjacency queries +print("successors of qubit 1 (out edges):", topo.successors(1)) # [PhysicalQubit(2)] +print("predecessors of qubit 1 (in edges):", topo.predecessors(1)) # [PhysicalQubit(0)] +print("undirected neighbours of qubit 1:", topo.neighbors_undirected(1)) # [PhysicalQubit(0), PhysicalQubit(2)] +print("out-degree of qubit 1:", topo.out_degree(1)) # 1 +print("in-degree of qubit 1:", topo.in_degree(1)) # 1 + +# Undirected edge list (undirected_edges is a method and needs parentheses) +edges = topo.undirected_edges() +print("undirected edge list:", edges) +``` + +**Notes**: +- successors(q) returns the list of qubits directly reachable from q through directed edges +- predecessors(q) returns the list of qubits whose directed edges point to q +- +neighbors_undirected(q) merges the two directions, and a repeated node appears only once +- contains_qubit(q) returns False for a non-existent qubit and does not raise an exception +- out_degree(q) and in_degree(q) return 0 for a non-existent qubit + +--- + +## Dynamic topology modification + +```python +from cqlib.device import Topology + +topo = Topology([0, 1, 2], [(0, 1, "CX"), (1, 2, "CZ")]) + +# Add new qubits +topo.add_qubits([3, 4]) +print("qubit count after adding:", topo.num_qubits) # 5 + +# Add new couplings (note: new edges also need the triple form) +topo.add_couplings([(2, 3, "CX"), (3, 4, "CZ")]) +print("coupling count after adding:", topo.num_couplings) # 4 + +# Remove couplings (only the given direction is removed) +topo.remove_couplings([(2, 3)]) +print("coupling count after removal:", topo.num_couplings) # 3 (coupling (2,3) removed) + +# Remove qubits (all associated couplings are removed with them) +topo.remove_qubits([4]) +print("qubit count after removal:", topo.num_qubits) # 4 +print("coupling count after removal:", topo.num_couplings) # 2 (coupling (3,4) removed) +``` + +**Edge cases**: +- add_qubits([...]): adding a qubit that already exists raises ValueError +- add_couplings([...]): if a coupling endpoint is not in the topology, or an existing coupling is added, ValueError is raised +- +remove_qubits([...]): if the qubit does not exist, ValueError is raised +- +remove_couplings([...]) takes (control, target) two-element tuples for the couplings parameter (**without names**) +- Removing a qubit automatically removes all its incoming and outgoing coupling edges + +--- + +## Engineering meaning of directed coupling + +In real quantum hardware, the directionality of two-qubit gates is an important physical constraint: + +| Scenario | Directed semantics | Example | +|---|---|---| +| CX gate | Only a fixed direction is allowed | CX(control=0, target=1) | +| CZ gate | Usually no direction restriction | Both directions can be executed | +| Hardware calibration | Different directions may have different fidelities | (0→1) error rate 1%, (1→0) error rate 2% | + +During the routing stage, the compiler uses query methods such as supports_directed_coupling() to determine whether a SWAP or a bridge gate needs to be inserted on the current coupling. + +--- + +## Next steps + +- [Device modeling](2_device.md): master the global default plus local override calibration strategy of Device +- [Layout mapping](3_layout.md): learn the bidirectional logical-physical qubit mapping and SWAP routing operations of Layout +- [Noise model](4_noise.md): understand the use of noise channels such as NoiseModel, SingleQubitNoise, TwoQubitNoise and ReadoutError +- [Execution result and status](5_result.md): become familiar with the complete lifecycle and error handling of Outcome, Status and ExecutionResult diff --git a/docs/documentation/en/1_cqlib/2_device/2_device.md b/docs/documentation/en/1_cqlib/2_device/2_device.md new file mode 100644 index 00000000..97e1c0eb --- /dev/null +++ b/docs/documentation/en/1_cqlib/2_device/2_device.md @@ -0,0 +1,194 @@ +# Device modeling + +The Device module is responsible for aggregating the full hardware characteristics of a backend. It combines the abstract physical topology (Topology) with concrete calibration parameters, providing data support for noise-aware compilation and high-fidelity simulation. + +Cqlib adopts a **"global default plus local override"** calibration strategy: when a parameter is queried, the local value is returned first, and the query falls back to the global default when no local value is configured. + +--- + +## Core objects + +| Object | Use | +|---|---| +| InstructionProp | The physical behavior of a specific gate instruction (error rate, execution duration); an Instruction object (created through Instruction.from_standard_gate()) must be passed to the constructor | +| QubitProp | Single-qubit properties (T1/T2, readout error, frequency, native gate list) | +| EdgeProp | Coupling edge properties (native two-qubit instruction set) | +| Device | Top-level entity that integrates topology and properties, providing global defaults and query interfaces | + +--- + +## Building a device baseline + +```python +from cqlib.circuit import StandardGate +from cqlib.device import Device, Topology + +topo = Topology([0, 1, 2], [(0, 1, "CX"), (1, 2, "CZ")]) +device = Device("demo_backend", [0, 1, 2], topo) +device.default_t1 = 50.0 +device.default_t2 = 35.0 +device.default_readout_error = 0.05 +device.default_single_qubit_error = 0.001 +device.default_two_qubit_error = 0.01 + +print("device name:", device.name) +print("register qubit count:", len(device.qubits)) +``` + +**Note**: global defaults are used only for subsequent fallback queries. If no local value is set for a qubit, calling get_t1(q) returns default_t1. + +### Factory methods + +Device provides factory methods for quickly constructing common topology structures, saving the step of creating a Topology manually: + +```python +from cqlib.device import Device + +d1 = Device.line("line_dev", num_qubits=5) # unidirectional line +d2 = Device.bidirectional_line("bi_line", num_qubits=5) # bidirectional line +d3 = Device.ring("ring_dev", num_qubits=4) # bidirectional ring +d4 = Device.star("star_dev", num_qubits=5, center=0) # bidirectional star +d5 = Device.grid("grid_dev", rows=3, cols=4) # bidirectional grid (row-major order) +d6 = Device.from_edges("custom", num_qubits=4, edges=[(0, 1), (1, 2)]) # custom directed edges + +print("line:", d1.num_usable_qubits) +print("bidirectional line:", d2.num_usable_qubits) +print("ring:", d3.num_usable_qubits) +print("star:", d4.num_usable_qubits) +print("grid:", d5.num_usable_qubits) +print("custom:", d6.num_usable_qubits) +``` + +--- + +## Injecting local calibration data + +```python +from cqlib.circuit import Instruction, StandardGate +from cqlib.device import Device, EdgeProp, InstructionProp, QubitProp, Topology + +topo = Topology([0, 1, 2], [(0, 1, "CX"), (1, 2, "CZ")]) +device = Device("dev", [0, 1, 2], topo) + +# ---- single-qubit calibration ---- +q0 = QubitProp(readout_error=0.02) +q0.t1 = 80.0 # T1 relaxation time (microseconds) +q0.t2 = 70.0 # T2 decoherence time (microseconds) +q0.frequency = 5.1 # frequency (GHz) + +# set the measurement discrimination error +q0.prob_meas0_prep1 = 0.02 # P(measure 0 | prepare 1) +q0.prob_meas1_prep0 = 0.01 # P(measure 1 | prepare 0) + +# Add a native single-qubit gate (note: use add_native_instruction(); appending to native_instructions has no effect) +x_prop = InstructionProp( + Instruction.from_standard_gate(StandardGate.X), + error_rate=0.001, +) +x_prop.length = 20.0 # gate duration (nanoseconds) +q0.add_native_instruction(x_prop) + +device.add_qubit_properties(0, q0) + +# ---- coupling edge calibration ---- +cx_prop = InstructionProp( + Instruction.from_standard_gate(StandardGate.CX), + error_rate=0.015, +) +cx_prop.length = 200.0 + +edge = EdgeProp() +edge.add_native_instruction(cx_prop) +device.add_edge_properties(0, 1, edge) + +print("local properties of qubit 0 injected") +``` + +**Note**: +- The first parameter of the InstructionProp constructor must be an Instruction object, created with Instruction.from_standard_gate(StandardGate.X). Passing StandardGate.X directly is not allowed +- QubitProp.native_instructions is a read-only property that returns a new list on every read: it cannot be assigned directly (q0.native_instructions = [...] raises AttributeError), and calling .append() on it has no effect (only a temporary copy is modified); entries must be added through add_native_instruction() +- EdgeProp.native_instructions is also a read-only property, and entries must be added through the add_native_instruction() method + +--- + +## Parameter queries and the fallback mechanism + +```python +from cqlib.circuit import Instruction, StandardGate +from cqlib.device import Device, Topology + +topo = Topology([0, 1, 2], [(0, 1, "CX"), (1, 2, "CZ")]) +device = Device("dev", [0, 1, 2], topo) +device.default_t1 = 50.0 +device.default_single_qubit_error = 0.001 + +# falls back to the default when no local value is set +print("T1 of qubit 1 (default fallback):", device.get_t1(1)) + +# after setting local calibration for qubit 0, the local value takes priority +q0_prop = QubitProp(readout_error=0.02) +q0_prop.t1 = 80.0 +device.add_qubit_properties(0, q0_prop) +print("T1 of qubit 0 (local value):", device.get_t1(0)) + +# query the single-qubit gate error rate (note: the third parameter must be an Instruction object) +x_inst = Instruction.from_standard_gate(StandardGate.X) +print("X gate error of qubit 0 (default fallback):", device.single_qubit_error(0, x_inst)) + +# query the readout error (global default) +device.default_readout_error = 0.05 +print("readout error of qubit 0 (default fallback):", device.get_readout_error(0)) +``` + +**Fallback chain**: +- get_t1(q): returns QubitProp(q).t1 first, and returns device.default_t1 if it is not set +- get_readout_error(q): returns QubitProp(q).readout_error first, and returns device.default_readout_error if it is not set +- single_qubit_error(q, inst): looks up in order → ① native gate error ② single-qubit default error ③ device global default +- If a qubit is unavailable (not registered or marked as invalid), all the queries above return None + +--- + +## Invalid qubit management + +```python +from cqlib.device import Device, Topology + +topo = Topology([0, 1, 2], [(0, 1, "CX"), (1, 2, "CZ")]) +device = Device("dev", [0, 1, 2], topo) + +# mark qubit 2 as invalid (offline/faulty) +device.invalid_qubits = [2] # note: use a list, not a set + +print("number of usable qubits:", device.num_usable_qubits) # 2 +print("usable qubit list:", device.usable_qubits) # [PhysicalQubit(0), PhysicalQubit(1)] +print("is qubit 2 usable:", device.is_usable_qubit(2)) # False +``` + +--- + +## Robustness validation + +```python +from cqlib.device import Device, QubitProp, Topology + +topo = Topology([0, 1, 2], [(0, 1, "CX"), (1, 2, "CZ")]) +device = Device("dev", [0, 1, 2], topo) + +try: + device.add_qubit_properties(99, QubitProp(0.01)) +except ValueError as e: + print("adding properties for a nonexistent qubit:", e) + +try: + device.add_edge_properties(0, 9, EdgeProp()) +except ValueError as e: + print("adding properties for a nonexistent edge:", e) +``` + +--- + +## Next steps + +- [Layout mapping](3_layout.md): learn the bidirectional logical-physical qubit mapping and SWAP routing operations of Layout +- [Noise model](4_noise.md): understand the use of noise channels such as NoiseModel, SingleQubitNoise, TwoQubitNoise and ReadoutError +- [Execution result and status](5_result.md): become familiar with the complete lifecycle and error handling of Outcome, Status and ExecutionResult diff --git a/docs/documentation/en/1_cqlib/2_device/3_layout.md b/docs/documentation/en/1_cqlib/2_device/3_layout.md new file mode 100644 index 00000000..a2428a4b --- /dev/null +++ b/docs/documentation/en/1_cqlib/2_device/3_layout.md @@ -0,0 +1,139 @@ +# Layout mapping + +Layout manages the bidirectional mapping from logical qubits to physical qubits, and is the core data structure of the compilation and routing stage. + +--- + +## Core concepts + +- **Logical qubit (LogicalQubit)**: a virtual qubit defined in an algorithm/circuit +- **Physical qubit (PhysicalQubit)**: the index of a real physical qubit on a hardware chip +- **Vacant physical qubit (Vacant PhysicalQubit)**: a physical position not occupied by any logical qubit, which can be used for subsequent binding + +--- + +## Constructing a mapping + +Layout supports several construction forms: + +```python +from cqlib.device import Layout + +# option 1: automatic sequential mapping +# logical qubits [0, 1] are mapped to physical qubits [10, 11] automatically +# physical qubit 12 stays vacant +layout = Layout(logical=[0, 1], physical=[10, 11, 12]) +print("number of logical qubits:", layout.num_logical) # 2 +print("number of physical qubits:", layout.num_physical) # 3 +print("number of vacant physical qubits:", layout.num_vacant_physical) # 1 + +# option 2: specify an initial mapping with from_pairs +# (logical, physical) pairs set the mapping explicitly; the other physical qubits stay vacant +layout2 = Layout.from_pairs([(0, 2), (1, 0)], physical_count=4) +print("from_pairs vacant count:", layout2.num_vacant_physical) # 2 +``` + +**Note**: +- The init_map parameter of Layout.__init__ requires the dict[Qubit, Qubit] type; passing {0: 11} directly raises TypeError due to the type mismatch +- If an initial mapping needs to be specified, Layout.from_pairs() is recommended instead +- The lengths of the logical and physical lists should satisfy len(logical) <= len(physical), otherwise ValueError is raised + +--- + +## Querying a mapping + +```python +from cqlib.device import Layout + +layout = Layout.from_pairs([(0, 11), (1, 10)], physical_count=13) + +print("logical qubit list:", layout.logical_qubits) +print("physical qubit list:", layout.physical_qubits) +print("vacant physical qubits:", layout.vacant_physical_qubits) + +# forward lookup: logical -> physical +print("logical 0 maps to physical:", layout.get_physical(0)) # P11 + +# reverse lookup: physical -> logical +print("physical 11 maps to logical:", layout.get_logical(11)) # L0 + +# check whether a physical qubit is vacant +print("is physical 10 vacant:", layout.is_physical_vacant(10)) # False (occupied by logical 1) +print("is physical 12 vacant:", layout.is_physical_vacant(12)) # True + +# get the full mapping dict +print("logical -> physical mapping:", layout.l2p_map) +print("physical -> logical mapping:", layout.p2l_map) +``` + +**Notes**: +- get_physical(logical_id) returns None for an unbound logical qubit +- get_logical(physical_id) returns None for a vacant physical qubit +- p2l_map contains only the physical qubits already occupied by logical qubits; vacant qubits do not appear in it + +--- + +## Updating a mapping + +```python +from cqlib.device import Layout + +# initial mapping: logical 0 -> physical 11, logical 1 -> physical 12 +layout = Layout.from_pairs([(0, 11), (1, 12)], physical_count=13) +print("initial vacant:", layout.num_vacant_physical) # 11 + +# bind: bind a new logical qubit to a vacant physical qubit +layout.bind(2, 10) +print("vacant after bind:", layout.num_vacant_physical) # 10 + +# unbind: unbind a logical qubit and release its physical qubit +released = layout.unbind(0) +print("released after unbind:", released) # P11 +print("vacant after unbind:", layout.num_vacant_physical) # 11 + +# swap_physical: swap the logical qubits carried by two physical qubits (the core routing operation) +layout3 = Layout.from_pairs([(0, 11), (1, 12)], physical_count=13) +layout3.swap_physical(11, 12) +print("physical 11 -> logical after SWAP:", layout3.get_logical(11)) # L1 +print("physical 12 -> logical after SWAP:", layout3.get_logical(12)) # L0 +``` + +**Edge cases**: +- bind(logical, physical): if physical is already occupied, or logical is already bound, ValueError is raised +- unbind(logical): if logical is not bound, ValueError is raised +- swap_physical(a, b): if or is not in the layout, ValueError is raised. One of them is allowed to be a vacant qubit (equivalent to moving a logical qubit) + +--- + +## Robustness guarantees + +```python +from cqlib.device import Layout + +layout = Layout.from_pairs([(0, 11), (1, 12)], physical_count=13) + +try: + layout.swap_physical(11, 99) # 99 is not in the layout +except ValueError as e: + print("invalid physical qubit:", e) + +try: + layout.bind(0, 11) # physical 11 is already occupied +except ValueError as e: + print("bind an already occupied physical qubit:", e) + +try: + layout.swap_physical(11, 12) # normal operation, no exception is raised + print("SWAP succeeded") + print("physical 11 -> logical:", layout.get_logical(11)) + print("physical 12 -> logical:", layout.get_logical(12)) +except ValueError as e: + print("SWAP failed:", e) +``` + +--- + +## Next steps + +- [Noise model](4_noise.md): understand the use of noise channels such as NoiseModel, SingleQubitNoise, TwoQubitNoise and ReadoutError +- [Execution result and status](5_result.md): become familiar with the complete lifecycle and error handling of Outcome, Status and ExecutionResult diff --git a/docs/documentation/en/1_cqlib/2_device/4_noise.md b/docs/documentation/en/1_cqlib/2_device/4_noise.md new file mode 100644 index 00000000..10b16be9 --- /dev/null +++ b/docs/documentation/en/1_cqlib/2_device/4_noise.md @@ -0,0 +1,188 @@ +# Noise model + +cqlib.device provides a complete modeling interface ranging from single-qubit noise and two-qubit noise to the overall noise container, used for channel modeling in quantum noise simulation. + +--- + +## Single-qubit noise + +SingleQubitNoise provides several static factory methods to create different types of noise channels: + +| Factory method | Channel | Meaning | +|---|---|---| +| bit_flip(p) | Bit flip | Executes an X gate with probability p | +| phase_flip(p) | Phase flip | Executes a Z gate with probability p | +| pauli(px, py, pz) | General Pauli | Independently specifies the X/Y/Z error probabilities | +| depolarizing(p) | Depolarizing | Applies an X/Y/Z error randomly with probability p | +| amplitude_damping(gamma) | Amplitude damping | T1 energy relaxation model | +| phase_damping(lambda_) | Phase damping | T2 pure decoherence model | + +```python +from cqlib.device import SingleQubitNoise + +sq = SingleQubitNoise.depolarizing(0.01) +pauli = SingleQubitNoise.pauli(px=0.001, py=0.0005, pz=0.002) +bitflip = SingleQubitNoise.bit_flip(0.01) +amp = SingleQubitNoise.amplitude_damping(gamma=0.05) +phase = SingleQubitNoise.phase_damping(0.1) + +# Kraus operator export +kraus = sq.to_kraus() +print("number of Kraus operators:", len(kraus)) # 4 +print("Kraus matrix shape:", kraus[0].shape) # (2, 2) + +# validity check +print("depolarizing noise valid:", sq.is_valid()) # True +print("amplitude damping valid:", amp.is_valid()) # True +``` + +**Note**: +- SingleQubitNoise has no .kind property. To distinguish noise types, the noise type must be tracked through the construction method +- pauli(px, py, pz) requires px + py + pz <= 1, otherwise the constructor raises ValueError + +--- + +## Two-qubit noise + +```python +from cqlib.device import SingleQubitNoise, TwoQubitNoise +from cqlib.qis import Pauli + +# depolarizing noise (15 non-identity Pauli operators, equally probable) +tq = TwoQubitNoise.depolarizing(0.01) +print("depolarizing Kraus shape:", tq.to_kraus()[0].shape) # (4, 4) + +# independent noise (single-qubit noise applied independently to each qubit) +bind = TwoQubitNoise.independent( + SingleQubitNoise.phase_flip(0.02), + SingleQubitNoise.bit_flip(0.03), +) +print("independent noise Kraus shape:", bind.to_kraus()[0].shape) + +# correlated Pauli noise +corr = TwoQubitNoise.correlated_pauli(Pauli.x(), Pauli.x(), p=0.01) +print("correlated Pauli Kraus shape:", corr.to_kraus()[0].shape) +``` + +**Pauli constructor notes** (all are static methods that return a Pauli object): + +| Correct usage | Incorrect usage | +|---|---| +| Pauli.x() | Pauli.X | +| Pauli.y() | Pauli.Y | +| Pauli.z() | Pauli.Z | +| Pauli.i() | Pauli.I | + +--- + +## Readout error + +ReadoutError describes the discrimination error in the measurement process: + +```python +from cqlib.device import ReadoutError + +ro = ReadoutError(p_0_given_1=0.02, p_1_given_0=0.01) +print("P(measure 0 | prepare 1):", ro.p_0_given_1) # 0.02 +print("P(measure 1 | prepare 0):", ro.p_1_given_0) # 0.01 +print("readout error valid:", ro.is_valid()) # True +``` + +--- + +## NoiseModel: the noise container + +NoiseModel aggregates all noise sources, and supports addition, removal and query by qubit/gate type: + +```python +from cqlib.circuit import StandardGate +from cqlib.device import NoiseModel, OperationKey, ReadoutError, SingleQubitNoise, TwoQubitNoise + +nm = NoiseModel() + +# add noise (all add_* methods return None and raise ValueError when parameter validation fails) +nm.add_readout_error(0, ReadoutError(0.02, 0.01)) +nm.add_single_qubit_error(StandardGate.X, 0, SingleQubitNoise.bit_flip(0.005)) +nm.add_two_qubit_error(StandardGate.CX, 0, 1, TwoQubitNoise.depolarizing(0.02)) + +# readout error query +ro = nm.get_readout_error(0) +print("readout error:", ro) + +# single-qubit noise query (through an OperationKey) +skey = OperationKey.new_single(StandardGate.X, 0) +errs = nm.get_single_qubit_errors(skey) +print("X gate noise channel count:", len(errs)) + +# two-qubit noise query +tkey = OperationKey.new_double(StandardGate.CX, 0, 1) +errs2 = nm.get_two_qubit_errors(tkey) +print("CX gate noise channel count:", len(errs2)) +``` + +**Notes**: +- The gate parameter of add_single_qubit_error(gate, qubit, noise) takes a StandardGate enum value (such as StandardGate.X) rather than an Instruction object +- add_two_qubit_error(gate, q0, q1, noise) also uses StandardGate enum values +- OperationKey.new_single(gate, q0) and +new_double(gate, q0, q1) use StandardGate +- get_single_qubit_errors(key) returns list[SingleQubitNoise] | None, and returns None when there is no match + +--- + +## Parameter validation + +NoiseModel.add_*() and the noise constructors raise ValueError when a parameter is invalid: + +```python +from cqlib.circuit import StandardGate +from cqlib.device import NoiseModel, SingleQubitNoise, TwoQubitNoise + +nm = NoiseModel() + +try: + # probability outside the range [0, 1] + nm.add_single_qubit_error(StandardGate.X, 0, SingleQubitNoise.bit_flip(1.5)) +except ValueError as e: + print("invalid probability rejected:", e) + +try: + # two-qubit gate acting on the same qubit + nm.add_two_qubit_error(StandardGate.CX, 0, 0, TwoQubitNoise.depolarizing(0.01)) +except ValueError as e: + print("invalid configuration rejected:", e) +``` + +--- + +## Combining noise with a device + +In actual use, Device provides gate error rate query interfaces, while NoiseModel provides detailed channel models. The two can be used together: + +```python +from cqlib.circuit import Instruction, StandardGate +from cqlib.device import Device, NoiseModel, OperationKey, SingleQubitNoise, Topology + +topo = Topology([0, 1, 2], [(0, 1, "CX"), (1, 2, "CZ")]) +device = Device("noisy", [0, 1, 2], topo) +device.default_single_qubit_error = 0.001 + +noise = NoiseModel() +noise.add_single_qubit_error(StandardGate.H, 0, SingleQubitNoise.depolarizing(0.002)) + +# Device query (note that an Instruction object is required) +h_inst = Instruction.from_standard_gate(StandardGate.H) +print("H gate error reported by the device:", device.single_qubit_error(0, h_inst)) + +# NoiseModel noise channel query +skey = OperationKey.new_single(StandardGate.H, 0) +channels = noise.get_single_qubit_errors(skey) +print("noise channel count:", len(channels)) +``` + +--- + +## Next steps + +- [Execution result and status](5_result.md): become familiar with the complete lifecycle and error handling of Outcome, Status and ExecutionResult +- [Quantum Information](../3_qis/0_overview.md): master the basics such as Statevector, DensityMatrix and Pauli +- [Compilation and optimization](../4_compiler/0_overview.md): learn about the layout, routing and optimization of the compilation pipeline diff --git a/docs/documentation/en/1_cqlib/2_device/5_result.md b/docs/documentation/en/1_cqlib/2_device/5_result.md new file mode 100644 index 00000000..996b2338 --- /dev/null +++ b/docs/documentation/en/1_cqlib/2_device/5_result.md @@ -0,0 +1,151 @@ +# Execution result and status + +The execution result module of cqlib.device provides measurement result encapsulation, task status tracking and probability calculation. + +--- + +## Outcome: measurement result + +Outcome uses little-endian bit order: the rightmost character of the string corresponds to Qubit 0, and the leftmost corresponds to Qubit N-1. + +```python +from cqlib.device import Outcome + +# create from a bit string (little-endian: rightmost = Qubit 0) +o = Outcome("101") +print("is bit 0 one:", o.is_one(0)) # True +print("is bit 1 one:", o.is_one(1)) # False +print("full bitstring (3):", o.to_bitstring(3)) # "101" + +# construct from a bit string (equivalent to direct construction) +o2 = Outcome.from_bitstring("101") +print("o == o2:", o == o2) # True + +# construct from an index list (specifying which bit positions are 1) +o3 = Outcome.from_indices(width=3, indices=[0, 2]) +print("constructed from indices:", o3.to_bitstring(3)) # "101" +``` + +**Notes**: +- The string may contain only '0' and '1'; other characters raise ValueError +- +to_bitstring(num_qubits) if +num_qubits is greater than the actual width, the high-order bits are padded with zeros + +--- + +## Status: task status + +Status represents the execution stage of a quantum task, and supports the following five states: + +| State | Constructor | Terminal | +|---|---|---| +| Queued | Status.queued() | No | +| Running | Status.running() | No | +| Completed | Status.completed() | Yes | +| Failed | Status.failed(msg, code) | Yes | +| Cancelled | Status.cancelled() | Yes | + +```python +from cqlib.device import Status + +q = Status.queued() +c = Status.completed() +f = Status.failed("backend down", 500) +x = Status.cancelled() + +print("queued:", q.kind, "terminal?", q.is_terminal()) +print("completed:", c.kind, "success?", c.is_success()) +print("failed:", f.kind, f.error_msg, f.error_code) +print("cancelled:", x.kind, "terminal?", x.is_terminal()) +``` + +**Notes**: +- Status.kind is a property (not a method) and returns a string, such as "completed" or "failed" +- is_terminal() returns True for completed, failed and cancelled +- is_success() returns True only for completed +- error_msg and error_code have values only when kind == "failed"; otherwise they return None + +--- + +## ExecutionResult: the complete execution result + +```python +from cqlib.device import ExecutionResult + +result = ExecutionResult("q-task-001", [0, 1], 1000, 2, "Tianyan-176-2") +print("status on creation:", result.status.kind) # queued + +# mark as running +result.start() +print("status after start:", result.status.kind) # running + +# finish and fill in the measurement counts +result.finish({"00": 600, "11": 400}) +result.calc_probabilities() # compute the probability distribution + +print("task ID:", result.task_id) +print("shots:", result.shots) +print("counts:", result.counts) +print("probabilities:", result.probabilities) +print("backend:", result.backend) +``` + +### Constructing directly from counts + +If measurement results are already available, from_counts can be used to complete creation and filling in one step: + +```python +from cqlib.device import ExecutionResult + +result = ExecutionResult.from_counts( + task_id="q-task-002", qubits=[0, 1], + shots=1024, num_qubits=2, + counts={"00": 512, "11": 512}, + backend="simulator", +) +print("status:", result.status.kind) # completed (finished automatically) +print("probabilities:", result.probabilities) # {"00": 0.5, "11": 0.5} +``` + +--- + +## Exception flow handling + +```python +from cqlib.device import ExecutionResult + +# failure scenario +f = ExecutionResult("task-fail", [0], 10, 1, None) +f.fail("timeout", 408) +print("failed status:", f.status.kind) # failed +print("error message:", f.status.error_msg) # timeout +print("error code:", f.status.error_code) # 408 + +# cancellation scenario +c = ExecutionResult("task-cancel", [0], 10, 1, None) +c.cancel() +print("cancelled status:", c.status.kind) # cancelled +``` + +--- + +## Input validation + +```python +from cqlib.device import ExecutionResult + +r = ExecutionResult("bad", [0], 10, 1, None) +try: + r.finish({"2": 1}) # "2" is not a valid binary string +except ValueError as e: + print("invalid counts:", e) +``` + +--- + +## Next steps + +- [Quantum Information](../3_qis/0_overview.md): master the basics such as Statevector, DensityMatrix and Pauli +- [Compilation and optimization](../4_compiler/0_overview.md): learn about the layout, routing and optimization of the compilation pipeline +- [Visualization](../5_visualization/0_overview.md): learn about circuit drawing and result visualization diff --git a/docs/documentation/en/1_cqlib/3_qis/0_overview.md b/docs/documentation/en/1_cqlib/3_qis/0_overview.md new file mode 100644 index 00000000..05508b2a --- /dev/null +++ b/docs/documentation/en/1_cqlib/3_qis/0_overview.md @@ -0,0 +1,159 @@ +# QIS Quantum Information + +`cqlib.qis` provides fundamental objects and local simulation capabilities related to quantum information science, for inspecting quantum states, computing observables, constructing Pauli Hamiltonians, generating Trotter evolution circuits, and analyzing fidelity, entropy and entanglement metrics. + +The examples in this chapter all run in a local Python environment, without connecting to a cloud platform or submitting tasks to real quantum hardware. Before starting, complete [Cqlib installation and environment setup](../../0_get_started/1_installation.md) and confirm that the imported Cqlib version is the one to be tested. + +```python +import cqlib + +print(cqlib.__file__) +``` + +--- + +## Common entry points + +The Python entry points of QIS are concentrated in `cqlib.qis`, and state simulators can also be imported from the `cqlib.qis.state` subpackage. + +```python +from cqlib.qis import ( + Statevector, + DensityMatrix, + DensityMatrixNoise, + StabilizerState, + StabilizerCircuitResult, + RuntimeValue, + ClassicalState, + Phase, + Pauli, + PauliString, + Hamiltonian, + TrotterMode, + metrics, + entropy, +) +``` + +| Object | Main use | Common scenarios | +|---|---|---| +| `Statevector` | Pure-state simulation, storing `2^n` complex amplitudes | Ideal circuit verification, small-scale VQE/QAOA energy computation, state sampling | +| `DensityMatrix` | Density matrix simulation, storing a `2^n × 2^n` complex matrix | Mixed states, Kraus noise, partial trace, physicality checks | +| `DensityMatrixNoise` | Density matrix simulator with a `NoiseModel` | Gate noise, readout error, comparison of noisy circuits | +| `StabilizerState` | Clifford stabilizer simulation | Larger Clifford circuits, stabilizer generators, fast sampling | +| `Pauli` / `PauliString` | Single-qubit and multi-qubit Pauli operators | Observables, commutation checks, measurement expectation values | +| `Hamiltonian` | Sparse sum of Pauli terms | VQE, QAOA, Ising model, time evolution | +| `TrotterMode` | Trotter-Suzuki decomposition mode | Construction of `e^{-iHt}` evolution circuits | +| `metrics` / `entropy` | Quantum state distance, purity, entropy and entanglement metrics | State similarity, mixedness, entanglement analysis | + +These objects usually do not replace `Circuit`; they are used to execute and interpret `Circuit`. The circuit expresses program structure, and QIS turns the circuit into analyzable states, observables and numerical metrics. + +--- + +## Starting from the Bell state + +The Bell state is the most suitable example for getting started with QIS: it is short, but it simultaneously contains superposition, entanglement, probability distributions and observable expectation values. + +```python +from cqlib import Circuit +from cqlib.qis import Hamiltonian, PauliString, Statevector + +circuit = Circuit(2) +circuit.h(0) +circuit.cx(0, 1) + +state = Statevector.from_circuit(circuit) + +hamiltonian = Hamiltonian(2) +hamiltonian.add_term(PauliString.from_str("ZZ"), 1.0) + +print(state.probabilities()) +print(hamiltonian.expectation_statevector(state)) +``` + +The computational-basis probabilities of an ideal Bell state should be concentrated on `|00>` and `|11>`, so the expectation value of `ZZ` is close to `1.0`. Such checks are often used to confirm that the circuit structure, the qubit indices and the observable definitions are consistent. + +--- + +## How to choose a state representation + +Different state representations differ in memory and semantics. When writing an algorithm prototype, it is recommended to first clarify which kind of state the problem requires. + +| Task | Recommended object | Reason | +|---|---|---| +| Only ideal pure states are needed | `Statevector` | Lower memory overhead than a density matrix, suitable for quick verification | +| Mixed states or Kraus channels are needed | `DensityMatrix` | Can express coherence terms, decoherence and non-pure states | +| A device noise model needs to be applied automatically | `DensityMatrixNoise` | Combined with `NoiseModel`, noise is injected automatically after gates | +| The circuit contains only Clifford gates | `StabilizerState` | More efficient for Clifford structures, suitable for stabilizer analysis | +| Expectation values need to be estimated only from sampling probabilities | `PauliString` / `Hamiltonian` | Expectation values can be computed from Pauli measurement results | + +If a circuit contains measurement, reset or dynamic control flow, the whole circuit usually cannot be regarded as a single unitary matrix. In that case the choice depends on the specific goal: visualization for debugging structure, `Statevector` for analyzing ideal unitary fragments, and `DensityMatrix` or `DensityMatrixNoise` for analyzing noise and mixed states. + +--- + +## A typical QIS workflow + +A common closed loop is as follows: first construct the circuit, then choose a state simulator, then define observables, and finally compute probabilities, expectation values or metrics. + +```python +from cqlib import Circuit +from cqlib.qis import Hamiltonian, PauliString, Statevector, metrics + +ansatz = Circuit(2) +ansatz.ry(0, 0.3) +ansatz.cx(0, 1) +ansatz.ry(1, -0.2) + +state = Statevector.from_circuit(ansatz) + +observable = Hamiltonian.from_list([ + (PauliString.from_str("ZI"), -1.0), + (PauliString.from_str("IZ"), -1.0), + (PauliString.from_str("ZZ"), 0.5), +]) + +print("probabilities:", state.probabilities()) +print("energy:", observable.expectation_statevector(state)) +print("purity:", metrics.purity_pure(state)) +``` + +This flow corresponds to the inner computation of many algorithms: VQE cares about `energy`, QAOA cares about the expectation value of the cost Hamiltonian, and error diagnosis cares about fidelity, trace distance, purity and entropy. + +--- + +## Learning path for this chapter + +The reading order is as follows: + +1. [Statevector pure-state simulation](1_statevector.md): learn pure-state construction, gate application, circuit execution, sampling and expectation values. +2. [DensityMatrix mixed-state simulation](2_density_matrix.md): learn density matrices, Kraus noise, partial trace and physicality checks. +3. [DensityMatrixNoise noisy simulation](3_density_matrix_noise.md): learn how to apply `NoiseModel` to gate noise and readout error simulation. +4. [StabilizerState stabilizer simulation](4_stabilizer.md): learn the stabilizer representation, measurement and generator analysis of Clifford circuits. +5. [Pauli, PauliString and Hamiltonian](5_pauli_and_hamiltonian.md): learn the Pauli group, commutation, Hamiltonians, expectation values and Trotter evolution. +6. [Quantum state metrics and entropy](6_metrics_entropy.md): learn purity, fidelity, trace distance, Von Neumann entropy, Rényi entropy and entanglement metrics. + +It is recommended to read the first two sections first, in order to establish the basic semantics of pure and mixed states, and then read the Pauli and Hamiltonian sections. Noisy simulation, stabilizer simulation and metric computation can be read in an interleaved way according to algorithm requirements. + +--- + +## When to use the QIS module + +In quantum program development, the QIS module is usually used in the following places: + +- after writing a small circuit, verifying whether the final probability distribution matches expectations; +- after binding an ansatz to a set of parameters, computing observable expectation values; +- comparing fidelity or trace distance among an ideal state, a noisy state and a target state; +- performing a partial trace on a mixed state to inspect the state of a subsystem; +- using PauliString to check commutation, measurement bases and Hamiltonian terms; +- converting a Pauli Hamiltonian into a Trotter evolution circuit; +- using the stabilizer simulator to quickly check a Clifford circuit. + +QIS numerical results cannot replace circuit visualization and unit tests. In critical algorithm modules, it is recommended to keep circuit diagrams, probability checks, expectation value checks and the necessary physicality checks at the same time. + +--- + +## Next steps + +- [Statevector pure-state simulation](1_statevector.md): first use an ideal pure state to check gate sequences, probability distributions, sampling and observable expectation values. +- [DensityMatrix mixed-state simulation](2_density_matrix.md): learn density matrices, Kraus noise, partial trace and physicality verification. +- [Pauli, PauliString and Hamiltonian](5_pauli_and_hamiltonian.md): master the Pauli representation, Hamiltonian modeling, expectation value computation and Trotter evolution. diff --git a/docs/documentation/en/1_cqlib/3_qis/1_statevector.md b/docs/documentation/en/1_cqlib/3_qis/1_statevector.md new file mode 100644 index 00000000..737eeedd --- /dev/null +++ b/docs/documentation/en/1_cqlib/3_qis/1_statevector.md @@ -0,0 +1,196 @@ +# Statevector Pure-State Simulation + +`Statevector` is used for local ideal pure-state simulation. It stores an `n`-qubit state as a complex amplitude vector of length `2^n`, suitable for verifying small and medium-scale unitary circuits, checking probability distributions, computing Pauli/Hamiltonian expectation values, and simulating finite-shot sampling. + +`Statevector` is initialized to `|0...0>` by default. None of the examples connect to hardware or introduce noise. + +--- + +## Task: prepare and read a Bell state + +```python +from cqlib.qis import Statevector + +state = Statevector(2) +state.apply_h(0) +state.apply_cx(0, 1) + +print(state.data) +print(state.probabilities()) +``` + +`data` returns the complex amplitude array, and `probabilities()` returns the computational-basis probability distribution. The ideal probabilities of a Bell state are concentrated on indices `0` and `3`, that is, `|00>` and `|11>`. + +When reading `Statevector` results, three things should be confirmed: + +- whether the length of the amplitude array is `2 ** num_qubits`; +- whether the sum of probabilities is close to `1.0`; +- whether the main probability peaks fall on the expected bitstrings. + +--- + +## Constructing a state from initial amplitudes + +If a set of normalized complex amplitudes is already available, `from_state()` can be used to construct a pure state directly. + +```python +import numpy as np +from cqlib.qis import Statevector + +plus = Statevector.from_state( + 1, + np.array([1 / np.sqrt(2), 1 / np.sqrt(2)], dtype=complex), +) + +print(plus.probabilities()) +``` + +The length of the passed array must be equal to `2 ** num_qubits`, and the state must be normalized. This entry point is suitable for converting external numerical results into Cqlib state objects, which can then be used to compute expectation values, fidelity or sampling results. + +--- + +## Executing pure-state simulation from a Circuit + +A more common usage is to construct a `Circuit` first and then use `from_circuit()` to execute the entire ideal circuit. + +```python +from cqlib import Circuit +from cqlib.qis import Statevector + +circuit = Circuit(2) +circuit.h(0) +circuit.cx(0, 1) + +state = Statevector.from_circuit(circuit) +print(state.probabilities()) +``` + +`Statevector.from_circuit()` is suitable for executing ideal circuit fragments that contain no non-unitary operations. If the circuit contains measurement, reset or dynamic control flow, first confirm whether these operations belong to the current simulation goal; when necessary, switch to step-by-step simulation, density matrix simulation or a dedicated dynamic execution flow. + +--- + +## Applying a circuit in place to an existing state + +When different circuit fragments need to be appended repeatedly starting from the same initial state, the state can be created first and `apply_circuit()` then called. + +```python +from cqlib import Circuit +from cqlib.qis import Statevector + +prefix = Circuit(2) +prefix.h(0) + +suffix = Circuit(2) +suffix.cx(0, 1) + +state = Statevector(2) +state.apply_circuit(prefix) +state.apply_circuit(suffix) + +print(state.probabilities()) +``` + +This style is suitable for debugging in segments. After each module is executed, probabilities can be printed or expectation values computed, in order to locate which segment of the circuit changed the state structure. + +--- + +## Applying common quantum gates directly + +`Statevector` provides `apply_*` methods corresponding to standard gates, including Pauli gates, Clifford gates, rotation gates, controlled gates, two-qubit rotation gates, `fSim`, `CCX` and user-defined unitary matrices. + +```python +import numpy as np +from cqlib.qis import Statevector + +state = Statevector(2) +state.apply_x(0) +state.apply_rz(0, np.pi / 4) +state.apply_cx(0, 1) +state.apply_rzz(0, 1, 0.2) + +print(state.probabilities()) +``` + +For custom matrices, `apply_single_qubit_gate()`, `apply_double_qubits_gate()` or `apply_unitary_gate()` can be used. + +```python +import numpy as np +from cqlib.qis import Statevector + +state = Statevector(2) +state.apply_x(0) + +swap = np.array( + [ + [1, 0, 0, 0], + [0, 0, 1, 0], + [0, 1, 0, 0], + [0, 0, 0, 1], + ], + dtype=complex, +) +state.apply_unitary_gate([0, 1], swap) + +print(state.probabilities()) +``` + +When custom matrices are used, the matrix dimension must be ensured to match the number of qubits it acts on, and the unitarity requirement must be satisfied. + +--- + +## Computing observable expectation values + +`Statevector.expectation()` accepts a `PauliString` or a `Hamiltonian`. + +```python +from cqlib.qis import Hamiltonian, PauliString, Statevector + +state = Statevector(2) +state.apply_h(0) +state.apply_cx(0, 1) + +zz = PauliString.from_str("ZZ") +print(state.expectation(zz)) + +hamiltonian = Hamiltonian.from_list([ + (PauliString.from_str("ZZ"), 1.0), + (PauliString.from_str("XX"), 0.5), +]) +print(state.expectation(hamiltonian)) +``` + +`expectation_statevector(state)` can also be called from the observable side. In VQE or QAOA, it is recommended to settle on one style, so that state objects and observables are not confused in logs and post-processing code. + +--- + +## Measurement, reset and sampling + +`measure()` measures the specified qubit and collapses the current state, while `measure_all()` measures all qubits. `sample_shots()` is used to sample according to the current distribution and does not modify the original state, which makes it applicable to debugging scenarios. + +```python +from cqlib.qis import Statevector + +state = Statevector(2) +state.apply_h(0) +state.apply_cx(0, 1) + +shots = state.sample_shots(1000) +counts = {} +for outcome in shots: + bitstring = outcome.to_bitstring(state.num_qubits) + counts[bitstring] = counts.get(bitstring, 0) + 1 + +print(counts) +print(state.probabilities()) +``` + +If exact theoretical probabilities are needed, use `probabilities()`; if the statistical fluctuations of a finite number of shots need to be simulated, use `sample_shots()`. + + +--- + +## Next steps + +- [DensityMatrix mixed-state simulation](2_density_matrix.md): switch from pure-state simulation to a density matrix when mixed states, Kraus noise and partial trace are needed. +- [Pauli, PauliString and Hamiltonian](5_pauli_and_hamiltonian.md): learn how to connect state results to observables, energy functions and Trotter evolution. +- [Visualizing quantum states](../5_visualization/7_state_visualization.md): interpret pure-state simulation results with Bloch, state city and Pauli vector diagrams. diff --git a/docs/documentation/en/1_cqlib/3_qis/2_density_matrix.md b/docs/documentation/en/1_cqlib/3_qis/2_density_matrix.md new file mode 100644 index 00000000..25d4e28e --- /dev/null +++ b/docs/documentation/en/1_cqlib/3_qis/2_density_matrix.md @@ -0,0 +1,166 @@ +# DensityMatrix Mixed-State Simulation + +`DensityMatrix` represents quantum states with a density matrix. It can express pure states as well as mixed states and states after noise, suitable for Kraus channels, partial trace, physicality checks, mixed-state expectation values and entropy metric analysis. + +The memory scale of a density matrix is `2^n × 2^n`, usually heavier than `Statevector`. It should be preferred only when mixed-state semantics, noise channels or subsystem analysis are needed. + +--- + +## Task: prepare a `|+>` state with a density matrix + +```python +from cqlib.qis import DensityMatrix + +density = DensityMatrix(1) +density.apply_h(0) + +print(density.data) +print(density.trace()) +print(density.probabilities()) +``` + +`data` returns the two-dimensional density matrix, `trace()` should be close to `1.0`, and `probabilities()` returns the diagonal probabilities in the computational basis. For the `|+>` state the probabilities are `[0.5, 0.5]`, but the density matrix also contains off-diagonal coherence terms. + +--- + +## Creating from pure-state amplitudes or a circuit + +`DensityMatrix.from_state()` takes a qubit count and pure-state amplitudes; `DensityMatrix.from_circuit()` applies the circuit to the initial `|0...0>` and returns the resulting density matrix. + +```python +import numpy as np +from cqlib import Circuit +from cqlib.qis import DensityMatrix + +plus = DensityMatrix.from_state( + 1, + np.array([1 / np.sqrt(2), 1 / np.sqrt(2)], dtype=complex), +) +print(plus.data) + +circuit = Circuit(2) +circuit.h(0) +circuit.cx(0, 1) + +bell_density = DensityMatrix.from_circuit(circuit) +print(bell_density.probabilities()) +``` + +If a complete density matrix is already available, `from_density_matrix()` can be used. The input matrix must satisfy the dimension, trace and physicality requirements. + +```python +import numpy as np +from cqlib.qis import DensityMatrix + +mixed = DensityMatrix.from_density_matrix( + 1, + np.array( + [ + 0.7 + 0.0j, 0.0 + 0.0j, + 0.0 + 0.0j, 0.3 + 0.0j, + ], + dtype=complex, + ), +) + +print(mixed.probabilities()) +``` + +--- + +## Applying Kraus noise + +Kraus operators can be applied directly to a density matrix. The following example applies amplitude damping to the `|1>` state. + +```python +import numpy as np +from cqlib.qis import DensityMatrix + +gamma = 0.05 +k0 = np.array([[1, 0], [0, np.sqrt(1 - gamma)]], dtype=complex) +k1 = np.array([[0, np.sqrt(gamma)], [0, 0]], dtype=complex) + +density = DensityMatrix(1) +density.apply_x(0) + +density.apply_kraus([0], [k0.flatten(), k1.flatten()]) + +print(density.probabilities()) +``` + +Kraus channels are suitable for manually verifying the mathematical form of noise. If a device-level `NoiseModel` is already available, [DensityMatrixNoise noisy simulation](3_density_matrix_noise.md) can be used to add noise automatically after gates. + +--- + +## Performing a partial trace on a subsystem + +`partial_trace(keep=[...])` keeps the specified qubits and traces out the rest. It is suitable for analyzing a local subsystem within an entangled state. + +```python +from cqlib import Circuit +from cqlib.qis import DensityMatrix + +circuit = Circuit(2) +circuit.h(0) +circuit.cx(0, 1) + +bell_density = DensityMatrix.from_circuit(circuit) +subsystem = bell_density.partial_trace(keep=[0]) + +print(subsystem.data) +print(subsystem.probabilities()) +``` + +Tracing out any single qubit of a Bell state yields a reduced density matrix close to the maximally mixed state. This result shows that a single qubit has no definite Bloch direction by itself, while the global two-qubit state still carries entanglement correlations. + +--- + +## Physicality checks + +When a density matrix comes from external data, numerical reconstruction or a custom noise channel, it should be checked whether it is still a valid quantum state. + +```python +from cqlib.qis import DensityMatrix + +density = DensityMatrix(1) +density.apply_h(0) + +tol = 1e-10 +print(density.is_hermitian(tol)) +print(density.is_positive_semidefinite(tol)) +density.validate_physical(tol) +``` + +A physical density matrix should at least be Hermitian, positive semidefinite and have a trace of `1`. If `validate_physical()` raises an exception, it usually indicates a problem in the input matrix, the noise channel or the numerical post-processing. + +--- + +## Expectation values, measurement and sampling + +`DensityMatrix.expectation()` likewise accepts a `PauliString` or a `Hamiltonian`. In addition, the density matrix provides state simulation interfaces such as `measure()`, `measure_all()`, `sample_shots()`, `sample()` and `probs()`. + +```python +from cqlib.qis import DensityMatrix, Hamiltonian, PauliString + +density = DensityMatrix(2) +density.apply_h(0) +density.apply_cx(0, 1) + +hamiltonian = Hamiltonian.from_list([ + (PauliString.from_str("ZZ"), 1.0), + (PauliString.from_str("XX"), 0.5), +]) + +print(density.expectation(hamiltonian)) +print(density.sample_shots(8)) +``` + +Measurement methods change the current state; sampling methods are suitable for generating finite-shot results. When performing noise simulation or state metric analysis, it is recommended to save a `copy()` first, so that measurement collapse does not affect subsequent computation. + +--- + +## Next steps + +- [DensityMatrixNoise noisy simulation](3_density_matrix_noise.md): extend manual Kraus noise to automatic noisy circuit simulation based on `NoiseModel`. +- [Quantum state metrics and entropy](6_metrics_entropy.md): interpret density matrix results with purity, entropy, fidelity and entanglement metrics. +- [Visualizing quantum states](../5_visualization/7_state_visualization.md): inspect the coherence terms and Pauli expansion of a density matrix with state city and Pauli vector diagrams. diff --git a/docs/documentation/en/1_cqlib/3_qis/3_density_matrix_noise.md b/docs/documentation/en/1_cqlib/3_qis/3_density_matrix_noise.md new file mode 100644 index 00000000..045cf59a --- /dev/null +++ b/docs/documentation/en/1_cqlib/3_qis/3_density_matrix_noise.md @@ -0,0 +1,158 @@ +# DensityMatrixNoise Noisy Simulation + +`DensityMatrixNoise` is a density matrix simulator with a noise model. When executing quantum gates it injects single-qubit gate noise, two-qubit gate noise and readout error automatically according to the `NoiseModel`, suitable for comparing the probabilities, expectation values and sampling results of ideal and noisy circuits. + +The underlying representation is still a density matrix, so the scale grows quickly with the number of qubits. It is recommended to verify the noise configuration with a small circuit first, and then extend it to algorithm experiments. + +--- + +## Task: add gate noise to a Bell state + +```python +from cqlib import Circuit +from cqlib.circuit import StandardGate +from cqlib.device import NoiseModel, SingleQubitNoise, TwoQubitNoise +from cqlib.qis import DensityMatrixNoise + +noise_model = NoiseModel() +noise_model.add_single_qubit_error( + StandardGate.H, + 0, + SingleQubitNoise.depolarizing(0.001), +) +noise_model.add_two_qubit_error( + StandardGate.CX, + 0, + 1, + TwoQubitNoise.depolarizing(0.01), +) + +circuit = Circuit(2) +circuit.h(0) +circuit.cx(0, 1) + +simulator = DensityMatrixNoise.from_circuit(circuit, noise_model) +print(simulator.probabilities()) +``` + +An ideal Bell state has main peaks only on `00` and `11`. After noise is added, `01` and `10` may show non-zero probabilities. When reading a diagram or values, first confirm which gates and which qubits the noise model is configured on. + +--- + +## Configuring common noise types + +`NoiseModel` can add single-qubit gate noise, two-qubit gate noise and readout error separately. + +```python +from cqlib.circuit import StandardGate +from cqlib.device import NoiseModel, ReadoutError, SingleQubitNoise, TwoQubitNoise + +noise_model = NoiseModel() + +noise_model.add_single_qubit_error( + StandardGate.RY, + 0, + SingleQubitNoise.bit_flip(0.002), +) +noise_model.add_single_qubit_error( + StandardGate.H, + 1, + SingleQubitNoise.phase_flip(0.001), +) +noise_model.add_two_qubit_error( + StandardGate.CX, + 0, + 1, + TwoQubitNoise.depolarizing(0.01), +) +noise_model.add_readout_error( + 0, + ReadoutError(p_0_given_1=0.02, p_1_given_0=0.01), +) +``` + +Single-qubit noise and two-qubit noise affect the evolution of the quantum state; readout error affects the observed probabilities or sampling results, and is not equivalent to decoherence of the quantum state after a gate. + +--- + +## Distinguishing state probabilities from readout probabilities + +`probabilities()` returns the quantum state probabilities without readout error; `probabilities_with_readout(qubits)` superimposes readout error on the specified measurement qubits. + +```python +from cqlib.circuit import StandardGate +from cqlib.device import NoiseModel, ReadoutError +from cqlib.qis import DensityMatrixNoise + +noise_model = NoiseModel() +noise_model.add_readout_error(0, ReadoutError(0.02, 0.03)) + +simulator = DensityMatrixNoise(1, noise_model) +simulator.apply_x(0) + +print("state probabilities:", simulator.probabilities()) +print("readout probabilities:", simulator.probabilities_with_readout([0])) +``` + +To analyze only the real quantum state after gate noise, inspect `probabilities()` or the underlying density matrix; to simulate experimental readout, inspect the probabilities with readout or the sampling interfaces. + +--- + +## Executing noisy simulation gate by gate + +Besides `from_circuit()`, the `apply_*` methods can also be called gate by gate, as with a state simulator. After each gate is executed, the corresponding noise is looked up from the current `NoiseModel`. + +```python +from cqlib.circuit import StandardGate +from cqlib.device import NoiseModel, SingleQubitNoise +from cqlib.qis import DensityMatrixNoise + +noise_model = NoiseModel() +noise_model.add_single_qubit_error( + StandardGate.X, + 0, + SingleQubitNoise.bit_flip(0.01), +) + +simulator = DensityMatrixNoise(1, noise_model) +simulator.apply_x(0) + +print(simulator.probabilities()) +``` + +Gate-by-gate execution is suitable for locating the source of noise. In a complex circuit, if the result is abnormal, the circuit can be split into several segments, and the probabilities or expectation values checked after each segment. + +--- + +## Expectation values and sampling + +`DensityMatrixNoise` supports expectation value and sampling interfaces similar to those of `Statevector` and `DensityMatrix`. + +```python +from cqlib import Circuit +from cqlib.qis import DensityMatrixNoise, Hamiltonian, PauliString + +circuit = Circuit(2) +circuit.h(0) +circuit.cx(0, 1) + +simulator = DensityMatrixNoise.from_circuit(circuit) + +observable = Hamiltonian.from_list([ + (PauliString.from_str("ZZ"), 1.0), +]) + +print(simulator.expectation(observable)) +print(simulator.sample_shots(16)) +``` + +If readout error is configured in the `NoiseModel`, an ordinary expectation value usually denotes the expectation value of the quantum state after noisy evolution; observed results with readout error should be handled separately through the readout probabilities or the corresponding sampling interfaces. + + +--- + +## Next steps + +- [DensityMatrix mixed-state simulation](2_density_matrix.md): return to the density matrix itself, to understand Kraus channels, partial trace and physicality checks. +- [Visualizing execution results](../5_visualization/6_result_visualization.md): plot noisy sampling results as a histogram or probability distribution, and inspect the main peaks and the long tail. +- [Noise model](../2_device/4_noise.md): learn how `NoiseModel`, gate noise and readout error are modeled in the device module. diff --git a/docs/documentation/en/1_cqlib/3_qis/4_stabilizer.md b/docs/documentation/en/1_cqlib/3_qis/4_stabilizer.md new file mode 100644 index 00000000..8989c999 --- /dev/null +++ b/docs/documentation/en/1_cqlib/3_qis/4_stabilizer.md @@ -0,0 +1,151 @@ +# StabilizerState Stabilizer Simulation + +`StabilizerState` represents Clifford circuits with a stabilizer tableau. It is suitable for simulating circuits that contain only Clifford gates, and can directly read stabilizer generators, destabilizers, Pauli expectation values and measurement results. + +When a circuit contains arbitrary-angle rotations, `T` gates, `fSim` or other non-Clifford operations, `Statevector` or `DensityMatrix` should be used instead. + +--- + +## Task: simulate a Bell state with stabilizers + +```python +from cqlib.qis import StabilizerState + +state = StabilizerState(2) +state.apply_h(0) +state.apply_cx(0, 1) + +print(state.probabilities()) +print(state.get_stabilizers()) +``` + +The stabilizer generators of a Bell state can be used to check the entanglement structure. Compared with looking only at probabilities, stabilizer generators express more directly which Pauli constraints the state satisfies. + +--- + +## Constructing a stabilizer state from a Circuit + +If a Clifford circuit is already available, a stabilizer state can be constructed directly from the circuit. + +```python +from cqlib import Circuit +from cqlib.qis import StabilizerState + +circuit = Circuit(3) +circuit.h(0) +circuit.cx(0, 1) +circuit.cx(1, 2) + +state = StabilizerState.from_circuit(circuit) +print(state.probabilities()) +``` + +`apply_circuit()` can also be used to apply a circuit in place to an existing stabilizer state. + +```python +prefix = Circuit(3) +prefix.h(0) + +suffix = Circuit(3) +suffix.cx(0, 1) +suffix.cx(1, 2) + +state = StabilizerState(3) +state.apply_circuit(prefix) +state.apply_circuit(suffix) + +print(state.get_stabilizers()) +``` + +This segmented approach is suitable for checking the intermediate stabilizer structure of Clifford encoding circuits, error correction circuits or large-scale GHZ circuits. + +--- + +## Executing a stabilizer circuit with classical state + +`run_circuit()` returns a `StabilizerCircuitResult`, which contains the final stabilizer state and the classical state. + +```python +from cqlib import Circuit +from cqlib.qis import StabilizerState + +circuit = Circuit(2) +circuit.h(0) +circuit.cx(0, 1) +circuit.measure(0) + +result = StabilizerState.run_circuit(circuit) + +print(result.state) +print(result.classical) +``` + +When a circuit contains measurement or classical values in a dynamic circuit, `run_circuit()` is more suitable than reading only the final quantum state for preserving the classical execution results. + +--- + +## Measurement, reset and sampling + +Stabilizer states support single-qubit measurement, full measurement, reset and finite-shot sampling. + +```python +from cqlib.qis import StabilizerState + +state = StabilizerState(2) +state.apply_h(0) +state.apply_cx(0, 1) + +shots = state.sample_shots(32) +counts = {} +for outcome in shots: + bitstring = outcome.to_bitstring(state.num_qubits) + counts[bitstring] = counts.get(bitstring, 0) + 1 + +print(counts) + +measured = state.measure_all() +print(measured.to_bitstring(2)) +``` + +`measure()` and `measure_all()` collapse the current stabilizer state; `sample_shots()` is suitable for generating sampling results without handling the collapse process manually. + +--- + +## Inspecting stabilizers, destabilizers and Pauli expectation values + +```python +from cqlib.qis import PauliString, StabilizerState + +state = StabilizerState(2) +state.apply_h(0) +state.apply_cx(0, 1) + +print("stabilizers:", state.get_stabilizers()) +print("destabilizers:", state.get_destabilizers()) +print("ZZ:", state.pauli_expectation(PauliString.from_str("ZZ"))) +print("XI:", state.pauli_expectation(PauliString.from_str("XI"))) +print(state.to_stim_format()) +``` + +`pauli_expectation()` returns `-1`, `0` or `1`. This is suitable for quickly checking whether a given Pauli observable is determined by the current stabilizer state. + +--- + +## Supported scope + +`StabilizerState` is suitable for the following operations: + +- single-qubit Clifford gates: `H`, `S`, `Sdg`, `X`, `Y`, `Z`, `X2p`, `X2m`, `Y2p`, `Y2m`; +- two-qubit Clifford gates: `CX`, `CY`, `CZ`, `SWAP`; +- measurement, reset, sampling and probability reading under stabilizer semantics. + +If a circuit contains non-Clifford operations, the stabilizer simulator cannot express the complete state exactly. In that case, return to `Statevector` or `DensityMatrix`, or first isolate the non-Clifford fragments for separate analysis. + + +--- + +## Next steps + +- [Statevector pure-state simulation](1_statevector.md): when a circuit contains non-Clifford gates, continue verification with the pure-state simulator. +- [Pauli, PauliString and Hamiltonian](5_pauli_and_hamiltonian.md): understand the PauliString representation and the Pauli expectation values behind stabilizer generators. +- [Debugging circuits with text diagrams](../5_visualization/1_draw_text.md): when a stabilizer circuit is long, first check the gate order and control qubits with a text diagram. diff --git a/docs/documentation/en/1_cqlib/3_qis/5_pauli_and_hamiltonian.md b/docs/documentation/en/1_cqlib/3_qis/5_pauli_and_hamiltonian.md new file mode 100644 index 00000000..1bb00f40 --- /dev/null +++ b/docs/documentation/en/1_cqlib/3_qis/5_pauli_and_hamiltonian.md @@ -0,0 +1,193 @@ +# Pauli, PauliString and Hamiltonian + +Pauli operators and Pauli Hamiltonians are the foundation of VQE, QAOA, quantum simulation, stabilizer analysis and measurement post-processing. `cqlib.qis` provides the single-qubit `Pauli`, the multi-qubit `PauliString`, the global phase `Phase` and the Pauli term sum `Hamiltonian`. + +The focus of this section is not circuit construction, but how to describe observables, determine commutation, compute expectation values and convert a Hamiltonian into an evolution circuit. + +--- + +## Task: understand single-qubit Pauli operators and phases + +```python +from cqlib.qis import Pauli + +x = Pauli.x() +y = Pauli.y() +z = Pauli.z() + +result, phase = x.mul_with_phase(z) + +print(result) +print(phase) +print(phase.to_complex()) +print(y.to_symplectic()) +print(z.to_matrix()) +``` + +Pauli multiplication produces a global phase. Use `mul_with_phase()` when the phase needs to be preserved; ordinary multiplication can be used when only the Pauli type matters, but it should not be used to interpret the complete phase relation. + +--- + +## Constructing a PauliString + +`PauliString` represents a multi-qubit Pauli operator. It can be created from a string, or set qubit by qubit. + +```python +from cqlib.qis import Pauli, PauliString + +string = PauliString.from_str("XZI") +print(string.num_qubits) +print(string.x_bits) +print(string.z_bits) +print(string.x_mask) +print(string.z_mask) + +manual = PauliString(3) +manual.set_pauli(0, Pauli.x()) +manual.set_pauli(1, Pauli.z()) +manual.set_pauli(2, Pauli.i()) + +print(manual) +``` + +When strings and bitstring probabilities are used together for post-processing, the qubit ordering convention must be unified. In particular, when computing expectation values from a probability dictionary, confirm which qubit each position of the result string corresponds to. + +--- + +## Determining commutation and multiplication + +```python +from cqlib.qis import PauliString + +xx = PauliString.from_str("XX") +zz = PauliString.from_str("ZZ") +zi = PauliString.from_str("ZI") +iz = PauliString.from_str("IZ") + +print(xx.commutes_with(zz)) +print(zi.commutes_with(iz)) +print(xx * zz) +``` + +Commutation determines whether Pauli terms can be combined in the same measurement basis, and also affects the evolution decomposition of a Hamiltonian. When building a VQE or QAOA measurement flow, the commutation structure of the Pauli terms should be checked as early as possible. + +--- + +## Computing Pauli expectation values from a state or probabilities + +`PauliString` can compute expectation values directly against a `Statevector` or a `DensityMatrix`, and can also estimate expectation values from a measurement probability dictionary. + +```python +from cqlib.qis import PauliString, Statevector + +state = Statevector(2) +state.apply_h(0) +state.apply_cx(0, 1) + +zz = PauliString.from_str("ZZ") +print(zz.expectation_statevector(state)) + +probs = { + "00": 0.5, + "11": 0.5, +} +print(zz.expectation(probs)) +``` + +Computing an expectation value from a probability dictionary is only suitable for probabilities already obtained in the corresponding Pauli measurement basis. For a PauliString containing `X` or `Y`, computational-basis sampling probabilities cannot be treated directly as the corresponding measurement results, unless the correct basis-change measurement has already been performed in the circuit. + +--- + +## Constructing a Hamiltonian + +`Hamiltonian` is a sparse sum of PauliStrings with coefficients, of the form `H = Σ c_k P_k`. + +```python +from cqlib.qis import Hamiltonian, PauliString + +hamiltonian = Hamiltonian(2) +hamiltonian.add_term(PauliString.from_str("ZZ"), 1.0) +hamiltonian.add_term(PauliString.from_str("XI"), 0.5) +hamiltonian.add_term(PauliString.from_str("ZZ"), -0.2) + +print(hamiltonian.num_terms) +hamiltonian.simplify() +print(hamiltonian.terms) +``` + +It can also be created from a list in one call: + +```python +hamiltonian = Hamiltonian.from_list([ + (PauliString.from_str("ZI"), -1.0), + (PauliString.from_str("IZ"), -1.0), + (PauliString.from_str("ZZ"), 0.5), +]) +``` + +`simplify()` merges duplicate PauliStrings and absorbs the internal phase of each PauliString into its coefficient. After constructing a large Hamiltonian, it is recommended to call `simplify()` first before moving on to simulation, measurement grouping or evolution decomposition. + +--- + +## Computing Hamiltonian expectation values and variance + +```python +from cqlib.qis import Hamiltonian, PauliString, Statevector + +state = Statevector(2) +state.apply_h(0) +state.apply_cx(0, 1) + +hamiltonian = Hamiltonian.from_list([ + (PauliString.from_str("ZZ"), 1.0), + (PauliString.from_str("XX"), 0.5), +]) + +print(hamiltonian.expectation_statevector(state)) +print(hamiltonian.variance_statevector(state)) +``` + +If the input is a density matrix, `expectation_density_matrix()` can be used. If the input comes from real sampling results, `expectation_probs()` can be used to aggregate the probabilities in each Pauli measurement basis. + +--- + +## Constructing a Trotter evolution circuit + +`Hamiltonian` can be converted into a quantum circuit that approximately implements `e^{-iHt}`. `TrotterMode` provides first-order, second-order and randomized modes. + +```python +from cqlib.qis import Hamiltonian, PauliString, TrotterMode + +hamiltonian = Hamiltonian.from_list([ + (PauliString.from_str("ZZ"), 0.5), + (PauliString.from_str("XI"), 0.3), +]) + +circuit = hamiltonian.to_trotter_circuit( + time=1.0, + steps=4, + mode=TrotterMode.first_order(), +) + +print(circuit) +``` + +For commuting terms, `to_evolution_circuit()` can use a more direct evolution decomposition; for non-commuting terms, it falls back to the specified Trotter mode. + +```python +circuit = hamiltonian.to_evolution_circuit( + time=1.0, + steps=4, + mode=TrotterMode.second_order(), +) +``` + +The accuracy of a time evolution circuit depends on whether the Hamiltonian terms commute, the total evolution time, the number of Trotter steps and the decomposition order. Before a formal experiment, an error comparison should first be performed on a small-scale system. + +--- + +## Next steps + +- [Statevector pure-state simulation](1_statevector.md): apply PauliString and Hamiltonian to an ideal pure state, and verify expectation values and energy functions. +- [DensityMatrix mixed-state simulation](2_density_matrix.md): apply the same set of observables to mixed-state and noisy-state analysis. +- [Quantum state metrics and entropy](6_metrics_entropy.md): beyond energy, continue comparing fidelity, trace distance, purity and entanglement metrics. diff --git a/docs/documentation/en/1_cqlib/3_qis/6_metrics_entropy.md b/docs/documentation/en/1_cqlib/3_qis/6_metrics_entropy.md new file mode 100644 index 00000000..867b4511 --- /dev/null +++ b/docs/documentation/en/1_cqlib/3_qis/6_metrics_entropy.md @@ -0,0 +1,179 @@ +# Quantum State Metrics and Entropy + +`cqlib.qis.metrics` and `cqlib.qis.entropy` provide quantum state similarity, mixedness, entropy and entanglement metrics. They are commonly used to compare ideal and noisy states, to judge whether a density matrix has decohered, to analyze subsystem entanglement, and to generate auxiliary evaluation metrics for algorithm experiments. + +Before use, first confirm whether the input object is a `Statevector` or a `DensityMatrix`. The function names for pure-state metrics and mixed-state metrics differ, and the two should not be mixed up. + +--- + +## Overview of common functions + +| Module | Function | Input | Meaning | +|---|---|---|---| +| `metrics` | `purity_pure` | `Statevector` | Pure-state purity | +| `metrics` | `purity_mixed` | `DensityMatrix` | Mixed-state purity `Tr(ρ²)` | +| `metrics` | `state_fidelity_pure` | Two `Statevector` | Pure-state fidelity | +| `metrics` | `state_fidelity_pure_mixed` | `Statevector`, `DensityMatrix` | Pure and mixed state fidelity | +| `metrics` | `state_fidelity_mixed` | Two `DensityMatrix` | Mixed-state fidelity | +| `metrics` | `trace_distance_pure` | Two `Statevector` | Pure-state trace distance | +| `metrics` | `trace_distance_mixed` | Two `DensityMatrix` | Mixed-state trace distance | +| `metrics` | `entropy` | `DensityMatrix` | Von Neumann entropy | +| `metrics` | `partial_transpose` | `DensityMatrix`, qubit list | Partial transpose | +| `metrics` | `logarithmic_negativity` | `DensityMatrix`, subsystem | Logarithmic negativity | +| `entropy` | `linear_entropy` | `DensityMatrix` | Linear entropy | +| `entropy` | `renyi_entropy` | `DensityMatrix`, `alpha` | Rényi entropy | +| `entropy` | `entanglement_entropy_pure` | `Statevector`, subsystem | Pure-state entanglement entropy | +| `entropy` | `negativity` | `DensityMatrix`, subsystem | Negativity | +| `entropy` | `concurrence` | two-qubit `DensityMatrix` | Two-qubit concurrence | +| `entropy` | `entanglement_of_formation` | two-qubit `DensityMatrix` | Entanglement of formation | + +--- + +## Task: compare two pure states + +```python +from cqlib.qis import Statevector, metrics + +plus = Statevector(1) +plus.apply_h(0) + +minus = Statevector(1) +minus.apply_h(0) +minus.apply_z(0) + +print(metrics.purity_pure(plus)) +print(metrics.state_fidelity_pure(plus, minus)) +print(metrics.trace_distance_pure(plus, minus)) +``` + +The closer the fidelity is to `1`, the more similar the two states are; the closer the trace distance is to `0`, the more similar the two states are. For orthogonal pure states, the fidelity should be close to `0` and the trace distance should be close to `1`. + +--- + +## Analyzing mixed-state purity and entropy + +```python +import numpy as np +from cqlib.qis import DensityMatrix, entropy, metrics + +mixed = DensityMatrix.from_density_matrix( + 1, + np.array( + [ + 0.7 + 0.0j, 0.0 + 0.0j, + 0.0 + 0.0j, 0.3 + 0.0j, + ], + dtype=complex, + ), +) + +print(metrics.purity_mixed(mixed)) +print(metrics.entropy(mixed)) +print(entropy.linear_entropy(mixed)) +print(entropy.renyi_entropy(mixed, alpha=2.0)) +``` + +The closer the purity is to `1`, the closer the state is to a pure state; the larger the entropy and the linear entropy, the more mixed the state. When interpreting noisy results, it is recommended to inspect both the probability distribution and the purity/entropy metrics, because similar probabilities do not necessarily mean the same coherence structure. + +--- + +## Computing the entanglement entropy of a Bell state + +For pure states, the entanglement entropy can be obtained by reducing over a subsystem. + +```python +from cqlib.qis import Statevector, entropy + +state = Statevector(2) +state.apply_h(0) +state.apply_cx(0, 1) + +print(entropy.entanglement_entropy_pure(state, [0])) +``` + +The reduced density matrix of any single-qubit subsystem of a Bell state is exactly the maximally mixed state I/2, so the entanglement entropy is exactly `1` bit. For a product state, the entanglement entropy is exactly `0`. + +--- + +## Computing negativity and logarithmic negativity + +Negativity or logarithmic negativity is commonly used for mixed-state entanglement analysis. The following example first constructs a density matrix from a Bell state circuit, and then computes the entanglement metric of subsystem `[0]`. + +```python +from cqlib import Circuit +from cqlib.qis import DensityMatrix, entropy, metrics + +circuit = Circuit(2) +circuit.h(0) +circuit.cx(0, 1) + +density = DensityMatrix.from_circuit(circuit) + +print(entropy.negativity(density, [0])) +print(metrics.logarithmic_negativity(density, sys_a=[0])) +``` + +A negativity of `0` usually means that no entanglement is detected by the PPT criterion under the corresponding partition; a larger negativity means that the negative eigenvalues appearing after the partial transpose are more pronounced. + +--- + +## Two-qubit concurrence and entanglement of formation + +`concurrence()` and `entanglement_of_formation()` target two-qubit density matrices. + +```python +from cqlib.qis import DensityMatrix, entropy + +state = DensityMatrix(2) +state.apply_h(0) +state.apply_cx(0, 1) + +print(entropy.concurrence(state)) +print(entropy.entanglement_of_formation(state)) +``` + +These two metrics are suitable for two-qubit entanglement analysis. For systems with more qubits, use entanglement entropy, negativity, logarithmic negativity or other metrics defined by subsystem partition instead. + +--- + +## Comparing ideal and noisy states + +The following example compares an ideal Bell state with a noisy density matrix. + +```python +from cqlib import Circuit +from cqlib.circuit import StandardGate +from cqlib.device import NoiseModel, TwoQubitNoise +from cqlib.qis import DensityMatrix, DensityMatrixNoise, Statevector, metrics + +circuit = Circuit(2) +circuit.h(0) +circuit.cx(0, 1) + +ideal_state = Statevector.from_circuit(circuit) +ideal_density = DensityMatrix.from_circuit(circuit) + +noise_model = NoiseModel() +noise_model.add_two_qubit_error( + StandardGate.CX, + 0, + 1, + TwoQubitNoise.depolarizing(0.02), +) +noisy = DensityMatrixNoise.from_circuit(circuit, noise_model) +noisy_density = DensityMatrix.from_density_matrix(2, noisy.state.reshape(-1)) + +print(metrics.state_fidelity_pure_mixed(ideal_state, noisy_density)) +print(metrics.trace_distance_mixed(ideal_density, noisy_density)) +print(metrics.purity_mixed(noisy_density)) +``` + +Such metrics are suitable for answering "how far the noisy state is from the ideal state". Looking only at the final counts may ignore changes in coherence terms, phase and mixedness. + +--- + +## Next steps + +- [DensityMatrix mixed-state simulation](2_density_matrix.md): return to density matrix construction, partial trace and physicality verification, to understand where the metric inputs come from. +- [DensityMatrixNoise noisy simulation](3_density_matrix_noise.md): use noisy simulation to generate the mixed-state results to be compared. +- [Visualizing quantum states](../5_visualization/7_state_visualization.md): interpret numerical metrics together with Bloch, state city and Pauli vector diagrams. diff --git a/docs/documentation/en/1_cqlib/4_compiler/0_overview.md b/docs/documentation/en/1_cqlib/4_compiler/0_overview.md new file mode 100644 index 00000000..2f68ad94 --- /dev/null +++ b/docs/documentation/en/1_cqlib/4_compiler/0_overview.md @@ -0,0 +1,226 @@ +# Compilation and optimization + +The recommended entry point for the Cqlib 2.0 Python binding is **`cqlib.compile`**: a single call completes canonicalization, knowledge rule optimization, decomposition, optional device layout and SABRE routing, and target gate set translation, plus the native instruction lowering, fixed-point optimization and validation required by a strict device target. + +--- + +## Common entry points + +```python +from cqlib import Circuit +from cqlib.compile import CompileConfig, CompileMode, compile +from cqlib.compile.transform.layout import vf2_perfect_layout, sabre_layout +from cqlib.compile.transform.routing import route_sabre +from cqlib.compile.transform import KnowledgeRewriter, RewriteConfig +``` + +--- + +## Recommended compilation pipeline + +```python +from cqlib import Circuit +from cqlib.compile import CompileMode, compile +from cqlib.device import Device + +circuit = Circuit(3) +circuit.h(0) +circuit.cx(0, 2) + +device = Device.line("line-3", 3) + +result = compile( + circuit, + mode=CompileMode.enhanced(), + device=device, + target_basis=["H", "CX", "RZ"], + seed=42, +) + +print("changed:", result.changed) +for step in result.steps: + if step.changed or not step.skipped: + print(step.stage, step.name, step.reason) + +compiled = result.circuit +``` + +The example above passes both `device` and `target_basis`, so `TopologyBasis` semantics are selected: the device is used for capacity, layout and routing, and the explicit gate set constrains the output, but the output is not promised to satisfy the native instruction capabilities of the device. Only a strict `Device` target configured with native capabilities runs the SABRE Pareto beam described below, and only in enhanced mode. + +--- + +## Step-by-step pipeline debugging + +When layout, routing or rule optimization needs to be observed separately, the calls can be split apart: + +```python +from cqlib.compile.transform.layout import LayoutObjective, vf2_perfect_layout +from cqlib.compile.transform.routing import route_sabre +from cqlib.compile.sabre import SabreConfig + +objective = LayoutObjective.topology_only() +config = SabreConfig.deterministic_seeded(42) + +# 1) layout only (no SWAP insertion) +layout_result = vf2_perfect_layout(circuit, device, objective) + +# 2) layout + routing (with SWAP insertion) +route_result = route_sabre(circuit, device, objective, config) +print("swap_count:", route_result.swap_count) +``` + +--- + +## Compilation goals + +1. Adapt the logical circuit to the target device topology (layout + routing, inserting SWAPs when necessary); +2. Reduce the number of two-qubit gates, the circuit depth and redundant single-qubit gates; +3. Lower the gate sequence to the target native gate set. + +--- + +## Pipeline overview + +```text +Logical circuit (Circuit) + ->canonicalize.input + ->decompose.definitions + ->optimize.pre_decomposition + ->decompose.unitary / decompose.mc_gates + ->canonicalize.after_decomposition + ->optimize.post_decomposition + -> decompose.routing_basis [physical target] + -> route.sabre [physical target] + -> post-routing resynthesis / cleanup [Enhanced + physical target] + -> translate.target_basis / target cleanup [explicit target gate set] + ->canonicalize.output + -> lower.device_instructions [strict Device target] + -> native-input canonicalization / fixed point [strict Device target] + -> validate.device / validate.topology [by target type] + -> select.sabre_pareto_beam [Enhanced + strict Device target] + ->result.circuit +``` + +`canonicalize.output` only ends the tidying of the generic output representation. For a strict `Device` target, +exact native instruction lowering, `optimize.native_fixed_point` and `validate.device` still run afterwards. + +### Pareto selection for enhanced strict devices + +Enhanced strict device compilation saves an immutable pre-routing prefix before the first routing, and takes the +ordinary complete compilation result as candidate 0: + +```text +Save the pre-routing prefix + -> candidate 0: route + full suffix + validate.device + ->bounded SABRE Pareto beam + -> each explored candidate starts from the same prefix + -> route + the same full suffix + validate.device + ->select.sabre_pareto_beam + ├─ an improved candidate satisfying the contract exists: select the validated winner + └─ otherwise: keep the validated candidate 0 + ->result.circuit +``` + +A candidate must satisfy the exact Pareto contract in every control-flow scope and must strictly improve either +the native two-qubit gate count or the depth before it can replace candidate 0. Therefore, `selection` appearing +after `validation` does not mean the output bypassed validation: selection does not transform the circuit again, +it only decides between candidates that have already completed the same validation suffix. + +On a successful return, `result.steps` records the common prefix, the finally kept baseline or winner path, and +the trailing `select.sabre_pareto_beam`; it is not a complete execution log of every explored branch. The number +of candidate attempts, the number of discards, the first recoverable error, and the quality summaries of +candidate 0 and the winner are recorded in the `reason` of the selection. + +--- + +## CompileMode + +| Mode | Description | +|------|------| +| `CompileMode.normal()` | Predictable production default: conservative rewrite budget and SABRE trials | +| `CompileMode.enhanced()` | Higher rewrite, resynthesis, native fixed-point optimization and SABRE search budgets, plus post-routing / target gate set cleanup; a strict `Device` target also runs a bounded SABRE Pareto beam across validated candidates | + +--- + +## CompileConfig essentials + +| Parameter | Where it is set | Purpose | +|------|---------|------| +| `mode` | `CompileConfig` | `normal` / `enhanced` | +| `target` | `CompileConfig` | Target constraint; see [`CompileTarget`](../../../../api/en/python/4_compile/1_compiler.md) | +| `resource_policy` | `CompileConfig` | Ancilla policy for the decomposition stage | +| `device` | `compile()` | Usable qubits, topology, native gates and optional calibration; used alone it selects a strict `Device` target | +| `target_basis` | `compile()` | Explicit target gate set; used together with `device` it selects `TopologyBasis`, which does not promise device native compatibility and does not run the Pareto beam | +| `initial_layout` | `compile()` or a `CompileTarget` constructor | Skip automatic layout and use the given mapping directly for SABRE routing | +| `seed` | `compile()` or a `CompileTarget` constructor | Random trials for heuristic layout / routing | + +--- + +## CompileConfig and CompilerWorkflow + +To compile multiple circuits with the same configuration, use `CompilerWorkflow`: + +```python +from cqlib import Circuit +from cqlib.compile import CompileConfig, CompileMode, CompileTarget, CompilerWorkflow +from cqlib.device import Device + +circuits = [] +c1 = Circuit(2) +c1.cx(0, 1) +circuits.append(c1) + +c2 = Circuit(3) +c2.h(0) +c2.cx(0, 2) +circuits.append(c2) + +config = CompileConfig( + mode=CompileMode.enhanced(), + target=CompileTarget.topology_basis( + Device.line("line-8", 8), + ["H", "CX", "RZ"], + seed=42, + ), +) +workflow = CompilerWorkflow(config) + +for circuit in circuits: + result = workflow.run(circuit) + print(result.changed, len(result.circuit.operations)) +``` + +--- + +## Decomposition and resource policy + +The `decompose.definitions` / `decompose.unitary` / `decompose.mc_gates` stages in the workflow can also be called separately (useful when debugging multi-controlled gate decomposition): + +```python +from cqlib.compile.resource import ResourcePolicy +from cqlib.compile.transform.decompose import ( + decompose_mc_gates_for_device, + decompose_unitaries, +) + +# multi-controlled gate decomposition (constrained by device capacity) +result = decompose_mc_gates_for_device( + circuit, + device, + resource_policy=ResourcePolicy(max_pre_layout_clean_ancillas=2), +) + +# matrix unitary gate decomposition +unitary_result = decompose_unitaries(circuit) +``` + +`ResourcePolicy` controls the number of **clean ancillas** the compiler may create; the **hard capacity** of the device is determined by `device.num_usable_qubits`, and the two are independent. + +--- + +## Next steps + +- [Initial layout (Layout)](1_layout.md): learn initial mapping algorithms such as VF2, greedy and sabre_layout. +- [SABRE routing mapping](2_sabre_mapping.md): learn how heuristic SWAPs route a circuit onto the device topology. +- [Template matching and knowledge rule optimization](3_template_optimization.md): master the local optimization capabilities of `compile()` and `KnowledgeRewriter`. +- [Commutation and Clifford-RZ optimization](4_commutative_and_clifford.md): understand how commutation checking supports rotation merging and rule reordering. diff --git a/docs/documentation/en/1_cqlib/4_compiler/1_layout.md b/docs/documentation/en/1_cqlib/4_compiler/1_layout.md new file mode 100644 index 00000000..a4df35cd --- /dev/null +++ b/docs/documentation/en/1_cqlib/4_compiler/1_layout.md @@ -0,0 +1,136 @@ +# Initial Layout (Layout) + +Initial layout is responsible only for choosing the initial mapping from logical qubits to physical qubits, and does not insert any SWAP gates. + +--- + +## Python entry points + +```python +from cqlib import Circuit +from cqlib.compile.transform.layout import ( + LayoutObjective, + Vf2LayoutConfig, + vf2_perfect_layout, + greedy_layout, + sabre_layout, + trivial_layout, +) +from cqlib.device import Device +``` + +--- + +## Quick examples: VF2 perfect embedding + +```python +circuit = Circuit(3) +circuit.cx(0, 1) +circuit.cx(1, 2) + +device = Device.line("line-5", 5) +objective = LayoutObjective.topology_only() + +result = vf2_perfect_layout(circuit, device, objective) +print(result.layout) # logical -> physical mapping +print(result.diagnostics.is_perfect) # whether all interactions are adjacent +``` + +If there is no perfect embedding, `vf2_perfect_layout` raises a `ValueError`; in that case switch to `greedy_layout` or `sabre_layout`, and then hand over to `route_sabre` or `route_with_layout`. + +--- + +## Trivial layout (identity mapping) + +Logical qubit `i → physical i`, used for baseline comparison or for an already aligned topology: + +```python +result = trivial_layout(circuit, device, objective) +print(result.layout.l2p_map) +``` + +--- + +## Greedy layout + +```python +objective = LayoutObjective.fidelity_aware() +result = greedy_layout(circuit, device, objective) +print("is_perfect:", result.diagnostics.is_perfect) +print("score:", result.score.total if result.score else None) +``` + +Characteristics: deterministic and fast, suitable as a seed for SABRE layout; on long chains or with large fan-out, `is_perfect=False` is possible. + +--- + +## SABRE initial layout + +```python +from cqlib.compile.sabre import SabreConfig + +config = SabreConfig.deterministic_seeded(42) +result = sabre_layout(circuit, device, objective, config) +``` + +`sabre_layout` generates multiple candidate groups and refines them through forward/backward trial runs, and still does not insert SWAP gates into the output. + +--- + +## LayoutObjective (layout scoring) + +| Constructor | Behavior | +|----------|------| +| `LayoutObjective.topology_only()` | Topological distance and direction mismatch only | +| `LayoutObjective.fidelity_aware()` | Default fidelity weights | +| `LayoutObjective.fidelity_required(device)` | Requires the device to have usable calibration | +| `LayoutObjective.auto_from_device(device)` | fidelity if calibration is available, topology otherwise | + +In enhanced mode, `compile(..., device=...)` uses the `fidelity_required` logic when the device has calibration. + +--- + +## VF2 configuration + +```python +from cqlib.compile.transform.layout import Vf2EdgeRequirement + +config = Vf2LayoutConfig( + candidate_limit=10, + edge_requirement=Vf2EdgeRequirement.positive_interactions(), +) +result = vf2_perfect_layout(circuit, device, objective, config) +``` + +--- + +## Passing layout results to routing + +```python +from cqlib.compile.transform.routing import route_with_layout +from cqlib.compile.sabre import SabreConfig + +layout_result = vf2_perfect_layout(circuit, device, objective) +routed = route_with_layout( + circuit, + device, + layout_result.layout, + SabreConfig.deterministic_seeded(42), +) +print("swaps:", routed.swap_count) +``` + +--- + +## Notes + +- The input is a **logical** `Circuit` plus a `Device`; the output `LayoutResult.layout` is a `cqlib.device.Layout`. +- The layout stage **does not change gate semantics** and **does not insert SWAP gates**. +- Device calibration participates in fidelity scoring through the readout / two-qubit error fields on `Device`. + +--- + +## Next steps + +- [SABRE routing mapping](2_sabre_mapping.md): pass the layout result to `route_sabre` or `route_with_layout` to complete physical routing. +- [Compilation and optimization](0_overview.md): review the `compile()` workflow and how the compilation stages relate to each other. diff --git a/docs/documentation/en/1_cqlib/4_compiler/2_sabre_mapping.md b/docs/documentation/en/1_cqlib/4_compiler/2_sabre_mapping.md new file mode 100644 index 00000000..093864e7 --- /dev/null +++ b/docs/documentation/en/1_cqlib/4_compiler/2_sabre_mapping.md @@ -0,0 +1,161 @@ +# SABRE Routing Mapping + +SABRE routes logical two-qubit gates onto the physical couplings allowed by the device by means of heuristic SWAPs. + +--- + +## Two entry points + +| API | Purpose | +|-----|------| +| `route_sabre(circuit, device, objective, config)` | Automatic `sabre_layout` + routing | +| `route_with_layout(circuit, device, initial_layout, config)` | Routing only, skipping the layout search | + +```python +from cqlib import Circuit +from cqlib.circuit import Instruction, StandardGate +from cqlib.compile.transform.layout import LayoutObjective +from cqlib.compile.transform.routing import route_sabre, route_with_layout +from cqlib.compile.sabre import SabreConfig +from cqlib.device import Device, Layout + +circuit = Circuit(3) +circuit.cx(0, 2) + +device = Device.line("line-3", 3) +device.native_gates = [ + Instruction.from_standard_gate(StandardGate.H), + Instruction.from_standard_gate(StandardGate.CX), +] +objective = LayoutObjective.topology_only() +config = SabreConfig.deterministic_seeded(42) + +result = route_sabre(circuit, device, objective, config) +print("swap_count:", result.swap_count) +print("ops:", len(result.circuit.operations)) +``` + +--- + +## Integration with the compile workflow + +The `device` below must already be configured with executable native instruction capabilities; using `device` alone as the target selects strict device compilation semantics. + +```python +from cqlib.compile import CompileMode, compile + +result = compile( + circuit, + mode=CompileMode.enhanced(), + device=device, + seed=42, +) + +for step in result.steps: + if step.name in { + "route.sabre", + "validate.device", + "select.sabre_pareto_beam", + }: + print(step.stage, step.name, step.changed, step.reason) +``` + +When `initial_layout` is already provided, the workflow skips the automatic layout and still uses the same SABRE router. + +### Pareto beam for enhanced strict devices + +For `CompileMode.enhanced()` and a strict `Device` target, the complete compilation result of the first `route.sabre` is candidate 0. Only after candidate 0 has completed post-routing cleanup, native instruction lowering, native fixed-point optimization and `validate.device` does the compiler start the bounded Pareto beam. Each explored candidate starts from the same pre-routing prefix and re-executes the same suffix up to device validation. + +`select.sabre_pareto_beam` accepts only validated candidates that do not degrade the exact native quality in any control-flow scope and that strictly reduce the native two-qubit gate count or depth; when no such candidate exists, candidate 0 is kept. This selection is the last orchestration and diagnostic step and does not transform the circuit further after validation. + +`result.steps` records the finally kept path, not every route that was attempted. The number of candidates, the discard situation, whether a winner was accepted, and the quality summaries of candidate 0 and the winner can be read from `select.sabre_pareto_beam.reason`. A `TopologyBasis` target does not run this beam even when SABRE routing is performed; for example, passing both `device` and `target_basis` to `compile()` selects `TopologyBasis` semantics. + +--- + +## Example: routing only (skipping the layout search) + +```python +from cqlib.device import Layout + +initial = Layout.from_pairs([(0, 0), (1, 2), (2, 1)], physical_count=3) +routed = route_with_layout(circuit, device, initial, config) +print("swaps:", routed.swap_count) +``` + +--- + +## Example: passing initial_layout to compile + +```python +from cqlib.compile import compile + +result = compile( + circuit, + device=device, + initial_layout=initial, + seed=42, +) +``` + +--- + +## SabreConfig + +| Field | Meaning | +|------|------| +| `layout_trials` | Number of random initial layouts; interaction-aware, greedy and VF2 candidates are added in addition | +| `layout_assignment_budget` | Search limit for movement component assignment on disconnected devices | +| `vf2_prepass` | Bounded VF2 pre-check used when an exact embedding is promising | +| `refinement_iterations` | Number of forward + backward refinement rounds per candidate | +| `routing_trials` | Total number of full routes allocated across lightweight refinement checkpoints per initial candidate | +| `seed` | Deterministic seed | +| `heuristic` | `SabreHeuristicConfig`: a lookahead that is single-layer by default, scaled by device width and stopped early for highly repetitive circuits; multiplicative congestion suppresses consecutive reuse of a physical qubit; exactly tied candidates are chosen by trial seed | + +```python +from cqlib.compile.sabre import SabreConfig + +config = SabreConfig( + seed=42, + layout_trials=24, + refinement_iterations=2, + routing_trials=2, +) +``` + +Shorthand constructor: `SabreConfig.deterministic_seeded(42)`. + +--- + +## Example: a reproducible record with a fixed seed + +```python +compile_record = { + "seed": 123, + "layout_trials": 24, + "refinement_iterations": 2, + "routing_trials": 2, +} +``` + +Routing involves randomness, so a formal experiment must fix and record the `seed`. + +Automatic layout + routing uses a fused search: each candidate keeps the lightweight checkpoints produced by its initial, forward and backward refinements, and `routing_trials` is allocated to checkpoints that are cheaper and map differently; full routes take part directly in a global streaming reduction, so routing is not repeated after layout scoring. The final ordering is fixed by predicted native 2Q count, native 2Q depth, native total depth and stable candidate index. + +This describes the in-routing selection of a single `route_sabre()` and of workflow candidate 0. The Pareto beam that enhanced strict device compilation runs afterwards continues to lower, optimize and validate the candidates, and then decides whether to replace candidate 0 based on the final exact native quality. + +--- + +## Output semantics + +- `result.circuit`: the circuit on physical qubit numbering, including the inserted `SWAP` gates; +- `swap_count`: should match the number of SWAP gates in the circuit; +- `layout_score`: the diagnostic score of the winning layout under the requested objective, not the basis on which SABRE makes its final choice; +- `diagnostics.native_two_qubit_count`, `native_two_qubit_depth`, `native_total_depth`: structural native quality estimates of the winning route; +- Routing guarantees undirected physical adjacency. + +--- + +## Next steps + +- [Template matching and knowledge rule optimization](3_template_optimization.md): learn how knowledge rules clean up and rewrite the circuit further after routing. +- [Initial layout (Layout)](1_layout.md): review the initial layout algorithms and how `LayoutObjective` scores them. diff --git a/docs/documentation/en/1_cqlib/4_compiler/3_template_optimization.md b/docs/documentation/en/1_cqlib/4_compiler/3_template_optimization.md new file mode 100644 index 00000000..f850488e --- /dev/null +++ b/docs/documentation/en/1_cqlib/4_compiler/3_template_optimization.md @@ -0,0 +1,208 @@ +# Template Matching and Knowledge Rule Optimization + +Template optimization is used to find a local gate sequence in a circuit and replace it with a sequence that is semantically equivalent, lower in cost or better suited to the target gate set. In everyday use, the template list usually does not need to be maintained manually; calling `compile()` directly applies the built-in rules to complete common optimizations. + +To observe or debug the local optimization process, `KnowledgeRewriter` can be used. It and `compile()` use the same set of built-in knowledge rules. A rule usually consists of the following parts: + +- `match`: the source operation sequence to be recognized; +- `require`: optional parameter constraints, such as equal angles, or equality modulo `2π` / `4π`; +- `rewrite`: the target operation sequence after replacement. + +Typical rules include cancellation of adjacent inverse gates, rotation merging, zero-angle normalization, gate decomposition, target gate set rewriting and explicit commutation rules. + +Neither `compile()` nor `KnowledgeRewriter` modifies the input circuit; instead they return a result object containing a new circuit. The circuits before and after optimization should remain quantum-semantically equivalent; if a rule involves a global phase, Cqlib explicitly preserves or folds the corresponding `GPhase` information. + +--- + +## Using it in the compilation pipeline + +It is recommended to use template optimization through the unified `compile()` entry point. The compilation workflow automatically combines canonicalization, definition expansion, knowledge rule rewriting, gate decomposition, optional routing and target gate set translation. + +```python +from cqlib.circuit import Circuit +from cqlib.compile import compile + +circuit = Circuit(1) +circuit.h(0) +circuit.h(0) + +result = compile(circuit) +optimized = result.circuit + +print("changed:", result.changed) +print("before:", len(circuit.operations)) +print("after:", len(optimized.operations)) + +for step in result.steps: + if "optimize" in step.name: + print(step.name, step.changed) +``` + +For most usage scenarios, the rule rewriter does not need to be called directly. `compile()` chooses the production configuration and canonicalizes the circuit representation once more before output. + +--- + +## Running knowledge rule rewriting directly + +To run local rule optimization separately in tests, diagnostics or a custom compilation flow, `KnowledgeRewriter` can be used directly. + +```python +from cqlib.circuit import Circuit +from cqlib.compile.transform import KnowledgeRewriter + +circuit = Circuit(1) +circuit.x(0) +circuit.x(0) + +result = KnowledgeRewriter.production().run(circuit) +optimized = result.circuit + +print("changed:", result.changed) +print("rounds:", result.stats.rounds_executed) +print("rules:", result.stats.rules_applied) +print("after:", len(optimized.operations)) +``` + +`KnowledgeRewriter.production()` uses conservative optimization rules, with simplification, cancellation, merging and canonicalization rules enabled by default. The rewriter accepts only rewrites that bring a local benefit, avoiding back-and-forth switching between equivalent forms of a circuit. + +A functional entry point is also available: + +```python +from cqlib.compile.transform import rewrite_circuit + +result = rewrite_circuit(circuit) +optimized = result.circuit +``` + +--- + +## Rule examples + +Built-in rules are described with a lightweight DSL. For example, H-H cancellation can be understood as: + +```text +rule cancel_h { + match { H 0, H 0 } + rewrite {} +} +``` + +The rotation merging rule can be understood as: + +```text +rule merge_rz { + match { RZ(a) 0, RZ(b) 0 } + rewrite { RZ(a + b) 0 } +} +``` + +Rules with conditions check the parameter relations first. For example, two mutually inverse `RZ` gates are removed only when the sum of their angles satisfies the corresponding modular relation; if they differ by a global phase, the rule explicitly generates a `GPhase`, which the canonicalizer then folds into the global phase of the circuit. + +--- + +## Optimization configuration + +`RewriteConfig` controls the rule search scope and the way rules are applied. On the Python side, configuration items are passed through the constructor. + +```python +from cqlib.compile.transform import KnowledgeRewriter, RewriteConfig + +config = RewriteConfig( + max_rounds=8, + max_window_ops=16, + max_pattern_len=8, + recurse_control_flow=True, + skip_labeled_ops=True, +) + +result = KnowledgeRewriter(config).run(circuit) +``` + +The meanings of several common parameters are as follows: + +- `max_rounds`: the maximum number of rewriting rounds; if a fixed point is reached early, execution stops early; +- `max_window_ops`: the maximum operation window that a single local search may inspect; +- `max_pattern_len`: the maximum length of a matchable rule; +- `recurse_control_flow`: whether to recursively optimize control flow bodies; +- `skip_labeled_ops`: whether to skip operations carrying a label, so that debug markers or external conventions are not broken. + +To use only the default production configuration, simply write: + +```python +config = RewriteConfig.production() +result = KnowledgeRewriter(config).run(circuit) +``` + +--- + +## Target gate set rewriting + +Template rules are used not only to "reduce the gate count", but also to rewrite a circuit to a specified target gate set. The recommended way to specify the target gate set is through the `target_basis` parameter of `compile()`: + +```python +from cqlib.circuit import Circuit +from cqlib.compile import compile + +circuit = Circuit(2) +circuit.cx(0, 1) + +result = compile(circuit, target_basis=["H", "CZ"]) +optimized = result.circuit + +print([op.instruction for op in optimized.operations]) +``` + +To call knowledge rule rewriting directly in a custom compilation flow, use the lowering configuration and pass the target instructions explicitly: + +```python +from cqlib.circuit import Circuit, Instruction, StandardGate +from cqlib.compile.transform import KnowledgeRewriter, RewriteConfig, RewriteMode + +circuit = Circuit(2) +circuit.cx(0, 1) + +basis = [ + Instruction.from_standard_gate(StandardGate.H), + Instruction.from_standard_gate(StandardGate.CZ), + Instruction.from_standard_gate(StandardGate.RZ), +] + +config = RewriteConfig( + mode=RewriteMode.lowering(), + target_instructions=basis, +) + +result = KnowledgeRewriter(config).run(circuit) +``` + +In the complete compilation workflow, target gate set translation happens after physical routing, because routing may insert new `SWAP` gates or expose new local cleanup opportunities. + +--- + +## Relation to traditional template matching + +Traditional template optimization usually starts from "given a template circuit, find an equivalent subsequence in the circuit". The current Cqlib implementation is closer to "knowledge rule rewriting": + +- templates are structured into verified rules; +- parameter relations are expressed by `require` conditions; +- rules are enabled by category, which makes it easy to distinguish optimization, decomposition and hardware-native gate translation; +- the rewriter uses a local cost model to control whether a rule is applied; +- the workflow invokes it repeatedly at multiple stages, until a fixed point or a budget limit is reached. + +Template optimization can therefore be understood as "local gate sequence rewriting based on a built-in knowledge base". The recommended entry point in this documentation is `compile()`; use `KnowledgeRewriter` when the effect of individual local rules needs to be observed. + +--- + +## Usage recommendations + +- for ordinary compilation, prefer `compile()` rather than manually chaining multiple optimization steps; +- in custom flows, prefer `KnowledgeRewriter.production()`; use the lowering mode only for target gate set translation; +- when adding a new rule, structural legality, parameter constraints and semantic equivalence must all be considered; +- before and after optimization, compare the gate count, the two-qubit gate count, the depth and, for critical circuits, matrix or state equivalence; +- if a rule may change the global phase, explicitly preserve `GPhase` or confirm that the global phase has no effect on the task. + +--- + +## Next steps + +- [Commutation and Clifford-RZ optimization](4_commutative_and_clifford.md): learn how commutation checking helps rule reordering and rotation merging. diff --git a/docs/documentation/en/1_cqlib/4_compiler/4_commutative_and_clifford.md b/docs/documentation/en/1_cqlib/4_compiler/4_commutative_and_clifford.md new file mode 100644 index 00000000..c49fdbfd --- /dev/null +++ b/docs/documentation/en/1_cqlib/4_compiler/4_commutative_and_clifford.md @@ -0,0 +1,180 @@ +# Commutation and Clifford-RZ Optimization + +Commutation analysis is used to determine whether two specific operations can exchange order. It does not necessarily reduce the gate count directly, but it creates the conditions for subsequent rule matching, rotation merging, cancellation of redundant gates and target gate set cleanup. Clifford-RZ optimization is common in circuit fragments that contain `H`, `S`, `X`, `Z`, `CX`, `CZ` together with phase gates such as `RZ`, `Phase` and `T`. + +In everyday use, calling `compile()` directly is recommended. The compiler automatically applies commutation checks, canonicalization and knowledge rule rewriting at the appropriate stages. `CommutationChecker` needs to be used directly only when writing custom analysis or debugging optimization rules. + +Neither `compile()` nor the commutation checking interfaces modify the input circuit; they return a new result object or proof object. A commutation check is only responsible for answering "whether these two operations can be exchanged safely", and is not an optimization step that directly rewrites an entire circuit. + +--- + +## Commutation checking entry points + +In Python, a commutation check takes `ValueOperation` objects as input. Each `ValueOperation` is a complete operation application, containing a gate, the qubits it acts on and parameters. + +```python +from cqlib.circuit import Parameter, Qubit, StandardGate, ValueOperation +from cqlib.compile.commutation import CommutationChecker + +lhs = ValueOperation.from_standard_gate( + StandardGate.CX, + [Qubit(0), Qubit(1)], +) +rhs = ValueOperation.from_standard_gate( + StandardGate.RZ(Parameter("theta")), + [Qubit(0)], +) + +checker = CommutationChecker.builtin() +proof = checker.check(lhs, rhs) + +if proof is not None: + print("exact:", proof.is_exact()) + print("phase:", proof.phase) +``` + +A shared functional entry point is also available: + +```python +from cqlib.compile.commutation import check_commutation + +proof = check_commutation(lhs, rhs) +``` + +The return value may be: + +- an exact commutation proof: the two operations can be exchanged exactly; +- a commutation proof with a global phase: the exchange introduces a global phase; +- `None`: the current checker cannot prove commutation, which is not equivalent to having proved non-commutation. + +This conservative semantics is important. The compiler uses a commutation conclusion only when a proof exists, so that insufficient proof coverage does not break the circuit semantics. + +--- + +## Check order + +The built-in checker attempts proofs in a fixed order: + +1. fast local facts: identity gates, global phases, gates acting on disjoint qubit sets, and identical operations; +2. algebraic proofs: Pauli axes, Pauli rotations, diagonal gates, controlled single-axis gates and some symmetric two-qubit gates; +3. rule library proofs: explicit exchange rules of the form `A; B -> B; A` extracted from the built-in knowledge rules; +4. small-scale matrix checks: within a qubit-count limit, local matrices are constructed to verify whether `AB` and `BA` are equal or differ only by a global phase. + +The default configuration enables rule library proofs and the matrix fallback check, and limits the joint support of the matrix check to 4 qubits. + +```python +from cqlib.compile.commutation import CommutationChecker, CommutationConfig + +config = CommutationConfig( + enable_rule_oracle=True, + enable_matrix_fallback=True, + max_matrix_qubits=4, +) + +checker = CommutationChecker.with_config(config) +proof = checker.check(lhs, rhs) +``` + +If speed matters more in large-scale static analysis, the matrix fallback check can be turned off; if full reliance on structural rules is preferred, rule library proofs and matrix checks can both be turned off, keeping only the basic structural facts and the algebraic proofs. + +--- + +## Common commutation relations + +The compiler recognizes several classes of common relations: + +- two gates acting on disjoint qubit sets always commute exactly; +- diagonal gates commute with each other, for example `RZ`, `Phase`, `S`, `T`, `CZ`, `RZZ`; +- rotations about the same axis commute, for example consecutive `RZ(a)` and `RZ(b)`; +- Pauli string rotations commute when the number of anticommuting positions is even; +- `CX` commutes with Z-axis operations on the control qubit; +- `CX` commutes with X-axis operations on the target qubit; +- `CZ` commutes with Z-axis operations on either endpoint; +- some two-qubit gate families such as `SWAP`, `FSIM` and symmetric Pauli rotations have additional structural commutation facts when applied to the same unordered qubit pair. + +These conclusions help the rule rewriter move mergeable gates together. For example: + +```text +RZ(a) q; S q; RZ(b) q +``` + +Since `RZ` and `S` both belong to the Z-axis phase family, this can be rearranged as: + +```text +S q; RZ(a) q; RZ(b) q +``` + +The rotation merging rule can then merge the two `RZ` gates into `RZ(a + b)`. + +--- + +## Optimization approach for Clifford-RZ fragments + +Clifford-RZ circuits usually consist of alternating discrete Clifford gates and Z-axis rotations. The optimization goal is not simply to reorder all gates, but to perform local rearrangement while preserving semantics: + +- cancel adjacent self-inverse gates, for example `H H`, `X X`, `CX CX`; +- cancel inverse gate pairs, for example `S SDG`, `T TDG`; +- merge same-axis rotations, for example `RZ(a) RZ(b) -> RZ(a + b)`; +- remove zero-angle rotations and identity gates; +- rewrite Clifford/phase combinations such as `S S` and `T T` into shorter forms; +- preserve or fold the necessary `GPhase` before and after target gate set translation. + +These optimizations are handled by the canonicalizer and the knowledge rule rewriter respectively. They are generally used indirectly through `compile()`, so there is no need to look for a separate `CliffordRzOptimization` class. + +```python +from cqlib.circuit import Circuit +from cqlib.compile import CompileMode, compile + +circuit = Circuit(1) +circuit.rz(0, 0.1) +circuit.rz(0, 0.2) +circuit.rz(0, -0.3) + +result = compile(circuit, mode=CompileMode.enhanced()) +optimized = result.circuit + +print("changed:", result.changed) +print("before:", len(circuit.operations)) +print("after:", len(optimized.operations)) +``` + +`CompileMode.enhanced()` uses higher budgets for rule search, resynthesis, native fixed-point optimization and SABRE search, and adds cleanup steps after routing and target gate set translation. For a strict `Device` target it also lets several routing candidates each complete the subsequent lowering, optimization and validation, and then selects the final result through a bounded Pareto beam. For circuits with many Clifford-RZ fragments, where new adjacent gates are easily exposed after routing, the enhanced mode usually finds additional merging or cancellation opportunities more easily. + +--- + +## Interaction with template rules + +The relationship between commutation analysis and template optimization can be understood as follows: + +1. a commutation check proves that two adjacent operations can be exchanged; +2. after the exchange, a local pattern that was previously separated becomes an adjacent pattern; +3. the knowledge rule library matches that pattern; +4. the rewriter applies cancellation, merging or normalization rules; +5. the canonicalizer cleans up parameters, global phase and representation details. + +For example, the commutation rules of the Z-axis gate family can arrange `RZ`, `S`, `T` and `Phase` into positions where merging or cancellation is easier. The commutation rules of `CX` and `CZ` can move single-qubit phase gates around a two-qubit gate, reducing redundancy in the subsequent target gate set translation. + +--- + +## Global phase + +Some exchanges or cancellations hold only up to a global phase. Cqlib represents such cases explicitly with the phase information of `Commutation` and with `GPhase`. A top-level `GPhase` is merged into the `global_phase` of the circuit during canonicalization; a phase inside a control flow body cannot be promoted to a global phase freely, so it is kept in the body representation. + +In algorithm verification, statevector comparison and gate matrix comparison, it must be made explicit whether the global phase is ignored. For sampling probabilities, the global phase is usually unobservable; for controlled sub-circuits, phase estimation and scenarios where the fragment is further wrapped as a composite gate, it should be preserved with care. + +--- + +## Usage recommendations + +- for user-level compilation, prefer `compile()` and let the workflow schedule commutation, rule rewriting and canonicalization automatically; +- in custom analysis, use `CommutationChecker.builtin()` and treat `None` as "not proved" rather than "non-commuting"; +- for batch analysis of large circuits, the matrix fallback check can be turned off to avoid the overhead of local matrix construction; +- after Clifford-RZ optimization, record the gate count, the two-qubit gate count, the circuit depth and the change in global phase; +- for critical circuits, it is recommended to use matrix equivalence or state equivalence tests to confirm that the semantics are unchanged before and after optimization. + +--- + +## Next steps + +- [Compilation and optimization](0_overview.md): review the complete `compile()` pipeline and the configuration essentials of each stage. +- [Template matching and knowledge rule optimization](3_template_optimization.md): learn how knowledge rule rewriting works together with commutation analysis. diff --git a/docs/documentation/en/1_cqlib/5_visualization/0_overview.md b/docs/documentation/en/1_cqlib/5_visualization/0_overview.md new file mode 100644 index 00000000..2c557d39 --- /dev/null +++ b/docs/documentation/en/1_cqlib/5_visualization/0_overview.md @@ -0,0 +1,108 @@ +# Visualization + +The visualization capabilities of Cqlib are used to inspect three kinds of object during quantum program development: circuit structure, measurement results and quantum states. This chapter starts from practical usage scenarios and shows how to generate figures, save results and check quantum semantics from the figures. + +Before starting, complete the [Cqlib Installation and Environment Setup](../../0_get_started/1_installation.md) and confirm that Cqlib imports correctly. + +```python +import cqlib + +print(cqlib.__file__) +``` + +If multiple Cqlib checkouts exist on the machine, confirm first that the one printed here is the implementation to be used. The visualization examples rely on the Python bindings of `cqlib.visualization` and on local figure rendering capability; they do not connect to a cloud platform or submit tasks to real quantum hardware. + +--- + +## Starting from the Bell state + +The Bell state is the most suitable example for getting started with visualization: the circuit is short, yet it contains superposition, entanglement, measurement and statistical results at the same time. + +```python +from cqlib import Circuit +from cqlib.visualization import draw_text, draw_figure + +circuit = Circuit(2) +circuit.h(0) +circuit.cx(0, 1) +circuit.measure(0) +circuit.measure(1) + +print(draw_text(circuit)) +draw_figure(circuit, output_path="assets/bell.png") +``` + +The text diagram output is as follows: + +```text + + Q0: ───H──■──M─ + │ + Q1: ──────X──M─ + +``` + +The generated PNG circuit diagram is as follows: + +![Bell state circuit](assets/bell.png) + +This diagram checks whether the circuit semantics are correct: + +- The `H` gate acts on `Q0` first, creating superposition; +- `CX` takes `Q0` as the control and `Q1` as the target, creating entanglement; +- Both measurements come after the entangling operation and do not destroy the state early. + +When the gate order, control qubits and target qubits need to be confirmed quickly, the text diagram is usually fastest. When a stable display is needed in Gitee, Markdown, reports or presentation material, PNG is more compatible; when a scalable vector graphic is needed, `output_path` can be changed to `.svg`. + +--- + +## Learning path in this chapter + +The reading order is as follows: + +1. [Debugging circuits with text diagrams](1_draw_text.md): inspect gate order, bit order, parameters and composite gates in the terminal. +2. [Generating PNG circuit diagrams](2_draw_figure.md): generate figure files for Notebooks, documentation sites and reports. +3. [Notebook and documentation integration](3_notebook_and_docs.md): save and reference visualization results in Notebooks and Markdown. +4. [Visualization strategies for complex circuits](4_visualization_practices.md): handle parameterized circuits, before-and-after mapping comparison and display of large circuits. +5. [Control flow and special circuit structures](5_control_flow_and_special.md): read dynamic control flow, non-unitary instructions and custom gate figures. +6. [Visualizing execution results](6_result_visualization.md): inspect sampling results with bar charts and probability distributions. +7. [Visualizing quantum states](7_state_visualization.md): understand states with Bloch, state city and Pauli vector figures. + +Reading in this order is recommended. The first five sections address whether a circuit is constructed as expected; the last two address how to interpret objects after execution or simulation. + +--- + +## When to draw figures + +In quantum program development, visualization is usually not the last step but a means of inspection after each key transform. The following points are suitable places to draw figures: + +- After writing a multi-qubit gate by hand, check the control qubits and target qubits; +- After constructing a parameterized ansatz, check whether each layer repeats as expected; +- After wrapping a sub-circuit as a `CircuitGate`, check the module boundary; +- After decomposing composite gates, check the underlying gate sequence; +- After adding dynamic control flow, check the branch, loop and control-transfer markers; +- After compilation or mapping, check SWAP insertion and two-qubit gate positions; +- After sampling or simulation, check whether the result distribution matches expectations. + +Visualization cannot replace matrix verification, probability verification or unit tests, but it exposes problems such as bit order, measurement position, missing parameters and excessive circuit depth very quickly. + +--- + +## How to choose a diagram + +| Development task | Recommended diagram | Points to inspect | +|---|---|---| +| Quickly check circuit structure | Text circuit diagram | Gate order, control qubits, target qubits, measurement position | +| Write a Notebook or report | PNG circuit diagram | Clarity of structure, module boundaries, bit display order | +| Read dynamic control flow or special instructions | PNG circuit diagram | Branches, loops, `barrier`, `reset`, `delay`, custom gate labels | +| Inspect sampling results | Histogram / distribution | Dominant peaks, low-probability noise terms, shot count and normalized probabilities | +| Understand single-qubit states | Bloch diagram | Direction and length of the Bloch vector | +| Understand multi-qubit states or density matrices | State city / Pauli vector | Coherence terms, Pauli expectation values and the global structure of entangled states | + +--- + +## Next steps + +- [Debugging circuits with text diagrams](1_draw_text.md): check gate order, control qubits, target qubits and measurement positions quickly in the terminal. +- [Generating PNG circuit diagrams](2_draw_figure.md): save circuits that need to go into Notebooks, Markdown or reports as image files. +- [Visualizing execution results](6_result_visualization.md): after a circuit runs or is sampled, inspect the dominant peaks, noise terms and bitstring order with result figures. diff --git a/docs/documentation/en/1_cqlib/5_visualization/1_draw_text.md b/docs/documentation/en/1_cqlib/5_visualization/1_draw_text.md new file mode 100644 index 00000000..504d56f5 --- /dev/null +++ b/docs/documentation/en/1_cqlib/5_visualization/1_draw_text.md @@ -0,0 +1,213 @@ +# Debugging circuits with text diagrams + +Text diagrams are suitable for quickly inspecting circuits in terminals, logs, Markdown and unit tests. + +--- + +## Task: inspect a Bell-state circuit + +First construct a Bell-state circuit with measurements: + +```python +from cqlib import Circuit +from cqlib.visualization import draw_text + +circuit = Circuit(2) +circuit.h(0) +circuit.cx(0, 1) +circuit.measure(0) +circuit.measure(1) + +print(draw_text(circuit)) +``` + +Output: + +```text + + Q0: ───H──■──M─ + │ + Q1: ──────X──M─ + +``` + +Three points matter when reading a text diagram: + +- Whether `H` acts on qubit `0`; +- Whether the control of `CX` is on `0` and the target is on `1`; +- Whether the measurements come after the entangling operation. + +When debugging algorithm circuits, a text diagram can be printed after each layer of structure is added, avoiding structural troubleshooting only after the whole circuit has been generated. + +Text diagrams are better suited to quick debugging. Their advantages are being lightweight and copyable, and being easy to put into issues and test failure logs; when the full structure must be shown in a report or presentation material, a PNG diagram can be generated at the same time. + +--- + +## Checking the bit display order + +Some papers, backends or frontend interfaces draw high-order bits at the top. `reverse_bits=True` only changes the display order, not the circuit semantics. + +```python +print(draw_text(circuit)) +print(draw_text(circuit, reverse_bits=True)) +``` + +The output after reversing the display order is as follows: + +```text + + Q1: ──────X──M─ + │ + Q0: ───H──■──M─ + +``` + + +--- + +## Debugging parameterized circuits + +Parameterized circuits easily become hard to read because of symbolic names, binding order or overly long expressions. A text diagram can be used to confirm the circuit topology first, and a decision can then be made on whether to display parameters. + +```python +from cqlib import Circuit, Parameter +from cqlib.visualization import draw_text + +theta = Parameter("theta") +phi = Parameter("phi") + +ansatz = Circuit(2) +ansatz.ry(0, theta) +ansatz.rz(0, phi) +ansatz.cx(0, 1) +ansatz.ry(1, theta + phi) + +print(draw_text(ansatz)) +print(draw_text(ansatz, show_params=False)) +``` + +When parameters are displayed, the text diagram keeps the symbolic expressions: + +```text + + Q0: ───RY(theta)──RZ(phi)──■────────────────── + │ + Q1: ───────────────────────X──RY(phi + theta)─ + +``` + +With parameters hidden, the diagram keeps only the gates and the circuit topology: + +```text + + Q0: ───RY──RZ──■───── + │ + Q1: ───────────X──RY─ + +``` + +This step is commonly used when debugging variational circuits: confirm the entangler connectivity first, then check the parameter table or the parameter vector passed by the optimizer separately. A diagram with hidden parameters must not be used to state specific angle values. + +--- + +## Handling long circuits + +When a circuit is deep, the default text diagram may exceed the terminal width. `line_width` controls folding. + +```python +layer = Circuit(2) +for _ in range(8): + layer.h(0) + layer.cx(0, 1) + layer.rz(1, 0.2) + +print(draw_text(layer, line_width=80)) +``` + +The output folds after exceeding the specified width: + +```text + » + Q0: ───H──■─────H─────■─────H─────■─────H─────■─────H─────■─────H─────■─────H─────■─» + │ │ │ │ │ │ │ » + Q1: ──────X──RZ(0.2)──X──RZ(0.2)──X──RZ(0.2)──X──RZ(0.2)──X──RZ(0.2)──X──RZ(0.2)──X─» + » + +« +« Q0: ──────H─────■────────── +« │ +« Q1: ───RZ(0.2)──X──RZ(0.2)─ +« +``` + + + +--- + +## Inspecting the internal structure of composite gates + +When a circuit contains composite gates wrapped by `to_gate()`, the default diagram keeps the module boundaries. To debug internal details, the display can be expanded. + +```python +from cqlib import Circuit +from cqlib.visualization import draw_text + +block = Circuit(2) +block.h(0) +block.cx(0, 1) +bell_gate = block.to_gate("Bell") + +main = Circuit(4) +main.append_circuit_gate(bell_gate, [0, 1]) +main.append_circuit_gate(bell_gate, [2, 3]) + +print(draw_text(main)) +print(draw_text(main, decompose_circuit_gates=True)) +``` + +The default display keeps the two `Bell` modules: + +```text + ┌──────┐ + Q0: ───│ │─ + │ Bell │ + Q1: ───│ │─ + └──────┘ + Q2: ───│ │─ + │ Bell │ + Q3: ───│ │─ + └──────┘ +``` + +After decomposing the composite gates, the `H` and `CX` inside the modules become visible: + +```text + + Q0: ───H──■─ + │ + Q1: ──────X─ + + Q2: ───H──■─ + │ + Q3: ──────X─ + +``` + + +--- + +## How to use text diagrams + +- Use text diagrams in issues, logs and debug output; +- For small circuits of 2 to 4 qubits, a text diagram is usually clear enough; +- Parameters can be hidden when inspecting structure and shown again when checking parameter binding; +- Keep text diagram snapshots for complex circuits and verify behavior together with numerical tests. +- If a diagram involves the bit-order conventions of a backend, a paper or other frameworks, confirm that `reverse_bits` only changes the display order and not the circuit execution semantics. + +--- + +## Next steps + +- [Generating PNG circuit diagrams](2_draw_figure.md): save small circuits whose structure has been confirmed as images better suited to documentation and reports. +- [Notebook and documentation integration](3_notebook_and_docs.md): keep the code that generates figures and the Markdown references in the same set of experiment records. +- [Visualization strategies for complex circuits](4_visualization_practices.md): use segmentation, decomposition and comparison figures to locate structural problems as circuits get deeper or modules multiply. diff --git a/docs/documentation/en/1_cqlib/5_visualization/2_draw_figure.md b/docs/documentation/en/1_cqlib/5_visualization/2_draw_figure.md new file mode 100644 index 00000000..b60576ac --- /dev/null +++ b/docs/documentation/en/1_cqlib/5_visualization/2_draw_figure.md @@ -0,0 +1,168 @@ +# Generating PNG circuit diagrams + +PNG circuit diagrams are suitable for Gitee, Markdown, Notebooks, web documentation, reports and presentation material. Compared with text diagrams, figure-based circuits are better suited to showing more complex structures; when a vector graphic is needed, the export suffix can be changed to `.svg`. + +--- + +## Task: save a Bell-state circuit diagram + +```python +from cqlib import Circuit +from cqlib.visualization import draw_figure + +circuit = Circuit(2) +circuit.h(0) +circuit.cx(0, 1) +circuit.measure(0) +circuit.measure(1) + +svg = draw_figure(circuit, output_path="assets/bell.png") +print(svg[:80]) +``` + +The generated result is as follows: + +![Bell state circuit](assets/bell.png) + +`draw_figure` returns an SVG string and also writes a file when `output_path` is passed. When `output_path` uses the `.png` suffix, a PNG file is written; the generated `assets/bell.png` can be placed directly in a Markdown, HTML, PPT or paper asset directory. + +```markdown +![Bell state circuit](assets/bell.png) +``` + +--- + +## Inline display in a Notebook + +In a Notebook, `draw_figure(circuit)` can be used as the last line of a cell for direct display. + +```python +from cqlib.visualization import draw_figure + +draw_figure(circuit) +``` + +When display must be controlled explicitly, it can be delegated to IPython: + +```python +from IPython.display import SVG, display + +display(SVG(draw_figure(circuit))) +``` + +If the same Notebook serves both exploration and the keeping of experiment records, passing `output_path` explicitly is recommended. The diagram seen in the Notebook and the PNG referenced in Markdown then come from the same piece of code. + +--- + +## Folding long circuits + +When a circuit is deep, laying it out flat makes the figure too wide. `fold` controls figure folding. + +```python +deep = Circuit(2) +for _ in range(12): + deep.h(0) + deep.cx(0, 1) + deep.rz(1, 0.1) + +draw_figure(deep, fold=20, output_path="assets/deep_folded.png") +``` + +The folded circuit diagram is as follows: + +![Folded deep circuit](assets/deep_folded.png) + +Folding only changes the canvas layout, not the circuit execution order. When reading a folded diagram, follow the connection direction from the end of each segment to the start of the next. + +--- + +## Controlling information density in figures + +When a parameterized circuit shows all expressions in the figure, readability may suffer. + +```python +from cqlib import Circuit, Parameter +from cqlib.visualization import draw_figure + +theta = Parameter("theta") + +ansatz = Circuit(2) +ansatz.ry(0, theta) +ansatz.cx(0, 1) +ansatz.ry(1, 2 * theta) + +draw_figure(ansatz, output_path="assets/ansatz_with_params.png") +draw_figure(ansatz, show_params=False, output_path="assets/ansatz_structure.png") +``` + +The diagram with parameters kept: + +![Parameterized ansatz with parameters](assets/ansatz_with_params.png) + +The structure diagram with parameters hidden: + +![Parameterized ansatz structure](assets/ansatz_structure.png) + +--- + +## Showing the initial state and bit order + + +```python +draw_figure(circuit, initial_state=True, output_path="assets/bell_initial_state.png") +``` + +The diagram with initial-state markers is as follows: + +![Bell circuit with initial states](assets/bell_initial_state.png) + +When alignment with the display conventions of a backend, a paper figure or other frameworks is needed, the display order can be reversed: + +```python +draw_figure(circuit, reverse_bits=True, output_path="assets/bell_reverse_bits.png") +``` + +The diagram after reversing the display order is as follows: + +![Bell circuit with reversed bits](assets/bell_reverse_bits.png) + +These two options only affect figure display and do not change the gate order, bit indices or measurement semantics of `Circuit`. + +--- + +## Decomposing composite gates for troubleshooting + +Keeping composite gates as module boundaries is suitable for explaining algorithm structure; decomposing composite gates is suitable for troubleshooting the underlying gate sequence. + +```python +block = Circuit(2) +block.h(0) +block.cx(0, 1) +bell_gate = block.to_gate("Bell") + +main = Circuit(2) +main.append_circuit_gate(bell_gate, [0, 1]) + +draw_figure(main, output_path="assets/bell_gate.png") +draw_figure( + main, + decompose_circuit_gates=True, + output_path="assets/bell_gate_decomposed.png", +) +``` + +The diagram with composite gate boundaries kept: + +![Bell circuit gate](assets/bell_gate.png) + +The diagram after decomposing composite gates: + +![Bell circuit gate decomposed](assets/bell_gate_decomposed.png) + +--- + +## Next steps + +- [Notebook and documentation integration](3_notebook_and_docs.md): fix the figure generation path, the asset directory and the Markdown references. +- [Visualization strategies for complex circuits](4_visualization_practices.md): show larger circuits with staged figures, module boundaries and before-and-after mapping comparison. +- [Control flow and special circuit structures](5_control_flow_and_special.md): read circuit diagrams that contain branches, loops, `reset`, `delay` and custom gates. diff --git a/docs/documentation/en/1_cqlib/5_visualization/3_notebook_and_docs.md b/docs/documentation/en/1_cqlib/5_visualization/3_notebook_and_docs.md new file mode 100644 index 00000000..1ac7c5a4 --- /dev/null +++ b/docs/documentation/en/1_cqlib/5_visualization/3_notebook_and_docs.md @@ -0,0 +1,144 @@ +# Notebook and documentation integration + +This page shows how to display a PNG generated by Cqlib in a Notebook and save the same figure into an asset directory that Markdown can reference. + +--- + +## Task: display and save the same figure in a Notebook + +```python +from pathlib import Path + +from cqlib import Circuit +from cqlib.visualization import draw_figure + +assets = Path("assets") +assets.mkdir(exist_ok=True) + +circuit = Circuit(2) +circuit.h(0) +circuit.cx(0, 1) +circuit.measure(0) +circuit.measure(1) + +draw_figure(circuit, output_path=str(assets / "bell.png")) +``` + +In a Notebook, the last line above displays the figure inline and saves the PNG to `assets/bell.png` at the same time. This approach suits experiment records, reports and paper supplementary material. + +The generated result is as follows: + +![Bell state circuit](assets/bell.png) + +--- + +## Referencing in Markdown + +After saving the PNG, reference it directly in Markdown: + +```markdown +![Bell state circuit](assets/bell.png) +``` + +After referencing the image, the check points of this circuit diagram can be recorded below the figure: + +```markdown +Figure 1 shows the Bell state preparation circuit: first apply an H gate to q0, then apply a CX gate with q0 as the control and q1 as the target. +``` + +A quantum circuit diagram usually needs to state the control qubits, target qubits, measurement positions and bit order clearly. Otherwise the figure only shows the circuit shape and cannot convey a verification conclusion. + +--- + +## Organizing documentation asset directories + +Keeping a separate asset directory for each group of experiments or each report prevents different figures from overwriting each other: + +```text +docs/ + visualization/ + tutorial.md + assets/ + bell.png + ansatz_structure.png + mapped_before.png + mapped_after.png +``` + +File names should describe the task rather than use temporary names such as `figure1.png` or `test.png`. + +--- + +## Regenerating figures automatically + +When a group of experiments contains several figures, the generation logic can be consolidated in a single code block or script. + +```python +from pathlib import Path + +from cqlib import Circuit +from cqlib.visualization import draw_figure + +assets = Path("assets") +assets.mkdir(exist_ok=True) + +bell = Circuit(2) +bell.h(0) +bell.cx(0, 1) + +draw_figure(bell, output_path=str(assets / "bell.png")) +draw_figure(bell, reverse_bits=True, output_path=str(assets / "bell_reverse_bits.png")) +``` + +The two generated figures can be referenced separately in Markdown: + +![Bell state circuit](assets/bell.png) + +![Bell state circuit with reversed bits](assets/bell_reverse_bits.png) + +When the circuit construction code changes, rerunning this section updates the illustrations accordingly. If a generated figure does not match expectations, check the circuit construction and the visualization parameters first rather than editing the image content by hand. + +--- + +## Saving visualization results + +- For small circuits, both a text diagram and a PNG can be kept; +- For large circuits, save the key stages in preference to every intermediate circuit; +- Save the original PNG rather than keeping only a screenshot; +- Use specific task names for file names, such as `bell_reverse_bits.png` and `mapped_after.png`; +- When a figure involves bit order, measurement order or backend mapping, the corresponding convention must be written clearly in the experiment record. + +--- + +## Common issues + +When a figure is not displayed automatically in a Notebook, the returned SVG string can be displayed explicitly with IPython: + +```python +from IPython.display import SVG, display +from cqlib.visualization import draw_figure + +display(SVG(draw_figure(circuit))) +``` + +When a script only saves a file without displaying the figure, check whether `output_path` writes to the expected directory and confirm that the generated file can be opened by a local image viewer. + +--- + +## Checks after updating figures + +After modifying the circuit or the drawing code, the following order of checks can be used: + +1. Rerun the code block or script that generates the PNG; +2. Confirm that the file names referenced in Markdown have not changed; +3. Open the generated PNG and check whether the control qubits, target qubits, measurement positions and bit order still match expectations; +4. If sampling results or state figures are included, confirm whether the data source is constructed results, local simulation or hardware results; +5. Check that file names contain no temporary names and that code blocks contain no absolute paths or local user directories. + +--- + +## Next steps + +- [Generating PNG circuit diagrams](2_draw_figure.md): return to the drawing parameters and adjust folding, parameter display, initial-state markers and bit display order. +- [Visualization strategies for complex circuits](4_visualization_practices.md): organize a maintainable set of figures for multi-stage algorithms, mapping results and large circuits. +- [Visualizing execution results](6_result_visualization.md): bring sampling result figures into the same Notebook and Markdown asset directory. diff --git a/docs/documentation/en/1_cqlib/5_visualization/4_visualization_practices.md b/docs/documentation/en/1_cqlib/5_visualization/4_visualization_practices.md new file mode 100644 index 00000000..4a340f94 --- /dev/null +++ b/docs/documentation/en/1_cqlib/5_visualization/4_visualization_practices.md @@ -0,0 +1,215 @@ +# Visualization strategies for complex circuits + +When circuit size grows, drawing the whole circuit at once may be hard to read. A more practical approach is to inspect the key structures first, then use a small number of figures to compare scale-up, module decomposition or the changes before and after compilation and mapping. + +--- + +## Task: showing a QAOA-style circuit in stages + +First split the circuit into state preparation, the problem layer and the mixer layer: + +```python +from cqlib import Circuit +from cqlib.visualization import draw_figure, draw_text + +prepare = Circuit(3) +for q in range(3): + prepare.h(q) + +cost = Circuit(3) +cost.rzz(0, 1, 0.3) +cost.rzz(1, 2, 0.3) + +mixer = Circuit(3) +for q in range(3): + mixer.rx(q, 0.7) +``` + +Drawing each segment separately makes it easier to check that state preparation, the problem layer and the mixer layer are each correct. + +```python +print(draw_text(prepare)) +print(draw_text(cost)) +print(draw_text(mixer)) + +draw_figure(prepare, output_path="assets/qaoa_prepare.png") +draw_figure(cost, output_path="assets/qaoa_cost.png") +draw_figure(mixer, output_path="assets/qaoa_mixer.png") +``` + +State preparation layer: + +![QAOA prepare layer](assets/qaoa_prepare.png) + +Problem layer: + +![QAOA cost layer](assets/qaoa_cost.png) + +Mixer layer: + +![QAOA mixer layer](assets/qaoa_mixer.png) + +A complete QAOA circuit can be composed of these modules repeated layer by layer. Checking the single-layer structure before extending the number of layers finds gate-order errors or errors in the qubits a gate acts on earlier. + +--- + +## Showing module boundaries first, then decomposition details + +For reusable modules such as ansatz, oracle and feature map, the `CircuitGate` boundary is usually kept first. + +```python +block = Circuit(2) +block.ry(0, 0.2) +block.cx(0, 1) +block.ry(1, 0.4) +block_gate = block.to_gate("Layer") + +ansatz = Circuit(4) +ansatz.append_circuit_gate(block_gate, [0, 1]) +ansatz.append_circuit_gate(block_gate, [2, 3]) + +draw_figure(ansatz, output_path="assets/ansatz_modules.png") +draw_figure( + ansatz, + decompose_circuit_gates=True, + output_path="assets/ansatz_decomposed.png", +) +``` + +The diagram with module boundaries kept: + +![Ansatz modules](assets/ansatz_modules.png) + +The diagram after decomposing the modules: + +![Ansatz decomposed](assets/ansatz_decomposed.png) + +With module boundaries kept, the hierarchical structure of the ansatz is easier to see; after decomposing composite gates, checking whether the underlying gate sequence matches expectations is easier. + +--- + +## Before-and-after mapping comparison + +In compilation and routing scenarios, the focus of the diagram is not the matrix of each gate but whether SWAPs are inserted and whether two-qubit gates satisfy the topology constraints. + +```python +from cqlib.compile import compile +from cqlib.device import Device + +original = Circuit(3) +original.h(0) +original.cx(0, 2) +original.cx(1, 2) + +result = compile( + original, + device=Device.line("line-3", 3), + target_basis=["H", "CX"], + seed=42, +) +mapped = result.circuit + +print("before") +print(draw_text(original, line_width=100)) + +print("after") +print(draw_text(mapped, line_width=100)) + +draw_figure(original, output_path="assets/mapped_before.png") +draw_figure(mapped, output_path="assets/mapped_after.png") +``` + +Before mapping: + +```text + + Q0: ───H──■──── + │ + Q1: ──────┼──■─ + │ │ + Q2: ──────X──X─ + +``` + +![Circuit before routing](assets/mapped_before.png) + +After mapping: + +```text + + Q0: ───H──■──── + │ + Q1: ──────X──X─ + │ + Q2: ─────────■─ + +``` + +![Circuit after routing](assets/mapped_after.png) + +When comparing the before-and-after diagrams, check whether SWAPs were added, whether the circuit depth increased and whether the relationship between logical qubits and physical qubits needs to be recorded. + +If the mapping result depends on a random seed, heuristic search or device topology, the seed must be fixed and the topology used by the example must be recorded. Calibration, available qubits and gate error rates of real hardware devices change over time, so a single mapping result cannot be treated as a permanent guarantee. + +--- + +## Showing only key slices of large circuits + +For multi-layer ansatz or automatically generated circuits, the complete diagram is usually too wide. The following combination can be used to inspect key slices: + +```python +print("num operations:", len(ansatz)) +print(draw_text(ansatz, show_params=False, line_width=100)) + +draw_figure( + ansatz, + show_params=False, + fold=80, + output_path="assets/ansatz_overview.png", +) +``` + +The overview diagram is as follows: + +![Ansatz overview](assets/ansatz_overview.png) + +Also record a small table: + +| Content | Presentation | +|---|---| +| Single-layer structure | PNG circuit diagram | +| Layer count, gate count, depth | Table | +| Differences before and after compilation | Before-and-after comparison diagram | +| Parameter values | Separate table or formula | + +This approach makes problems easier to locate than laying out dozens of circuit layers in full, and is better suited to experiment records or technical reports. + +--- + +## Visualization cannot replace verification + +Visualization is good at finding structural problems but cannot prove that a quantum program is correct. Key circuits should still be accompanied by numerical checks: + +```python +from cqlib import Circuit +from cqlib.qis import Statevector + +check = Circuit(2) +check.h(0) +check.cx(0, 1) + +state = Statevector.from_circuit(check) +print(state.probabilities()) +``` + +For workflows that involve random sampling, a fixed seed or tolerance-based comparison is also needed. For hardware execution workflows, results from a real device cannot be required to match an ideal simulation exactly; the effects of shot noise, topology mapping, gate errors and measurement errors should be described in the documentation. + +The recommended combination is therefore: use text diagrams for quick checks on small circuits, save PNGs for key structures, and verify core conclusions with probabilities, matrices or test assertions. Visualization helps inspect structure and is not responsible for proving an algorithm correct on its own. + +--- + +## Next steps + +- [Control flow and special circuit structures](5_control_flow_and_special.md): apply the same diagram-reading method to dynamic control flow, non-unitary instructions and custom gates. +- [Visualizing execution results](6_result_visualization.md): after the circuit structure is confirmed, inspect sampling results with histograms and distributions. +- [Visualizing quantum states](7_state_visualization.md): when the state itself must be explained, supplement the structure diagram with Bloch, state city and Pauli vector figures. diff --git a/docs/documentation/en/1_cqlib/5_visualization/5_control_flow_and_special.md b/docs/documentation/en/1_cqlib/5_visualization/5_control_flow_and_special.md new file mode 100644 index 00000000..930e755a --- /dev/null +++ b/docs/documentation/en/1_cqlib/5_visualization/5_control_flow_and_special.md @@ -0,0 +1,184 @@ +# Control flow and special circuit structures + +Besides basic gates and ordinary parameterized circuits, Cqlib circuit diagrams can also display dynamic control flow, non-unitary instructions and custom gates. For such circuits, reading the diagram is not only about gate order: it also covers which qubit lanes a control-flow block spans, where a branch ends, whether a loop contains `break` / `continue`, and whether special gate labels preserve the algorithmic meaning. + +The following examples only generate circuit diagrams and do not execute tasks on real hardware. + +--- + +## Task: reading a dynamic control-flow diagram + +First construct a dynamic circuit containing `if/else`, `while`, `for`, `switch`, `break` and `continue`. To keep the control-flow labels in the diagram short and stable, the diagram-reading example here uses literal conditions: + +```python +from cqlib import Circuit +from cqlib.circuit import ClassicalExpr, ClassicalType +from cqlib.visualization import draw_figure + +dynamic = Circuit(2) +dynamic.h(0) + +def then_body(body): + body.x(1) + +def else_body(body): + body.z(1) + +dynamic.if_else( + ClassicalExpr.bool_literal(True), + then_body, + else_body, +) + +def continue_body(body): + body.measure(1) + body.continue_loop() + +dynamic.while_( + ClassicalExpr.bool_literal(True), + continue_body, +) + +def break_body(body): + body.h(0) + body.break_loop() + +dynamic.while_( + ClassicalExpr.bool_literal(False), + break_body, +) + +loop_var = dynamic.var(ClassicalType.uint(3)) + +def for_body(body, index): + body.rx(0, 0.1) + +dynamic.for_uint( + loop_var, + ClassicalExpr.uint_literal(3, 0), + ClassicalExpr.uint_literal(3, 3), + ClassicalExpr.uint_literal(3, 1), + for_body, +) + +def case_zero(body): + body.x(0) + +def case_one(body): + body.z(1) + +def switch_default(body): + body.h(0) + body.cx(0, 1) + +def build_switch(builder): + builder.value(0, case_zero) + builder.value(1, case_one) + builder.default(switch_default) + +dynamic.switch(ClassicalExpr.uint_literal(2, 1), build_switch) + +draw_figure(dynamic, fold=10, output_path="assets/dynamic_control_flow.png") +``` + +The generated control-flow circuit diagram is as follows: + +![Dynamic control-flow circuit](assets/dynamic_control_flow.png) + +Reading this diagram, each control-flow block can be checked in turn: + +- The `If`, `Else` and `End` markers give the entry, alternative branch and end position of a conditional branch; +- The first `While` block uses a `true` condition, and the `Continue` at the end of the loop body means the next loop check is entered; +- The second `While` block contains `Break`, meaning the loop body can exit the innermost loop directly; +- The `For` block shows the loop range, making the iteration bounds easy to confirm; +- The `Switch` block shows the selector expression and lists the case and default branches; +- The vertical extent of a control-flow block corresponds to the qubit lanes actually used by the body; lanes not involved in that body must not be misread as being operated on by the control block. + +Conditions in real dynamic circuits usually come from mid-circuit measurement or classical storage, as written below: + +```python +from cqlib import Circuit +from cqlib.circuit import ClassicalExpr, ClassicalType, Instruction, StandardGate +from cqlib.compile import compile +from cqlib.device import Device + +dynamic = Circuit(2) +dynamic.h(0) +measured = dynamic.measure(0) +condition = measured.expr().to_bool() + +def then_body(body): + body.x(1) + +def else_body(body): + body.z(1) + +dynamic.if_else(condition, then_body, else_body) + +keep_running = dynamic.var(ClassicalType.bool()) +dynamic.store(keep_running, ClassicalExpr.bool_literal(True)) + +def loop_body(body): + loop_measurement = body.measure(1) + body.store(keep_running, loop_measurement.expr().to_bool()) + body.continue_loop() + +dynamic.while_(keep_running.expr(), loop_body) + +device = Device.line("line-2", 2) +device.native_gates = [ + Instruction.from_standard_gate(StandardGate.H), + Instruction.from_standard_gate(StandardGate.X), + Instruction.from_standard_gate(StandardGate.Z), +] +compiled = compile(dynamic, device=device, seed=42) +``` + +Layout and routing treat a control-flow body as a structured sub-circuit and recursively process the gates and control-transfer markers in the body. The circuit diagram only expresses structure, branch positions and control-flow boundaries; it does not express which path a single runtime execution actually takes, nor does it estimate branch probabilities or loop counts. + +--- + +## Task: reading special instructions and non-basic gates + +The following circuit contains `barrier`, `delay`, `reset`, `fSim`, a multi-controlled gate and a custom `UnitaryGate` at the same time: + +```python +from cqlib import Circuit +from cqlib.circuit import MCGate, StandardGate, UnitaryGate +from cqlib.visualization import draw_figure + +special = Circuit(4) +special.h(0) +special.barrier([0, 1, 2, 3]) +special.delay(0, 40.0) +special.fsim(1, 2, 0.21, -0.44) +special.reset(3) + +special.append_mc_gate(MCGate(2, StandardGate.X), [0, 1, 2]) +special.append_unitary_gate(UnitaryGate("Oracle", 2), [2, 3]) + +draw_figure(special, output_path="assets/special_directives_and_gates.png") +``` + +The generated circuit diagram is as follows: + +![Special directives and gates](assets/special_directives_and_gates.png) + +This diagram helps check several common issues: + +- `barrier` is a vertical line that preserves segmentation or scheduling boundaries and does not represent a quantum gate; +- `delay` shows the waiting duration, which suits checking hardware timing or idle segments; +- `reset` is shown as a re-preparation to `|0>`; it is a non-unitary instruction; +- `fSim` is a two-qubit parameterized gate and spans the two qubits it acts on in the diagram; +- Multi-controlled gates are drawn in the order of control qubits first and target qubits after; +- A custom `UnitaryGate` displays the label supplied for it in preference, such as `Oracle` here. + +If a custom gate or composite gate label is too long, the figure can still show the structure, but whether the label expresses the meaning of the algorithm module well enough should be confirmed when reading. For scenarios that require troubleshooting at the level of the underlying gate sequence, refer back to the `decompose_circuit_gates=True` example in [Generating PNG circuit diagrams](2_draw_figure.md). + +--- + +## Next steps + +- [Generating PNG circuit diagrams](2_draw_figure.md): return to the basic drawing options and adjust folding, parameter display, initial-state markers and composite gate decomposition. +- [Visualization strategies for complex circuits](4_visualization_practices.md): when control flow or special gates are placed into a larger algorithm circuit, use segmentation and comparison figures to keep it readable. +- [Visualizing execution results](6_result_visualization.md): after execution or sampling, inspect the bitstring distribution produced by a dynamic circuit with result figures. diff --git a/docs/documentation/en/1_cqlib/5_visualization/6_result_visualization.md b/docs/documentation/en/1_cqlib/5_visualization/6_result_visualization.md new file mode 100644 index 00000000..ccec3412 --- /dev/null +++ b/docs/documentation/en/1_cqlib/5_visualization/6_result_visualization.md @@ -0,0 +1,190 @@ +# Visualizing execution results + +A constructed `ExecutionResult` is used below to show how to plot sampling counts and normalized probability distributions. + +A locally constructed result object is used here; no cloud platform is connected and no task is submitted to real hardware. + +--- + +## Task: inspecting Bell-state sampling results + +After real execution or simulation sampling, a mapping from bitstrings to counts is usually obtained. Here the result object is constructed directly from counts so that the focus stays on how to read the result figures. + +```python +from cqlib.device import ExecutionResult +from cqlib.visualization import plot_distribution, plot_histogram + +counts = {"00": 512, "11": 488} + +result = ExecutionResult.from_counts( + "bell-demo", + [0, 1], + sum(counts.values()), + 2, + counts, +) +``` + +`counts` means that only `00` and `11` were observed in 1000 shots. The bitstrings are written directly here according to the display convention of the result object; if the result comes from hardware or another SDK, the mapping from measurement bits to bitstrings must be confirmed first. For an ideal Bell state, `00` and `11` should be the dominant peaks, and `01` and `10` should not appear with high probability. + +--- + +## Plotting the raw counts bar chart + +```python +plot_histogram( + result, + title="Bell-state counts", + output_path="assets/bell_counts.png", +) +``` + +The generated counts figure is as follows: + +![Bell-state counts](assets/bell_counts.png) + +The counts figure is suitable for showing real shot counts. When comparing the raw sampling volume across different shot settings, hardware tasks or before and after error mitigation, prefer the histogram. When reading the figure, look at the dominant peaks first and then check whether bitstrings that should not appear are present. + +--- + +## Plotting the probability distribution + +```python +plot_distribution( + result, + title="Bell-state probabilities", + output_path="assets/bell_distribution.png", +) +``` + +The generated probability distribution is as follows: + +![Bell-state probabilities](assets/bell_distribution.png) + +The probability distribution normalizes the counts and is better suited to comparison with theoretical probabilities. For a Bell state, the ideal distribution should be close to: + +```text +P(00) = 0.5 +P(11) = 0.5 +P(01) = 0.0 +P(10) = 0.0 +``` + +When a small number of `01` or `10` results appear in an experiment, the circuit should not immediately be judged faulty. The judgment should combine the shot count, noise model, measurement error and backend information. + +--- + +## Keeping dominant peaks and merging the long tail + +When only the most frequent items are to be kept, the number displayed can be limited. The remaining items are merged, which makes the dominant peaks easier to see. + +```python +noisy_counts = { + "00": 470, + "11": 450, + "01": 45, + "10": 35, +} + +noisy_result = ExecutionResult.from_counts( + "bell-noisy-demo", + [0, 1], + sum(noisy_counts.values()), + 2, + noisy_counts, +) + +plot_distribution( + noisy_result, + number_to_keep=2, + sort="desc", + title="Dominant outcomes", + output_path="assets/bell_dominant_outcomes.png", +) +``` + +The figure after keeping only the dominant peaks is as follows: + +![Dominant outcomes](assets/bell_dominant_outcomes.png) + +When using this kind of figure, confirm what the merged results are and whether this affects the conclusion. `number_to_keep` is suitable for reporting the dominant-peak structure but not for hiding error terms; if low-probability items are themselves the focus of the analysis, show the full distribution or list the merged counts separately. + +--- + +## Marking a target outcome + +When debugging search algorithms or classification tasks, a particular target bitstring often needs to be emphasized. `target_string` can mark the target item and sort by Hamming distance. + +```python +search_counts = { + "100": 420, + "101": 260, + "110": 180, + "000": 90, + "011": 50, +} + +search_result = ExecutionResult.from_counts( + "search-demo", + [0, 1, 2], + sum(search_counts.values()), + 3, + search_counts, +) + +plot_histogram( + search_result, + sort="hamming", + target_string="100", + title="Counts ordered by distance to target", + output_path="assets/search_hamming_counts.png", +) +``` + +The figure sorted by distance to the target is as follows: + +![Counts ordered by Hamming distance](assets/search_hamming_counts.png) + +This kind of figure is suitable for checking whether results concentrate near the target. The definition of the target bitstring and the measurement bit order must be clarified before use. + +--- + +## Generating result figures for reports + +When a result figure is to be put into a report or presentation material, the canvas size, colors, legend and bar label display can all be controlled. The same set of Bell-state counts is still used below; only the figure presentation is adjusted: + +```python +plot_histogram( + result, + figsize=(4.8, 3.2), + color=["#0f766e"], + legend=["simulated counts"], + bar_labels=False, + title="Bell-state counts for report", + output_path="assets/bell_report_counts.png", +) +``` + +The generated report counts figure is as follows: + +![Bell-state report counts](assets/bell_report_counts.png) + +These settings suit material with many figures and limited layout space. With `bar_labels` disabled, the figure is more concise; if specific counts need to be reviewed item by item, keep the raw result data as well or use the default figure with labels. + +--- + +## Key points for checking result figures + +- Confirm whether the results come from simulation, constructed data or real hardware; +- Record the shot count rather than only looking at normalized probabilities; +- Keep error and noise explanations for hardware results rather than attributing deviations simply to circuit errors; +- When comparing with a theoretical distribution, confirm the bitstring order and the measurement mapping first; +- For a formal report, both the PNG figure and the raw counts data can be saved. + +--- + +## Next steps + +- [Visualizing quantum states](7_state_visualization.md): when the simulated state itself must be explained, supplement the sampling results with Bloch, state city and Pauli vector figures. +- [Notebook and documentation integration](3_notebook_and_docs.md): save the result figures, the raw counts and the image references together in the experiment records. +- [Visualization strategies for complex circuits](4_visualization_practices.md): check result figures and circuit diagrams side by side to confirm whether a structural change explains a distribution change. diff --git a/docs/documentation/en/1_cqlib/5_visualization/7_state_visualization.md b/docs/documentation/en/1_cqlib/5_visualization/7_state_visualization.md new file mode 100644 index 00000000..6122d0a6 --- /dev/null +++ b/docs/documentation/en/1_cqlib/5_visualization/7_state_visualization.md @@ -0,0 +1,288 @@ +# Visualizing quantum states + +State figures are used to understand quantum states themselves rather than measurement sampling results. Cqlib currently supports drawing a Bloch multivector, state city and Pauli vector from a `Statevector` or `DensityMatrix`, and also supports drawing a single Bloch vector directly. + +The following examples are all completed in local simulation and do not involve real hardware. + +--- + +## Task: observing a single-qubit superposition state + +First construct a `|+>` state: + +```python +from cqlib.qis import Statevector +from cqlib.visualization import ( + plot_bloch_multivector, + plot_bloch_vector, + plot_state_city, + plot_state_paulivec, +) + +state = Statevector(1) +state.apply_h(0) +``` + +The `|+>` state lies along the positive X axis on the Bloch sphere, so a Bloch figure can be used to quickly check whether state preparation matches expectations. The state object here comes from a local `Statevector`, not from sampling counts. + +```python +plot_bloch_multivector( + state, + title="|+> state", + output_path="assets/plus_bloch.png", +) +``` + +The generated Bloch figure is as follows: + +![Bloch multivector for plus state](assets/plus_bloch.png) + +To draw a manually supplied Bloch vector, a three-dimensional vector can be passed directly: + +```python +plot_bloch_vector( + [1.0, 0.0, 0.0], + title="Bloch vector along +X", + output_path="assets/bloch_x.png", +) +``` + +The Bloch figure corresponding to the manual vector is as follows: + +![Bloch vector along X](assets/bloch_x.png) + +Passing a three-dimensional vector manually is suitable for explaining Bloch sphere directions; when drawing from a circuit or a state object, `plot_bloch_multivector` should be preferred, to avoid convention errors introduced by manual vector computation. + +--- + +## Inspecting the density matrix structure + +`plot_state_city` shows the real and imaginary parts of the density matrix. It is suitable for checking whether coherence terms exist in the state and whether the mixed-state or pure-state structure matches expectations. + +```python +plot_state_city( + state, + title="State city for |+>", + output_path="assets/plus_state_city.png", +) +``` + +The generated state city figure is as follows: + +![State city for plus state](assets/plus_state_city.png) + +For `|+>`, the off-diagonal elements of the density matrix are non-zero, indicating coherence between `|0>` and `|1>`. + +--- + +## Inspecting the Pauli expansion + +The Pauli vector shows the expectation values of the state in the Pauli basis and suits connection to VQE, QAOA, error diagnosis and observable analysis. + +```python +plot_state_paulivec( + state, + title="Pauli vector for |+>", + output_path="assets/plus_paulivec.png", +) +``` + +The generated Pauli vector figure is as follows: + +![Pauli vector for plus state](assets/plus_paulivec.png) + +For the `|+>` state, the expectation value along `X` should be the dominant term. This kind of figure explains the physical meaning better than looking at complex amplitudes directly. + +--- + +## Statevector and DensityMatrix share the same set of state figures + +The same pure state can also be written as a density matrix. The state visualization interfaces of Cqlib accept `Statevector` and `DensityMatrix`, so the same drawing logic can compare an ideal state and a noisy state. + +```python +from cqlib.qis import DensityMatrix + +density = DensityMatrix(1) +density.apply_h(0) + +plot_state_city(density, output_path="assets/plus_density_city.png") +plot_state_paulivec(density, output_path="assets/plus_density_paulivec.png") +``` + +The figures generated from density matrix input are as follows: + +![Density-matrix state city](assets/plus_density_city.png) + +![Density-matrix Pauli vector](assets/plus_density_paulivec.png) + +When using state figures, whether the input is a statevector or a density matrix must be clarified first. The former usually represents an ideal pure state, while the latter can express mixed states and states after noise. + +A simple mixed state is constructed below to show how the state city exposes the diagonal structure: + +```python +mixed = DensityMatrix.from_density_matrix( + 1, + [ + 0.7 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.3 + 0.0j, + ], +) + +plot_state_city( + mixed, + title="Mixed one-qubit state", + output_path="assets/mixed_state_city.png", +) +``` + +The state city of the mixed state is as follows: + +![Mixed one-qubit state city](assets/mixed_state_city.png) + +This example has no off-diagonal coherence terms, so the main information in the figure is concentrated in the diagonal elements. Comparing it with the state city of `|+>` shows the difference between a coherent state and a classical probability mixture directly. + +--- + +## How to read multi-qubit states + +For multi-qubit states, the Bloch multivector draws a reduced Bloch vector for each qubit. Taking the Bell state as an example: + +```python +from cqlib import Circuit +from cqlib.qis import Statevector +from cqlib.visualization import plot_bloch_multivector, plot_state_city + +bell = Circuit(2) +bell.h(0) +bell.cx(0, 1) + +bell_state = Statevector.from_circuit(bell) + +plot_bloch_multivector(bell_state, output_path="assets/bell_bloch_multivector.png") +plot_state_city(bell_state, output_path="assets/bell_state_city.png") +``` + +The reduced Bloch figure of the Bell state is as follows: + +![Bell-state Bloch multivector](assets/bell_bloch_multivector.png) + +The state city figure of the Bell state is as follows: + +![Bell-state state city](assets/bell_state_city.png) + +For a maximally entangled state, the reduced state of a single qubit may look close to a mixed state. In this case the global state must not be judged as carrying no information from a single Bloch sphere alone; the state city, probability distribution or entanglement measures should be analyzed together. + +Read only from the local Bloch vector of each qubit, the Bell state appears to have no direction; the state city, however, still shows the correlation structure in the global density matrix. Multi-qubit states therefore usually need several kinds of state figure together for a judgment. + +--- + +## Checking the multi-qubit label order + +The horizontal-axis labels of a multi-qubit state city and Pauli vector depend on the bit order. When alignment with the display conventions of a paper, a hardware backend or other SDKs is needed, `reverse_bits=True` can generate a comparison figure. + +An asymmetric two-qubit diagonal density matrix is constructed below, so that `01` and `10` have different weights: + +```python +asymmetric = DensityMatrix.from_density_matrix( + 2, + [ + 0.05 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, + 0.0 + 0.0j, 0.15 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, + 0.0 + 0.0j, 0.0 + 0.0j, 0.75 + 0.0j, 0.0 + 0.0j, + 0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.05 + 0.0j, + ], +) + +plot_state_city( + asymmetric, + title="Asymmetric basis weights", + output_path="assets/asymmetric_state_city.png", +) +plot_state_city( + asymmetric, + reverse_bits=True, + title="Asymmetric basis weights, reversed bits", + output_path="assets/asymmetric_state_city_reverse_bits.png", +) +``` + +State city with the default display order: + +![Asymmetric state city](assets/asymmetric_state_city.png) + +State city after reversing the display order: + +![Asymmetric state city with reversed bits](assets/asymmetric_state_city_reverse_bits.png) + +`reverse_bits=True` does not change the state itself, only the display order of the basis labels in the figure. For this asymmetric state, `01` and `10` have different weights, so reversing the labels directly affects the conclusion drawn from the figure. + +The Pauli vector can be generated for comparison in the same way: + +```python +plot_state_paulivec( + asymmetric, + title="Asymmetric Pauli vector", + output_path="assets/asymmetric_paulivec.png", +) +plot_state_paulivec( + asymmetric, + reverse_bits=True, + title="Asymmetric Pauli vector, reversed bits", + output_path="assets/asymmetric_paulivec_reverse_bits.png", +) +``` + +Default Pauli vector: + +![Asymmetric Pauli vector](assets/asymmetric_paulivec.png) + +Pauli vector after reversing the display order: + +![Asymmetric Pauli vector with reversed bits](assets/asymmetric_paulivec_reverse_bits.png) + +In multi-qubit state figures, confirm the display order of the basis labels and Pauli strings before interpreting coherence terms, diagonal weights or Pauli expectation values. + +--- + +## Generating state figures for reports + +When state figures are used in reports or presentation material, the canvas size, colors and transparency can be controlled. Taking the same asymmetric density matrix as an example, a more compact Pauli vector is generated below: + +```python +plot_state_paulivec( + asymmetric, + figsize=(5.0, 3.0), + color=["#2563eb", "#dc2626"], + alpha=0.85, + title="Asymmetric Pauli vector for report", + output_path="assets/asymmetric_paulivec_report.png", +) +``` + +The generated report Pauli vector is as follows: + +![Asymmetric Pauli vector for report](assets/asymmetric_paulivec_report.png) + +This kind of figure suits reports with limited layout space. During debugging, using the default figure first is still recommended, so that color, transparency and size settings interfere less with reading the figure. + +--- + +## Key points for checking state figures + +- Confirm whether the state comes from circuit simulation, direct construction or noise evolution; +- For single-qubit states, prefer the Bloch figure to explain the direction; +- For multi-qubit entangled states, do not look only at single-qubit Bloch vectors; +- For multi-qubit figures, confirm the bit order, basis labels and Pauli string order first; +- Density matrix figures suit explaining coherence terms and mixed states; +- Pauli vectors suit connection to observables and expectation values. +- Unlike result figures, state figures show the quantum state itself, simulated or constructed; real hardware usually provides only sampling results directly, unless a separate tomography or estimation workflow exists. + +--- + +## Next steps + +- [Visualizing execution results](6_result_visualization.md): compare state figures with sampling results to distinguish simulated state structure from the actual bitstring distribution. +- [Notebook and documentation integration](3_notebook_and_docs.md): save state figures and the generating code into a documentation asset directory that can be rerun. +- [Visualization strategies for complex circuits](4_visualization_practices.md): return to the circuit structure and check whether an anomaly in a state figure comes from gate order, mapping or module decomposition. diff --git a/docs/documentation/en/1_cqlib/5_visualization/assets/ansatz_decomposed.png b/docs/documentation/en/1_cqlib/5_visualization/assets/ansatz_decomposed.png new file mode 100644 index 00000000..8accaf26 Binary files /dev/null and b/docs/documentation/en/1_cqlib/5_visualization/assets/ansatz_decomposed.png differ diff --git a/docs/documentation/en/1_cqlib/5_visualization/assets/ansatz_modules.png b/docs/documentation/en/1_cqlib/5_visualization/assets/ansatz_modules.png new file mode 100644 index 00000000..25b10e79 Binary files /dev/null and b/docs/documentation/en/1_cqlib/5_visualization/assets/ansatz_modules.png differ diff --git a/docs/documentation/en/1_cqlib/5_visualization/assets/ansatz_overview.png b/docs/documentation/en/1_cqlib/5_visualization/assets/ansatz_overview.png new file mode 100644 index 00000000..25b10e79 Binary files /dev/null and b/docs/documentation/en/1_cqlib/5_visualization/assets/ansatz_overview.png differ diff --git a/docs/documentation/en/1_cqlib/5_visualization/assets/ansatz_structure.png b/docs/documentation/en/1_cqlib/5_visualization/assets/ansatz_structure.png new file mode 100644 index 00000000..a8ec5462 Binary files /dev/null and b/docs/documentation/en/1_cqlib/5_visualization/assets/ansatz_structure.png differ diff --git a/docs/documentation/en/1_cqlib/5_visualization/assets/ansatz_with_params.png b/docs/documentation/en/1_cqlib/5_visualization/assets/ansatz_with_params.png new file mode 100644 index 00000000..0e3b0343 Binary files /dev/null and b/docs/documentation/en/1_cqlib/5_visualization/assets/ansatz_with_params.png differ diff --git a/docs/documentation/en/1_cqlib/5_visualization/assets/asymmetric_paulivec.png b/docs/documentation/en/1_cqlib/5_visualization/assets/asymmetric_paulivec.png new file mode 100644 index 00000000..59102fa9 Binary files /dev/null and b/docs/documentation/en/1_cqlib/5_visualization/assets/asymmetric_paulivec.png differ diff --git a/docs/documentation/en/1_cqlib/5_visualization/assets/asymmetric_paulivec_report.png b/docs/documentation/en/1_cqlib/5_visualization/assets/asymmetric_paulivec_report.png new file mode 100644 index 00000000..7168b937 Binary files /dev/null and b/docs/documentation/en/1_cqlib/5_visualization/assets/asymmetric_paulivec_report.png differ diff --git a/docs/documentation/en/1_cqlib/5_visualization/assets/asymmetric_paulivec_reverse_bits.png b/docs/documentation/en/1_cqlib/5_visualization/assets/asymmetric_paulivec_reverse_bits.png new file mode 100644 index 00000000..7c4af68a Binary files /dev/null and b/docs/documentation/en/1_cqlib/5_visualization/assets/asymmetric_paulivec_reverse_bits.png differ diff --git a/docs/documentation/en/1_cqlib/5_visualization/assets/asymmetric_state_city.png b/docs/documentation/en/1_cqlib/5_visualization/assets/asymmetric_state_city.png new file mode 100644 index 00000000..212442c1 Binary files /dev/null and b/docs/documentation/en/1_cqlib/5_visualization/assets/asymmetric_state_city.png differ diff --git a/docs/documentation/en/1_cqlib/5_visualization/assets/asymmetric_state_city_reverse_bits.png b/docs/documentation/en/1_cqlib/5_visualization/assets/asymmetric_state_city_reverse_bits.png new file mode 100644 index 00000000..43c11b13 Binary files /dev/null and b/docs/documentation/en/1_cqlib/5_visualization/assets/asymmetric_state_city_reverse_bits.png differ diff --git a/docs/documentation/en/1_cqlib/5_visualization/assets/bell.png b/docs/documentation/en/1_cqlib/5_visualization/assets/bell.png new file mode 100644 index 00000000..1c96801c Binary files /dev/null and b/docs/documentation/en/1_cqlib/5_visualization/assets/bell.png differ diff --git a/docs/documentation/en/1_cqlib/5_visualization/assets/bell_bloch_multivector.png b/docs/documentation/en/1_cqlib/5_visualization/assets/bell_bloch_multivector.png new file mode 100644 index 00000000..092cebbe Binary files /dev/null and b/docs/documentation/en/1_cqlib/5_visualization/assets/bell_bloch_multivector.png differ diff --git a/docs/documentation/en/1_cqlib/5_visualization/assets/bell_counts.png b/docs/documentation/en/1_cqlib/5_visualization/assets/bell_counts.png new file mode 100644 index 00000000..b0a8234d Binary files /dev/null and b/docs/documentation/en/1_cqlib/5_visualization/assets/bell_counts.png differ diff --git a/docs/documentation/en/1_cqlib/5_visualization/assets/bell_distribution.png b/docs/documentation/en/1_cqlib/5_visualization/assets/bell_distribution.png new file mode 100644 index 00000000..e2eac06b Binary files /dev/null and b/docs/documentation/en/1_cqlib/5_visualization/assets/bell_distribution.png differ diff --git a/docs/documentation/en/1_cqlib/5_visualization/assets/bell_dominant_outcomes.png b/docs/documentation/en/1_cqlib/5_visualization/assets/bell_dominant_outcomes.png new file mode 100644 index 00000000..57a177be Binary files /dev/null and b/docs/documentation/en/1_cqlib/5_visualization/assets/bell_dominant_outcomes.png differ diff --git a/docs/documentation/en/1_cqlib/5_visualization/assets/bell_gate.png b/docs/documentation/en/1_cqlib/5_visualization/assets/bell_gate.png new file mode 100644 index 00000000..ab18a56e Binary files /dev/null and b/docs/documentation/en/1_cqlib/5_visualization/assets/bell_gate.png differ diff --git a/docs/documentation/en/1_cqlib/5_visualization/assets/bell_gate_decomposed.png b/docs/documentation/en/1_cqlib/5_visualization/assets/bell_gate_decomposed.png new file mode 100644 index 00000000..270c7194 Binary files /dev/null and b/docs/documentation/en/1_cqlib/5_visualization/assets/bell_gate_decomposed.png differ diff --git a/docs/documentation/en/1_cqlib/5_visualization/assets/bell_initial_state.png b/docs/documentation/en/1_cqlib/5_visualization/assets/bell_initial_state.png new file mode 100644 index 00000000..9454e25b Binary files /dev/null and b/docs/documentation/en/1_cqlib/5_visualization/assets/bell_initial_state.png differ diff --git a/docs/documentation/en/1_cqlib/5_visualization/assets/bell_report_counts.png b/docs/documentation/en/1_cqlib/5_visualization/assets/bell_report_counts.png new file mode 100644 index 00000000..3f9365aa Binary files /dev/null and b/docs/documentation/en/1_cqlib/5_visualization/assets/bell_report_counts.png differ diff --git a/docs/documentation/en/1_cqlib/5_visualization/assets/bell_reverse_bits.png b/docs/documentation/en/1_cqlib/5_visualization/assets/bell_reverse_bits.png new file mode 100644 index 00000000..104cbfed Binary files /dev/null and b/docs/documentation/en/1_cqlib/5_visualization/assets/bell_reverse_bits.png differ diff --git a/docs/documentation/en/1_cqlib/5_visualization/assets/bell_state_city.png b/docs/documentation/en/1_cqlib/5_visualization/assets/bell_state_city.png new file mode 100644 index 00000000..b78ec407 Binary files /dev/null and b/docs/documentation/en/1_cqlib/5_visualization/assets/bell_state_city.png differ diff --git a/docs/documentation/en/1_cqlib/5_visualization/assets/bloch_x.png b/docs/documentation/en/1_cqlib/5_visualization/assets/bloch_x.png new file mode 100644 index 00000000..0d53f458 Binary files /dev/null and b/docs/documentation/en/1_cqlib/5_visualization/assets/bloch_x.png differ diff --git a/docs/documentation/en/1_cqlib/5_visualization/assets/deep_folded.png b/docs/documentation/en/1_cqlib/5_visualization/assets/deep_folded.png new file mode 100644 index 00000000..80a1dc05 Binary files /dev/null and b/docs/documentation/en/1_cqlib/5_visualization/assets/deep_folded.png differ diff --git a/docs/documentation/en/1_cqlib/5_visualization/assets/dynamic_control_flow.png b/docs/documentation/en/1_cqlib/5_visualization/assets/dynamic_control_flow.png new file mode 100644 index 00000000..7c5f91c4 Binary files /dev/null and b/docs/documentation/en/1_cqlib/5_visualization/assets/dynamic_control_flow.png differ diff --git a/docs/documentation/en/1_cqlib/5_visualization/assets/mapped_after.png b/docs/documentation/en/1_cqlib/5_visualization/assets/mapped_after.png new file mode 100644 index 00000000..91b668fa Binary files /dev/null and b/docs/documentation/en/1_cqlib/5_visualization/assets/mapped_after.png differ diff --git a/docs/documentation/en/1_cqlib/5_visualization/assets/mapped_before.png b/docs/documentation/en/1_cqlib/5_visualization/assets/mapped_before.png new file mode 100644 index 00000000..a2ddb9d7 Binary files /dev/null and b/docs/documentation/en/1_cqlib/5_visualization/assets/mapped_before.png differ diff --git a/docs/documentation/en/1_cqlib/5_visualization/assets/mixed_state_city.png b/docs/documentation/en/1_cqlib/5_visualization/assets/mixed_state_city.png new file mode 100644 index 00000000..8a31482b Binary files /dev/null and b/docs/documentation/en/1_cqlib/5_visualization/assets/mixed_state_city.png differ diff --git a/docs/documentation/en/1_cqlib/5_visualization/assets/plus_bloch.png b/docs/documentation/en/1_cqlib/5_visualization/assets/plus_bloch.png new file mode 100644 index 00000000..31dace81 Binary files /dev/null and b/docs/documentation/en/1_cqlib/5_visualization/assets/plus_bloch.png differ diff --git a/docs/documentation/en/1_cqlib/5_visualization/assets/plus_density_city.png b/docs/documentation/en/1_cqlib/5_visualization/assets/plus_density_city.png new file mode 100644 index 00000000..aede6ecc Binary files /dev/null and b/docs/documentation/en/1_cqlib/5_visualization/assets/plus_density_city.png differ diff --git a/docs/documentation/en/1_cqlib/5_visualization/assets/plus_density_paulivec.png b/docs/documentation/en/1_cqlib/5_visualization/assets/plus_density_paulivec.png new file mode 100644 index 00000000..1728d823 Binary files /dev/null and b/docs/documentation/en/1_cqlib/5_visualization/assets/plus_density_paulivec.png differ diff --git a/docs/documentation/en/1_cqlib/5_visualization/assets/plus_paulivec.png b/docs/documentation/en/1_cqlib/5_visualization/assets/plus_paulivec.png new file mode 100644 index 00000000..0db905f1 Binary files /dev/null and b/docs/documentation/en/1_cqlib/5_visualization/assets/plus_paulivec.png differ diff --git a/docs/documentation/en/1_cqlib/5_visualization/assets/plus_state_city.png b/docs/documentation/en/1_cqlib/5_visualization/assets/plus_state_city.png new file mode 100644 index 00000000..69f10881 Binary files /dev/null and b/docs/documentation/en/1_cqlib/5_visualization/assets/plus_state_city.png differ diff --git a/docs/documentation/en/1_cqlib/5_visualization/assets/qaoa_cost.png b/docs/documentation/en/1_cqlib/5_visualization/assets/qaoa_cost.png new file mode 100644 index 00000000..9e94156d Binary files /dev/null and b/docs/documentation/en/1_cqlib/5_visualization/assets/qaoa_cost.png differ diff --git a/docs/documentation/en/1_cqlib/5_visualization/assets/qaoa_mixer.png b/docs/documentation/en/1_cqlib/5_visualization/assets/qaoa_mixer.png new file mode 100644 index 00000000..79237e1c Binary files /dev/null and b/docs/documentation/en/1_cqlib/5_visualization/assets/qaoa_mixer.png differ diff --git a/docs/documentation/en/1_cqlib/5_visualization/assets/qaoa_prepare.png b/docs/documentation/en/1_cqlib/5_visualization/assets/qaoa_prepare.png new file mode 100644 index 00000000..fab889db Binary files /dev/null and b/docs/documentation/en/1_cqlib/5_visualization/assets/qaoa_prepare.png differ diff --git a/docs/documentation/en/1_cqlib/5_visualization/assets/search_hamming_counts.png b/docs/documentation/en/1_cqlib/5_visualization/assets/search_hamming_counts.png new file mode 100644 index 00000000..a4104ec8 Binary files /dev/null and b/docs/documentation/en/1_cqlib/5_visualization/assets/search_hamming_counts.png differ diff --git a/docs/documentation/en/1_cqlib/5_visualization/assets/special_directives_and_gates.png b/docs/documentation/en/1_cqlib/5_visualization/assets/special_directives_and_gates.png new file mode 100644 index 00000000..4f6572bd Binary files /dev/null and b/docs/documentation/en/1_cqlib/5_visualization/assets/special_directives_and_gates.png differ diff --git a/docs/documentation/en/1_cqlib/6_error_mitigation/0_overview.md b/docs/documentation/en/1_cqlib/6_error_mitigation/0_overview.md new file mode 100644 index 00000000..fa255ee4 --- /dev/null +++ b/docs/documentation/en/1_cqlib/6_error_mitigation/0_overview.md @@ -0,0 +1,138 @@ +# Error Mitigation + +The Cqlib Python bindings provide error mitigation capabilities at the expectation-value level through **`cqlib.error_mitigation`**, currently supporting: + +- **Zero-noise extrapolation (ZNE)**: constructs a circuit family at different noise strengths through gate folding, then extrapolates to the zero-noise limit; +- **Virtual distillation (VD)**: estimates `Tr(O ρ^M) / Tr(ρ^M)` through copy-swap circuits; +- **Unified pipeline (`ErrorMitigation`)**: wraps the above methods in the order `run()` → `get_mitigated()`. + +Error mitigation reduces the systematic bias of **observable expectation values**; it is not quantum error correction. It usually increases the number of circuit executions and requires an **estimator** (a backend or simulator callback) to be supplied externally. + +--- + +## Common entry points + +```python +import cqlib.error_mitigation as em +from cqlib.circuit import Circuit +from cqlib.qis import Hamiltonian, PauliString + +from cqlib.error_mitigation import ( + ErrorMitigation, + ExtrapolateMethod, + MitigationMethod, + ProcessArgs, + RunArgs, + VirtualDistillation, + ZNEMitigation, + ZneConfig, +) +``` + +> `cqlib.__init__` does not re-export the symbols above; import `cqlib.error_mitigation` explicitly. + +--- + +## Recommended workflow + +Most scenarios use the unified entry point `ErrorMitigation`: + +```python +import cqlib.error_mitigation as em +from cqlib.circuit import Circuit +from cqlib.qis import Hamiltonian, PauliString + +circuit = Circuit(1) +circuit.x(0) + +hamiltonian = Hamiltonian.from_list([(PauliString.from_str("Z"), 1.0)]) + +mitigation = em.ErrorMitigation( + circuit, + em.MitigationMethod.zne(em.ZneConfig([0, 1, 2])), +) + +def estimator(run_circuit, observable, shots): + # replace with an expectation-value estimate from a simulator or a real backend + return (0.5 * len(run_circuit.operations), 0.0) + +mitigation.run(hamiltonian, em.RunArgs.zne(shots=128), estimator) +result = mitigation.get_mitigated( + em.ProcessArgs.zne(em.ExtrapolateMethod.polynomial(), degree=1) +) + +print("expectation:", result.expectation) +print("variance:", result.variance) +``` + +When folding, copy-swap or extrapolation steps need to be debugged separately, the lower-level APIs `ZNEMitigation` or `VirtualDistillation` can be used instead. + +--- + +## Estimator contract + +All mitigation methods rely on the same **Estimator** signature: + +```python +from collections.abc import Callable + +from cqlib.circuit import Circuit +from cqlib.qis import Hamiltonian + +Estimator = Callable[ + [Circuit, Hamiltonian | None, int | None], + tuple[float, float], +] +``` + +| Parameter | Meaning | +|------|------| +| `run_circuit` | The circuit to be executed (possibly a folded circuit or a copy-swap circuit) | +| `observable` | The `Hamiltonian` to be estimated; `None` for the denominator circuit | +| `shots` | The shot count of this execution; may be `None` in some APIs | +| Return value | `(expectation, variance)` | + +Cqlib does **not** provide a built-in simulator or backend estimator; connect `estimator` to QIS simulation, a density matrix noise model or the Tianyan backend. + +--- + +## Data flow overview + +```text +Original Circuit + Hamiltonian + ↓ +Construct the mitigation circuit family (ZNE folding / VD copy-swap) + ↓ +estimator runs each circuit and returns (expectation, variance) + ↓ +ZNE: extrapolate to noise_factor = 0 +VD: compute the numerator / denominator ratio + ↓ +MitigatedResult(expectation, variance?) +``` + +--- + +## Module breakdown + +| Module | Description | +|------|------| +| `cqlib.error_mitigation.zne` | `ZNEMitigation`, `ExtrapolateMethod`, `ZneConfig` | +| `cqlib.error_mitigation.virtual_distillation` | `VirtualDistillation`, `VirtualDistillationConfig` | +| `cqlib.error_mitigation.unified` | `ErrorMitigation`, `MitigationMethod`, `RunArgs`, `ProcessArgs` | + +--- + +## Capability boundaries + +- This module focuses on **ZNE** and **Virtual Distillation**; +- **Readout error correction** is not part of this module; see the Tianyan chapter [`5_readout_mitigation.md`](../7_tianyan/5_readout_mitigation.md); +- Each `ErrorMitigation` instance can call `run()` only once and `get_mitigated()` only once. + +--- + +## Next steps + +- [Zero-Noise Extrapolation (ZNE)](1_zne.md): learn gate folding, `noise_factors` and extrapolation methods. +- [Virtual Distillation (VD)](2_virtual_distillation.md): understand copy-swap circuits and ratio estimation. +- [Unified pipeline and Estimator](3_unified_api.md): master the `ErrorMitigation` state machine and error handling. diff --git a/docs/documentation/en/1_cqlib/6_error_mitigation/1_zne.md b/docs/documentation/en/1_cqlib/6_error_mitigation/1_zne.md new file mode 100644 index 00000000..b70e0f15 --- /dev/null +++ b/docs/documentation/en/1_cqlib/6_error_mitigation/1_zne.md @@ -0,0 +1,159 @@ +# Zero-Noise Extrapolation (ZNE) + +Zero-noise extrapolation (ZNE) actively amplifies circuit noise through unitary folding to obtain several noisy expectations, then extrapolates to the zero-noise point. + +--- + +## Python entry points + +```python +import cqlib.error_mitigation as em +from cqlib.circuit import Circuit +from cqlib.qis import Hamiltonian, PauliString +``` + +--- + +## Quick example: folding, execution and extrapolation + +```python +circuit = Circuit(1) +circuit.x(0) + +hamiltonian = Hamiltonian.from_list([(PauliString.from_str("Z"), 1.0)]) + +zne = em.ZNEMitigation(circuit, [0, 1, 2]) + +print(zne.fold_levels) # [0, 1, 2] +print(zne.noise_factors) # [1, 3, 5] + +folded = zne.fold_circuits() +print([len(c.operations) for c in folded]) # [1, 3, 5] + +def estimator(folded_circuit, observable, shots): + return (0.5 * len(folded_circuit.operations), 0.0) + +noisy = zne.run_em_sequence_with_shots(None, hamiltonian, 256, estimator) +print("noisy:", noisy) # [0.5, 1.5, 2.5] + +mitigated = zne.extrapolate( + noisy, + em.ExtrapolateMethod.polynomial(), + degree=1, +) +print("mitigated:", mitigated) +``` + +`ZNEMitigation` holds a copy of the original circuit; `fold_circuits()` does not modify the input `circuit`. + +--- + +## fold_levels and noise_factors + +Each `fold_level` corresponds to a noise factor: + +```text +noise_factor = 2 * fold_level + 1 +``` + +| `fold_level` | `noise_factor` | Meaning | +|--------------|----------------|------| +| `0` | `1` | No folding | +| `1` | `3` | One `U(U†U)` fold | +| `2` | `5` | Two folds | + +`fold_levels` must contain non-negative integers; at least two points are needed for extrapolation. `ZneConfig` and `ZNEMitigation` do not validate negative numbers at construction; a negative value raises `ErrorMitigationError` when `ErrorMitigation` is constructed. + +--- + +## Selective gate folding + +With the default `gate_set=None`, folding is global. To fold only specific gates, pass a list of target instructions: + +```python +from cqlib.circuit import Instruction, StandardGate + +gate_set = [Instruction.from_standard_gate(StandardGate.X)] +folded = zne.fold_circuits(gate_set) +``` + +Only operations whose names match an instruction in `gate_set` are folded. + +--- + +## Running the folded sequence + +| API | Description | +|-----|------| +| `run_em_sequence(gate_set, hamiltonian, estimator)` | Executes the folded circuits one by one and returns a list of expectation values | +| `run_em_sequence_with_shots(gate_set, hamiltonian, shots, estimator)` | Same as above, and passes `shots` to the estimator | + +`estimator` must be callable and must return a `(float, float)` tuple. The type signature is as follows: + +```python +from collections.abc import Callable + +from cqlib.circuit import Circuit +from cqlib.qis import Hamiltonian + +Estimator = Callable[ + [Circuit, Hamiltonian | None, int | None], + tuple[float, float], +] +``` + +--- + +## Extrapolation methods + +| API | Description | +|-----|------| +| `extrapolate(noisy_results, method, degree)` | General entry point | +| `poly_extrapolate(noisy_results, degree)` | Polynomial fit | +| `exp_extrapolate(noisy_results)` | Exponential decay fit (log space) | + +`ExtrapolateMethod` construction: + +```python +em.ExtrapolateMethod.polynomial() +em.ExtrapolateMethod.exponential() +``` + +For polynomial extrapolation, `degree` should be less than `len(noisy_results)`; first-degree polynomial extrapolation is the most common default. + +--- + +## Integration with the unified pipeline + +```python +def estimator(folded_circuit, observable, shots): + return (0.5 * len(folded_circuit.operations), 0.0) + +mitigation = em.ErrorMitigation( + circuit, + em.MitigationMethod.zne(em.ZneConfig([0, 1, 2])), +) + +mitigation.run(hamiltonian, em.RunArgs.zne(shots=128), estimator) +result = mitigation.get_mitigated( + em.ProcessArgs.zne(em.ExtrapolateMethod.polynomial(), degree=1) +) + +print(result.expectation) +print(result.variance) # None for ZNE +``` + +--- + +## Notes + +- ZNE mitigates expectation-value bias and does not guarantee recovery of the fully noiseless state; +- Folding increases circuit depth and execution cost; `fold_levels` and `shots` should be recorded for formal experiments; +- Extrapolation results are sensitive to estimator quality, so control verification is recommended for key circuits. + +--- + +## Next steps + +- [Virtual Distillation (VD)](2_virtual_distillation.md): understand multi-copy copy-swap and ratio estimation. +- [Unified pipeline and Estimator](3_unified_api.md): wrap the complete ZNE workflow with `ErrorMitigation`. diff --git a/docs/documentation/en/1_cqlib/6_error_mitigation/2_virtual_distillation.md b/docs/documentation/en/1_cqlib/6_error_mitigation/2_virtual_distillation.md new file mode 100644 index 00000000..1d5b463c --- /dev/null +++ b/docs/documentation/en/1_cqlib/6_error_mitigation/2_virtual_distillation.md @@ -0,0 +1,120 @@ +# Virtual Distillation (VD) + +Virtual distillation (VD) estimates the following through multiple copies of the density matrix and a **copy-swap** circuit: + +```text +Tr(O ρ^M) / Tr(ρ^M) +``` + +Here `M` is the number of copies (`copies`) and `O` is the observable represented by a `Hamiltonian`. + +--- + +## Python entry points + +```python +import cqlib.error_mitigation as em +from cqlib.circuit import Circuit +from cqlib.qis import Hamiltonian, PauliString +``` + +--- + +## Quick example: copy-swap and run_vd + +```python +circuit = Circuit(1) +hamiltonian = Hamiltonian.from_list([(PauliString.from_str("Z"), 1.0)]) + +vd = em.VirtualDistillation(circuit, copies=2) + +copy_swap = vd.build_copy_swap_circuit() +print(copy_swap.width) # 2 + +def estimator(run_circuit, observable, shots): + if observable is None: + return (2.0, 1.0) # denominator: Tr(ρ^M) + return (1.5, 0.25) # numerator: Tr(O ρ^M) + +mean, var = vd.run_vd(hamiltonian, shots_numerator=3, shots_denominator=2, estimator=estimator) +print("expectation:", mean) # 0.75 +print("variance:", var) +``` + +`copies` must be **≥ 2**; otherwise `ErrorMitigationError` is raised. + +--- + +## Constructing the copy-swap circuit + +```python +vd = em.VirtualDistillation(circuit, copies=2) +copy_swap = vd.build_copy_swap_circuit() +``` + +- The output circuit width is `copies` × the base circuit width; +- The copy registers are coupled internally through pairwise `SWAP`; +- The original `circuit` is not modified. + +The number of copies can be updated through `set_copies(copies)`, and the circuit can then be reconstructed. + +--- + +## Step-by-step execution + +| API | Description | +|-----|------| +| `run_denominator_circuit(shots, estimator)` | Estimates `Tr(ρ^M)`; `observable=None` | +| `run_numerator_circuit(hamiltonian, shots, estimator)` | Estimates `Tr(O ρ^M)` | +| `run_vd(hamiltonian, shots_numerator, shots_denominator, estimator)` | Completes the numerator and denominator in one call and returns their ratio | + +`estimator` receives `observable=None` in the denominator call and a concrete `Hamiltonian` in the numerator call. + +--- + +## copies and resource overhead + +| `copies` | Circuit width | Typical effect | +|----------|----------|----------| +| `2` | 2 qubits | Minimum configuration, lower overhead | +| `3` | 3 qubits | Stronger distillation, higher overhead | + +The larger `M` is, the more the dominant eigenstate component is emphasized, but the preparation and measurement cost increases significantly. + +--- + +## Integration with the unified pipeline + +```python +mitigation = em.ErrorMitigation( + circuit, + em.MitigationMethod.virtual_distillation(em.VirtualDistillationConfig(2)), +) + +def estimator(run_circuit, observable, shots): + if observable is None: + return (2.0, 1.0) + return (1.5, 0.25) + +mitigation.run(hamiltonian, em.RunArgs.virtual_distillation(3, 2), estimator) +result = mitigation.get_mitigated(em.ProcessArgs.virtual_distillation()) + +print(result.expectation) +print(result.variance) +``` + +The `MitigatedResult.variance` of VD usually has a value. + +--- + +## Notes + +- VD suits scenarios where the dominant eigenstate is to be amplified and the impact of mixed noise reduced; +- The `Hamiltonian` is expanded internally according to the number of copies, so Pauli terms do not need to be copied manually; +- Formal experiments should record the shot budget of the numerator and the denominator separately. + +--- + +## Next steps + +- [Unified pipeline and Estimator](3_unified_api.md): learn how `ErrorMitigation` wraps the VD workflow. diff --git a/docs/documentation/en/1_cqlib/6_error_mitigation/3_unified_api.md b/docs/documentation/en/1_cqlib/6_error_mitigation/3_unified_api.md new file mode 100644 index 00000000..b370fd5d --- /dev/null +++ b/docs/documentation/en/1_cqlib/6_error_mitigation/3_unified_api.md @@ -0,0 +1,165 @@ +# Unified pipeline and Estimator + +`ErrorMitigation` is the recommended sequential entry point: `run()` first collects the estimator output of each mitigation circuit, then `get_mitigated()` performs method-specific post-processing. + +--- + +## Python entry points + +```python +import cqlib.error_mitigation as em +from cqlib.circuit import Circuit +from cqlib.qis import Hamiltonian, PauliString +``` + +--- + +## State machine + +Each `ErrorMitigation` instance follows a fixed lifecycle: + +```text +create -> run() -> get_mitigated() -> end +``` + +| Rule | Description | +|------|------| +| `run()` can be called only once | A repeated call raises `ErrorMitigationError` | +| `run()` must be called before `get_mitigated()` | Otherwise `ErrorMitigationError` is raised | +| `get_mitigated()` can be called only once | A repeated call raises `ErrorMitigationError` | + +--- + +## ZNE pipeline example + +```python +circuit = Circuit(1) +circuit.x(0) +hamiltonian = Hamiltonian.from_list([(PauliString.from_str("Z"), 1.0)]) + +mitigation = em.ErrorMitigation( + circuit, + em.MitigationMethod.zne(em.ZneConfig([0, 1, 2])), +) + +def estimator(run_circuit, observable, shots): + assert observable is not None + assert shots == 128 + return (0.5 * len(run_circuit.operations), 0.0) + +mitigation.run(hamiltonian, em.RunArgs.zne(shots=128), estimator) +result = mitigation.get_mitigated( + em.ProcessArgs.zne(em.ExtrapolateMethod.polynomial(), degree=1) +) + +print(result.expectation) +print(result.variance) # ZNE: None +``` + +--- + +## Virtual Distillation pipeline example + +```python +circuit = Circuit(1) +hamiltonian = Hamiltonian.from_list([(PauliString.from_str("Z"), 1.0)]) + +mitigation = em.ErrorMitigation( + circuit, + em.MitigationMethod.virtual_distillation(em.VirtualDistillationConfig(2)), +) + +def estimator(run_circuit, observable, shots): + assert run_circuit.width == 2 + if observable is None: + return (2.0, 1.0) + return (1.5, 0.25) + +mitigation.run(hamiltonian, em.RunArgs.virtual_distillation(3, 2), estimator) +result = mitigation.get_mitigated(em.ProcessArgs.virtual_distillation()) + +print(result.expectation) +print(result.variance) +``` + +--- + +## Configuration objects + +### MitigationMethod + +```python +em.MitigationMethod.zne(em.ZneConfig([0, 1, 2])) +em.MitigationMethod.virtual_distillation(em.VirtualDistillationConfig(2)) +``` + +### RunArgs + +| Constructor | Parameters | +|----------|------| +| `RunArgs.zne(gate_set=None, shots=None)` | Optional selective fold gate set and shot count | +| `RunArgs.virtual_distillation(shots_numerator, shots_denominator)` | Numerator/denominator shot budget | + +### ProcessArgs + +| Constructor | Parameters | +|----------|------| +| `ProcessArgs.zne(method, degree=None)` | Extrapolation method and polynomial degree | +| `ProcessArgs.virtual_distillation()` | No additional parameters | + +### MitigatedResult + +| Field | Description | +|------|------| +| `expectation` | Expectation value after mitigation | +| `variance` | Variance after mitigation; `None` for ZNE and usually a value for VD | + +--- + +## Key points for implementing an Estimator + +```python +def estimator(run_circuit, observable, shots): + # run_circuit: may be a folded circuit or a copy-swap circuit + # observable: a Hamiltonian or None (VD denominator) + # shots: an int or None + return (expectation, variance) +``` + +Confirm the following when implementing: + +- The return value must be `(float, float)`; +- The `run_em_sequence_with_shots` of ZNE passes `shots` to the estimator; +- On the VD denominator path, `observable is None`; +- Exceptions inside the estimator are propagated upwards as-is. + +--- + +## Common errors + +| Scenario | Exception | +|------|------| +| `fold_levels` contains a negative number | `ErrorMitigationError` is raised when `ErrorMitigation` is constructed (`ZneConfig([-1])` itself does not raise) | +| `copies < 2` | Constructing `VirtualDistillation` or `ErrorMitigation` raises `ErrorMitigationError` | +| `get_mitigated()` without `run()` | `ErrorMitigationError` | +| `estimator` is not callable | `TypeError` | +| estimator return value has the wrong format | `TypeError` / `ValueError` | + +--- + +## Lower-level APIs and module exports + +Besides `ErrorMitigation`, the following can also be used directly: + +- `ZNEMitigation` — see [Zero-Noise Extrapolation (ZNE)](1_zne.md); +- `VirtualDistillation` — see [Virtual Distillation](2_virtual_distillation.md). + +`cqlib.error_mitigation.__all__` exports: `Estimator`, `ErrorMitigationError`, `ExtrapolateMethod`, `ZneConfig`, `VirtualDistillationConfig`, `MitigationMethod`, `RunArgs`, `ProcessArgs`, `MitigatedResult`, `ZNEMitigation`, `VirtualDistillation`, `ErrorMitigation`. + +--- + +## Notes + +- The unified pipeline and the lower-level APIs share the same `Estimator` type alias; +- Readout error correction belongs to the Tianyan module and is not part of `cqlib.error_mitigation`; +- When integrating with real hardware, wrap the estimator as backend shot acquisition and expectation-value aggregation logic. diff --git a/docs/documentation/en/1_cqlib/7_tianyan/0_overview.md b/docs/documentation/en/1_cqlib/7_tianyan/0_overview.md new file mode 100644 index 00000000..c19564a4 --- /dev/null +++ b/docs/documentation/en/1_cqlib/7_tianyan/0_overview.md @@ -0,0 +1,149 @@ +# Tianyan quantum cloud platform client overview + +`cqlib-tianyan` is the client library in the Cqlib ecosystem used to connect to the Tianyan quantum cloud platform. Its responsibility is not to construct the quantum circuits themselves, but to submit already prepared quantum circuits to cloud quantum backends and to convert the task results returned by the cloud into the Cqlib unified `ExecutionResult` object. + +From the usage chain, it sits between "circuit construction / IR conversion" and "cloud execution / result retrieval": + +```mermaid +flowchart LR + A["Cqlib Circuit"] --> B["cqlib.ir.qcis.dumps"] + C["Existing QCIS program"] --> D["QCIS circuit text"] + B --> D + D --> E["cqlib_tianyan.TianyanBackend.run"] + E --> F["TaskHandle"] + F --> G["wait/status"] + G --> H["cqlib.device.ExecutionResult"] + H --> I["counts / probabilities / backend / task_id"] +``` + +## 1. Module role + +`cqlib-tianyan` mainly provides the following capabilities: + +| Capability | Corresponding object | Description | +|---|---|---| +| Platform authentication | `TianyanPlatform.login`, `from_credentials` | Log in with an API Key, with local credential storage and automatic refresh | +| Backend discovery | `list_backends`, `get_backend` | Obtain available quantum backends, device status, billing type and qubit count | +| Device configuration | `TianyanBackend.device_config` | Download device information such as topology, calibration and readout error, returning a `cqlib.device.Device` | +| Task submission | `run`, `run_raw`, `run_with_mode`, `submit` | Submit QCIS circuits, with batch submission support | +| Result retrieval | `TaskHandle.status`, `wait`, `wait_raw` | Query task status, block until results are available, and return the Cqlib unified result object | +| Readout error correction | `CalibrationMode` | Correct measurement counts for readout error based on device calibration data | + +## 2. Relationship with other Cqlib modules + +The relationship between `cqlib-tianyan` and the Cqlib core modules is as follows: + +| Module | Role in the Tianyan execution chain | +|---|---| +| `cqlib.circuit` | Build local quantum circuits | +| `cqlib.ir.qcis` | Convert a `Circuit` into QCIS text, or load existing QCIS text | +| `cqlib_tianyan` | Log in to the platform, select a backend, submit QCIS and poll for results | +| `cqlib.device` | Carry device configuration and execution results, such as `Device` and `ExecutionResult` | +| `cqlib.visualization` | Inspect the circuit structure before submission and check whether the circuit matches expectations | + +Recommended workflow: + +```text +Circuit construction +-> QCIS export +-> Tianyan backend submission +-> TaskHandle waits for results +-> ExecutionResult analysis +``` + +## 3. Installation and environment + +The Python package name is `cqlib-tianyan`, and the import module name is `cqlib_tianyan`. + +```bash +pip install cqlib-tianyan +``` + +To install from source for development: + +```bash +cd crates/binding-python +maturin develop +``` + +`cqlib` must be installed as well, because the result object returned by the Tianyan client is `cqlib.device.ExecutionResult` and the device configuration object is `cqlib.device.Device`. + +## 4. Minimal example + +```python +import os +from cqlib_tianyan import TianyanPlatform + +platform = TianyanPlatform.login(os.environ["TIANYAN_API_KEY"]) +backend = platform.get_backend("tianyan-287") + +qcis = "H Q1\nM Q1" +task = backend.run([qcis], shots=1000) + +results = task.wait(timeout_secs=120.0, poll_interval_secs=5.0) +result = results[0] + +print(result.task_id) +print(result.counts) +print(result.probabilities) +``` + +This example does four things: + +1. Log in to the platform with the API Key from an environment variable. +2. Select a backend device. +3. Submit one QCIS circuit. +4. Wait for the task to complete and read the measurement counts. + +## 5. Core object relationships + +```mermaid +classDiagram + class TianyanPlatform { + +login(api_key) + +from_credentials() + +list_backends() + +get_backend(name) + +submit(circuits, shots, device_name) + } + class TianyanBackend { + +name + +display_name + +status + +toll + +num_qubits() + +is_available() + +run(circuits, shots) + +run_raw(circuits, shots) + +run_with_mode(circuits, shots, mode) + +device_config() + } + class TaskHandle { + +task_ids + +device_name + +shots + +submitted_at + +status() + +wait(timeout_secs, poll_interval_secs) + +wait_raw(timeout_secs, poll_interval_secs) + } + class ExecutionResult { + +task_id + +qubits + +shots + +counts + +probabilities + +backend + } + TianyanPlatform --> TianyanBackend + TianyanBackend --> TaskHandle + TaskHandle --> ExecutionResult +``` + +## Next steps + +- [Authentication and configuration](1_auth_config.md): learn about API Key login, credential storage, custom domains and configuration options. +- [Backend and device configuration](2_backend_device.md): learn about backend lists, status checks, device topology and calibration configuration. +- [Task submission and result retrieval](3_task_result.md): learn about submitting QCIS, batch tasks, polling and the result object. +- [QCIS and IR integration](4_qcis_ir_workflow.md): learn how to export QCIS from a Cqlib `Circuit` and submit it to Tianyan. +- [Readout error correction](5_readout_mitigation.md): learn about `CalibrationMode` and the difference between corrected and raw results. diff --git a/docs/documentation/en/1_cqlib/7_tianyan/1_auth_config.md b/docs/documentation/en/1_cqlib/7_tianyan/1_auth_config.md new file mode 100644 index 00000000..33d084b5 --- /dev/null +++ b/docs/documentation/en/1_cqlib/7_tianyan/1_auth_config.md @@ -0,0 +1,163 @@ +# Authentication and configuration + +Before using the Tianyan platform, authentication must be completed first. `cqlib-tianyan` logs in with an API Key, and a `TianyanPlatform` object is created after a successful login. Subsequent backend queries, task submission and result queries all start from this object. + +Corresponding import: + +```python +from cqlib_tianyan import TianyanPlatform, TianyanConfig, TianyanError +``` + +## 1. Logging in with an API Key + +It is recommended to keep the API Key in an environment variable rather than writing it into code: + +```bash +export TIANYAN_API_KEY="your_api_key" +``` + +Python example: + +```python +import os +from cqlib_tianyan import TianyanPlatform + +platform = TianyanPlatform.login(os.environ["TIANYAN_API_KEY"]) +``` + +The default platform domain is: + +```text +qc.zdxlz.com +``` + +The default base URL is: + +```text +https://qc.zdxlz.com +``` + +## 2. Credential storage and reuse + +By default, credentials are saved locally after a successful login: + +| System | Default path | +|---|---| +| macOS / Linux | `~/.cqlib/tianyan/credentials.json` | +| Windows | `%APPDATA%\cqlib\tianyan\credentials.json` | + +The saved credentials can later be reused directly: + +```python +from cqlib_tianyan import TianyanPlatform + +platform = TianyanPlatform.from_credentials() +``` + +If the saved token has expired, the saved API Key is used to refresh it automatically by default. + +## 3. Disabling credential storage + +To avoid writing credentials to disk, `save_credentials` can be turned off: + +```python +platform = TianyanPlatform.login( + os.environ["TIANYAN_API_KEY"], + save_credentials=False, +) +``` + +To avoid automatic refresh when the token expires, `auto_refresh` can also be turned off: + +```python +platform = TianyanPlatform.login( + os.environ["TIANYAN_API_KEY"], + save_credentials=False, + auto_refresh=False, +) +``` + +## 4. Custom credential path + +```python +platform = TianyanPlatform.login( + os.environ["TIANYAN_API_KEY"], + credentials_path="/secure/path/tianyan_credentials.json", +) +``` + +Loading from a custom path: + +```python +platform = TianyanPlatform.from_credentials( + credentials_path="/secure/path/tianyan_credentials.json", +) +``` + +## 5. Custom platform domain + +By default, `domain` does not need to be set. If the deployment environment uses a custom domain, write: + +```python +platform = TianyanPlatform.login( + os.environ["TIANYAN_API_KEY"], + domain="qc.zdxlz.com", +) +``` + +`domain` only needs the host name; `https://` is not required. + +## 6. TianyanConfig + +`TianyanConfig` is used to view or organize configuration options. In the Python bindings, `login` and `from_credentials` already accept configuration keyword arguments directly, so ordinary users do not necessarily need to create a `TianyanConfig` explicitly. + +```python +from cqlib_tianyan import TianyanConfig + +config = TianyanConfig( + domain="qc.zdxlz.com", + save_credentials=True, + auto_refresh=True, + credentials_path="/secure/path/tianyan_credentials.json", +) + +print(config.base_url) +print(config.credentials_path) +``` + +Configuration options: + +| Parameter | Default value | Description | +|---|---|---| +| `domain` | `qc.zdxlz.com` | Platform host name | +| `save_credentials` | `True` | Whether to save credentials after login or refresh | +| `auto_refresh` | `True` | Whether to log in again automatically after the token expires | +| `credentials_path` | Platform default path | Path to the local credentials JSON file | + +## 7. Error handling + +Exceptions are raised when platform authentication, network requests, task submission or result queries fail. Recommended pattern: + +```python +from cqlib_tianyan import TianyanPlatform, TianyanError + +try: + platform = TianyanPlatform.login("invalid_api_key") +except Exception as exc: + print(f"login failed: {exc}") +``` + +In the current Python abi3 bindings, it is recommended to catch `Exception` in practice and then handle it according to the error message. + +## 8. Security recommendations + +- Do not write API Keys into a source code repository. +- Prefer environment variables or a secret management system to store API Keys. +- On a shared machine, a custom `credentials_path` is recommended. +- In CI, disable credential persistence: `save_credentials=False`. +- For temporary testing only, `save_credentials` and `auto_refresh` can be turned off at the same time. + +## Next steps + +- [Backend and device configuration](2_backend_device.md): list platform backends, select a target device, and obtain device topology and calibration information. +- [Task submission and result retrieval](3_task_result.md): after backend selection is complete, submit QCIS circuits and poll for cloud execution results. diff --git a/docs/documentation/en/1_cqlib/7_tianyan/2_backend_device.md b/docs/documentation/en/1_cqlib/7_tianyan/2_backend_device.md new file mode 100644 index 00000000..9488514d --- /dev/null +++ b/docs/documentation/en/1_cqlib/7_tianyan/2_backend_device.md @@ -0,0 +1,143 @@ +# Backend and device configuration + +After authentication is complete, a quantum backend must be selected. A backend on the Tianyan platform is represented by `TianyanBackend`, which provides attributes such as the backend name, display name, status and billing type, as well as interfaces for querying the physical qubit count and downloading the device configuration. + +## 1. Listing backends + +```python +import os +from cqlib_tianyan import TianyanPlatform + +platform = TianyanPlatform.login(os.environ["TIANYAN_API_KEY"]) +backends = platform.list_backends() + +for backend in backends: + print(backend.name, backend.display_name, backend.status, backend.toll) +``` + +`list_backends()` returns a list of `TianyanBackend`. + +## 2. Backend object attributes and methods + +| Member | Form | Return type | Description | +|---|---|---|---| +| `name` | Attribute | `str` | Backend device identifier, used to submit tasks, for example `tianyan-287` | +| `display_name` | Attribute | `str` | User-friendly display name | +| `status` | Attribute | `DeviceStatus` | Device running status | +| `toll` | Attribute | `DeviceToll` | Billing type | +| `num_qubits()` | Method | `int` | Returns the total number of physical qubits in the device configuration, including disabled qubits; the first call downloads the device configuration, and subsequent calls reuse the cache | + +Example: + +```python +for backend in platform.list_backends(): + print(f"name={backend.name}") + print(f"display={backend.display_name}") + print(f"status={backend.status}") + print(f"toll={backend.toll}") + +# after the backend is selected, query the total physical qubit count in its device configuration +backend = platform.get_backend("tianyan-287") +print(f"qubits={backend.num_qubits()}") +``` + +`num_qubits()` is a method rather than an attribute. The first call to this method may issue a network request to obtain the device configuration; to determine the currently available physical qubits, further check the device topology and disabled qubit information returned by `device_config()`. + +## 3. Backend status + +`DeviceStatus` can be compared with strings: + +| Value | Meaning | +|---|---| +| `running` | The device is online and can accept tasks | +| `calibration` | The device is being calibrated; tasks may be queued | +| `under_maintenance` | Under maintenance; temporarily unavailable | +| `offline` | Offline | +| `unknown` | The platform returned an unrecognized status | + +It is recommended to check before submission: + +```python +backend = platform.get_backend("tianyan-287") + +if not backend.is_available(): + raise RuntimeError(f"backend unavailable: {backend.status}") +``` + +A direct comparison is also possible: + +```python +if backend.status == "running": + print("backend can accept submissions") +``` + +## 4. Billing type + +`DeviceToll` represents the backend billing type: + +| Value | Meaning | +|---|---| +| `free` | Free task | +| `paid` | Consumes quota or is billed | +| `unknown` | The platform returned an unrecognized billing type | + +```python +if backend.toll == "paid": + print("this backend may consume quota, confirm before submitting") +``` + +## 5. Getting a specific backend + +```python +backend = platform.get_backend("tianyan-287") +``` + +If the backend does not exist, an exception is raised. + +## 6. Getting the device configuration + +`device_config()` downloads device topology, calibration and error information and returns a `cqlib.device.Device` object: + +```python +device = backend.device_config() + +print(device.name) +print(device.num_usable_qubits) +print(device.topology) +``` + +The result is cached inside the backend object; repeated calls on the same `TianyanBackend` do not download it again each time. + +## 7. Uses of the device configuration + +The device configuration can be used to: + +- Inspect available physical qubits. +- Inspect the coupling topology. +- Assist the compiler in layout mapping and gate routing. +- Obtain readout fidelity for readout error correction. +- Analyze single-qubit gate, two-qubit gate and measurement errors. + +Typical flow: + +```python +backend = platform.get_backend("tianyan-287") +device = backend.device_config() + +# the device can later be passed to the compilation or analysis module +``` + +## 8. Backend selection recommendations + +| Scenario | Recommendation | +|---|---| +| Only testing the API chain | Choose a `running` backend that is free or low-cost | +| A specific physical qubit is required | Check the device topology and available qubits first | +| Better result quality is required | Pay attention to readout error, two-qubit gate error and calibration time | +| Batch submission | Test with a small number of circuits first, then expand the batch | +| Error correction is required | Confirm that the device configuration has usable calibration data | + +## Next steps + +- [Task submission and result retrieval](3_task_result.md): use the selected backend to submit QCIS circuits, query task status and obtain results. +- [QCIS and IR integration](4_qcis_ir_workflow.md): learn how to export QCIS from a Cqlib `Circuit` and then submit it to the Tianyan platform. diff --git a/docs/documentation/en/1_cqlib/7_tianyan/3_task_result.md b/docs/documentation/en/1_cqlib/7_tianyan/3_task_result.md new file mode 100644 index 00000000..c7caa3ae --- /dev/null +++ b/docs/documentation/en/1_cqlib/7_tianyan/3_task_result.md @@ -0,0 +1,191 @@ +# Task submission and result retrieval + +The basic unit of circuit execution on the Tianyan platform is the task. In `cqlib-tianyan`, a `TaskHandle` is returned after task submission, through which the task status can be queried or results can be awaited in a blocking manner. + +## 1. Submitting a QCIS circuit + +The task submission interface in the Python bindings accepts a list of QCIS strings: + +The `backend` in the examples below is a `TianyanBackend` instance; see [Backend and device configuration](2_backend_device.md) for how to obtain one. + +```python +qcis = "H Q1\nM Q1" +task = backend.run([qcis], shots=1000) +``` + +Parameter description: + +| Parameter | Description | +|---|---| +| `circuits` | List of QCIS strings | +| `shots` | Number of shots for each circuit | + +The return value is a `TaskHandle`. + +## 2. Submitting through a backend + +```python +import os +from cqlib_tianyan import TianyanPlatform + +platform = TianyanPlatform.login(os.environ["TIANYAN_API_KEY"]) +backend = platform.get_backend("tianyan-287") + +circuits = [ + "H Q1\nM Q1", + "X Q1\nM Q1", +] + +task = backend.run(circuits, shots=1000) +print(task.task_ids) +``` + +## 3. Submitting directly through the platform + +If the target device name is already known, `get_backend` can be skipped: + +```python +task = platform.submit( + circuits=["H Q1\nM Q1"], + shots=1000, + device_name="tianyan-287", +) +``` + +This approach suits server-side or automation scripts, because it removes one backend list query. + +## 4. TaskHandle fields + +| Field | Description | +|---|---| +| `task_ids` | List of task IDs returned by the platform | +| `device_name` | Name of the backend used for submission | +| `shots` | Shots requested for each circuit | +| `submitted_at` | Submission time, an ISO 8601 string | + +Example: + +```python +print(task.task_ids) +print(task.device_name) +print(task.shots) +print(task.submitted_at) +``` + +## 5. Non-blocking query + +`status()` queries only once and returns the results that have already completed. Circuits that have not yet completed do not appear in the returned list. + +```python +partial_results = task.status() + +print(f"completed {len(partial_results)} / {len(task.task_ids)}") +for result in partial_results: + print(result.task_id, result.counts) +``` + +Suitable for implementing polling logic manually, or for refreshing task status periodically in a UI. + +## 6. Blocking wait + +`wait()` polls until all circuits are complete, or until a timeout occurs. + +```python +results = task.wait( + timeout_secs=120.0, + poll_interval_secs=5.0, +) + +for result in results: + print(result.task_id) + print(result.counts) + print(result.probabilities) +``` + +Parameter description: + +| Parameter | Description | +|---|---| +| `timeout_secs` | Maximum number of seconds to wait | +| `poll_interval_secs` | Polling interval in seconds, default `5.0` | + +`wait()` releases the Python GIL while blocking, so it does not block other Python threads from running. + +## 7. Getting raw results + +By default, `wait()` decides whether to apply readout error correction according to the `CalibrationMode` at submission time. To obtain raw counts only, use: + +```python +raw_results = task.wait_raw( + timeout_secs=120.0, + poll_interval_secs=5.0, +) +``` + +## 8. Result object + +`wait()`, `wait_raw()` and `status()` return a list of `cqlib.device.ExecutionResult`. + +Common fields: + +| Field | Description | +|---|---| +| `task_id` | Platform task ID | +| `qubits` | Measured qubits | +| `shots` | Actual number of shots | +| `num_qubits` | Number of qubits in the result object | +| `backend` | Backend name | +| `counts` | Measurement counts dictionary | +| `probabilities` | Probabilities normalized from counts | +| `status` | Execution status | + +Example: + +```python +result = results[0] + +print(result.task_id) +print(result.backend) +print(result.qubits) +print(result.counts) +print(result.probabilities) +``` + +## 9. Batch submission + +A single request on the Tianyan platform accepts at most 50 circuits. `cqlib-tianyan` splits larger inputs automatically internally: + +```python +circuits = ["H Q1\nM Q1"] * 120 + +task = backend.run(circuits, shots=1000) +print(len(task.task_ids)) # 120 +``` + +Internally, submission is batched as `50 + 50 + 20`, and all task IDs are merged into the same `TaskHandle`. + +## 10. Pre-submission checks + +It is recommended to check the following before submission: + +- Whether the backend is `running`. +- Whether the QCIS text can be parsed correctly by `cqlib.ir.qcis.loads`. +- Whether the circuit contains gates not supported by the target device. +- Whether measurement has been added. +- Whether shots meet the experimental requirements and platform limits. +- Whether a batch task needs a small-scale trial run first. + +## 11. Common issues + +| Symptom | Common cause | Handling | +|---|---|---| +| Submission fails | Backend unavailable, QCIS format error, invalid API Key or network problem | Check `backend.status`, the QCIS text and the authentication status | +| `wait` times out | Long queue wait time or the task has not completed | Increase `timeout_secs`, or use `status()` to query in batches | +| Fewer results returned than submitted circuits | `status()` was used, which returns only completed results | Use `wait()` to wait for all of them to complete | +| counts is empty | The platform result has not completed or parsing failed | Check the task status and the raw error message | +| Probabilities and counts are inconsistent | Probabilities are normalized from counts, and the correction mode may change counts | Compare `wait()` with `wait_raw()` | + +## Next steps + +- [QCIS and IR integration](4_qcis_ir_workflow.md): export a Cqlib `Circuit` to QCIS and connect it to the Tianyan task submission flow. +- [Readout error correction](5_readout_mitigation.md): learn about `CalibrationMode` and the difference between raw results and corrected results. diff --git a/docs/documentation/en/1_cqlib/7_tianyan/4_qcis_ir_workflow.md b/docs/documentation/en/1_cqlib/7_tianyan/4_qcis_ir_workflow.md new file mode 100644 index 00000000..7a9eb06a --- /dev/null +++ b/docs/documentation/en/1_cqlib/7_tianyan/4_qcis_ir_workflow.md @@ -0,0 +1,172 @@ +# QCIS and IR integration + +Tianyan platform task submission uses QCIS circuit text. Therefore, there are usually two ways to prepare submission content in Cqlib: + +- Use existing QCIS program text, such as QCIS generated by a hardware-side toolchain, a historical experiment file or an external system. +- Build the circuit with `cqlib.circuit.Circuit` first, then export QCIS through `cqlib.ir.qcis.dumps`. + +This page focuses on the second way, because it links Cqlib circuit construction, visualization, IR conversion and Tianyan execution into a complete flow. + +## 1. From Circuit to QCIS + +```python +from cqlib import Circuit +from cqlib.ir import qcis + +circuit = Circuit(2) +circuit.h(0) +circuit.cz(0, 1) +circuit.measure(0) +circuit.measure(1) + +qcis_text = qcis.dumps(circuit) +print(qcis_text) +``` + +Possible output: + +```text +H Q0 +CZ Q0 Q1 +M Q0 +M Q1 +``` + +## 2. Submitting the exported QCIS + +```python +import os +from cqlib import Circuit +from cqlib.ir import qcis +from cqlib_tianyan import TianyanPlatform + +circuit = Circuit(2) +circuit.h(0) +circuit.cz(0, 1) +circuit.measure(0) +circuit.measure(1) + +qcis_text = qcis.dumps(circuit) + +platform = TianyanPlatform.login(os.environ["TIANYAN_API_KEY"]) +backend = platform.get_backend("tianyan-287") + +task = backend.run([qcis_text], shots=1000) +results = task.wait(timeout_secs=120.0) + +print(results[0].counts) +``` + +## 3. Validating QCIS before submission + +If the QCIS comes from an external system, a historical experiment file or manually written program text, it is recommended to parse it once with Cqlib IR first: + +```python +from cqlib.ir import qcis + +qcis_text = "H Q1\nM Q1" +circuit = qcis.loads(qcis_text) + +print(circuit.num_qubits) +print(len(circuit.operations)) +``` + +If `qcis.loads` fails, the text itself may not conform to the QCIS support scope of the current Cqlib, and direct submission to the cloud is not recommended. + +## 4. Visualization check + +Before submission, the circuit can be inspected with the Cqlib visualization module: + +```python +from cqlib.ir import qcis +from cqlib.visualization import draw_text + +circuit = qcis.loads("H Q1\nM Q1") +print(draw_text(circuit)) +``` + +This step is suitable for troubleshooting: + +- Whether the qubit numbering is correct. +- Whether the gate order is correct. +- Whether measurement is present. +- Whether the circuit matches expectations. + +## 5. QCIS gate set boundaries + +`qcis.dumps(circuit)` rejects circuits that cannot be represented as QCIS. Common cases: + +| Case | Description | Handling | +|---|---|---| +| Custom `CircuitGate` | QCIS does not express user-defined gates directly | Call `decompose()` first | +| Arbitrary-matrix `UnitaryGate` | QCIS is gate instruction text and does not store arbitrary matrices | Decompose into basic gates first | +| Complex control flow | QCIS does not express general classical control flow | Switch to another format or compile and expand first | +| Ordinary identity gate | `I Qn t` in QCIS means delay, not an ordinary identity gate | Do not treat an ordinary I as a QCIS delay | +| Global phase | Hardware instructions generally do not need a global phase | Remove or ignore it before submission | + +## 6. Physical qubit numbering + +Tianyan QCIS uses physical qubit numbering, for example: + +```text +H Q1 +M Q1 +``` + +Here `Q1` is a physical qubit number, and it is not necessarily equal to the first logical qubit in the local logical circuit. Before actual submission, the target device topology and available qubits need to be confirmed. + +If a local circuit is constructed directly with `Circuit(2)` and then exported to QCIS, the default result is `Q0`, `Q1`. If the target device is expected to use specific physical qubits, for example `Q1`, `Q8`, this needs to be handled explicitly at the construction or mapping stage. + +Hand-writing QCIS that specifies physical qubits: + +```python +qcis_text = "H Q1\nH Q8\nCZ Q1 Q8\nH Q8\nM Q1\nM Q8" +``` + +This example uses `H + CZ + H` to implement a CNOT structure equivalently on the target qubits. + +## 7. Relationship with OpenQASM + +If an OpenQASM circuit already exists, it can be converted into a `Circuit` through Cqlib IR first, and then exported to QCIS: + +```python +from cqlib.ir import qasm3, qcis + +qasm3_text = """ +OPENQASM 3.0; +include "stdgates.inc"; +qubit[2] q; +h q[0]; +cz q[0],q[1]; +""" + +circuit = qasm3.loads(qasm3_text) +qcis_text = qcis.dumps(circuit) +``` + +Note: the conversion succeeds only when the gates and semantics in the OpenQASM circuit can be expressed by QCIS. + +## 8. Recommended execution flow + +```mermaid +flowchart TD + A["Build a Circuit or prepare an existing QCIS program"] --> B["validate with qcis.loads / qcis.dumps"] + B --> C["Visualization check"] + C --> D["Select a TianyanBackend"] + D --> E["backend.run"] + E --> F["TaskHandle.wait"] + F --> G["ExecutionResult counts/probabilities"] +``` + +## 9. Engineering recommendations + +- Save the original `Circuit`, the exported QCIS and the final result for key experiments. +- When external QCIS program text is used, parse and validate it with `qcis.loads` first. +- Before using physical qubits, check `backend.device_config()` first. +- If QCIS export fails, do not force string concatenation; prefer to return to the circuit decomposition or compilation stage. +- Keep `task.task_ids` for cloud tasks to facilitate later tracking. + +## Next steps + +- [Readout error correction](5_readout_mitigation.md): obtain corrected measurement results based on device calibration data. +- [QCIS format description](../1_ir/1_qcis.md): learn about the support scope of Cqlib for QCIS instructions, import and export. diff --git a/docs/documentation/en/1_cqlib/7_tianyan/5_readout_mitigation.md b/docs/documentation/en/1_cqlib/7_tianyan/5_readout_mitigation.md new file mode 100644 index 00000000..157308d0 --- /dev/null +++ b/docs/documentation/en/1_cqlib/7_tianyan/5_readout_mitigation.md @@ -0,0 +1,184 @@ +# Readout error correction + +Measurements on real quantum devices have readout errors. When obtaining results, `cqlib-tianyan` can use device calibration data to correct measurement counts for readout error. This capability is controlled by `CalibrationMode`. + +The `backend` in the examples below is a `TianyanBackend` instance; see [Backend and device configuration](2_backend_device.md) for how to obtain one. + +Corresponding import: + +```python +from cqlib_tianyan import CalibrationMode +``` + +## 1. Why readout error correction is needed + +Ideally, if a qubit is truly in `|0>`, the measurement result should always be `0`; if it is truly in `|1>`, the measurement result should always be `1`. But hardware readout has errors: + +```text +true 0 -> may be read as 1 +true 1 -> may be read as 0 +``` + +Platform calibration data usually gives the readout fidelity of each qubit: + +| Symbol | Meaning | +|---|---| +| `f00` | Probability of reading 0 when prepared as 0 | +| `f11` | Probability of reading 1 when prepared as 1 | + +`cqlib-tianyan` builds a confusion matrix from these data and performs an approximate inversion on the observed counts. + +## 2. CalibrationMode + +`CalibrationMode` has three modes: + +| Mode | Behavior | +|---|---| +| `auto` | Default mode. Correction is applied automatically when calibration data exists and the number of measured qubits does not exceed the threshold; otherwise it falls back to raw counts | +| `enabled` | Forced correction. An error is returned when there is no calibration data or resources are insufficient | +| `disabled` | No correction; raw counts are always returned | + +Creation: + +```python +from cqlib_tianyan import CalibrationMode + +mode = CalibrationMode("auto") +assert mode == "auto" +``` + +In most cases a string can be passed directly: + +```python +task = backend.run_with_mode( + circuits=["H Q1\nM Q1"], + shots=1000, + mode="disabled", +) +``` + +## 3. Default behavior + +`backend.run()` uses the `auto` mode by default: + +```python +task = backend.run(["H Q1\nM Q1"], shots=1000) +results = task.wait(timeout_secs=120.0) +``` + +The `auto` mode applies readout error correction automatically when the conditions are met. + +## 4. Getting raw results + +To obtain raw counts only without any correction, use `run_raw`: + +```python +task = backend.run_raw(["H Q1\nM Q1"], shots=1000) +raw_results = task.wait(timeout_secs=120.0) +``` + +Or use `wait_raw` on an existing task: + +```python +task = backend.run(["H Q1\nM Q1"], shots=1000) +raw_results = task.wait_raw(timeout_secs=120.0) +``` + +## 5. Specifying the correction mode explicitly + +```python +# automatic correction +task = backend.run_with_mode(["H Q1\nM Q1"], shots=1000, mode="auto") + +# forced correction +task = backend.run_with_mode(["H Q1\nM Q1"], shots=1000, mode="enabled") + +# no correction +task = backend.run_with_mode(["H Q1\nM Q1"], shots=1000, mode="disabled") +``` + +A `CalibrationMode` object can also be passed: + +```python +mode = CalibrationMode("disabled") +task = backend.run_with_mode(["H Q1\nM Q1"], shots=1000, mode=mode) +``` + +## 6. Qubit count threshold of the auto mode + +Readout error correction requires building a confusion matrix. If the number of measured qubits is `n`, the full matrix size is: + +```text +2^n x 2^n +``` + +The memory complexity is approximately: + +```text +O(4^n) +``` + +Therefore the `auto` mode applies correction automatically only when the number of measured qubits does not exceed 14 by default. Above the threshold it falls back to raw counts, avoiding excessive memory overhead. + +If forced correction is genuinely required, use: + +```python +task = backend.run_with_mode(circuits, shots=1000, mode="enabled") +``` + +However, this requires the caller to confirm that memory resources are sufficient. + +## 7. Comparing results before and after correction + +```python +qcis = "H Q1\nM Q1" + +task = backend.run([qcis], shots=1000) + +calibrated = task.wait(timeout_secs=120.0) +raw = task.wait_raw(timeout_secs=120.0) + +print("corrected:", calibrated[0].counts, calibrated[0].probabilities) +print("raw:", raw[0].counts, raw[0].probabilities) +``` + +Note: the corrected counts are still returned in `ExecutionResult`, in the `counts` field, and `probabilities` are the probabilities normalized from counts. + +## 8. Relationship with the device configuration + +Readout error correction depends on device calibration data. The device configuration can be checked first: + +```python +device = backend.device_config() +``` + +If there is no usable calibration data: + +- `auto` falls back to raw counts. +- `enabled` raises an error. +- `disabled` is unaffected. + +## 9. Selection recommendations + +| Scenario | Recommended mode | +|---|---| +| Ordinary experiments | `auto` | +| Raw hardware output only | `disabled` or `wait_raw()` | +| Validating an error correction algorithm | `enabled` | +| A large number of measured qubits | `disabled`, or `enabled` after evaluating memory manually | +| Comparing hardware output and correction effects | Use `wait()` and `wait_raw()` together | + +## 10. Common issues + +| Symptom | Cause | Handling | +|---|---|---| +| `auto` does not change the result noticeably | No calibration data, the number of measured qubits exceeds the threshold, or the readout error itself is small | Verify with `enabled`, or check the device calibration data | +| `enabled` raises an error | Missing calibration data or insufficient resources | Switch to `auto` or `disabled` | +| Very small probability entries appear in the result | Inverse matrix correction redistributes probabilities | Set a post-processing threshold according to the experimental requirements | +| Large-qubit-count tasks are slow | The correction matrix size grows exponentially with the qubit count | Disable correction or split the experiment | + +## Next steps + +- [Cqlib tutorial entry point](../../0_get_started/0_introduction.md): return to the start of the tutorials and continue with other Cqlib modules. +- [QCIS format description](../1_ir/1_qcis.md): learn about the QCIS text format, the import and export interfaces and the support boundaries. +- [Device module](../2_device/0_overview.md): learn about device topology, device configuration, noise models and the execution result object. diff --git a/docs/en/index.rst b/docs/en/index.rst new file mode 100644 index 00000000..024052a8 --- /dev/null +++ b/docs/en/index.rst @@ -0,0 +1,296 @@ +Cqlib Documentation +=================== + +.. toctree:: + :maxdepth: 2 + :caption: Getting Started + + ../documentation/en/0_get_started/0_introduction + ../documentation/en/0_get_started/1_installation + ../documentation/en/0_get_started/2_quickstart + + +.. toctree:: + :maxdepth: 2 + :caption: Quantum Circuit + + ../documentation/en/1_cqlib/0_circuit/0_overview + ../documentation/en/1_cqlib/0_circuit/1_gates + ../documentation/en/1_cqlib/0_circuit/2_structures + ../documentation/en/1_cqlib/0_circuit/3_parameters + ../documentation/en/1_cqlib/0_circuit/4_circuit_analysis + ../documentation/en/1_cqlib/0_circuit/5_control_flow + + +.. toctree:: + :maxdepth: 2 + :caption: Intermediate Representation + + ../documentation/en/1_cqlib/1_ir/0_overview + ../documentation/en/1_cqlib/1_ir/1_qcis + ../documentation/en/1_cqlib/1_ir/2_qasm2 + ../documentation/en/1_cqlib/1_ir/3_qasm3 + ../documentation/en/1_cqlib/1_ir/4_conversion_workflow + + +.. toctree:: + :maxdepth: 2 + :caption: Devices and Backends + + ../documentation/en/1_cqlib/2_device/0_overview + ../documentation/en/1_cqlib/2_device/1_topology + ../documentation/en/1_cqlib/2_device/2_device + ../documentation/en/1_cqlib/2_device/3_layout + ../documentation/en/1_cqlib/2_device/4_noise + ../documentation/en/1_cqlib/2_device/5_result + + +.. toctree:: + :maxdepth: 2 + :caption: Quantum Information + + ../documentation/en/1_cqlib/3_qis/0_overview + ../documentation/en/1_cqlib/3_qis/1_statevector + ../documentation/en/1_cqlib/3_qis/2_density_matrix + ../documentation/en/1_cqlib/3_qis/3_density_matrix_noise + ../documentation/en/1_cqlib/3_qis/4_stabilizer + ../documentation/en/1_cqlib/3_qis/5_pauli_and_hamiltonian + ../documentation/en/1_cqlib/3_qis/6_metrics_entropy + + +.. toctree:: + :maxdepth: 2 + :caption: Compilation + + ../documentation/en/1_cqlib/4_compiler/0_overview + ../documentation/en/1_cqlib/4_compiler/1_layout + ../documentation/en/1_cqlib/4_compiler/2_sabre_mapping + ../documentation/en/1_cqlib/4_compiler/3_template_optimization + ../documentation/en/1_cqlib/4_compiler/4_commutative_and_clifford + + +.. toctree:: + :maxdepth: 2 + :caption: Visualization + + ../documentation/en/1_cqlib/5_visualization/0_overview + ../documentation/en/1_cqlib/5_visualization/1_draw_text + ../documentation/en/1_cqlib/5_visualization/2_draw_figure + ../documentation/en/1_cqlib/5_visualization/3_notebook_and_docs + ../documentation/en/1_cqlib/5_visualization/4_visualization_practices + ../documentation/en/1_cqlib/5_visualization/5_control_flow_and_special + ../documentation/en/1_cqlib/5_visualization/6_result_visualization + ../documentation/en/1_cqlib/5_visualization/7_state_visualization + + +.. toctree:: + :maxdepth: 2 + :caption: Error Mitigation + + ../documentation/en/1_cqlib/6_error_mitigation/0_overview + ../documentation/en/1_cqlib/6_error_mitigation/1_zne + ../documentation/en/1_cqlib/6_error_mitigation/2_virtual_distillation + ../documentation/en/1_cqlib/6_error_mitigation/3_unified_api + + +.. toctree:: + :maxdepth: 2 + :caption: Tianyan Platform + + ../documentation/en/1_cqlib/7_tianyan/0_overview + ../documentation/en/1_cqlib/7_tianyan/1_auth_config + ../documentation/en/1_cqlib/7_tianyan/2_backend_device + ../documentation/en/1_cqlib/7_tianyan/3_task_result + ../documentation/en/1_cqlib/7_tianyan/4_qcis_ir_workflow + ../documentation/en/1_cqlib/7_tianyan/5_readout_mitigation + + +.. toctree:: + :maxdepth: 2 + :caption: Python API Reference + + ../api/en/python/0_overview + ../api/en/python/6_error_mitigation/0_overview + ../api/en/python/2_device/0_overview + ../api/en/python/5_visualization/0_overview + ../api/en/python/3_qis/0_overview + ../api/en/python/4_compile/0_overview + ../api/en/python/1_ir/0_overview + ../api/en/python/0_circuit/0_overview + ../api/en/python/0_circuit/1_circuit + ../api/en/python/4_compile/1_compiler + ../api/en/python/5_visualization/1_draw_text + ../api/en/python/1_ir/1_qcis + ../api/en/python/3_qis/1_statevector + ../api/en/python/2_device/1_topology + ../api/en/python/6_error_mitigation/1_zne + ../api/en/python/3_qis/2_density_matrix + ../api/en/python/5_visualization/2_draw_figure + ../api/en/python/2_device/2_properties_device + ../api/en/python/1_ir/2_qasm2 + ../api/en/python/0_circuit/2_qubit + ../api/en/python/4_compile/2_transform + ../api/en/python/6_error_mitigation/2_virtual_distillation + ../api/en/python/3_qis/3_density_matrix_noise + ../api/en/python/2_device/3_layout + ../api/en/python/4_compile/3_layout + ../api/en/python/0_circuit/3_parameter + ../api/en/python/1_ir/3_qasm3 + ../api/en/python/5_visualization/3_state_plots + ../api/en/python/6_error_mitigation/3_unified + ../api/en/python/2_device/4_noise + ../api/en/python/0_circuit/4_operation_instruction + ../api/en/python/5_visualization/4_result_plots + ../api/en/python/4_compile/4_routing + ../api/en/python/3_qis/4_stabilizer + ../api/en/python/3_qis/5_classical_state + ../api/en/python/0_circuit/5_gates_standard + ../api/en/python/5_visualization/5_render_to_file + ../api/en/python/2_device/5_result + ../api/en/python/4_compile/5_sabre + ../api/en/python/4_compile/6_decompose_resynthesis + ../api/en/python/0_circuit/6_gates_unitary + ../api/en/python/3_qis/6_pauli + ../api/en/python/2_device/6_qubits + ../api/en/python/0_circuit/7_gates_mc_gate + ../api/en/python/3_qis/7_hamiltonian + ../api/en/python/4_compile/7_knowledge + ../api/en/python/3_qis/8_evolution + ../api/en/python/0_circuit/8_gates_circuit_gate + ../api/en/python/4_compile/8_resource + ../api/en/python/0_circuit/9_classical_control_flow + ../api/en/python/4_compile/9_commutation + ../api/en/python/3_qis/9_metrics_entropy + ../api/en/python/0_circuit/10_symbolic_matrix + ../api/en/python/0_circuit/11_ansatz + ../api/en/python/0_circuit/12_circuit_to_matrix + ../api/en/python/0_circuit/13_circuit_dag + + +.. toctree:: + :maxdepth: 2 + :caption: Rust API Reference + + ../api/en/rust/0_overview + ../api/en/rust/6_error_mitigation/0_overview + ../api/en/rust/2_device/0_overview + ../api/en/rust/5_visualization/0_overview + ../api/en/rust/3_qis/0_overview + ../api/en/rust/4_compile/0_overview + ../api/en/rust/1_ir/0_overview + ../api/en/rust/0_circuit/0_overview + ../api/en/rust/0_circuit/1_circuit + ../api/en/rust/4_compile/1_compiler + ../api/en/rust/5_visualization/1_draw_text + ../api/en/rust/1_ir/1_qcis + ../api/en/rust/3_qis/1_statevector + ../api/en/rust/2_device/1_topology + ../api/en/rust/6_error_mitigation/1_zne + ../api/en/rust/3_qis/2_density_matrix + ../api/en/rust/5_visualization/2_draw_figure + ../api/en/rust/2_device/2_properties_device + ../api/en/rust/1_ir/2_qasm2 + ../api/en/rust/0_circuit/2_qubit + ../api/en/rust/4_compile/2_transform + ../api/en/rust/6_error_mitigation/2_virtual_distillation + ../api/en/rust/3_qis/3_density_matrix_noise + ../api/en/rust/2_device/3_layout + ../api/en/rust/4_compile/3_layout + ../api/en/rust/0_circuit/3_parameter + ../api/en/rust/1_ir/3_qasm3 + ../api/en/rust/5_visualization/3_state_plots + ../api/en/rust/6_error_mitigation/3_unified + ../api/en/rust/2_device/4_noise + ../api/en/rust/0_circuit/4_operation_instruction + ../api/en/rust/5_visualization/4_result_plots + ../api/en/rust/4_compile/4_routing + ../api/en/rust/3_qis/4_stabilizer + ../api/en/rust/3_qis/5_classical_state + ../api/en/rust/0_circuit/5_gate_standard + ../api/en/rust/5_visualization/5_render_to_file + ../api/en/rust/2_device/5_result + ../api/en/rust/4_compile/5_sabre + ../api/en/rust/4_compile/6_decompose_resynthesis + ../api/en/rust/0_circuit/6_gate_unitary + ../api/en/rust/3_qis/6_pauli + ../api/en/rust/2_device/6_qubits + ../api/en/rust/5_visualization/6_visual_ir + ../api/en/rust/0_circuit/7_gate_mc_gate + ../api/en/rust/3_qis/7_hamiltonian + ../api/en/rust/4_compile/7_knowledge + ../api/en/rust/3_qis/8_evolution + ../api/en/rust/0_circuit/8_gate_circuit_gate + ../api/en/rust/4_compile/8_resource + ../api/en/rust/0_circuit/9_classical_control_flow + ../api/en/rust/4_compile/9_commutation + ../api/en/rust/3_qis/9_metrics_entropy + ../api/en/rust/0_circuit/10_symbolic_matrix + ../api/en/rust/0_circuit/11_ansatz + ../api/en/rust/0_circuit/12_cfg + ../api/en/rust/0_circuit/13_circuit_to_matrix + ../api/en/rust/0_circuit/14_circuit_dag + + +.. toctree:: + :maxdepth: 1 + :caption: C API Reference + + ../api/en/c/0_overview + ../api/en/c/0_circuit/0_overview + ../api/en/c/0_circuit/1_circuit + ../api/en/c/0_circuit/2_qubit + ../api/en/c/0_circuit/3_parameter + ../api/en/c/0_circuit/4_operation_instruction + ../api/en/c/0_circuit/5_gate_standard + ../api/en/c/0_circuit/6_gate_unitary + ../api/en/c/0_circuit/7_gate_mc_gate + ../api/en/c/0_circuit/8_gate_circuit_gate + ../api/en/c/0_circuit/9_classical_control_flow + ../api/en/c/0_circuit/10_symbolic_matrix + ../api/en/c/0_circuit/11_ansatz + ../api/en/c/0_circuit/12_cfg + ../api/en/c/0_circuit/13_circuit_to_matrix + ../api/en/c/0_circuit/14_circuit_dag + ../api/en/c/1_ir/0_overview + ../api/en/c/1_ir/1_qcis + ../api/en/c/1_ir/2_qasm2 + ../api/en/c/1_ir/3_qasm3 + ../api/en/c/2_device/0_overview + ../api/en/c/2_device/1_topology + ../api/en/c/2_device/2_properties_device + ../api/en/c/2_device/3_layout + ../api/en/c/2_device/4_noise + ../api/en/c/2_device/5_result + ../api/en/c/2_device/6_qubits + ../api/en/c/3_qis/0_overview + ../api/en/c/3_qis/1_statevector + ../api/en/c/3_qis/2_density_matrix + ../api/en/c/3_qis/3_density_matrix_noise + ../api/en/c/3_qis/4_stabilizer + ../api/en/c/3_qis/5_classical_state + ../api/en/c/3_qis/6_pauli + ../api/en/c/3_qis/7_hamiltonian + ../api/en/c/3_qis/8_evolution + ../api/en/c/3_qis/9_metrics_entropy + ../api/en/c/4_compile/0_overview + ../api/en/c/4_compile/1_compiler + ../api/en/c/4_compile/2_transform + ../api/en/c/4_compile/3_layout + ../api/en/c/4_compile/4_routing + ../api/en/c/4_compile/5_sabre + ../api/en/c/4_compile/6_decompose_resynthesis + ../api/en/c/4_compile/7_knowledge + ../api/en/c/4_compile/8_resource + ../api/en/c/4_compile/9_commutation + ../api/en/c/5_visualization/0_overview + ../api/en/c/5_visualization/1_draw_text + ../api/en/c/5_visualization/2_draw_figure + ../api/en/c/5_visualization/3_state_plots + ../api/en/c/5_visualization/4_result_plots + ../api/en/c/5_visualization/5_render_to_file + ../api/en/c/5_visualization/6_visual_ir + ../api/en/c/6_error_mitigation/0_overview + ../api/en/c/6_error_mitigation/1_zne + ../api/en/c/6_error_mitigation/2_virtual_distillation + ../api/en/c/6_error_mitigation/3_unified + diff --git a/docs/index.rst b/docs/index.rst index 0f0cfbb0..ab8ce29e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,97 +1,9 @@ -Cqlib 文档 +Cqlib 概览 ========== .. toctree:: - :maxdepth: 2 - :caption: 快速开始 + :maxdepth: 1 + :caption: 语言 / Language - documentation/0_get_started/0_introduction - documentation/0_get_started/1_installation - documentation/0_get_started/2_quickstart - -.. toctree:: - :maxdepth: 2 - :caption: 量子线路 - - documentation/1_cqlib/0_circuit/0_overview - documentation/1_cqlib/0_circuit/1_gates - documentation/1_cqlib/0_circuit/2_structures - documentation/1_cqlib/0_circuit/3_parameters - documentation/1_cqlib/0_circuit/4_circuit_analysis - documentation/1_cqlib/0_circuit/5_control_flow - -.. toctree:: - :maxdepth: 2 - :caption: 中间表示 - - documentation/1_cqlib/1_ir/0_overview - documentation/1_cqlib/1_ir/1_qcis - documentation/1_cqlib/1_ir/2_qasm2 - documentation/1_cqlib/1_ir/3_qasm3 - documentation/1_cqlib/1_ir/4_conversion_workflow - -.. toctree:: - :maxdepth: 2 - :caption: 设备与后端 - - documentation/1_cqlib/2_device/0_overview - documentation/1_cqlib/2_device/1_topology - documentation/1_cqlib/2_device/2_device - documentation/1_cqlib/2_device/3_layout - documentation/1_cqlib/2_device/4_noise - documentation/1_cqlib/2_device/5_result - -.. toctree:: - :maxdepth: 2 - :caption: 量子信息 - - documentation/1_cqlib/3_qis/0_overview - documentation/1_cqlib/3_qis/1_statevector - documentation/1_cqlib/3_qis/2_density_matrix - documentation/1_cqlib/3_qis/3_density_matrix_noise - documentation/1_cqlib/3_qis/4_stabilizer - documentation/1_cqlib/3_qis/5_pauli_and_hamiltonian - documentation/1_cqlib/3_qis/6_metrics_entropy - -.. toctree:: - :maxdepth: 2 - :caption: 编译优化 - - documentation/1_cqlib/4_compiler/0_overview - documentation/1_cqlib/4_compiler/1_layout - documentation/1_cqlib/4_compiler/2_sabre_mapping - documentation/1_cqlib/4_compiler/3_template_optimization - documentation/1_cqlib/4_compiler/4_commutative_and_clifford - -.. toctree:: - :maxdepth: 2 - :caption: 可视化 - - documentation/1_cqlib/5_visualization/0_overview - documentation/1_cqlib/5_visualization/1_draw_text - documentation/1_cqlib/5_visualization/2_draw_figure - documentation/1_cqlib/5_visualization/3_notebook_and_docs - documentation/1_cqlib/5_visualization/4_visualization_practices - documentation/1_cqlib/5_visualization/5_control_flow_and_special - documentation/1_cqlib/5_visualization/6_result_visualization - documentation/1_cqlib/5_visualization/7_state_visualization - -.. toctree:: - :maxdepth: 2 - :caption: 错误缓解 - - documentation/1_cqlib/6_error_mitigation/0_overview - documentation/1_cqlib/6_error_mitigation/1_zne - documentation/1_cqlib/6_error_mitigation/2_virtual_distillation - documentation/1_cqlib/6_error_mitigation/3_unified_api - -.. toctree:: - :maxdepth: 2 - :caption: 天衍平台 - - documentation/1_cqlib/7_tianyan/0_overview - documentation/1_cqlib/7_tianyan/1_auth_config - documentation/1_cqlib/7_tianyan/2_backend_device - documentation/1_cqlib/7_tianyan/3_task_result - documentation/1_cqlib/7_tianyan/4_qcis_ir_workflow - documentation/1_cqlib/7_tianyan/5_readout_mitigation + 中文 + English diff --git a/docs/releasing.md b/docs/releasing.md new file mode 100644 index 00000000..bf40e612 --- /dev/null +++ b/docs/releasing.md @@ -0,0 +1,118 @@ +# CI and Manual Releases + +## File Organization + +The structure follows Qiskit's separation of entry-point workflows, reusable +workflows, and tools scripts, without adopting its automated publishing steps +or modifying project source code, existing tests, or Cargo configuration. + +| File | Responsibility | +| --- | --- | +| `.github/workflows/ci.yml` | PR entry point: calls lint and development tests on three operating systems | +| `.github/workflows/lint.yml` | Reusable Rust fmt / Clippy, Python Ruff, and C formatting checks | +| `.github/workflows/tests.yml` | Reusable Rust workspace, pytest, and C static/shared linking tests | +| `.github/workflows/wheels.yml` | Tag entry point: validates versions, runs five-platform tests, and calls the build workflow | +| `.github/workflows/wheels-build.yml` | Builds and stores wheels and C SDKs for five platforms, plus sdist and .crate packages | +| `tools/ci.py` | Reads versions, packages and tests C SDKs, tests installed wheels, and builds source packages | +| `tools/c-sdk/` | CMake test configuration and README distributed with each C SDK | + +Routine commands live directly in workflows; cross-platform file handling lives +in the Python helper script. Rust 1.97.1 is pinned only in CI, without changing +developer toolchains or the project's declared MSRV. Ruff and clang-format +versions match the existing pre-commit configuration. + +## Trigger Behavior + +PRs run `cargo test --workspace --locked`, the full pytest suite against a +development installation, and C tests on Linux x86_64, Windows x86_64, and macOS +arm64. Formatting and Clippy checks run separately. `maturin develop` is used +only for a temporary development installation; no distribution packages are +built and no artifacts are uploaded. Cargo caching only accelerates tests. + +Tags first undergo version checks and the same development tests on all five +platforms. Distribution packages are built only after these checks pass. +All workflows have only `contents: read` permissions. They require no publishing +credentials, create no Releases, and publish nothing to PyPI or crates.io. + +## Tags and Versions + +Tags use the Python version to identify a release batch: + +- Stable releases: `vX.Y.Z`. +- Prereleases: `vX.Y.Z-alpha.N`, `vX.Y.Z-beta.N`, or `vX.Y.Z-rc.N`, with N starting at 1. +- Current tag: `v1.4.0-beta.1`. +- Current Python source version: `1.4.0-beta1`, normalized to `1.4.0b1` for PEP 440 and wheel filenames. +- Current Rust/C version: `0.1.0-beta.1`; both inherit the workspace version. + +Python and Rust/C versions evolve independently and do not need matching +numbers. The Cargo.toml files at a given tag record the version mapping for that +release; no separate release.toml is maintained. +CI checks that the tag is equivalent to the Python version, core dependency +versions are consistent across bindings, the minimum Python version is 3.10, +and `abi3-py310` is enabled. Maintainers must update versions before tagging. + +## Platforms and Artifacts + +| Platform | Runner / Build Environment | +| --- | --- | +| Windows x86_64 | windows-2022 / MSVC | +| Linux glibc 2.28+ x86_64 | ubuntu-24.04 / manylinux_2_28_x86_64 | +| Linux glibc 2.28+ aarch64 | ubuntu-24.04-arm / manylinux_2_28_aarch64 | +| macOS 11+ x86_64 | macos-15-intel / deployment target 11.0 | +| macOS 11+ arm64 | macos-15 / deployment target 11.0 | + +Linux wheels and C SDKs are built and tested inside architecture-native +manylinux containers. Ubuntu host binaries are not used as glibc 2.28 release +artifacts. Full workspace tests run on the host to support libpython linking +for the Python binding's Rust tests; core, C, and facade Rust tests also run +inside the container. Containers use a separate Cargo target directory. +Images use the latest tag, with the resolved digest recorded in logs. +On macOS, CI checks the shared library's minimum deployment version. Tests +actually run on macOS 15; this does not constitute execution testing on macOS 11. + +Each platform produces one `cp310-abi3` wheel. That same wheel is installed into +separate CPython 3.10, 3.11, 3.12, 3.13, and 3.14 environments for import checks, +pip check, and the full pytest suite. Tests are copied alongside the installed +package to satisfy existing sibling-directory assertions. They do not import +cqlib from the checkout, and existing tests are not modified, skipped, or marked +xfail. Future Python versions require extending the validation matrix. + +Each platform also produces one C SDK archive: ZIP on Windows and tar.gz +elsewhere. Each archive contains `cqlib_c.h`, static and shared libraries, +Windows import libraries where applicable, a README, LICENSE.txt, and runnable +C tests. Static and shared linking tests are compiled and run separately; +C assertions remain enabled in Release builds. Linux shared libraries must +not require GLIBC symbol versions newer than 2.28. + +The source job generates one sdist and one `cqlib-core-.crate`. +It runs `cargo package --locked -p cqlib-core`; Cargo's default verification +extracts and independently compiles the package. The command does not use +`--no-verify` and does not need to be executed twice. +The sdist is generated with maturin and checked for inclusion of the local core +dependency, without rewriting the checkout's Cargo.lock. + +32-bit targets, musllinux/Alpine, other CPU architectures, universal2, PyPy, +and free-threaded Python are not currently supported. + +## Downloading and Publishing + +Wait for the entire tag workflow to succeed before publishing; an artifact +produced by a single successful job is not sufficient. +Download the five `release--` artifacts and the single +`release--sources` artifact. Together they contain 12 distribution files: +5 wheels, 1 sdist, 5 C SDK archives, and 1 .crate package. +GitHub artifacts are retained for 30 days, so release maintainers should +download and archive them promptly. + +- PyPI: manually check and upload the 5 wheels and sdist using Twine. +- crates.io: this release publishes only `cqlib-core`, so there is no multi-crate + publication order. `binding-python`, `binding-c`, and the `cqlib` facade are + outside this release's scope. This is a release policy, not a change to their + Cargo publish settings. Download and extract the .crate package, then run + `cargo publish --locked --dry-run` in the directory containing its Cargo.toml. + After verification, run `cargo publish --locked`. Cargo does not accept a + .crate path directly as an argument to publish; it repackages the extracted + sources. If the facade is published in the future, publish `cqlib-core` first, + wait until it can be resolved through the registry index, then publish `cqlib`. +- GitHub/Gitee Releases: maintainers manually create a Release at the same tag + and attach the C SDK archives and any other desired files. diff --git a/tools/c-sdk/CMakeLists.txt b/tools/c-sdk/CMakeLists.txt new file mode 100644 index 00000000..befa07af --- /dev/null +++ b/tools/c-sdk/CMakeLists.txt @@ -0,0 +1,57 @@ +cmake_minimum_required(VERSION 3.20) +project(cqlib_c_tests LANGUAGES C) +enable_testing() + +# Test the staged SDK, never an implicit -l lookup in the Cargo build tree. +if(NOT CQLIB_SDK_ROOT) + message(FATAL_ERROR "Set CQLIB_SDK_ROOT to an unpacked/staged C SDK") +endif() + +add_library(cqlib_static STATIC IMPORTED) +add_library(cqlib_shared SHARED IMPORTED) +if(WIN32) + set_target_properties(cqlib_static PROPERTIES + IMPORTED_LOCATION "${CQLIB_SDK_ROOT}/lib/binding_c.lib") + set_target_properties(cqlib_shared PROPERTIES + IMPORTED_LOCATION "${CQLIB_SDK_ROOT}/bin/binding_c.dll" + IMPORTED_IMPLIB "${CQLIB_SDK_ROOT}/lib/binding_c.dll.lib") +elseif(APPLE) + set_target_properties(cqlib_static PROPERTIES + IMPORTED_LOCATION "${CQLIB_SDK_ROOT}/lib/libbinding_c.a") + set_target_properties(cqlib_shared PROPERTIES + IMPORTED_LOCATION "${CQLIB_SDK_ROOT}/lib/libbinding_c.dylib") +else() + set_target_properties(cqlib_static PROPERTIES + IMPORTED_LOCATION "${CQLIB_SDK_ROOT}/lib/libbinding_c.a") + set_target_properties(cqlib_shared PROPERTIES + IMPORTED_LOCATION "${CQLIB_SDK_ROOT}/lib/libbinding_c.so") +endif() + +foreach(kind static shared) + add_executable(test_${kind} test_circuit.c) + target_include_directories(test_${kind} PRIVATE "${CQLIB_SDK_ROOT}/include") + target_link_libraries(test_${kind} PRIVATE cqlib_${kind}) + # The C tests put API calls inside assert(); retain them even in Release. + if(MSVC) + target_compile_options(test_${kind} PRIVATE /UNDEBUG) + else() + target_compile_options(test_${kind} PRIVATE -UNDEBUG) + endif() + if(WIN32) + target_link_libraries(test_${kind} PRIVATE + ntdll ws2_32 bcrypt userenv advapi32) + elseif(APPLE) + target_link_libraries(test_${kind} PRIVATE c++ "-framework CoreFoundation") + else() + target_link_libraries(test_${kind} PRIVATE pthread dl m) + endif() + set_target_properties(test_${kind} PROPERTIES + BUILD_RPATH "${CQLIB_SDK_ROOT}/lib") + add_test(NAME c_${kind} COMMAND test_${kind}) +endforeach() + +if(WIN32) + add_custom_command(TARGET test_shared POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "$" "$") +endif() diff --git a/tools/c-sdk/README.md b/tools/c-sdk/README.md new file mode 100644 index 00000000..0ea4b178 --- /dev/null +++ b/tools/c-sdk/README.md @@ -0,0 +1,58 @@ +# CQLib C SDK + +This archive contains the CQLib C ABI header, static and shared libraries, +Apache-2.0 license, and executable consumer tests. Its version follows the Rust +workspace, independently of the Python package version. + +## Supported platforms + +- Windows x86_64, MSVC ABI. Use the Visual Studio 2022 C/C++ toolchain and the + Microsoft Visual C++ runtime. `binding_c.lib` is the static library; + `binding_c.dll.lib` is the import library for `bin/binding_c.dll`. +- Linux x86_64 or aarch64, glibc 2.28 or newer. Libraries are built and tested in + the corresponding manylinux_2_28 image. +- macOS x86_64 or arm64, deployment target 11.0. These are separate archives, + not universal binaries. CI tests on current hosted macOS runners; it does not + execute on macOS 11 itself. + +32-bit targets, musl/Alpine, other CPU architectures and the Windows MinGW ABI +are outside this release's support matrix. + +## Verify the SDK + +Install CMake 3.20+ and a native C compiler. From the unpacked SDK directory: + +```sh +cmake -S tests -B build -DCQLIB_SDK_ROOT="$PWD" -DCMAKE_BUILD_TYPE=Release +cmake --build build --config Release +ctest --test-dir build -C Release --output-on-failure +``` + +On Windows, run from a Developer PowerShell and replace `"$PWD"` with +`"$($PWD.Path)"`. The tests explicitly link each library variant and retain +assertions in Release mode. Their CMake file is also a consumer-linking example. + +## Integrate + +Include `include/cqlib_c.h`. Never free CQLib-owned objects with the system +`free`: use their matching API functions such as `circuit_free` and `param_free`. + +For static linking, pass the full static-library path and these platform system +libraries, as demonstrated in `tests/CMakeLists.txt`: + +| Platform | Additional libraries | +| --- | --- | +| Linux | `pthread`, `dl`, `m` | +| macOS | `c++`, `CoreFoundation` framework | +| Windows | `ntdll`, `ws2_32`, `bcrypt`, `userenv`, `advapi32` | + +For shared linking, make the library discoverable at runtime. On Windows place +`binding_c.dll` alongside the executable (and link `binding_c.dll.lib`). On Linux +configure an application-relative RPATH such as `$ORIGIN/../lib`, or set +`LD_LIBRARY_PATH` for development. On macOS configure an application-relative +RPATH such as `@executable_path/../lib`; the dylib uses +`@rpath/libbinding_c.dylib` as its install name. No checkout or Cargo build +directory is needed by SDK consumers. + +The supported circuit construction and parameter APIs are documented in the +generated header and illustrated in `tests/test_circuit.c`. diff --git a/tools/ci.py b/tools/ci.py new file mode 100644 index 00000000..0eb347ce --- /dev/null +++ b/tools/ci.py @@ -0,0 +1,308 @@ +"""Small cross-platform helpers for CI; orchestration lives in workflows.""" + +import argparse +import os +from pathlib import Path +import re +import shutil +import subprocess +import sys +import tarfile +import tempfile + +try: + import tomllib +except ModuleNotFoundError: + import tomli as tomllib + +from packaging.version import Version +from packaging.utils import parse_wheel_filename + +ROOT = Path(__file__).resolve().parents[1] +BINDING = ROOT / "crates/binding-python" +DIST = ROOT / "target/ci-dist" +PLATFORMS = { + "windows-x86_64-msvc": "win_amd64", + "linux-glibc2.28-x86_64": "manylinux_2_28_x86_64", + "linux-glibc2.28-aarch64": "manylinux_2_28_aarch64", + "macos11-x86_64": "macosx_11_0_x86_64", + "macos11-arm64": "macosx_11_0_arm64", +} + + +def read_toml(path): + with Path(path).open("rb") as source: + return tomllib.load(source) + + +def run(*args, cwd=ROOT): + print("+", " ".join(map(str, args)), flush=True) + subprocess.run(list(map(str, args)), cwd=cwd, check=True) + + +def require(condition, message): + if not condition: + raise ValueError(message) + + +def versions(tag=None): + rust = read_toml(ROOT / "Cargo.toml")["workspace"]["package"]["version"] + binding = read_toml(BINDING / "Cargo.toml") + python = str(Version(binding["package"]["version"])) + require( + "abi3-py310" in binding["dependencies"]["pyo3"]["features"], + "Require abi3-py310", + ) + require( + read_toml(BINDING / "pyproject.toml")["project"]["requires-python"] == ">=3.10", + "Require Python >=3.10", + ) + for member in ("binding-python", "binding-c", "cqlib"): + manifest = read_toml(ROOT / "crates" / member / "Cargo.toml") + require( + manifest["dependencies"]["cqlib-core"]["version"] == rust, + f"{member}: cqlib-core dependency version mismatch", + ) + if tag is not None: + require( + re.fullmatch(r"v\d+\.\d+\.\d+(?:-(?:alpha|beta|rc)\.[1-9]\d*)?", tag), + "Tag must be vX.Y.Z or vX.Y.Z-{alpha,beta,rc}.N", + ) + require(Version(tag[1:]) == Version(python), "Tag must match Python version") + print(f"Python: {python}; Rust/C: {rust}", flush=True) + return python, rust + + +def library_files(): + if sys.platform == "win32": + return {"lib": ["binding_c.lib", "binding_c.dll.lib"], "bin": ["binding_c.dll"]} + suffix = "dylib" if sys.platform == "darwin" else "so" + return {"lib": ["libbinding_c.a", f"libbinding_c.{suffix}"]} + + +def build_and_test(platform=None): + release = platform is not None + run( + "cargo", + "build", + "--locked", + "-p", + "binding-c", + *(["--release"] if release else []), + ) + target = Path(os.environ.get("CARGO_TARGET_DIR", ROOT / "target")).resolve() + libraries = target / ("release" if release else "debug") + version = versions()[1] + name = f"cqlib-c-{version}-{platform or 'development'}" + with tempfile.TemporaryDirectory(prefix="cqlib-c-") as temporary: + sdk = Path(temporary) / name + (sdk / "include").mkdir(parents=True) + shutil.copy2(ROOT / "crates/binding-c/include/cqlib_c.h", sdk / "include") + for directory, filenames in library_files().items(): + (sdk / directory).mkdir() + for filename in filenames: + shutil.copy2(libraries / filename, sdk / directory) + shutil.copy2(ROOT / "LICENSE.txt", sdk) + shutil.copy2(ROOT / "tools/c-sdk/README.md", sdk / "README.md") + (sdk / "tests").mkdir() + shutil.copy2(ROOT / "crates/binding-c/tests/test_circuit.c", sdk / "tests") + shutil.copy2(ROOT / "tools/c-sdk/CMakeLists.txt", sdk / "tests") + if sys.platform == "darwin": + dylib = sdk / "lib/libbinding_c.dylib" + run("install_name_tool", "-id", "@rpath/libbinding_c.dylib", dylib) + if release: + output = subprocess.check_output(["otool", "-l", dylib], text=True) + # Inspect LC_BUILD_VERSION/LC_VERSION_MIN_MACOSX, not dylib versions. + blocks = re.findall( + r"cmd LC_(?:BUILD_VERSION|VERSION_MIN_MACOSX)\b(.*?)(?=Load command|$)", + output, + re.S, + ) + require(blocks, "Missing macOS deployment target") + for block in blocks: + minimum = re.search(r"(?:minos|version)\s+(\d+\.\d+)", block) + require( + minimum and tuple(map(int, minimum[1].split("."))) <= (11, 0), + "C SDK requires macOS newer than 11", + ) + if release and sys.platform == "linux": + output = subprocess.check_output( + ["readelf", "--version-info", sdk / "lib/libbinding_c.so"], text=True + ) + glibc_versions = [ + tuple(map(int, value.split("."))) + for value in re.findall(r"GLIBC_(\d+\.\d+)", output) + ] + require( + glibc_versions and max(glibc_versions) <= (2, 28), + "C SDK requires glibc newer than 2.28", + ) + run("ldd", sdk / "lib/libbinding_c.so") + build = Path(temporary) / "build" + run( + "cmake", + "-S", + sdk / "tests", + "-B", + build, + f"-DCQLIB_SDK_ROOT={sdk}", + "-DCMAKE_BUILD_TYPE=Release", + ) + run("cmake", "--build", build, "--config", "Release", "--parallel", "2") + run("ctest", "--test-dir", build, "-C", "Release", "--output-on-failure") + if release: + DIST.mkdir(parents=True, exist_ok=True) + shutil.make_archive( + str(DIST / name), + "zip" if sys.platform == "win32" else "gztar", + temporary, + name, + ) + + +def verify_wheel(directory=DIST): + wheels = list(Path(directory).glob("*.whl")) + require(len(wheels) == 1, "Expected exactly one wheel per platform") + wheel = wheels[0].resolve() + name, version, _, tags = parse_wheel_filename(wheel.name) + require( + name == "cqlib" and version == Version(versions()[0]), "Wrong wheel version" + ) + require( + all(t.interpreter == "cp310" and t.abi == "abi3" for t in tags), + "Wheel must use cp310-abi3", + ) + with tempfile.TemporaryDirectory(prefix="cqlib-wheel-") as temporary: + directory = Path(temporary) + run(sys.executable, "-m", "venv", directory / "venv") + python = ( + directory + / "venv" + / ("Scripts/python.exe" if os.name == "nt" else "bin/python") + ) + run( + python, + "-m", + "pip", + "install", + "--only-binary=:all:", + wheel, + "pytest==8.4.2", + "resvg-py==0.5.0", + ) + run(python, "-m", "pip", "check") + run( + python, + "-I", + "-c", + "import cqlib, cqlib._native, pathlib, sys; " + "root = pathlib.Path(sys.prefix).resolve(); " + "assert pathlib.Path(cqlib.__file__).resolve().is_relative_to(root); " + "assert pathlib.Path(cqlib._native.__file__).resolve().is_relative_to(root); " + "print(cqlib.__file__, cqlib._native.__file__)", + cwd=directory, + ) + site = Path( + subprocess.check_output( + [ + str(python), + "-I", + "-c", + "import sysconfig; print(sysconfig.get_path('purelib'))", + ], + text=True, + ).strip() + ) + # Existing location tests expect tests/ and cqlib/ to be siblings. + # Copy tests beside the installed wheel, never replace it with checkout sources. + tests = site / "tests" + shutil.copytree( + BINDING / "tests", + tests, + ignore=shutil.ignore_patterns("__pycache__", ".pytest_cache"), + ) + run( + python, + "-I", + "-m", + "pytest", + "--strict-markers", + "--import-mode=importlib", + tests, + cwd=directory, + ) + + +def release(platform): + require(platform in PLATFORMS, "Unsupported platform") + require( + not DIST.exists() or not any(DIST.iterdir()), + "Release directory must start empty", + ) + build_and_test(platform) + args = ( + ["--compatibility", "manylinux_2_28", "--auditwheel", "check"] + if sys.platform == "linux" + else [] + ) + run( + sys.executable, + "-m", + "maturin", + "build", + "--release", + "--locked", + "-i", + sys.executable, + "--out", + DIST, + *args, + cwd=BINDING, + ) + wheels = list(DIST.glob("*.whl")) + require(len(wheels) == 1, "Expected exactly one wheel") + _, _, _, tags = parse_wheel_filename(wheels[0].name) + require( + any(t.platform == PLATFORMS[platform] for t in tags), "Wrong wheel platform tag" + ) + verify_wheel() + + +def sources(): + DIST.mkdir(parents=True, exist_ok=True) + # Cargo's default verification extracts and compiles the .crate independently. + run("cargo", "package", "--locked", "-p", "cqlib-core") + shutil.copy2(ROOT / "target/package" / f"cqlib-core-{versions()[1]}.crate", DIST) + # maturin sdist does not support --locked. + lock = (ROOT / "Cargo.lock").read_bytes() + run(sys.executable, "-m", "maturin", "sdist", "--out", DIST, cwd=BINDING) + require( + (ROOT / "Cargo.lock").read_bytes() == lock, "sdist changed checkout Cargo.lock" + ) + sdist = DIST / f"cqlib-{versions()[0]}.tar.gz" + with tarfile.open(sdist) as archive: + require( + any("/crates/cqlib-core/" in name for name in archive.getnames()), + "sdist must include its local Rust core dependency", + ) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "command", choices=("versions", "ctest", "release", "wheel-test", "sources") + ) + parser.add_argument("--tag") + parser.add_argument("--wheel-dir", type=Path, default=DIST) + parser.add_argument("--platform", choices=PLATFORMS) + args = parser.parse_args() + if args.command == "versions": + versions(args.tag) + elif args.command == "ctest": + build_and_test() + elif args.command == "release": + release(args.platform) + elif args.command == "wheel-test": + verify_wheel(args.wheel_dir) + else: + sources()